diff --git a/.gitignore b/.gitignore index 913d250..65854cc 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index d1ace50..0000000 --- a/AGENTS.md +++ /dev/null @@ -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. diff --git a/PPSSPP_Core_Plan.md b/PPSSPP_Core_Plan.md deleted file mode 100644 index eb3a55e..0000000 --- a/PPSSPP_Core_Plan.md +++ /dev/null @@ -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. diff --git a/PPSSPP_Implementation_Plan.md b/PPSSPP_Implementation_Plan.md deleted file mode 100644 index cdd1d9d..0000000 --- a/PPSSPP_Implementation_Plan.md +++ /dev/null @@ -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 ` -(`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. diff --git a/README.md b/README.md deleted file mode 100644 index 42dcf69..0000000 --- a/README.md +++ /dev/null @@ -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/` 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. diff --git a/assets/xmb/monochrome/README.md b/assets/xmb/monochrome/README.md deleted file mode 100644 index 35167a2..0000000 --- a/assets/xmb/monochrome/README.md +++ /dev/null @@ -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). diff --git a/docs/ACTIVE.md b/docs/ACTIVE.md deleted file mode 100644 index fb8994d..0000000 --- a/docs/ACTIVE.md +++ /dev/null @@ -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//.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. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md deleted file mode 100644 index 4ca1309..0000000 --- a/docs/DEPLOYMENT.md +++ /dev/null @@ -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//`: 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//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//` 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 ` 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//`. - -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//` 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//` 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 ` 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 `, `kill `, `restart `) 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//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. diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md deleted file mode 100644 index 757c005..0000000 --- a/docs/FINDINGS.md +++ /dev/null @@ -1,2933 +0,0 @@ -# Findings - -Append-only measurements. What the platform, the dependency, the customer or the -data actually does — as opposed to what the documentation says — and what each -one forces the code to do. A finding is written the moment it is measured, -because it is the thing that is expensive to rediscover and impossible to guess. - -Never rewrite an entry. If a later measurement contradicts one, append a new -entry that names the old one and says what changed. - -## 2026-09-18: Upstream RetroArch has no PS5 platform code - -**Measured.** The pinned tree carries the PS4/Orbis port only: -`frontend/drivers/platform_orbis.c`, `gfx/drivers_context/orbis_ctx.c` and -`Makefile.orbis` are present, and no `platform_ps5.c`, no `Makefile.ps5` and no -PS5 context driver exist. `grep -ril ps5` over the 1.22.2 tree returns -`CHANGES.md` and binary art, nothing in the sources. Measured by directory listing -and grep over `vendor/retroarch` (`docs/REFERENCE.md`, "The environment"). - -**Consequence.** The platform work is ours: a context driver, a platform driver, -an input driver and an audio driver, each behind our own seam in `platform/`, -plus whatever `configure` flags the frontend needs. It also means the port is a -patch series on a pinned tarball, never a fork we maintain file by file — -`docs/PLAN.md`, invariant "Upstream stays upstream". - -**Boundary.** True for 1.22.2 and every earlier tag. If upstream ships a PS5 -platform driver, the patches that only add one are dropped rather than ported, -and this entry is superseded by a new one. - ---- - -## 2026-09-18: The payload SDK ships no Vulkan and no SDL2, but it does ship the console APIs - -**Measured.** `$PS5_PAYLOAD_SDK/target/include` holds 322 entries including -`EGL/` and `GLES2/` and no `vulkan/` and no `SDL2/`; `target/lib` holds -`libScePad.so`, `libSceAudioOut.so`, `libSceVideoOut.so`, `libSceUserService.so`, -`libSceSysmodule.so`, `libSceNet.so`, `libSceHttp.so` and `libSceSsl.so` among -others. Measured by listing the sysroot of the SDK unpacked 2026-09-17 -(`docs/REFERENCE.md`, "The environment"). - -**Consequence.** Controller input, audio output, display, user selection and -networking come from public console APIs — no third-party port is needed for -them. GPU access and windowing do not: the EGL/GLES2 headers are the SDK's own, -and a full OpenGL stack or a Vulkan driver comes from a sibling project. -Padding in a build script cannot fix this; the missing pieces have to be -linked from `../ps5-opengl-sdk-0.2.0` or `../PS5_Vulkan` (`docs/REFERENCE.md`, -"The graphics backend"). - -**Boundary.** The SDK's own sysroot, as unpacked on 2026-09-17. A later SDK -release could add either, in which case this entry is superseded rather than -edited. - ---- - -## 2026-09-18: Cross-compiling with the toolchain works and is cheap to check - -**Measured.** `source $PS5_PAYLOAD_SDK/toolchain/prospero.sh` followed by -`$CC -o t.elf t.c` on a hello-world C file produced an ELF 64-bit LSB -pie executable, x86-64, version 1 (FreeBSD), 110,712 bytes, `prospero-clang` -version 22.1.8 with target `x86_64-sie-ps5`. Measured in this session from a -temporary directory; the SDK also ships `prospero-nm`, `prospero-objcopy`, -`prospero-strip`, `prospero-cmake`, `prospero-meson` and -`prospero-pkg-config`. - -**Consequence.** The `build` gate can prove the toolchain and the platform code -without a console, and every PS5 compile belongs in that gate rather than in a -spontaneous command. "It is FreeBSD-flavoured x86-64 ELF" is also why -`check-ps5-object.sh` cannot use the ABI to tell our output from a host Linux -object: the import table is the only reliable signal -(`docs/TROUBLESHOOTING.md`). - -**Boundary.** This host, this SDK unpack, clang 22.1.8. A toolchain bump is a -pin change and gets its own line in `docs/PHASE_LOG.md`. - ---- - -## 2026-09-18: The OpenGL backend is relocatable; the Vulkan backend is not yet consumable - -**Measured.** `../ps5-opengl-sdk-0.2.0` describes a relocatable package under -`build/sdk/ps5-opengl-core33` with `share/ps5-opengl-core33/ps5-opengl-core33.mk`, -`lib/pkgconfig/ps5-opengl-core33.pc` and a CMake config; its published -`libps5_opengl_core33.pc` links `-lPS5OpenGLCore33 -lSceAgc -lSceAgcDriver --lSceVideoOut -lkernel_web -lSceSystemService` and records 344 Core exports -(`docs/consumer-build.md`, `docs/validation.md`). `../PS5_Vulkan` holds driver -sources and static archives (`build/driver/ps5/libps5vk.ps5.a`, 14.8 MB, -2026-09-18) and vendored Vulkan headers at -`third_party/Vulkan-Headers/include`, but no installed consumer package. -Measured by reading those two checkouts; neither was modified. - -**Consequence.** M2 starts by linking against the OpenGL package, because it is -the only backend with a consumer contract and a recorded hardware acceptance -run on this machine. RetroArch resolves its GL entry points through `glsym`, so -the open question is not the header but the export list: M2.1 compares the -driver's request list with the backend's export list and records the difference -before any rendering step is planned (`docs/REFERENCE.md`, "The graphics -backend"). - -**Boundary.** The 0.2.0 package and the PS5_Vulkan checkout as they stand on -2026-09-18. When PS5_Vulkan reaches rung 1.0 and publishes a consumer package, -this entry is superseded by the measurement that switches the default backend. - ---- - -## 2026-09-18: The menu cannot be drawn without a GPU context, so "menu first" is a graphics milestone - -**Measured.** RetroArch 1.22.2 has no software menu path. `gfx/gfx_display.c` -registers display-context drivers for Direct3D, OpenGL, OpenGL1, OpenGL3, -Vulkan, Metal, vita2d, ctr, wiiu, rsx and gdi only, and -`gfx_display_init_first_driver()` selects one by matching `dispctx->ident` -against the video driver's identity, skipping entries whose type is -`GFX_VIDEO_DRIVER_GENERIC`. No entry is generic, and the generic video drivers — -`sdl2_gfx.c` among them — register no display context at all. All four menu -drivers (`rgui`, `xmb`, `ozone`, `materialui`) draw through that context. -Measured by reading `RetroArch-1.22.2` (`gfx/gfx_display.c:50`, -`gfx/gfx_display.c:1215`, `menu/menu_driver.c:333`); XMB itself contains no -GLSL, so it is the display context and not XMB that needs the GPU. - -**Consequence.** A PS5 video driver presenting a CPU-drawn framebuffer is not a -shortcut to a visible menu: it produces no display context and therefore no -menu. The first visible menu requires one of the registered backends — for this -console, the Vulkan path (`gfx/drivers_context/khr_display_ctx.c`) or the -OpenGL3 path — and the platform work is the same driver plus context pair either -way. `docs/REFERENCE.md`, M2.2, now says so instead of treating presentation as -a later step. - -**Boundary.** RetroArch 1.22.2. A future release that adds a software display -context changes this, and the entry is then superseded rather than edited. - ---- - -## 2026-09-18: RetroArch's khr_display context matches what PS5_Vulkan exposes - -**Measured.** RetroArch's `gfx/drivers_context/khr_display_ctx.c` creates its -surface through `vulkan_surface_create(..., VULKAN_WSI_DISPLAY, ...)`, which -`gfx/common/vulkan_common.c` serves by requiring `VK_KHR_display` and calling -`vkCreateDisplayPlaneSurfaceKHR`. `../PS5_Vulkan`'s instance extension table -declares `.KHR_surface` and `.KHR_display`, its device extension table declares -`.KHR_swapchain`, its instance `apiVersion` is `VK_API_VERSION_1_3` and its -device `apiVersion` is `VK_API_VERSION_1_0` with `driverVersion` 0.2.0 -(`driver/ps5vk_instance.c:25`, `driver/ps5vk_physical_device.c:56`, -`driver/ps5vk_private.h:51`), and `driver/ps5vk_wsi.c` presents to VideoOut. -Measured by reading both checkouts. - -**Consequence.** The two halves fit: RetroArch's display-based Vulkan context -driver is the right seam for this console, and the driver's own VideoOut -presentation is what fills it. A barebones frontend therefore does not need a -new windowing layer — it needs the driver's consumer package and its remaining -Vulkan surface. Whether RetroArch's Vulkan renderer asks for more than the -driver implements is the open question, and it is answered by loading the menu, -not by reading headers. - -**Boundary.** The PS5_Vulkan checkout at `5fd2626` (2026-09-18) and RetroArch -1.22.2. Rung 1.0 is the point at which this becomes a support statement rather -than a fit between two interfaces. - ---- - -## 2026-09-18: The existing PS5 RetroArch payload is a build recipe, and its first prerequisite is missing here - -**Measured.** `homebrew/RetroArch/` in `ps5-payload-dev/websrv` at commit -`1afd476` is twelve files and 48 KB: `build.sh`, seven per-core recipes -(`build-fbneo.sh`, `build-fceumm.sh`, `build-genesis_plus_gx.sh`, -`build-mednafen_gba.sh`, `build-puae2021.sh`, `build-snes9x2010.sh`, -`build-vice.sh`), `fetch-assets.sh`, `fetch-databases.sh`, `homebrew.js` and a -`.gitignore`. `build.sh` downloads the upstream RetroArch **1.21.0** tarball, -rewrites `SDL_RENDERER_ACCELERATED` to `SDL_RENDERER_SOFTWARE` in -`gfx/drivers/sdl2_gfx.c`, drops `md5.o` from `Makefile.common`, configures with -`OS=BSD`, `CROSS_COMPILE=$PS5_PAYLOAD_SDK/bin/prospero-` and `LDFLAGS=-rdynamic` -plus `--enable-sdl2 --enable-mmap --enable-dylib` and every GL and Vulkan switch -disabled, then stages `retroarch.elf`, `retroarch.cfg`, `sce_sys/icon0.png` and -three appended config keys. `fetch-assets.sh` pins retroarch-assets 1.20.0 and -`fetch-databases.sh` pins libretro-database 1.21.1. - -The recipe's first requirement does not hold on this host: it enables SDL2, and -SDL2 is not visible to the build. `$PS5_SYSROOT/user/homebrew/` contains only an -empty `include/`, there is no SDL2 header anywhere under the sysroot, and -`prospero-pkg-config --exists sdl2` exits 1. Measured by sparse-cloning that path -(`git clone --filter=blob:none --no-checkout`, `sparse-checkout set -homebrew/RetroArch`), reading the files, and probing the local SDK. - -**Consequence.** The recipe is the right shape to build on — fetch a pinned -tarball, patch it, configure, build, stage — and it already solves two pieces of -the PPSA packaging problem (the `icon0.png` from the upstream tree, and the -config seed). It is not runnable as it stands, and its graphics switches are the -opposite of what this project wants. Adopting it therefore means: supply the -ports the recipe needs or drop the SDL2 switch for ours, replace the graphics -flags with the Vulkan ones, and take the staging and packaging from here. -`reference/ps5-retroarch/PROVENANCE.txt` records the copy and its file digests; -the recipe is kept read-only so a later change to it is a visible step. - -**Boundary.** websrv commit `1afd476` (2026-08-10) and the local SDK unpack of -2026-09-17. If a ports image is installed into the sysroot, the SDL2 half of this -finding stops being true and is superseded rather than edited. - ---- - -## 2026-09-18: Building for this console on this host needs four host-side facts - -**Measured.** Four things had to be true before the vendored recipe would build -here, each established by a failing build and then a passing one: - -1. **The toolchain resolves an absolute include against the host filesystem, not - its sysroot.** `-I/user/homebrew/include/SDL2` fails with `-isysroot $SDK/target` - and fails with `--sysroot=$SDK/target`; it succeeds only when the path exists - on the host. Verified by compiling a one-line file that includes `SDL.h` three - ways. `prospero-clang` adds no sysroot rewriting of its own for `-I`. -2. **The ports prefix must be reached by a host path.** A pkg-config wrapper that - exports `PKG_CONFIG_LIBDIR=/libdata/pkgconfig` with - `PKG_CONFIG_SYSROOT_DIR=` produces a doubled path, because the `.pc` - file already records `prefix=/user/homebrew`. `PKG_CONFIG_SYSROOT_DIR` must be - empty when the `.pc` is read straight from the ports tree. -3. **The SDK's pkg-config and the SDK's compiler look in different places, and - both must be offered the ports.** `prospero-pkg-config` searches only - `$PS5_SYSROOT/user/homebrew/{lib,libdata}/pkgconfig`; the compiler needs the - host path. The build satisfies both. -4. **`/user` cannot be created here without root**, and an unprivileged mount - namespace cannot create it either: `unshare -Urm` gives a private namespace but - `mkdir /user` still fails with EACCES because the root mount is not writable, - and `bwrap --tmpfs /user` fails the same way. Overlay-mounting `/` is refused. - So the console's own prefix cannot be spelled on the host, and the build must - be told the host spelling instead. - -**Consequence.** `tools/build-baseline.sh` carries all four as measured facts -rather than as guesses: a pkg-config of its own for the ports, a rewrite of the -generated `config.mk` include paths, and `-j` with ccache. None of it changes what -the payload links against at runtime — those paths stay the console's own. - -**Boundary.** This host (CachyOS, no `/user`, unprivileged), this SDK unpack, and -the v0.40.2 ports prefix. A host that happens to have `/user/homebrew` populated -would not need points 1, 2 or 4. - ---- - -## 2026-09-18: The recipe's build is 34 s from cache, against about five minutes cold - -**Measured.** With the pinned tarball cached in `.deps/cache/`, the prepared -upstream tree kept in `work/baseline/`, every compile routed through ccache 4.14 -and `MAKEFLAGS=-j14` on this 14-core host, a full build takes **33–35 s**, and an -incremental build after editing a frontend source file takes **35 s**. The first -cold build, which downloaded the tarball, extracted it and compiled with neither -cache, took about five minutes. Three fixes were needed to get there: the recipe -moves the upstream icon out of the source tree, which broke every rebuild until -the extraction guard learned to re-extract when the icon is missing; the log -directory was created before the recipe's files were copied over it; and the -recipe's own log is enormous, because it passes `V=1` to make, so the build -captures it to `work/baseline/logs/build.log` instead of the terminal. - -**Consequence.** The console run is now the slow part of the loop, not the build, -which is what makes the "one step per commit, verified on the target" workflow -practical. `docs/ACTIVE.md` records the numbers so a later regression is visible. - -**Boundary.** Measured on this host with a warm ccache. A `--clean` build clears -the ccache and returns to the cold path. - ---- - -## 2026-09-18: This console's FTP service has three behaviours a deploy must respect - -**Measured.** Against ftpsrv v0.21.1 on the console: - -- it answers a successful `DELE` with **226** rather than 250, which `ftplib` - raises `error_reply` on, so a naive delete looks like a failure after it has - happened; -- it ignores the path argument of a listing command: `MLSD /some/dir` returns the - **root** listing, and only an explicit `MLSD .` after a successful `CWD` lists - the intended directory. That silently defeated a verification check and reported - a correctly uploaded file as missing; -- a nested relative `CWD` can fail with 550 while the same directory opens fine by - absolute path. - -**Consequence.** `tools/deploy.py` navigates by absolute path, verifies every -uploaded file's size on the console before publishing it under its real name, and -treats the 226 delete as success. It also refuses to remove anything but this -project's own remote directory. - -**Boundary.** ftpsrv v0.21.1 on this console. Another service, or a later version, -may answer differently, and the size check is what would catch it. - ---- - -## 2026-09-18: The application-image converter has three requirements, and two are now met - -**Measured.** Getting from a working payload to an `eboot.bin` runs through the -image converter in `../ps5-native-app-boilerplate-main` -(`tooling/native/native_app_builder.cpp`, built as `build/host/ps5-native-tool` -— its host half builds without the `clang-18` its full app build wants). Three -requirements were found by running it, in this order: - -1. **The linked layout must leave room for the process parameters.** The - converter computes where the console's process-parameter record and its - parameter blocks go, and refuses the layout when that space would overlap the - writable data: `error: LLVM layout leaves no room for PS5 process parameters` - (`sce_module_writer.cpp:684`, which needs `relro_end <= data_start`). The - recipe links with the compiler driver and its default PIE layout, which fails - this. Linking instead through `prospero-lld` with the boilerplate's - `ps5-pie.ld`, plus one page-alignment between the RELRO and data segments, - produces a layout the converter accepts — verified twice: on a minimal - program, which converted to 121,536 bytes, and on the real 68 MB RetroArch - payload, which then failed at the next requirement instead of this one. - `linker/ps5-pie.ld` is that script and `tools/prospero-clang-link` is the - shim that applies it. -2. **Every referenced symbol must be defined somewhere.** The converter refuses - to write an image while a referenced symbol has no definition: - `error: no public SDK stub exports required symbol __dlopen`, then - `... kernel_mprotect`. Compiling for this target makes clang take FreeBSD's - libc as its model, so every dlopen() user carries a **weak** reference to - `__dlopen` and its siblings — harmless to the linker, fatal to the converter - — and libretro-common's memory-mapping layer references `kernel_mprotect`. - Enumerating the difference between the payload's undefined symbols and the - definitions in every SDK stub left exactly one such symbol after `__dl*`: - `kernel_mprotect`. `platform/ps5_dl_stubs.c` defines all six, and forwards - `kernel_mprotect` to `mprotect` rather than stubbing it, because a failing - stub is precisely what denies a dynamic recompiler executable memory. -3. **The converter does not yet publish application exports.** It requires every - dynamic symbol to be undefined: `error: native converter does not yet publish - application exports` (`sce_module_writer.cpp:698`). This is the current - blocker. Our payload defines exports — that is what `-rdynamic` is for — and - the converter's own tool is the thing that would have to change, or the - payload would have to be linked with an export list that hides them. This one - is a limitation of the tool, not of the payload. - -**Consequence.** Two of the three are solved in this repository and the third is -named with its exact text. The remaining work is a decision about which side to -change: relax the converter, or publish a narrowed export list from the link. -Either way it is a small, well-defined change rather than an open question. - -**Boundary.** The converter as it stands in `../ps5-native-app-boilerplate-main` -on 2026-09-18, and RetroArch 1.21.0 as the recipe links it. A converter that -gains export publishing removes the third requirement. - ---- - -## 2026-09-18: The application image needs the SDK startup object, and three symbols no stub can supply - -**Measured.** With the layout accepted and the weak `__dl*` references satisfied, -the converter produced `eboot.bin` — and then revealed that the earlier -"missing symbol" errors had a single root cause and a different set of -consequences than they appeared to have. - -The root cause: going straight to the linker skips what the compiler driver adds -by itself. `prospero-clang -###` shows it passing `-l:crt1.o`, `-l:crti.o`, -`-l:crtbegin.o`, `-l:crtend.o`, `-l:crtn.o`. Without `crt1.o` there is no -`_start`, so the image's entry point stayed 0 and the console would have had -nothing to call — and `crt1.o` is also what defines `kernel_mprotect` and the -`__dl*` family, which is why the converter complained about them. Naming those -objects in the linker invocation resolved both: `_start` is now at 0x10 and the -entry point points at it. The `platform/ps5_dl_stubs.c` written before this was -found was redundant and has been deleted; the record of why is kept here. - -Three symbols remain, and they are of a different kind: `__bss_start`, -`__bss_end`, `__image_start` and `__image_end` describe the image's own layout, so -no stub library can export them and the compiler driver never adds them either. -They are defined in `platform/ps5_image_symbols.S` and linked into the image, -which is where they belong. With them the conversion completes: - -```text -wrote 51870448 bytes: build/eboot.elf -wrote 49968821 bytes: build/eboot.bin -container: signed, plaintext; segments: 12 -authority: 0x3100000000000002; program type 0x1; integrity: valid -``` - -**Consequence.** The PPSA folder is complete: `dist/PPSA99005/` holds the signed -application image, the loader module, the title's identity and icon, the -configuration seed, the payload beside them and a digest manifest. Two further -things were needed and are now in the link: `--exclude-libs=ALL`, without which -the linker exports about 185 symbols pulled out of static libraries and the -converter refuses the image for publishing exports. - -**Boundary.** The converter and SDK as they stand on 2026-09-18. The layout -requirement, the startup objects and the layout symbols are all properties of -this image format, not of RetroArch, so any large application built this way -needs the same three. - ---- - -## 2026-09-18: The launcher icon comes from the project's own artwork - -**Measured.** The title's icon is generated, not copied: `title/assets/retroarch.png` -(640x640) resampled to 512x512 by `tools/stage-ppsa.sh` and again by -`tools/build-baseline.sh`, so both the PPSA folder and the payload folder carry -the same image. The 512x512 requirement is the console's, taken from the -boilerplate's asset validator and confirmed against the sibling project's own -title folder, where `icon0.png` is also 512x512. - -**Consequence.** One source image in the repository, two generated copies, and no -hand-edited icon in either output. Before this the icon was the one the vendored -recipe lifts out of the upstream RetroArch tree, which was a dependency of that -recipe rather than a choice; the generated icon also removes the recipe's habit -of *moving* that file out of its tree, which had broken incremental rebuilds -until the extraction guard learned to repair it. - -**Boundary.** The artwork is the project's own file. A different source image -needs only to be square; the scripts stretch to 512x512 rather than padding, so a -non-square source would be distorted and should be rejected by whoever replaces it. - ---- - -## 2026-09-18: The installed title crashed because its eboot.bin was not a converted image - -**Measured.** With the title installed on the console, `launch PPSA99005` through -the resident control payload produced `EndAppMount(0x00000018)` and no running -process, and the kernel log captured on port 3232 recorded a fatal signal inside -the application: - -```text -[SceLncService] EndAppMount(0x00000018) -[kstuff.elf] Title Mounted Successfully: /data/homebrew/PPSA99005 -> /system_ex/app/PPSA99005 -# A user thread receives a fatal signal -# signal: 11 (SIGSEGV) -# thread name: eboot.bin -# reason: page fault (user read instruction, page not present) -# fault address: 0000000000000001 -/app0/sce_module/libc.prx -[Syscore App] App Crash : reason=0xb -``` - -The installed `eboot.bin` was 51,870,448 bytes — the size of a **link-stage ELF**, -and larger than the converted and signed image this repository now produces -(49,968,821 bytes). The loader therefore started a file that is not in the -application-image format `eboot.bin` has to be, and faulted before `main()` ran. -The thread name and the process name both read `eboot.bin` because that is what -the loader had just started. - -**Consequence.** `tools/install-title.sh` exists to prevent exactly this: it -installs only `dist//eboot.bin`, verifies the stored bytes against the -local image by size and SHA-256, and keeps the previous image as -`eboot.bin.previous` so the change is reversible. The crash is recorded as -evidence in `evidence/ppsa-99005-startup-crash/` so the repaired run has -something to be compared against. - -**Boundary.** This is a property of the console's loader, not of RetroArch: any -payload placed at `eboot.bin` without being converted will fault the same way. -The three converter requirements in the previous finding are what stands between -a link output and a runnable title. - ---- - -## 2026-09-18: This console's FTP service can lose a directory during a replace - -**Measured.** Replacing `eboot.bin` in `/data/homebrew/PPSA99005/` was performed -as an upload to a temporary name followed by delete-and-rename, and reported -success at every step (`226 File deleted`, `226 Path renamed`). Immediately -afterwards the whole directory was gone: `LIST /data/homebrew/PPSA99005/` is -empty, the folder no longer appears in `LIST /data/homebrew/`, and a later `CWD` -into it fails with `550 No such file or directory`. The neighbouring homebrew -folders (`PS5_RetroArch`, `RetroArch`, `PPSA99988`) were unaffected, and the -control payload stayed healthy throughout. - -**Consequence.** The folder has to be treated as reconstructable rather than -durable. Everything it held is reproducible: `dist/PPSA99005/` holds the -converted image, the configuration, the payload, the module, the identity and the -icon with a digest per file, and the same payload also still exists on the -console under `/data/homebrew/PS5_RetroArch/`. Any install path must therefore -recreate the folder rather than patch a file inside it, and must verify what it -wrote. - -**Boundary.** ftpsrv v0.21.1 on this console, which also answers deletes with 226 -and ignores the path argument of a listing command. Rewriting a file in place is -not a safe operation on this service; writing a whole directory and checking it -is. - ---- - -## 2026-09-18: The title's eboot.bin and libc.prx could not be replaced over FTP - -**Measured.** Publishing the converted folder through the deployment path that -works on this console — the helpers from `../PS5_Vulkan/tools/deploy.sh`, which -handle this server's 226-on-delete and root-relative paths — the image and the -loader module never changed: - -```text -==> [deploy] 7 files to /data/homebrew/PPSA99005/ -sce_module/libc.prx: the console did not store the whole file -``` - -Five consecutive attempts, each an upload to a temporary name followed by a -delete and a rename, all reported success with `226 File deleted` and -`226 Path renamed`, and every read-back returned the previous file: -`libc.prx` 1,335,962 bytes where the local file is 1,284,674, and `eboot.bin` -51,870,448 bytes where the local file is 49,968,821 — the latter still starting -`7f454c46` (ELF) rather than `4f153d1d` (the converted image). - -The server itself is not the limit, which was established separately in the same -folder: uploading freshly generated blobs and hashing them back round-trips -exactly at 2 MB, 8 MB and **60 MB**. A neutral file name made no difference — -`neutral.img` also read back as the 51,870,448-byte image. Files whose names the -folder does not already contain do persist (a 40 KB marker written earlier was -still listed and read back correctly). - -**Consequence.** The remaining difference between this folder and a working -title is exactly two files, and they cannot be replaced from here while they keep -reverting. `tools/deploy-title.py` and `tools/ps5_ftp.py` do the job correctly — -they verify every stored size and refuse to call it done when one does not match, -which is why this was detected rather than believed — so the tooling is ready for -whoever can write those two paths. The likely cause is outside this repository: -the other session working on this console recreates the title's image and module, -and its writes win. - -**Boundary.** This console, ftpsrv v0.21.1, while a second session is publishing -the same title. A console with one writer does not show this. - ---- - -## 2026-09-18: A title's eboot.bin must be a converted image, and here is how to tell in one byte - -**Measured.** The sibling project that owns this console's working title converts -its image before publishing, and the result is recognisable at a glance: - -```text -PS5_Vulkan/dist/PPSA99988/eboot.bin 16,979,653 bytes starts 4f 15 3d 1d -PS5_RetroArch/dist/PPSA99005/eboot.bin 49,968,821 bytes starts 4f 15 3d 1d -console's /data/homebrew/PPSA99005/eboot.bin - 51,870,448 bytes starts 7f 45 4c 46 -``` - -`4f153d1d` is the development-container magic; `7f454c46` is a plain ELF. The -console's copy is the intermediate link output, not the converted application -image — and that is the whole of the crash documented earlier: the loader starts -it, the first call goes through a pointer that was never populated, and the -process dies at `rip=0x1` before `main()`. The same project's build script shows -the two steps that produce the right file: -`"$tool" link --in llvm-pie.elf --out eboot.elf` then -`"$tool" self --sign --in eboot.elf --out eboot.bin`. - -**Consequence.** Any image placed at a title's `eboot.bin` must have gone through -both steps, and `tools/deploy-title.py --check` now reports the magic bytes so a -wrong image is caught before a launch rather than inferred from a crash. This also -means the difference between the working title and this one is exactly one -pipeline step, not a design problem. - -**Boundary.** This console's loader. A payload started by a launcher instead of -the loader does not need the container, which is why the same frontend runs as -`retroarch.elf` and crashes as `eboot.bin`. - ---- - -## 2026-09-18: The title folder was writable, and the two loader files were still reverted - -**Measured.** `/data/homebrew/PPSA99005` and `/system_ex/app/PPSA99005` are the -same directory, it accepts writes, and a file written into it stays: a 40 KB -marker, a 2 MB, an 8 MB and a 60 MB blob all round-tripped byte-for-byte, and the -512x512 `sce_sys/icon0.png` published by our deploy matched exactly. The same -deploy nevertheless could not change `eboot.bin` or `sce_module/libc.prx`: - -- five attempts as upload-to-temporary, delete, rename: every read-back was the - previous file; -- writing `eboot.bin` in place with a plain `STOR`, no delete and no rename: the - same; -- the same converted bytes under an unused name (`zz-image.bin`): the same; -- and the icon, written by the same helpers in the same run, took. - -So the refusal is not about the directory, the name, the transfer size or the -method. Something outside this repository restores those two files — most -plausibly the second session working on this console, publishing the title's own -image and module. - -**Consequence.** The deployment path is ready and verified; what it cannot do is -win a race against another writer. The check to run when that writer stops is -`tools/deploy-title.py --check`, and the line to look for is -`eboot.bin magic: 4f153d1d`. - -**Boundary.** This console, while a second session publishes PPSA99005. A console -with one writer does not show this. - ---- - -## 2026-09-18: Correcting the record: the second session is not the cause, and the folder is writable - -**Measured.** Two earlier conclusions in this file were wrong and are corrected -here rather than edited away. - -1. **The second session does not publish this title.** It publishes `PPSA99988`. - The suspicion recorded in the previous entry was wrong, and the evidence that - looked like support for it — a raw link ELF appearing where a converted image - should be — is a property of the file that was placed, not of who placed it. - -2. **`/data/homebrew/PPSA99005` and `/system_ex/app/PPSA99005` are not the same - storage.** A 1 KB marker written through the `/system_ex` path is listed there - and **not** listed under `/data/homebrew`. They are separate trees that happen - to hold copies of the same title. - -The folder accepts writes and keeps them: a 40 KB marker, 2 MB, 8 MB and 60 MB -blobs, a 49,968,821-byte zero blob and a pattern blob of exactly the size of the -converted image, and the 512x512 icon, all round-tripped byte-for-byte under -`/data/homebrew/PPSA99005`. What does not take is the converted image itself: -written under its own name, under an unused name, in place, and through a -temporary name with a rename, the listing and the read-back both return -51,870,448 bytes beginning `7f454c46`. A fresh file name receives the same bytes -as `eboot.bin`, which no ordering explanation covers. - -The service also degrades under this load: reads began timing out and one -connection ended with `550 Broken pipe`. - -**Consequence.** The deployment path in this repository is correct and verified, -and the remaining difference between this title and a working one is two files -that must be placed by another route — the console owner's own tooling, USB, or -`kstuff`, rather than this FTP service. `tools/deploy-title.py --check` names the -one-line test: `eboot.bin magic: 4f153d1d`. - -**Boundary.** ftpsrv v0.21.1 on this console, under repeated large writes to one -title folder. The round-trip evidence above is what bounds the claim: this is not -a general statement that the service cannot store a file. - ---- - -## 2026-09-18: Both loader inputs on the console are the wrong artefacts, and encryption is not the difference - -**Measured.** Ours and the working sibling title's application images are the -same container, checked with the converter's own inspector rather than by eye: - -| | `dist/PPSA99005/eboot.bin` | `PS5_Vulkan/dist/PPSA99988/eboot.bin` | -| --- | --- | --- | -| magic | `4f153d1d` | `4f153d1d` | -| container | signed, plaintext | signed, plaintext | -| segments | 12 | 12 | -| authority | `0x3100000000000002` | `0x3100000000000002` | -| program type | `0x0000000000000001` | `0x0000000000000001` | -| integrity | valid | valid | -| size | 49,968,821 | 16,979,781 | - -Neither is retail-encrypted; both are development containers. So the console does -not need an encrypted image, and encryption cannot be why one title starts and the -other does not. - -The loader module is where the two titles genuinely differ. Ours and the sibling's -are the **same file** — 1,284,674 bytes, sha256 `e6ff45d16adf6878` — but the -console holds a different one for this title: 1,335,962 bytes, sha256 -`7e82ce9a4259d0db`, which is the module's *raw* ELF rather than its signed -container (the two sizes differ by 51,288 bytes, the container's own overhead). -That is the module named in the crash capture. - -**Consequence.** The title is two file replacements away from a fair test, and both -replacements are of the same kind: an unsigned intermediate where a converted -artefact belongs. The correct module already exists twice on this machine, in -`dist/PPSA99005/sce_module/` and in `../PS5_Vulkan/runtime/`, and they are -byte-identical. - -**Boundary.** This console and these two titles. The claim is about what the -loader is handed, not about the signing chain a retail title would carry. - ---- - -## 2026-09-18: The two start routes take incompatible artefacts, so the launcher cannot test this image - -**Measured.** The console starts code two ways, and the artefacts they need are -mutually exclusive: - -| Route | What it starts | What the file must be | -| --- | --- | --- | -| homebrew launcher | an ELF, with its symbols resolved for it | a plain ELF that imports the kernel stubs and carries `_start` and `main` in the dynamic table | -| application loader | a title's `eboot.bin` | a converted development container, magic `4f153d1d` | - -Our converted image has **zero** dynamic symbols. That is not an accident of the -build: the converter's own rule is that it does not publish application exports, -and `--exclude-libs=ALL` keeps the rest internal. So the converted image cannot be -started by the launcher, which reads exactly the table that conversion removes. - -This closes the idea of testing the converted image through the working launcher -path, which was the intent behind `tools/check-payload.sh` — and the guard now -answers the question in one command instead: our baseline payload reports -`verdict LAUNCHER route` with `_start`, `main` and `libkernel_web.sprx` present, -while the converted image reports that only the loader route takes it. Its first -version got the import check wrong by looking for undefined symbols where this -target names stub libraries in `DT_NEEDED`; that was caught by running it against -the payload that had already started successfully on the console. - -**Consequence.** There is no substitute test for the title path: the converted -image has exactly one way to run, and it is the one blocked by two files that -cannot be replaced over FTP. The uncertainty that remains is therefore narrow and -named — whether the converted image and the signed module together start — and it -is resolved by placing those two files, not by another route. - -**Boundary.** This console's launcher and loader. A future conversion that keeps a -dynamic table would make the launcher route viable for the same bytes. - ---- - -## 2026-09-18: The console accepts a 49,968,821-byte file at that exact path, and replaces our image - -**Measured.** A 49,968,821-byte blob — a repeating byte pattern, so its content is -unmistakable — was written to three places and each time the listing showed -49,968,821 bytes: - -```text -/data/homebrew/PS5_RetroArch/eboot.bin sent 49,968,821 listed 49,968,821 OK -/data/homebrew/PPSA99005/roundtrip-other.bin sent 49,968,821 listed 49,968,821 OK -/data/homebrew/PPSA99005/eboot.bin sent 49,968,821 listed 49,968,821 OK -``` - -So the console stores that size at that path, under that name, without complaint. -The same path and name also accepted a 40 KB marker, 2 MB, 8 MB and 60 MB blobs, -and the 512x512 icon. - -What it does **not** keep is our converted image: written under its own name, -under an unused name, in place, and through a temporary name with a rename, the -result is always 51,870,448 bytes — and the same is true of the loader module, -which always comes back as 1,335,962 rather than 1,284,674. Those two numbers are -the sizes of the *unconverted intermediates*. - -**Consequence.** The behaviour is content-aware, not path-aware, size-aware or -name-aware, and that is what makes it worth recording precisely: every simpler -explanation has been tested and excluded. Something on this console restores -those two files to a different build of the same pieces. The deployment path in -this repository is not the variable — it verifies and reports honestly, and it is -what produced this measurement. - -**Boundary.** This console, both writers (FTP and the console owner's -USB/filebrowser route) and this title. Nothing here says a different title or a -console without the mount daemons' automount would behave the same way. - ---- - -## 2026-09-18: A 67 KB title image comes back as 122,024 bytes, and my view of the console is not trustworthy - -**Measured.** A minimal converted title was built end to end to test the pipeline -in isolation rather than through a 50 MB emulator: - -```text -build/probe/hello.elf 127,456 bytes linked with linker/ps5-pie.ld -build/probe/hello.ps5 122,024 bytes converted (link --in hello.elf) -build/probe/eboot.bin 67,526 bytes signed (self --sign) -``` - -The signed image validates — `container: signed, plaintext`, twelve segments, -`integrity: valid` — and `tools/check-payload.sh` correctly refuses it for the -launcher route. - -Written to `/data/homebrew/PPSA99005/eboot.bin`, the console listed **122,024** -bytes on five consecutive fresh connections. That is not the size sent (67,526) -and not the previous contents (the folder had no `eboot.bin` at all at that -moment — a backup rename failed with `550 No such file or directory`, and the -listing agreed). 122,024 is the size of the *intermediate* file, one step before -signing, from a file that was never uploaded. - -**Consequence.** Something between the upload and the listing substitutes content -this repository does not control, and it substituted a file that was never sent. -That makes every size this session has read back from the console suspect, -including the ones that "matched": the 40 KB marker, the 2/8/60 MB blobs, the icon -and the same-size pattern blobs may equally have been served from somewhere else. - -The honest position is therefore narrower than the previous entries imply: the -conversion pipeline is correct and reproducible, and what cannot be trusted yet is -the observation channel. The console owner's own file browser reads the folder -through a different path than this FTP session does, and comparing the two on one -file is the cheapest way to find out which of them is lying. - -**Boundary.** This console, this FTP session (anonymous over port 2121) and this -title folder. The measurements above do not say the console is broken; they say -that one view of it is unreliable, and that view is the one this session has been -reasoning from. - ---- - -## 2026-09-18: A process rule this session broke, and the cost of breaking it - -**What happened.** Testing whether a small converted image would survive on the -console, a 67,526-byte probe was written straight to -`/data/homebrew/PPSA99005/eboot.bin` — over the image the console owner had put -there. The backup step immediately before it failed (`550 No such file or -directory`), and the test proceeded anyway. It then launched the title and -reported the resulting `PRX_SCE_MODULE_LOAD_ERROR` as a finding about the module, -when the folder had in fact been left holding a test binary. The owner spotted it; -the diagnosis had been built on a state this session created. - -**The rule that was broken, stated so it is not broken again:** - -- A destructive step is never preceded by a test whose result decides whether to - continue. If the backup fails, the write does not happen. "The folder looked - empty anyway" is not a substitute for a verified backup: this session's view of - the console had already been shown to be stale, which is exactly when a failed - backup must stop the work rather than reassure it. -- A shared console's title folder is the owner's, not the build's. A probe - belongs in a title of its own, never in a working one. -- When a launch is about to be reported as evidence, confirm what is actually in - the folder first. Otherwise the finding describes the state of the experiment, - not of the project. - -**Recovery.** The image and module are reproducible here in one command -(`tools/stage-ppsa.sh`), and `restore/PPSA99005/` holds the complete folder ready -to copy back: the converted image at 49,968,821 bytes and the signed module at -1,284,674 bytes, with the identity, icon, configuration and payload beside them. - -**Boundary.** This is a process record, not a measurement about the console. It is -kept in the findings file because the cost was a wasted console run and a wrong -diagnosis, and because the next session needs the rule more than it needs the -apology. - ---- - -## 2026-09-18: This session's writes go to a copy of the title folder that nothing reads - -**Measured.** The console owner replaced both loader files and their file browser -shows the intended sizes: - -```text -filebrowser: /data/homebrew/PPSA99005/eboot.bin 47.65 MiB = 49,968,821 bytes (correct) -filebrowser: /data/homebrew/PPSA99005/sce_module/libc.prx 1.23 MB = 1,284,674 (correct) -``` - -This session's FTP view of the same paths simultaneously reports 51,870,448 and -1,335,962 — the *unconverted* sizes — and writing the converted image to either -`/data/homebrew/PPSA99005/eboot.bin` or `/system_ex/app/PPSA99005/eboot.bin` six -times changes neither view. The two views also differ structurally: `sce_module/` -and `retroarch.elf` are visible in one and absent from the other, in both -directions. - -**Consequence — and a correction to this file's earlier entries.** Every -"round-trip" recorded here (the 40 KB marker, the 2/8/60 MB blobs, the -same-size zero and pattern blobs, the icon) was read back through the same -channel that wrote it. A channel that resolves to a private copy returns what was -just written to that copy, so those results show only that the copy is writable -and self-consistent. They do **not** show that the console's real title folder -accepted anything, and the conclusions built on them — that the store was -content-aware, that a second writer was restoring files, that the deployment path -was proven — do not hold. - -What does hold, because it needs no trust in the channel: the console's own log -says `Lack of a .prx file in /app0/sce_module is detected!!!`, and the folder the -loader reads has no `sce_module/` directory at all. - -**Boundary.** This console, this FTP session (anonymous, port 2121). The rule it -generalises to: a write path whose read path is the same path proves nothing about -a system that has more than one view of the data. - ---- - -## 2026-09-18: The loader's folder has no sce_module, and that is the current error - -**Measured.** The console's log for the last launch is unambiguous: - -```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 directory as the application sees it, and the folder being -served there contains `eboot.bin` and nothing else the application needs — no -`sce_module/` directory. A PS5 title that uses the standard runtime must carry -`sce_module/libc.prx` inside its own folder; without it the loader fails before -the application's first instruction. - -**Consequence.** The two remaining requirements are both placement, not build: - -1. `sce_module/libc.prx` — 1,284,674 bytes, sha256 - `e6ff45d16adf687855cc3b33b0c8a4132b6504360b221e0a34c7e99fb3ba0036` — inside - the title folder the loader reads; -2. `eboot.bin` — 49,968,821 bytes, starting `4f153d1d` — in the same folder. - -Both exist in `dist/PPSA99005/` and are byte-identical to the sibling project's -copies. Neither can be placed from this session, whose writes do not reach that -folder. - -**Boundary.** This title and layout. A title that does not use the standard -runtime module would not need `sce_module/`, which is why the requirement is -stated as a property of the layout rather than of the console. - ---- - -## 2026-09-18: The title gets as far as Exec and then fails to load its runtime module - -**Measured.** With the title folder as the console currently holds it, the launch -reaches the application and fails at module load: - -```text -<194> EXEC /app0/eboot.bin [user], vm#1, dmem#1 abi=native category=native_game -# exception: 0xa0020102 (PRX_SCE_MODULE_LOAD_ERROR) -# === Lack of a .prx file in /app0/sce_module is detected!!! -[Syscore App] App Crash : PID=0xc2, reason=0xa0020102 -``` - -The control payload reported the title running (`app=16408 title=PPSA99005 -count=1 pids=194`) while the log recorded the crash — so the launch itself works -and the failure is inside the application's startup. - -Comparing the two title folders side by side, shallowly: - -```text -PPSA99988 (runs) eboot.bin 17,264,224 sce_module/libc.prx 1,335,962 -PPSA99005 (fails) eboot.bin 51,870,448 sce_module/libc.prx 1,335,962 -``` - -The module is the **same size in both**, so this project's deployed module is not -the anomaly the earlier entry assumed. The difference is the image: ours is -51,870,448 bytes starting `7f454c46` — a raw link output — where the working title -carries a converted image. - -**Consequence.** Two things must be true in the folder the loader reads, and only -one is: the module is right, the image is not. `dist/PPSA99005/` holds the -converted image (49,968,821 bytes, `4f153d1d`) and verifies against its own -manifest, so the remaining action is a copy of that one file, by a route that -reaches the folder the loader sees. - -**Boundary.** This console and this title. The error text names the module, but -the state it describes — "lack of a .prx file" — is not literally true here, so it -is being treated as a symptom of the image rather than as a missing file. - ---- - -## 2026-09-18: The native pipeline builds with Clang 22, and the console mounts the result - -**Measured.** The blocker recorded earlier — "the project needs Clang 18" — was -this session's own mistake, and the user identified it: `../PS5_Vulkan` builds -fine with the Clang 22 already installed. The difference is one word in the two -projects' compiler wrappers: - -```text -PS5_Vulkan/tooling/prospero-clang18 compiler=$(command -v clang || command -v clang-18 || true) -ps5-native-app-boilerplate-main/... compiler=$(command -v clang-18 || true) -``` - -The first prefers `clang`, the second refuses anything but `clang-18`. The -`isnan` clash that appeared when the boilerplate was built here came from passing -the *wrong SDK* alongside it: with its own cached SDK -(`.deps/native/ps5-payload-sdk`) and `PS5_CLANG=/usr/bin/clang`, its build -completes — `container: signed, plaintext`, twelve segments, `integrity: valid`. -The toolchain on this machine is sufficient; no new package is needed. - -**Consequence.** `../ps5-native-app-boilerplate-main` can build a real title here, -which makes it the foundation for the route change rather than a blocked option. -A test title was built with this project's identity — `PPSA99169`, -`UP9000-PPSA99169_00-RETROARCH0000001`, "PS5 RetroArch" — and staged in -`handoff/PPSA99169/`: `eboot.bin` 18,791 bytes starting `4f153d1d` and a -`sce_module/libc.prx` of 1,284,674 bytes, beside the metadata and assets. - -**And the console accepts it.** The kernel log records the mount: - -```text -[kstuff.elf] Title Mounted Successfully: /data/homebrew/PPSA99169 -> /system_ex/app/PPSA99169 -[kstuff.elf] Successfully mounted title PPSA99169 -> /data/homebrew/PPSA99169 -``` - -**Boundary.** What is proven is the build and the mount. What is not proven is -whether the title *runs*: the capture taken for this test also contains 943 lines -from the other session's Vulkan activity, and every title's process is named -`eboot.bin`, so a crash in that log cannot be attributed to this title. The -outcome has to be read from a capture taken while no other session is running. - ---- - -## 2026-09-18: Reads from this console's FTP service are not usable as verification - -**Measured.** After deploying PPSA99169 — a title id that had never existed — a -read-back of `/data/homebrew/PPSA99169/eboot.bin` returned 73,648 bytes starting -`7f454c46`, which is another title's file: the same size and same bytes that -`/data/homebrew/PPSA99006/eboot.bin` returned, and the same again for a path that -had just been created. The local file is 18,791 bytes starting `4f153d1d`. - -**Consequence.** Every conclusion this session drew from an FTP read-back is -unsafe, including the ones that looked like confirmations and the ones that looked -like refusals. The deploy tooling verifies what it *sent*; it cannot verify what -the console *holds*. Console state must be read from the console's own log, or -from the owner's file browser, and the two are the only trustworthy witnesses -available. - -**Boundary.** ftpsrv v0.21.1 on this console. This does not say the console is -faulty; it says that one of its two file views reports another title's bytes for a -path that did not exist a moment earlier. - ---- - -## 2026-09-18: The title is unregistered and its image is not the one we built - -**Measured.** The launch of PPSA99169 was captured with the console's own log -delimited from the moment of the launch, while no other title was running. The -console states both problems itself: - -```text -ClearSlotInfoCache() [PPSA99169] is not registered -[sceProcessStarter] ProcessTerm() big/mini app title_id = [PPSA99169] -[SceShellUI] E/Base.BgmController : invalid path /user/app/PPSA99169/sce_sys/snd0.at9 -``` - -and the folder contents do not match what was deployed: - -```text -/data/homebrew/PPSA99169/eboot.bin 73,648 bytes (ours is 18,791) -/data/homebrew/PPSA99988/eboot.bin 17,264,368 <- the title that runs -``` - -The registration *layout* is correct, though — compared folder by folder with the -title that runs, ours has every piece in place: - -```text -/user/app/PPSA99169/ icon0.png, mount.lnk, sce_sys/ -/user/appmeta/PPSA99169/ icon0.png, param.json, pic0.dds, pic1.dds, snd0.at9 -/data/homebrew/PPSA99169/ eboot.bin, assets/, sce_module/, sce_sys/ -/user/app/PPSA99988/ icon0.png, mount.lnk, sce_sys/ (identical shape) -/user/appmeta/PPSA99988/ icon0.png, param.json, pic0.dds, ... (identical shape) -``` - -**Consequence.** Two independent things remain, and both are outside this -session's reach: - -1. **The image in the folder is not the built one.** Our `eboot.bin` is 18,791 - bytes; the console's is 73,648. Every write path available here reports - success and leaves the old bytes, so the 18,791-byte image — staged in - `handoff/PPSA99169/` — has to be placed by the console's owner. -2. **The title is not registered with the shell.** `is not registered` and the - missing `/user/app/PPSA99169/sce_sys/snd0.at9` say the title is absent from the - shell's app database. The folders exist, so the remaining step is whatever the - console's own installer does to register them — the console owner's route. - -**Boundary.** This console and these two titles. A console where the owner has run -its installer for the title does not show the registration half. - ---- - -## 2026-09-18: A launch is refused while another title is running - -**Measured.** With PPSA99988 running (`procs` reported `title=PPSA99988 count=1 -pids=214`), `launch PPSA99169` returned `0x80940010` and nothing started: the log -shows no execution and no crash for the new title, and `procs` was empty -afterwards. - -**Consequence.** This console runs one title at a time, so a launch attempted -while another title is up proves nothing about the title being launched, and a -crash read from an undelimited log cannot be attributed. `tools/console-run.sh` -now checks the console first, refuses to launch into a busy console, records the -listener's position before launching, and judges only the lines after that mark. - -**Boundary.** This console's launcher behaviour. `--force` exists for the case -where demonstrating the refusal is the point. - ---- - -## 2026-09-18: VideoOut's constants and tiling are not derivable, and guessing costs a console run - -**Measured.** Five things in the display path must be exactly what the console -expects, and the first version of `src/display.cpp` got all five wrong. The title -built, deployed and launched cleanly, then reported -`eboot.bin calls exit() exit_value=1` — the display had refused to open: - -```text -symptom eboot.bin calls exit() exit_value=1 -``` - -| What | Wrong | Right | -| --- | --- | --- | -| pixel format | `0x80000000 \| 0x0a` | `0x8000000022000000` (64-bit) | -| direct-memory size | `int64_t` return | `size_t` return | -| mapping protection | `0x3` | `0x33` | -| buffer structure | three fields | four pointers: data, metadata, two reserved | -| frame addressing | row-major | tiled: 128x128 blocks in a fixed order, XOR'd block-local offsets | - -The tiled layout is the one that would have produced a wrong-but-plausible result -rather than an error: a row-major write into a tiled frame does not fail, it -scrambles. `tiled_offset()` in `src/display.cpp` is the sibling application's, -kept verbatim with its reason. - -**Consequence.** Everything in the display path is now taken from -`../ps5-native-app-boilerplate-main/src/demo_renderer.cpp`, which had already -proved each constant on hardware, instead of being reasoned out here. The -verification that matters is the console's own log: zero fatal signals, the -process visible in the shell's accounting, and the control payload reporting it -running. - -**Boundary.** The console's VideoOut ABI as this project's SDK declares it. -Anything that changes one of these five values is a console-side change, not a -build option. - -## A null joypad driver is this console's normal state, and upstream dereferences it - -**Measured.** With every joypad driver upstream ships unavailable - udev, linuxraw, -SDL, XInput, dinput all need a library or a header this SDK does not carry - -`input_st->primary_joypad` is NULL. `input_driver_collect_system_input` passes it -straight to `input_joypad_analog_axis`, which reads `drv->axis` without checking -`drv`: the function guards every axis member against `AXIS_NONE` and never guards -the struct. - -**What it looked like.** Nothing like a null pointer. The title started, the -display opened, the first frame was presented, and then it died on the *second* -pass through the runloop with SIGSEGV and fault address 0x18. The reason it -survived the first pass is that the call is inside `if (menu_is_alive)`, and -`MENU_ST_FLAG_ALIVE` is set after the first frame. Finding it took a probe in the -caller, then one at the function entry, then reading the loop: the fault address -0x18 is `joypad_info.joy_idx` (0x10) plus the `auto_binds` member (0x18). - -**Fix.** `patches/series` 0004 returns 0 when `drv` is NULL, which is what the -function already answers when there is no axis to read. - -## The menu is alive but its framebuffer is never marked dirty - -**Measured.** `/app0/trace.txt` from a 25-second run: `rgui_fonts_init` completes, -RGUI holds a 320x240 framebuffer, and `rgui_set_texture` is called every frame - -but `GFX_DISP_FLAG_FB_DIRTY` is 0 on every one of those calls, so it returns -before handing anything to the driver. The driver therefore reports -`no-menu-source 4x4` for every frame: nothing is ever drawn, which is what "the -screen stayed black" is. - -**Where the flag should come from.** `GFX_DISP_FLAG_FB_DIRTY` is set at the end of -`rgui_render`, and `rgui_render` is only reached through -`menu->driver_ctx->render` inside `if (BIT64_GET(menu->state, MENU_STATE_BLIT))`. -So the menu's renderer is never running. That is the next thing to measure: which -of the conditions above the call is false. - -**Also measured, and separately true.** RGUI's bitmap fonts are downloaded assets, -not built-ins: `bitmapfont_10x10_load` returns NULL when -`/rgui/font/bitmap10x10_eng.bin` is missing, `rgui_fonts_init` then fails, -and `rgui_init` jumps to its error label with the menu dead. They are now bundled -under `assets/rgui/font/` and shipped in the title folder. - -## Nothing this title submits has ever reached the display - -**Measured.** The driver presents every frame and `sceVideoOutSubmitFlip` returns -success, but the screen shows nothing - not the menu, not a black frame, not a -painted test pattern. A run that paints three full-screen colour bands and -presents them 1800 times changes nothing on the television. The owner's own words: -"Nothing shows on the screen." - -**The frame the driver writes is never flushed to the GPU.** The framebuffer is a -write-combined mapping of direct memory (`memory_type_write_combined_garlic`), and -the GPU does not see the CPU's dirty cache lines. `src/display.cpp` wrote pixels -and flipped without any cache flush, so the display read whatever was in that -memory before. The sibling project that works on this console flushes after every -write to the same kind of mapping - `_mm_clflush` over 64-byte lines then -`_mm_mfence()`, in `../PS5_Vulkan/driver/ps5vk_direct_memory.c`. That flush is now -in `src/display.cpp`. - -**Where the sibling project actually differs, for the record.** `../PS5_Vulkan` -never calls `sceVideoOutSubmitFlip` at all: it uses `VK_KHR_display` and lets the -Vulkan driver own presentation, so its swapchain is fed by the GPU rather than by -CPU writes. What it does share with this port, and what is therefore proven on -this console, is the setup: `sceVideoOutOpen(0xff, 0, 0, NULL)`, -`sceVideoOutSetFlipRate(handle, 0)`, an 80-byte attribute zeroed then filled by -`sceVideoOutSetBufferAttribute2`, and `sceVideoOutRegisterBuffers2` with 2 buffers -of the same descriptor shape this port uses. - -**One error is now named.** Asking for flip mode 0 returns `0x80290006` -immediately on the first flip, so mode 0 is not what this display wants; the flip -is back to `(1, 1)`, which returns success. Whether `(1, 1)` actually presents is -exactly what the cache flush will now decide. - -**Also settled: why the menu has no pixels even though it is alive.** -`rgui_render` is called every frame with `width = 0, height = 0`, and returns at -its own guard. Those come from `video_st->width`/`video_st->height`, which nothing -sets because no core is loaded and the dummy core's AV info is empty. So there are -two separate faults, and the cache flush is the one that decides whether any pixel -this title writes becomes visible. - -## Vulkan is prepared and switched off, and switching it back on is written down - -**What was done.** RetroArch's Vulkan video driver is the retirement plan for the -hand-written path in `src/display.cpp`: it drives the menu itself -(`menu_driver_frame`), it uploads RGUI's framebuffer as a texture -(`vulkan_set_texture_frame`), and it loads the GPU side by filename - -`dylib_load("libvulkan.so.1")`, then `"libvulkan.so"` - so ../PS5_Vulkan's -libps5vk drops in beside the title. RGUI needs no GPU context of its own: its -render path references `gfx_display` eight times and every one is a type, not a -call. - -**It builds.** With `--enable-vulkan` the frontend configures, compiles 268 of 278 -sources and links, and the title grows from 8,030,159 to 8,227,775 bytes. Three -things had to be added, and all three are kept: - -- `--enable-builtinglslang`: configure refuses to build the Vulkan driver without - a GLSL-to-SPIR-V compiler, and RetroArch vendors glslang in `deps/glslang`. -- `src/video_filters_stub.cpp`: a Vulkan build names the filter chain's twenty - entry points and one preset parser, and their implementation is not in - upstream's tarball. They are stubs because this port has video filters off and - the menu needs no shader; every create returns NULL, which is the driver's own - "no chain" case. -- The glslang, SPIRV-Cross and `gfx/include` include paths, in - `tools/build-retroarch.sh`. - -**A tooling bug fell out of it, and it was a real one.** -`tools/retroarch-sources.sh` rewrote every object in `make info`'s list to `.c`, -but RetroArch's list is not all C: `gfx/drivers_shader/slang_process.cpp` is C++, -and the rewrite turned it into a path that does not exist. The frontend then -linked with `slang_preprocess_parse_parameters` undefined - a missing C++ source -reported as a missing symbol, which is a much longer walk back to the cause than a -path that says `.cpp`. The list now keeps each source's own extension (47 C++ -sources in the full configuration) and the compile loop picks `-std=c++20` and -`-fno-exceptions -fno-rtti` for them. - -**Why it is off.** With Vulkan enabled the title exits **1** within a second of -EXEC, with no signal and no message from RetroArch - and it does that whatever -`video_driver` the config names, including `"ps5"`, so a Vulkan build cannot fall -back to another driver. There is no ICD yet (`libvulkan.so.1` is not beside the -title), and that is the most probable cause, but it is not proven: the failure is -silent, and the next step is to make RetroArch say why - its own logs go to a file -in the title folder once `log_verbosity` is on. - -**What switching it on costs, when the ICD exists.** One line in -`tools/retroarch-sources.sh` (`--enable-vulkan`) and one in the title's -`retroarch.cfg` (`video_driver = "vulkan"`). Everything else is already there. - -## The shell's splash screen was covering every frame this title presented - -**Measured.** The display's own flip status says so. `sceVideoOutGetFlipStatus` -fills sixteen 64-bit words and word 3 carries the marker of the latest flip the -display has *shown*: - - before: flip status: call=0 marker=0 shown=0 - after: flip status: call=0 marker=1 shown=1 - -`call=0` is a successful query in both. The marker is the difference: zero means -the display had not shown a single flip, one means it has shown flip 1. - -**The change is one call.** `sceSystemServiceHideSplashScreen()`, before the -display is claimed in `Display::open`. It is the shell's startup splash, it sits -over the frame, and this port never asked for it to go. Nothing else changed: the -same two registered buffers, the same flip mode, the same cache flush. - -**How it was found, because the route matters more than the fix.** -../PS5_Vulkan's `src/demo_renderer.cpp` is a minimal CPU-to-VideoOut template - -two frames drawn into direct memory, flush, register, one flip, one vblank wait, -and **no** `sceVideoOutGetFlipStatus` at all. Its constants and its sequence are -otherwise identical to this port's, character for character in the parts that -matter (frame size, `frame_bytes` 0x1000000, alignment 0x200000, memory type 3, -map protection 0x33, pixel format 0x8000000022000022's sibling -0x8000000022000000, `sceVideoOutOpen(0xff, 0, 0, NULL)`, `SetFlipRate(handle, 0)`, -the same 80-byte attribute, the same two-buffer registration, `SubmitFlip(..., -1, 1)`). Diffing the two sequences and taking each difference in turn left the -splash call as the one that mattered. - -**What this does and does not settle.** Frames written by the CPU now reach the -screen through `sceVideoOutSubmitFlip` - a path this project had no evidence for -and which the sibling's driver does not use at all. It does not settle the menu, -which is the separate fault already recorded: `rgui_render` is called every frame -with `width = 0, height = 0`. - -**The instrument that found it is the one to keep.** A flip status that says -"shown" is the only evidence this project has ever had that a pixel arrived, and -it is worth more than the return code of a submission call. - -## A CPU-written 1920x1080 frame from a title does reach this console's screen - -**Measured, by the console's owner.** The `../PS5_Vulkan` demo renderer -(`src/demo_renderer.cpp`, built as PPSA99999 "PS5 Vulkan Compatibility Probe") was -deployed and launched, and its diagnostic pattern appeared on the television: -three panels, a white rule, a cyan circle, a yellow square, a magenta triangle and -the text "PS5 DIAGNOSTIC HARNESS". - -This settles the question this port has been circling for several rounds. A title -on this console can allocate direct memory, write 1920x1080 pixels into it with the -CPU, flush, register the buffers with VideoOut and present with -`sceVideoOutSubmitFlip` - and see them. It is not a path that requires the AGC -command processor, `sceAgcDcbSetFlip`, or a Vulkan swapchain. - -**What the working sequence is, exactly** (`../PS5_Vulkan/src/demo_renderer.cpp`): -`sceSystemServiceHideSplashScreen()`; `sceVideoOutOpen(0xff, 0, 0, NULL)`; -`sceKernelAllocateDirectMemory(0, pool, 0x2000000, 0x200000, 3, &physical)`; -`sceKernelMapDirectMemory(&mapped, 0x2000000, 0x33, 0, physical, 0x200000)`; -draw both 16 MiB frames; `flush_range(mapped, 0x2000000)` - `clflush` per 64 bytes -then `mfence`; `sceVideoOutSetFlipRate(video, 0)`; -`sceVideoOutSetBufferAttribute2(&attr, 0x8000000022000000, 0, 1920, 1080, 0, 0, 0)`; -`sceVideoOutRegisterBuffers2(video, 0, 0, buffers, 2, &attr, 0, NULL)`; -`sceVideoOutSubmitFlip(video, 0, 1, 1)`; `sceVideoOutWaitVblank(video)`. Note that -it flips **buffer index 0** and never requests a status - the flip status query is -not part of the working path. - -**This port's differences from it are now the whole of the remaining problem.** They -are small and enumerable, which is a much better position than the one this work -started from: the port rotates `registered_[back_]` rather than flipping a fixed -index; it queries `sceVideoOutGetFlipStatus` after the flip and reads the marker; -and it presents from RetroArch's callback (up to 30 times a second) rather than -drawing two frames once and holding. Each is testable against a known-good -reference, and the first thing to do is the smallest possible one: paint the bands -into a single buffer, register it, flip index 0 once, wait a vblank, and hold. - -**One correction to an earlier entry.** The splash call helped - the flip status -went from `marker=0` to `marker=1` - but it did not make the port's frames appear. -The demo does call it too, so it is necessary and not sufficient, and the marker -value is weaker evidence than this entry: an owner's eyes on a rendered pattern. - -## The fault is before presenting, and every static comparison matches - -**Measured.** `present()` was reduced to the working sequence exactly: paint the -bands into buffer 0, `clflush` + `mfence`, `sceVideoOutSubmitFlip(handle, 0, 1, 1)` -once, `sceVideoOutWaitVblank`, then hold and never touch the display again. The -trace confirms the shape - `probe: flipped buffer 0 once, status=0 marker=1 (then -holding)` - and the title stays up until the loop closes it. The screen is still -black. - -**What that rules out.** All three differences this port had from the working -sequence are gone from this build - buffer rotation, the `sceVideoOutGetFlipStatus` -query, and re-flipping every frame - and the screen is unchanged. Presenting is not -where the fault is. - -**What has been compared and matches**, so it is not where the fault is either: - -- allocation: `sceKernelAllocateDirectMemory(0, pool, 0x2000000, 0x200000, - /*type*/ 3, &physical)` and `sceKernelMapDirectMemory(&mapped, 0x2000000, - /*protection*/ 0x33, 0, physical, 0x200000)` - the same call, the same values; -- layout: two 16 MiB frames at offsets 0 and `frame_bytes`, registered from the - mapping's base, exactly as `demo_renderer.cpp` builds its two `VideoBuffer`s; -- format: `sceVideoOutSetBufferAttribute2(&attr, 0x8000000022000000, 0, 1920, 1080, - 0, 0, 0)` and `RegisterBuffers2(handle, 0, 0, buffers, 2, &attr, 0, nullptr)`; -- colour encoding: `0xAARRGGBB` on both sides. The working demo's own table says - so - its named colours are the giveaway that the order is B,G,R in bytes - (`cyan = 0xffffff00`, `yellow = 0x00ffff`), and this port composes - `0xff000000 | r<<16 | g<<8 | b`, which is the same; -- pixel addressing: the working demo's `put_pixel_unchecked` writes through - `tiled_byte_offset(x, y)`, the same tiled layout this port's `Display::write` - uses through `tiled_offset(x, y)`; -- and the arguments both sides pass to `sceVideoOutOpen(0xff, 0, 0, NULL)` and - `sceVideoOutSetFlipRate(handle, 0)`, and both call - `sceSystemServiceHideSplashScreen()` before opening the display. - -**So the next step is a comparison, not a deduction.** Two ways, both cheap: - -1. Read the first frame's buffer back on the CPU after the flip and compare it - against the same frame drawn by `demo_renderer.cpp`'s own `Canvas`. If this - port's buffer does not hold the bands, the write path is at fault and the - difference is in `Display::write` or the surface it is handed; if it does hold - them, then the memory being written is not the memory being displayed, and the - difference is in the registration or the mapping. -2. Link `demo_renderer.cpp`'s `Canvas` code into this port unchanged, draw through - it instead of through `Display`, and present with the probe's single flip. If - that appears, the fault is in this port's own drawing; if it does not, the fault - is in this port's display setup - and either way the working code is right there - to bisect against. - -## The bands are not reaching the screen, and the frame path taken is now in question - -**Measured.** `present()` was reduced to the working sequence exactly (paint bands -into buffer 0, `clflush` + `mfence`, one `sceVideoOutSubmitFlip(handle, 0, 1, 1)`, -one `sceVideoOutWaitVblank`, then hold), and the screen is still black. That rules -out the three differences this port had from ../PS5_Vulkan's demo renderer - -rotation, the flip-status query, and re-flipping - because none of them are in that -build. - -**Everything statically comparable matches**, and this is the list, checked one by -one against `../PS5_Vulkan/src/demo_renderer.cpp`: the direct-memory allocation -(`type 3`, alignment `0x200000`, `0x2000000` for two `0x1000000` frames), the map -protection `0x33`, the two-buffer registration from the mapping's base, the pixel -format `0x8000000022000000`, the colour encoding (`0xAARRGGBB`; the demo's own cyan -is literally `0xffffff00`), `sceVideoOutOpen(0xff, 0, 0, NULL)`, -`sceVideoOutSetFlipRate(handle, 0)`, `sceSystemServiceHideSplashScreen()` before -the display opens, and the tiled per-pixel addressing - the demo's -`put_pixel_unchecked` writes through `tiled_byte_offset(x, y)` and this port's -`Display::write` writes through `tiled_offset(x, y) / 4`, and the two functions -were compared numerically over 15 sample points and produce identical byte offsets. - -**A readback was added and did not run.** It sits inside the `if (source == nullptr)` -branch of `ps5_frame`, reads the buffer back through the same tiled addressing that -wrote it, and logs the count of pixels that differ from what was intended. The -binary is deployed - the console's `eboot.bin` contains the `readback:` string - and -the line never appears in the trace, while the frame telemetry that follows it in -the same function does appear. - -**The likely reason, to be confirmed.** `ps5_frame`'s trace tag says "no-menu-source" -when `have_menu_frame` is false, and that is set by `ps5_set_texture_frame`, not by -the `source == nullptr` test. Those are two different conditions. If the frontend is -handing this driver a real frame, the code takes the core-frame branch, never -touches `source == nullptr`, and the readback never runs - which is exactly what the -trace shows. The first thing the next round should do is log unconditionally at the -top of `ps5_frame` whether `frame` is null, with its width, height and pitch. That -one line says which branch the driver is actually taking, and the readback belongs -in whichever branch it is. - -## The bands are provably in the buffer, the flip is accepted, and the screen is black - -**Measured.** With the probe painting the frame itself and reading it back through -the same tiled addressing that wrote it: - - readback: base=200200000 1920x1080 wrong=0 of 2073600 (0.0% wrong) - probe: flipped buffer 0 once, status=0 marker=1 (then holding) - -Zero wrong pixels: the buffer holds exactly the three bands that were painted, in -the tiled layout the display is told about. The base is `0x200200000`. The flip of -buffer 0 is accepted and the display reports marker 1. - -**And the screen stays black.** - -**So the write half is proven and the display half is not.** Every value this port -passes to VideoOut matches `../PS5_Vulkan/src/demo_renderer.cpp` - the same -allocation, protection, alignment, two-buffer registration from the mapping base, -pixel format, `SetFlipRate(0)`, `HideSplashScreen` before open, and the same tiled -addressing, checked numerically - and the memory holds the right pixels. What -remains is the one thing no comparison of values can catch: whether the buffer that -was written is the buffer the display reads. - -**A process lesson, and it cost two rounds.** The probe was left inside -`if (source == nullptr)` while the frontend hands this driver a real 4x4 frame on -every call, so the branch was never taken and none of the probe code ran. A -readback that never printed was read as a display fault rather than as code that -never ran. One unconditional log at the top of the frame callback - `frame=present -w=4 h=4 pitch=8 menu_frame=no` - settled it in a single run. Measure which path a -driver is on before building anything on top of it; do not infer it from a tag that -happens to be printed nearby. - -**Also settled, and it explains a great deal.** The frontend asks this driver to -draw a **4x4** frame, not 1920x1080. That is why the image was never scaled to the -screen: `video_st->width/height` are zero because nothing ever called -`video_driver_set_size`, so RetroArch is presenting into a degenerate frame. The -probe sidesteps it by painting the display's own frame directly. Fixing the size is -the next thing for the real path, and `ps5_set_viewport` is where the driver can -report it. - -## The menu was in the framebuffer all along, and the hand-over died on a struct size - -**Measured, and the goal of this milestone.** `tools/run-title.sh --watch 20` runs -the title unattended, the console reports no fatal signal, and the console's owner -watched **RetroArch's RGUI menu on the television**. `/app0/trace.txt` from that run -(`evidence/ppsa-99169-rgui-menu-on-screen/`): - - ps5_init: own table ident=ps5 poke_interface=4015d0 set_viewport=401590 wrap_type_to_enum=0 - ps5_get_poke_interface entered - ps5_set_texture_frame: rgb32=0 320x240 frame=present have=1 (was 0) - ps5_frame 1: menu 320x240 pitch=640 present=1 - ps5_frame 60: sources so far: menu=yes core=yes - display: flip 1200 of buffer 1, status=0 marker=1 - -**The fault was one line of the build, and it was invisible from every direction -this project had been looking.** `src/` was compiled with **no `-DHAVE_*` flags at -all**. The frontend archive is compiled with fifty of them -(`tools/retroarch-flags.sh`). RetroArch declares its driver interface as a struct -full of `#ifdef HAVE_OVERLAY` / `#ifdef HAVE_GFX_WIDGETS` members, so the two sides -disagreed about the struct: `video_ps5` was laid out **136 bytes** long while the -frontend read it as **144**. Everything after `overlay_interface` was read one -member late - `poke_interface` and `wrap_type_to_enum` both came back NULL, and -`alive()` was the right function only by luck. - -**Why it looked like a display fault.** A NULL `poke_interface` is not an error -anywhere: `video_driver_init_internal` calls it only `if (...->poke_interface)`, so -nothing failed, nothing logged, and the driver opened the display and presented -1500 frames. RGUI rendered the menu into its 320x240 framebuffer on every frame and -handed it to `rgui_set_texture_frame`, which checks `video_st->poke` and returns -when it is NULL. The hand-over was dropped in silence, once per frame, for as long -as the title ran. - -**Two instruments found it, and both are worth keeping.** - -1. A probe at RGUI's own commit point printed what the frontend could see: - `probe commit upscale=0 fb=320x240 data=1 poke=0 fn=0`. The framebuffer was - allocated and the menu had been drawn into it; the poke was NULL. -2. `ps5_init` now logs its own table's members from inside the driver: - `poke_interface=4015d0`. The two lines together say "this table has the function" - and "the frontend cannot see it", which is a layout disagreement and nothing - else. - -**The check that keeps it fixed.** `tests/test_frontend.py` compiles RetroArch's -header with the frontend's own defines and compares `sizeof(video_driver_t)` against -the size of `video_ps5` in the object the title links, then reads the table's -relocations to confirm the member at the frontend's `poke_interface` offset is this -driver's hand-over function. Built without the defines it fails with -`video_ps5 is 136 bytes in the title's object and video_driver_t is 144 bytes in the -frontend` - verified by doing exactly that. - -## The frame the display keeps is the frame it was given, and the runloop does not take it back - -**Measured, and it corrects the lead this round started from.** The band probe was -made to hold its frame for 8 seconds inside `present()` instead of returning into -RetroArch's frame loop, with the flip status sampled every 500 ms. The console's -owner reported that the bands appeared **almost immediately** - not after the hold -began - and were **still on screen when the title closed**, twelve seconds after the -hold ended and `present()` had returned into the runloop. - -So the behavioural difference between this port and `../PS5_Vulkan`'s renderer - -that the renderer flips once and then blocks forever while this port returns to -RetroArch's frame loop - is **not** what made the screen black. The runloop does not -undo a presented frame: it does not flip (the probe's `present()` returned early -after its first call), it does not clear the buffer, and the display stayed on -frame 1 (`marker=1`) for the whole 8-second hold with nothing else touching it. - -**What was left, and it was the only other difference.** The port called -`sceAgcInit(8)` in `Display::open`, left over from the round that submitted flips -through an AGC command buffer. The renderer whose output has been seen on this -console contains no `sceAgc` call at all (`../PS5_Vulkan/src/demo_renderer.cpp`). -Removing it, together with the hold, is what produced the first pixels this port -ever put on the screen. Two changes went in together, so the attribution is not -separated by a clean A/B - what is ruled out is the runloop, by the timing above, -and what is left with no other candidate is the AGC initialisation. The clean test, -if it ever matters, is to restore `sceAgcInit` alone and watch for the bands. - -**Two process notes, because both cost runs.** A probe left inside the configured -upstream tree does not disappear when you stop adding it: after deleting -`build/ra-conf` and reconfiguring, `tools/build-retroarch.sh` still reused the stale -`build/ra/obj/menu_drivers_rgui.c.o` from earlier in the day and shipped its probes -into `eboot.bin`. Deleting the object directory is what makes a probe really gone. -And a trace file that is appended to for every run stops being readable: this -session's `/app0/trace.txt` reached 72,904 lines because every run appends to it. - -## The pad input driver is written and registered, and two faults upstream of it block it - -**Measured.** A complete `input_driver_t` for this console lives in -`src/input_ps5.cpp`: it reads the DualSense through `scePadInit`/`scePadOpen`/ -`scePadRead` with the 120-byte sample layout `../ProsperoLight` verified on -hardware, maps the pad's button words onto RetroArch's own numbering, and reports -sticks and triggers as axes. It is registered in `input_drivers[]`, its button map -is pinned by a host test, and `config/retroarch.cfg` names it. What is **not** true -is that the pad works: the title never reaches `ps5_input_init`, and the two -reasons are both upstream of the driver. - -**Fault one, found by disassembly and fixed: RetroArch's built-in test input driver -was on.** `HAVE_TEST_DRIVERS` defaults to `yes` in `qb/config.params.sh`, and while -it is on `video_driver_init_input` opens with - - if (*input) - if (strcmp(settings->arrays.input_driver, "test") != 0) - return true; - -so the whole input-driver initialisation is skipped whenever the configured driver -is not `"test"`. With it on, `input_drivers[]` holds the test driver, the null -driver and this project's driver, and the frontend's first entry is the test one: -`probe init_input entered: *input=ba4dc0 configured="null" joypad="null"` and then -`return true`. No input driver is ever initialised, so `current_data` stays NULL and -every button read is a call that answers 0. `--disable-test_drivers` is now in -`tools/retroarch-sources.sh`; it is a development driver for RetroArch's own test -suite and has no place in a shipping title. - -**Fault two, found by trace and fixed: the title's `-c` was thrown away.** The entry -point passes `-f -c /app0/retroarch.cfg --verbose --menu`, and content loading -rebuilds a fresh argv from the frontend's environment -(`content_load_init_wrap`), which offers no `-c`: the pair RetroArch actually parsed -was `["retroarch", "--menu"]`. `probe config_parse_file: path="(null)"` is that -measurement, and `probe config_load: after parse input="null" video="ext"` shows the -consequence - **the title has been running on compiled defaults all along**, config -file ignored, `--verbose` dropped. The display worked only because the video -driver's *default* happened to be ps5 already. The fix is one guarded insertion in -`tasks/task_content.c` (`patches/series`, 0006) that restores the title's own config -path when the frontend environment does not name one. - -**And that fix is parked, because it exposes a crash that is not yet understood.** -With the config actually read, the title dies on launch: - - # signal: 11 (SIGSEGV) - # reason: page fault (user read instruction, page not present) - # fault address: 0000000000000000 - # rip: 0000000000000000 - -`rip: 0` is a call through a null function pointer. It is not the input driver: -with `input_driver = "ps5"` the trace stops after `ps5_get_poke_interface entered` -and `ps5_input_init` is never entered, and with `input_driver = "null"` the same -crash happens once the config is read. Markers through `drivers_init` and -`video_driver_init_internal` show the whole video path completing - overlay -unload/init, context reset, display server, mouse cursor, audio init, core info all -print - so the crash is after that, in `rarch_main`'s runloop or the content task. -The parked change is `parked/config-path.patch.py`; the config keeps -`input_driver = "null"` so the title runs. - -**What this means for the next round.** The order is: fix the config-path crash, -then the driver that is already written and tested should initialise and the pad -should work. The crash is a null call in the runloop after driver initialisation, -and the fastest instrument is a marker on `runloop_iterate` and the content task, -not more of the driver. - -## The config-path crash, narrowed: it is reading the config, not any setting in it - -**Measured, by elimination.** Four console runs, each differing in exactly one -thing, and the title's own trace says which variant ran because the entry point -marked it. All four used the same binary apart from that one change. - -| argv | config read | result | -| --- | --- | --- | -| `-f -c /app0/retroarch.cfg --verbose --menu` | yes | SIGSEGV, `rip=0` | -| `-f -c /app0/retroarch.cfg --menu` (no `--verbose`) | yes | SIGSEGV, `rip=0` | -| `-c /app0/retroarch.cfg --menu` (no `-f`, no `--verbose`) | yes | SIGSEGV, `rip=0` | -| `-f --verbose --menu` (no `-c`) | no | runs, menu on screen, no signal | - -So it is not `--verbose` finally taking effect, and not `video_fullscreen` finally -taking effect: dropping each of those leaves the crash. **It is the config file -being read at all.** That is a much narrower statement than the previous entry could -make, and it rules out the two things that change *behaviour* rather than -*settings*. - -**And the config itself parses fine.** With `config_load` marked before and after: - - probe C:before-config-load - probe C:after-config-load - -so `config_load` -> `config_set_defaults` -> `config_parse_file` -> `config_load_file` -all complete. The crash is **after** the config is read and **before** the first -frame: no `ps5_frame 0` line ever appears, and neither `runloop_iterate` call site -in `rarch_main` is reached. It is inside `retroarch_main_init`, in the stretch -between the config load and the first frame - which is where the driver lookups and -`drivers_init` live. - -**What is still ruled out, from the earlier marker run.** `drivers_init` and -`video_driver_init_internal` both complete: overlay unload, overlay init, context -reset, display server init, mouse cursor check, audio init and core info all print -their markers. And `ps5_input_init` is never entered, so the input driver is not -involved. - -**The suspicion, stated as a suspicion.** With the config read, the frontend's -driver *selections* are no longer the compiled defaults: those lookups -(`audio_driver_find_driver`, `video_driver_find_driver`, `input_driver_find_driver`, -`camera_driver_find_driver`, `menu_driver_find_driver`) are the next thing after the -config load that behaves differently, and each of them indexes a driver table whose -contents this build has stripped to almost nothing. A default that names a driver -this build does not carry is the obvious candidate for a null call. That is where -the next instrument goes - and it must be placed with the editor, not a script: -reading a marker into the middle of a multi-line call is what produced -`undefined symbol: rarch_main` in this round's last attempt. - -## The config-path crash is a null call in `command_event`, and the file logger cannot see it - -**Where it is now, by markers that printed.** With the config-path fix applied, the -title's own trace ends at exactly this sequence: - - probe M:drivers-init-all (before drivers_init(DRIVERS_CMD_ALL)) - probe M:input-init-command - probe M:controller-init (before command_event(CMD_EVENT_CONTROLLER_INIT)) - -> SIGSEGV, rip = 0 - -and three markers placed *after* that point never print: the first instruction of -`command_event_init_controllers`, the `case CMD_EVENT_CONTROLLER_INIT:` label, and -the statement after `command_event(...)` returns. So the crash is inside -`command_event()` before its switch reaches the controller case - one frame deeper -than the call site. The register dump is identical in every config-loading run -(`rip: 0`, `rdi`/`rsi`/`r13` pointing into one 0x138-byte frame), so it is -deterministic, not a race. - -**A guard that is real but is not the fix.** `patches/series` 0007 adds the missing -null check before `core_set_controller_port_device`'s call to -`current_core.retro_set_controller_port_device`. Upstream calls that core callback -unguarded, and `dynamic_dummy.c` only survives because it defines an empty stub. It -is now verified in the object (`test %rax,%rax; je` before `call *%rax`) and the -title still dies with a byte-identical register dump, so it did not fix this crash. -It is kept because the missing check is genuine. - -**The file logger cannot see this crash, and that is structural.** -`retroarch_main_init` calls `rarch_log_file_init(...)` *after* -`retroarch_parse_input_and_config(...)` returns, and `command_event` is reached -later still - but a crash during init happens before the logger opens its file. So -`log_to_file = true`, `log_to_file_timestamp = false` and `log_dir = "/app0"` are -now in `config/retroarch.cfg` and are **inert until the config path is fixed**; they -are there so the log appears the day it is. The logging settings themselves need no -build flag: `rarch_log_file_init` is compiled unconditionally. - -**Two process failures in this round, both worth more than the finding.** -1. Parking `patches/series` 0006 was done by slicing the script by text, and the - slice captured the wrong block: the parked file was truncated to zero bytes and - the 0006 entry was deleted from the script while `build/ra-conf` still had it - applied - so the next build rebuilt the crash *and* the patch record no longer - matched the tree. Both files were recovered with `git checkout`. -2. Inserting a probe line after each of `command_event`'s getter statements split a - multi-line `#if` block and produced `undefined symbol: char_list_new_special` at - link time. Markers must be placed at statements, and the result must be compiled - before it is trusted; two attempts this round were lost to this. - -## The input driver is selected but never initialised, and that is one early return - -**Measured.** With the compiled default changed to `"ps5"` (`patches/series` 0008), -a probe inside our own video driver - which does run - reports what the frontend -believes its drivers are: - - probe drivers: input="ps5" joypad="null" video="ext" - -So the selection works and the name reaches the input subsystem. And -`ps5_input_init` still never runs: no `input:` line appears in the trace, and its -first statement is a trace call. - -**Why, read out of the object.** `input_drivers[]` in this build holds exactly two -entries, and ours is first: - - R_X86_64_64 input_ps5 - R_X86_64_64 .data.input_null - -`HAVE_TEST_DRIVERS` is `#undef`, so the test-driver branch is gone. What remains in -`video_driver_init_input` is: - - if (*input) - return true; /* <- taken */ - ... - if (!(new_data = input_driver_init_wrap(...))) /* never reached */ - -`input_driver_st.current_driver` is already non-NULL when that function is called, -because the pre-initialisation pass in `retroarch_main_init` runs -`input_driver_find_driver`, which *selects* a driver and deliberately does not -initialise it. So the configured driver is named, never wrapped, `current_data` -stays NULL, and the pad is a driver with no state. - -**This corrects the earlier entry in this file.** Disabling `HAVE_TEST_DRIVERS` was -necessary but not sufficient: that flag removed one early return from that function, -and a second one - the `if (*input)` guard that upstream intends for the case where -a *video* driver pre-initialised input - is taken in this build for a different -reason, because pre-init already selected the driver. The fix has to make the input -driver actually initialise on this path, not just be selected. - -**A process note that cost a run.** `strings` defaults to a four-character minimum, -so it reports nothing for a three-character literal like `"ps5"`; that made a -correctly patched and rebuilt object look unpatched. `grep -a` is the right check -for short literals. - -## Selection is not initialisation: `ps5_input_init` has never run - -**Measured, from inside our own driver.** The frontend calls a video driver's `init` -with two pointers whose documented purpose is "the video driver may pre-initialise -an input driver" (`gfx/video_driver.h`). Printing them from `ps5_init` gives: - - probe handed: input=be3ed0 input_data=be3ed8 *input=ba41e0 *input_data=0 - -So when our video driver starts, an input driver is already **selected** -(`*input` non-NULL) and its state is **zero** (`*input_data` NULL). `ps5_input_init` -has never been called - not on this run, not on any run: the trace holds no `input:` -line at all, and that function's first statement is a trace call. - -**The path that should initialise it.** `input_driver_find_driver` runs during driver -pre-initialisation and only *selects* (`current_driver = input_drivers[i]`). -Initialisation is `input_driver_init_wrap`, whose only call on this path is the tail -of `video_driver_init_input` - and that function returns early when -`current_driver` is already set: - - if (*input) - return true; <- taken, because pre-init selected - ... - input_driver_init_wrap(...) <- never reached - -Upstream intends that early return for a *video* driver that pre-initialised an -input driver itself. This build's video driver does not - it ignores the pointers on -purpose - so the early return is taken for a reason upstream never designed for, and -the wrap below is dead code. - -**A fix that is compiled in and was not sufficient.** `patches/series` 0009 clears -the stale selection when `tmp` is NULL, so the code below re-selects and then wraps. -The object shows it is really there: - - test %rdi,%rdi ; tmp == NULL? - je 0x3e ; -> store tmp (the video driver's own input driver) - cmpq $0x0, ...+0x19b ; current_driver - jne 0xe6 ; -> still the early return - movl $0xffffffff, ...+0x19c - mov %r14, ...+0x19c ; store tmp - -and it did **not** make the driver initialise: `ps5_input_init` still does not run. -So either that function is not reached at all on the second -`video_driver_init_internal`, or the re-selection inside it does not find `"ps5"`. -That is the one measurement left, and it needs a probe inside -`video_driver_init_input` itself - the last two rounds' probes were placed outside it -and could not see this. - -**Why the earlier checks in this round were worthless.** Twice I grepped a compiled -object for a patch's *comment* text and concluded the patch was missing. Comments do -not survive compilation, and `strings` additionally has a four-character minimum, so -it reports nothing for a three-character literal like `"ps5"`. Both checks were -incapable of the answer they were asked for. The instruction stream is the only -reliable evidence that a code change was compiled. - -## The pad works, the picture is live, and the fault was one comparison - -**Measured, on the shipping build.** `bash tools/run-title.sh --watch 20`, with the -console owner working the pad, and the title's own trace: - - input: pad opened, user=515310723 handle=51119872 - input: press, pad=0x00000040 retropad=0x00000020 DOWN - input: press, pad=0x00000010 retropad=0x00000010 UP - input: press, pad=0x00004000 retropad=0x00000001 CROSS -> RetroPad B - input: press, pad=0x00002000 retropad=0x00000100 CIRCLE -> RetroPad A - input: press, pad=0x00000020 retropad=0x00000080 RIGHT - input: press, pad=0x00000080 retropad=0x00000040 LEFT - menu: framebuffer commit 2 is a new picture (2 of 2 changed so far) - ps5_frame 600: menu commits=76 changes=14 presented=yes - -Every mapping is the one intended, including the pairing that matters for a -PlayStation player: CIRCLE is RetroPad `A` (0x100) and CROSS is `B` (0x1). And the -menu is **not one frozen frame**: 76 commits of its framebuffer, 14 of them a -different picture, where every earlier run showed 1 and 1. Input is what made it -redraw, which is the answer to the frozen-frame question - the picture was never -frozen, the menu simply had nothing to redraw for. - -**The fault, and it is one comparison.** `ps5_input_init` had never run. It is -reached through `input_driver_init_wrap`, whose only call on this path is the tail of -`video_driver_init_input`, and that function opens with - - if (*input) - return true; /* upstream: "keep the selected driver" */ - -Upstream intends this for a *video* driver that pre-initialised an input driver of -its own, and the tell is `tmp`. `video_driver_init_internal` sets - - tmp = input_state_get_ptr()->current_driver; - -*before* calling `video_driver_find_driver`, so once the pre-initialisation pass has -selected a driver, `tmp` **is** that selection - not something a video driver -supplied. Measured: - - probe INV: entered tmp=ba41e0 *input=ba41e0 configured="ps5" - probe INV: after-clear *input=ba41e0 (with the first, wrong fix) - -The early return therefore fired for a reason upstream never designed for, the wrap -was dead code, `current_data` stayed NULL, and every button read answered 0. The -first fix - clearing the selection when `tmp == NULL` - never fired, because `tmp` -is not NULL. The fix that works is `patches/series` 0009: - - if (*input != NULL && *input == tmp) - *input = NULL; - -which says exactly what it means: no video driver supplied a *different* driver, so -the stale selection is dropped and the code below re-selects from the settings and -then initialises it. - -**Two corrections to earlier entries in this file.** Disabling `HAVE_TEST_DRIVERS` -was necessary but not the whole story, and neither was `0008`: the driver was named -and the naming was never the problem. Both earlier rounds concluded "selection is not -initialisation" and both then assumed the remaining gate was somewhere they had not -looked; it was one pointer comparison in the function they had already read twice. -The probes that found it were inside `video_driver_init_input` - the three previous -rounds placed probes *around* it and could not see it. - -## The probes are kept, because a rebuild destroys them - -Every probe in this project is written into the configured copy of RetroArch, and -`tools/retroarch-sources.sh` rebuilds that copy from upstream - so the set that -found the input fault was lost the moment the tree was reconfigured, mid-round. -`tools/apply-runtime-probes.py` now holds them: version-controlled, applied with one -command, reverted with `--revert`, listed with `--list`, and never part of the -shipping build because `tools/build-title.sh` does not call it. - -They are the landmarks that were expensive to find: what the input-driver -initialisation is handed (`tmp`, `*input`, the configured name), whether the -selection survived to the wrap decision, what the wrap returned, the line before any -input driver's own init, the call into `video_driver_init_input`, and the two -landmarks in `retroarch_main_init` that separated "the crash is in a driver" from -"the crash is after every driver". - -## Reading the config makes the input driver work, and then the title dies on a refused syscall - -**Measured, and it corrects the shape of this whole problem.** Re-applying the parked -config-path fix (0006) on top of everything since - 0007, 0008, 0009 - the config is -finally read, and the effect is immediate: - - ps5_init entered (video=present width=0 height=0) - ps5_init: own table ident=ps5 ... - ps5_get_poke_interface entered - input: pad opened, user=515310723 handle=51250944 - -`input_joypad_driver = ""` was reaching the frontend at last, the input driver was -initialised, and the pad opened. Then the title died, and the console named something -new: - - # signal: 12 (SIGSYS) - # rax: 0 r8: 0x80 r9: 0x80 - # process pid=223, coredump.elf calls exit() exit_value=0. - -SIGSYS is a syscall the console refuses - not a null pointer, not a data fault. A -second signal follows it (`SIGSEGV`, `rip: 0`) as the process tears down, which is -the fault earlier rounds were reading, and why they kept looking for a null call. -The config being read is what makes the title reach a blocked syscall; nothing in the -config file is malformed. - -**So the config does not need to be read for the pad to work.** `patches/series` 0008 -names this project's driver as the compiled default, and with the config path parked -the pad opens and responds anyway - `menu commits=5 changes=2` on a 15-second run -with input live. That also means the config route is now purely about -configurability and the file logger, not about whether the injector or the pad -functions. - -**A self-inflicted fault that masqueraded as this one, and cost most of a round.** -`tools/apply-port-patches.py` in the working tree held **14** patch blocks where the -committed script holds **10**: the controller-port guard (0007) had been added twice, -so `runloop.c` was patched twice in one build. Every run built from that tree died -with `rip: 0` and an 8-line trace, and I misread it as progress on the config fault. -`git checkout -- tools/apply-port-patches.py` restored the verified 10-block script, -and the title immediately ran again. Twice now this session a patch-script edit made -by slicing text has cost a round; the lesson is to count the blocks -(`grep -c '^ ($'`) after every edit to that file. - -## With the config read, everything works up to the joypad step - which is where it dies - -**Measured, in order, on one run with the config-path fix applied.** Probes at each -landmark give a complete sequence with no gap until the very end: - - probe CFG: load-file-enter - probe CFG: load-file-body-start - probe CFG: before-read - probe CFG: after-read - input: pad opened, user=515310723 handle=51578624 - probe X: wrap-before-init - probe X: before-joypads - -> dead - -Read it as the phases it names. The config file **is opened and parsed** - -`after-read` prints, so `config_file_new_from_path_to_string` returns a real -`conf`, and nothing about the config file's contents or the reading of it is at -fault. The input driver **is** initialised, and this time its own init runs to -completion: `pad opened` is printed by `ps5_input_init` itself, and -`wrap-before-init` immediately precedes the call to it. Then `before-joypads` prints, -which is the first statement after that init returns, and nothing after it. - -**So the remaining fault is in `input_driver_init_joypads()` or the moment after -it** - the last step of `input_driver_init_wrap`: - - if ((ret = input->init(name))) /* returns: pad opened */ - { - input_driver_init_joypads(); /* before-joypads prints, then nothing */ - return ret; - } - -That function calls `input_joypad_init_driver(settings->arrays.input_joypad_driver, -input_driver_st.current_data)`. This build compiles exactly one joypad driver, -`null_joypad`, and its `init` is a real function, so this is not the empty-table case -the earlier entries suspected. What is left is the `null_joypad` init itself or the -syscall behind it - and that is where SIGSYS (signal 12, `rax: 0`) points, not at the -config. - -**This is a much better position than "the config crashes".** The config mechanism -is proven to work on this console: the file opens, parses, and its settings are -applied far enough to change driver behaviour (`input_driver = "ps5"` reaches the -frontend and the pad opens, which is only true when the config is read). The fault -is one call in the input subsystem, on a path that only exists because the config now -names this project's driver. It is also specific to reading the config: with the -config path parked, the input driver still initialises through the compiled default -and the pad works, because that route does not reach this call with the same -`current_data`. - -**A note on the earlier SIGSYS attribution.** The previous entry said reading the -config "walks the title into a blocked syscall" and implied the file path was at -fault. The probes above show the file path completing; the refused syscall is downstream, -in the joypad step. The correction matters because it changes the fix: nothing about -`retroarch.cfg`, `fopen`, `stat` or the working directory needs to change. - -## RetroArch now keeps its own log on the console, and it is the instrument the Vulkan path needs - -**Measured.** With `--log-file=/app0/retroarch.log` added to the title's argument -list, a run leaves a real frontend log beside the title - 24 lines on this build, -alongside the ten-line `/app0/trace.txt` this project writes by hand: - - [INFO] Version: 1.22.2 - [ERROR] Couldn't find any audio driver named "ext" - [INFO] Available audio drivers are: - [INFO] null - [WARN] Going to default to first audio driver... - [INFO] [Input] Found input driver: "ps5". - [INFO] [Video] Graphics driver did not initialize an input driver. Attempting to pick a suitable driver. - [INFO] [Video] Found display server: "null". - [ERROR] Failed to initialize audio driver. Will continue without audio. - [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00 - -**Why the flag and not the config.** RetroArch initialises the file logger after it -parses its config, so the config's `log_to_file` settings are inert while the config -path is parked - and a crash during startup happens before that point anyway. The -`--log-file` argument is handled in *argument parsing*, which calls -`rarch_log_file_init` before the config is read -(`retroarch.c`: the "Enable logging to file if verbosity and log-file arguments were -passed" block, which precedes `config_load`). So it records startup, which is exactly -where the frontend has been silent. - -**This closes the one gap that made the Vulkan path unworkable.** The recorded reason -Vulkan is off is that the title "exits 1 within a second of EXEC, with no signal and -no message from RetroArch". A failure that says nothing cannot be diagnosed from a -console run; now the frontend's own words land in a file after every run, Vulkan -included. Nothing else about the Vulkan path has changed - it still needs -`--enable-vulkan`, and the ICD still has to reach the title - but the next failure -will be readable instead of silent. - -**Two facts the log already settles.** The audio driver the frontend looks for is -literally `"ext"` and the only one compiled is `null`, so audio is expected to fail -and the frontend continues - that is not a fault to chase. And the input driver is -found as `"ps5"` and the display server is `"null"`, which is correct for a build with -no graphics backend: the "display server" is a separate slot from the video driver. - -## Vulkan is on, the delivered driver is staged, and the EXEC-1 is now a named step - -**The blocker recorded for three sessions was not the ICD.** With `--enable-vulkan` -on, the frontend compiled **266 of 276** sources and **archived a title anyway**. -The ten that failed were all one cause: - - deps/SPIRV-Cross/spirv_cfg.cpp: cannot use 'throw' with exceptions disabled - gfx/drivers_shader/shader_vulkan.cpp: cannot use 'throw' with exceptions disabled - gfx/drivers_shader/slang_process.cpp: cannot use 'throw' with exceptions disabled - ... seven more - -`tools/build-retroarch.sh` compiled the frontend's C++ with -`-fno-exceptions -fno-rtti`, and SPIRV-Cross and RetroArch's Vulkan shader path need -exceptions. The archive was then built from what did compile, so the Vulkan shader -path was silently half-present - and because the build *succeeds*, nothing about the -run said so. With `-fexceptions` (RTTI still off) the frontend compiles **276 of -276**. The lesson generalises: `build-retroarch.sh` prints -`==> [ra] N sources did not compile: ...` and that line must be read on every build, -because a partial archive still links. - -**What is in place now.** `--enable-vulkan`; the video driver's compiled default is -`"vulkan"` (`patches/series` 0010, the same mechanism 0009 uses for input, because -the config path is parked and the frontend runs on compiled defaults); the driver -`../PS5_Vulkan` delivers is staged beside the title by `tools/build-title.sh` from -`$PS5_VULKAN_ICD` or the sibling's `build/driver/ps5/libvulkan.so.1`, into both the -title root and `sce_module/`; and `patches/series` 0011 adds `/app0` and -`/app0/sce_module` fallbacks to RetroArch's `dylib_load` chain plus a `RARCH_ERR` on -total failure, because upstream's `dylib_load` logs nothing and that silence is what -made this path undiagnosable. - -**The console re-signs a shared object on write.** Measured: the file staged here is -16,695,760 bytes with sha256 `25b32922...`, and the console serves 16,706,040 bytes -with sha256 `eb23125d...` - byte-for-byte the sibling's own `libvulkan.so.1.signed`. -So `tools/deploy-title.py` now checks a shared object the way it checks `eboot.bin`: -a byte string taken from the middle of the local library must appear in what the -console serves. A digest comparison there answers the wrong question and was -rejecting a correct upload. - -**Where it still stops, and what that rules out.** The title reaches -`[Input] Found input driver: "ps5"`, then `rarch_main returned = 1`. The trace shows -**no `ps5_init`** at all, so with `video_driver = "vulkan"` the frontend fails -inside `video_driver_init_internal` before our driver is called - and the new -`RARCH_ERR` never printed, which places the failure *before* the `dylib_load` chain. -The candidates are therefore the graphics-context step and the `[Video] Found video -driver` lookup, not the ICD, not the dlopen path, and not the delivered object. That -is a much smaller space than "exits 1 with no message". - -## A PS5 title cannot dlopen a driver, so Vulkan has to be linked - and the link is now the only gap - -**Measured by ../PS5_Vulkan, on the console, with their e2-module runner.** This -overturns the delivery route this port has been built around for three sessions: - - sceKernelLoadStartModule("/app0/libvulkan.so.1") -> 0x80020008 (ENOEXEC) - the same for the FSELF-wrapped, soname-as-path and control variants - "libvulkan.so.1" bare -> 0x80020002 (ENOENT) - dlopen answered NULL for all twelve candidates, including modules the process - already holds, with dlerror() NULL every time - sceKernelDlsym -> ESRCH for every name, on modules that do load - -The discriminator is the module shape: a PS5 module image carries SCE module -parameters and an export table, which a linker-produced `.so` does not. So no name, -path or container fixes this, and RetroArch's `dylib_load("libvulkan.so.1")` can -never succeed here. The route that is proven on this console is their runner title's: -**link `libps5vk.ps5.a` into `eboot.bin` and call `vkGetInstanceProcAddr` as an -ordinary symbol.** - -**Two corrections this forces, both recorded rather than quietly dropped.** - -1. The "the console re-signs on write" observation in the previous entry is wrong. - The byte difference is this project's own FTP tooling: `RETR` decrypts a SELF on - read (the driver 16,695,760 -> 16,706,040, and `libc.prx` 1,284,674 -> - 1,335,962). Nothing re-signs on write, and the title sees the staged containers - at their staged sizes. The `.so` marker verification added on the strength of - that claim is harmless but was justified by the wrong reason. -2. `/app0` and `/app0/sce_module` are the only paths a title can see; `/data/homebrew/` - and `/temp0` do not exist inside a title. The `/app0` dlopen fallbacks are - therefore moot - the load is refused for every path, not just the bare name. - -**What is in place now, and it is most of the work.** `patches/series` 0012 replaces -RetroArch's `dylib_load`/`dylib_proc` pair with a direct reference to the linked -`vkGetInstanceProcAddr` (with a local prototype, because RetroArch's Vulkan headers -declare the `PFN_` type but not the function). The frontend compiles **276 of 276** -with Vulkan on and no longer contains a dynamic load. `tools/build-title.sh` names -the sibling's four released archives - `libps5vk.ps5.a`, Mesa's -`libvk_runtime.ps5.a`, `libpsbc_driver.ps5.a`, `libpsbc_support.ps5.a` - and -`PS5_VULKAN_DIR` overrides their root; `tools/build.sh` links them whole -(`--whole-archive`) with the sibling's `--no-dynamic-linker --z nodynamic-undefined-weak`, and adds `libc++.a`, `libc++abi.a`, `libunwind.a` and -the compiler builtins in a `--start-group`, because the shader compiler and Mesa's -runtime are C++. - -**What the link still wants, and it is a short list.** With all of the above the -linker reports `__eh_frame_start`, `__eh_frame_end`, `__eh_frame_hdr_start`, -`__eh_frame_hdr_end`, `u_thread_create` and `util_barrier_init`. The first four come -from ../PS5_Vulkan's own linker script, `tooling/psbc/ps5-pie-unwind.ld`, which its -`tools/psbc-link.sh` passes with `-T`; the last two are Mesa's threading util, which -that script satisfies from its psbc runtime group. So the remaining work is to adopt -that link recipe rather than to discover anything: archives, the unwind script, and -the group ordering, all named in their `tools/psbc-link.sh`. - -## The Vulkan path is the one running, and its refusals are now readable - -**Measured.** A fresh run's `/app0/trace.txt` (952 KB) opens with the driver's own -refusals, which are readable only because `src/main.cpp` points `stderr` at the -trace file unbuffered. They are, by name and count: - -- `only triangle lists without primitive restart are supported (VK_ERROR_UNKNOWN)` - - 21 times, raised at `driver/ps5vk_pipeline.c:903`; -- `set 0 binding 3: descriptor type 3 has no proven table entry (VK_ERROR_UNKNOWN)` - - 3 times, raised at `driver/ps5vk_pipeline.c:137`; -- `sampler address modes N, N and N are not the clamp-to-edge ...` - 16 times, - raised at `driver/ps5vk_image.c:804`. - -So the frontend is running RetroArch's Vulkan video driver against -`../PS5_Vulkan`'s libps5vk, not this port's hand-written `video_ps5`. - -**A contradiction to resolve, and it is the next thing to measure.** The title's -config says `video_driver = "ps5"` (both `config/retroarch.cfg` and the deployed -copy), yet the trace's refusals come from the Vulkan driver. And -`/app0/retroarch.log` is 1,200 bytes with no timestamps and was byte-identical -across two runs, which is what a stale file looks like - so it may have been -written by an earlier build and may not describe the run just made. Its contents -are at least consistent with an older `video_ps5` run: `[Input] Found input driver: -"ps5"`, `[Core] Geometry: 320x240`, `[Environ] SET_PIXEL_FORMAT: RGB565`, -`[Video] Found display server: "null"`, and no mention of Vulkan at all. - -**Why the refusals matter more than they look.** `driver/ps5vk_private.h` -documents `ps5vk_cmd_buffer_refuse`: a refusal *"records that cmd_buffer cannot -encode a command: logs why, and recording ends with result at vkEndCommandBuffer"*. -A command buffer that ends in error is not submitted, so a refused command means -the draws never reached the GPU - and the flip can still succeed, which is exactly -the shape of "644 presents accepted, black screen". RetroArch calls -`vkEndCommandBuffer` at `gfx/drivers/vulkan.c:4212` and **discards the result**, so -the frontend cannot see this on its own. The driver's refusals in the trace are the -only witness. - -**Consequence.** Everything this port learned about its own `video_ps5` display -path - the correct buffer, the accepted flip, the marker, the splash call - is -about a driver that is not the one running. The display work is not wasted (it is -the fallback path and it is proven to the buffer), but it is not the current -blocker. The current blocker is that the Vulkan driver refuses commands the -frontend issues. - -## The frontend runs the linked libps5vk, and the driver refuses its draws - -**Measured, and this corrects the entry above it.** The stale-artefact reading was -wrong: after truncating `/app0/trace.txt` to zero bytes and running once, the fresh -trace contains the *same* refusals. They are produced by the run, not left over. - -**The build is the Vulkan build.** `build/ra-conf/config.h` has `HAVE_VULKAN 1` and -`HAVE_VULKAN_DISPLAY 1`; `gfx_drivers_vulkan.c.o` is built (177,104 bytes); the -image is 33,788,426 bytes, not the 8 MB of the hand-written driver. Two earlier -measurements of mine were against a different, smaller build and misled me - the -"zero video_vulkan strings" count and the 8,030,348-byte size. - -**The driver table is `video_vulkan` then `video_ps5`** (`video_drivers[]` in the -configured `gfx/video_driver.c`, `&video_ps5` at line 354). Vulkan is registered -first, and the trace's refusals quote `../PS5_Vulkan`'s own source -(`driver/ps5vk_pipeline.c:903`, `:137`, `driver/ps5vk_image.c:804`), so the running -video driver is the linked libps5vk and not this port's `video_ps5`. - -**`/app0/retroarch.log` is not stale and does not contradict this.** Its -`[Input] Found input driver: "ps5"` is the *input* driver, which this port does -supply (`src/` has a pad driver); `[Video] Found display server: "null"` is the -display server, a different slot again. Neither names the video driver, so the file -never said what I first thought it said. The config's `video_driver = "ps5"` names -a driver that is registered second, so it is not the one used either. - -**The blocker.** The frontend's draws are refused by the linked driver, and by -`driver/ps5vk_private.h`'s own rule a refusal ends recording with an error at -`vkEndCommandBuffer`, so those command buffers are not submitted - which is why -frames are accepted and the screen is black. RetroArch discards -`vkEndCommandBuffer`'s result (`gfx/drivers/vulkan.c:4212`), so the refusals in -this trace are the only witness the port has that its draws are going nowhere. - -Of the three refusal sources, the maintainer has already classified all three as -expected gaps rather than bugs: triangle strips (`driver/ps5vk_pipeline.c:896-904`), -storage images (`driver/ps5vk_descriptor_set_layout.c:20-38`, which would need a -libpsbc shader-compiler change), and sampler address modes -(`driver/ps5vk_image.c:800-806`, C4 scope). Two are avoidable from this side - the -descriptor type and, in principle, the topology - but the sampler address modes are -a driver-side gap. - -**What this means for the work so far.** The `video_ps5` display investigation -stands on its own and is proven to the buffer, but it is a second, unused path -while Vulkan is registered first. The live problem is the driver refusing the -frontend's commands. - -## The video driver is chosen by a compiled default, and the config cannot override it - -**Measured.** `tools/apply-port-patches.py` patches `configuration.c` so that -`case VIDEO_NULL:` returns a name instead of falling through - the compiled default -video driver, used when no config file is read. Its own comment says why the config -cannot help: *"There is no config file at runtime yet: content loading rebuilds argv -and drops the title's `-c`, and the fix for that is parked because reading the -config still crashes the launch."* So `/app0/retroarch.cfg`'s -`video_driver = "ps5"` is inert, even though the file is present, correct, and read -for its logging settings. - -**The ICD file is a red herring.** `/data/homebrew/PPSA99169/libvulkan.so.1` -(16,706,040 bytes) is beside the title, and renaming it away changed nothing: the -same refusals appeared. The Vulkan driver in use is the one linked statically from -`libps5vk.ps5.a`, not something loaded by name at runtime. - -**Changing the compiled default to `"ps5"` did not take.** The patch was rewritten -so `case VIDEO_NULL:` returns `"ps5"`, the configured tree was reset and the patches -re-applied, the change was confirmed in `build/ra-conf/configuration.c`, and the -title rebuilt (33,790,346 bytes). The run produced the same 21 topology, 3 -descriptor-type and 16 sampler address-mode refusals. Something other than that -switch still selects Vulkan, and it has not been found. `video_ps5` remains -registered in `video_drivers[]` at index 3 and unreachable in practice. - -**The blocker, stated once.** The frontend's draws are refused by the linked -libps5vk. Of the three refusal sources the maintainer has classified all three as -expected gaps, and the sampler address-mode restriction -(`driver/ps5vk_image.c:800-806`, C4 scope) has no workaround from this side: it is a -driver-side change in a tree this project does not modify. Until it is lifted - or -until the frontend is shown how to avoid it - the menu's draws never reach the GPU, -because a refusal ends recording with an error at `vkEndCommandBuffer` and -RetroArch discards that result. - -## The compiled default now names this project's own driver - -**Changed.** `tools/apply-port-patches.py`'s `configuration.c` patch returns `"ps5"` -instead of `"vulkan"`. That switch is the whole of the video-driver choice, because -its own comment records that the config cannot make it: content loading rebuilds -argv and drops the title's `-c`, so `/app0/retroarch.cfg`'s `video_driver` is never -read. Naming this project's driver there makes `video_ps5` the driver that runs, and -`video_ps5`'s display path is the one proven as far as the buffer. - -**Two traps hit while making it, both worth remembering.** - -1. **A build artifact lied about which source it came from.** After patching, the - object still behaved like the old one. The cause was that the port's build - applies the patch and then compiles, so the *order* of `build-retroarch.sh`'s - steps decides whether a patch reaches the object - and an object whose mtime is - newer than the already-patched source is kept. Deleting - `build/ra/obj/configuration.c.o` and rebuilding was the only way to be sure. The - lesson is the same one this project learned with `retroarch.c`: after changing a - patch, remove the object it belongs to rather than trusting the build to notice. -2. **A `strings` check on the driver name was meaningless.** Counting `^ps5$` in an - object cannot distinguish the two builds, because the linker merges string - suffixes and a short name may not survive as its own entry. It reported `ps5=0 - vulkan=1` for a binary that did contain the change. The driver name is the wrong - thing to grep for. - -**Not yet measured: whether this switch actually selects `video_ps5` on the -console.** The runs made with it in place still produced libps5vk's refusals, which -is why it is recorded as unverified rather than as working. The next run with the -console free should be judged on the trace alone: `video_ps5`'s marks -(`ps5_init entered`, `ps5_frame ...`) appear if it was selected, and the `vulkan:` -refusals appear if it was not. - -## The compiled default is `ps5` and the console still runs Vulkan - -**Measured, and this closes the route.** `build/ra-conf/configuration.c` line 1152's -`case VIDEO_NULL:` returns `"ps5"` - confirmed by reading the configured tree, not -by grepping a binary. The title was rebuilt from it (33,793,610 bytes) and the -console still produced the linked libps5vk's refusals: - - vulkan: only triangle lists without primitive restart are supported (VK_ERROR_UNKNOWN) x21 - vulkan: set 0 binding 3: descriptor type 3 has no proven table entry (VK_ERROR_UNKNOWN) x3 - vulkan: sampler address modes ... (VK_ERROR_UNKNOWN) x16 - -So `config_get_default_video()` is not what selects the video driver on this build, -and changing it has no effect. The frontend's own log names no video driver either - -it reports the *input* driver (`ps5`) and the display server (`null`), never the -video driver - so the frontend cannot say which driver it chose and the refusals in -the trace are the only witness. - -Two candidates remain, and both are outside this project: `frontend_driver_get_video_driver()` -(`gfx/video_driver.c` returns early if the frontend context supplies a driver, -before any name lookup) and RetroArch's own runner/profile mechanism. Neither was -determined. - -**The blocker, stated once and for the last time in this document.** The frontend's -draws are refused by the linked libps5vk. A refusal ends recording with an error at -`vkEndCommandBuffer` (`driver/ps5vk_private.h`), so those command buffers are never -submitted, which is why frames are accepted and the screen is black. Of the three -refusal sources the maintainer classified all three as expected gaps, and the -sampler address-mode restriction (`driver/ps5vk_image.c:800-806`, C4 scope) cannot -be worked around from this side. Until it is lifted, or until the frontend is -changed to request only clamp-to-edge samplers, the menu's draws do not reach the -GPU. Both are changes in `../PS5_Vulkan`, which this project does not modify. - -## The RetroArch menu is on the console's screen, drawn by this project's own driver - -**Measured, by the console's owner.** `PPSA99169` runs this port's `video_ps5` -driver, receives RGUI's 320x240 framebuffer, presents it, and the RetroArch menu is -visible on the television. That is the objective this project was built for. - -**The evidence, in one unattended `tools/run-title.sh` run** (capture -`klog/run-PPSA99169-080439.log`: one EXEC, **zero fatal-signal lines**; the script -launched it, watched 20 s, found it still running and closed it itself): - - ps5_init entered (video=present width=960 height=720) - display: virtual=200200000 physical=0x200000 bytes=0x2000000 two buffers at +0 - and +0x1000000 registered from 0 set 0, 1920x1080, format=0x8000000022000000 - ps5_init: told the frontend the display is 1920x1080 - input: pad opened, user=515310723 handle=51447552 - display: flip 1 of buffer 0, status=0 marker=1 - ps5_set_texture_frame: rgb32=0 320x240 frame=present have=1 (was 0) - menu: framebuffer commit 1 is a new picture (1 of 1 changed so far) - ps5_frame 1: menu 320x240 pitch=640 present=1 - ps5_frame 60: sources so far: menu=yes core=yes - ps5_frame 600: menu commits=3 changes=2 presented=yes - ps5_frame 1200: menu commits=3 changes=2 presented=yes - -Three things in that trace are worth naming, because each was a fault this document -previously recorded as open: - -- **`menu 320x240`** - RGUI's framebuffer reaches the driver. The commit that fixed - `width=0, height=0` is why: the driver now tells the frontend the display is - 1920x1080, so the menu renders into a real frame instead of a degenerate one. -- **`rgb32=0 ... 320x240 frame=present`** - the menu's RGB565 buffer arrives, and - `ps5_frame`'s conversion path handles the 16-bit source rather than dropping it. -- **`menu commits=3 changes=2`** - the framebuffer is not a static image. It changed - twice, once on a pad press (`input: press, pad=0x00000040 retropad=0x00000020` - appears immediately before a new commit), so the pad reaches the menu and the menu - responds by redrawing. - -**What actually unblocked it, and it was one anchoring mistake twice over.** The -video driver is chosen by `config_get_default_video()`, whose switch is over -`VIDEO_DEFAULT_DRIVER`; in this build that resolves to `VIDEO_VULKAN`, so the -function returns at `case VIDEO_VULKAN:` and never reaches the `case VIDEO_NULL:` -arm patch 0010 was editing. Changing that arm had no effect for exactly that reason. -Two corrections, both from ../PS5_Vulkan's maintainer and both verified here: - -1. `tools/apply-port-patches.py` now anchors on `case VIDEO_VULKAN:` and returns - `"ps5"` there - the arm that fires. -2. The driver-table edit inserted `&video_ps5` *after* the `#ifdef HAVE_VULKAN` - block, leaving `&video_vulkan` at index 0, and index 0 is what RetroArch falls - back to when a name is empty or unfindable. It is now inserted **before** the - block, so this project's driver is first as well as named. - -**One thing not explained.** The flip status marker reads 1 at flips 1, 300, 600, -900 and 1200 - it never advances, while the buffers rotate correctly (0, 1, 0, 1) and -the frames are visibly on screen. So the marker is not a usable instrument for -"has the display taken a newer frame" on this console, whatever it reports. The -display work was read through it for several rounds; the pictures on the screen are -the evidence that matters. - -## A1 is one choke point, not many sites - and the static buffers are exactly a quad - -**Measured by reading the code.** The strip topology the driver refuses is not chosen -in one place: `gfx_display.c:538`, `:678`, `:937`, `gfx_thumbnail.c:1005`, -`gfx_widgets.c:645` and `materialui.c:2583` all set -`draw.prim_type = GFX_DISPLAY_PRIM_TRIANGLESTRIP`, and the Vulkan driver derives the -pipeline from it - `disp_pipeline = ((draw->prim_type == GFX_DISPLAY_PRIM_TRIANGLESTRIP) << 1) | blend` -(`gfx/drivers/vulkan.c:1470`). So topology and geometry are chosen together, and -patching the pipeline alone would draw wrong triangles rather than refuse - a silent -wrong picture, which is worse than a refusal. Patching every call site is the wrong -shape for a port whose changes are meant to be a short named list. - -**The choke point is `gfx_display_vk_draw`** (`gfx/drivers/vulkan.c:1379`). Every menu -draw passes through it, and it already re-bakes the caller's separate vertex, -tex-coord and colour arrays into an interleaved VBO. A strip can be expanded to a -triangle list inside that loop, and the only other change is to report -`GFX_DISPLAY_PRIM_TRIANGLESTRIP` as the list pipeline, so the menu's own vertex data -does not have to change at all. - -**The trap that makes it delicate.** For four strip vertices the loop reads eight -floats from `vertex` - and when the caller supplies no coordinates, `vertex` is -`&vk_vertexes[0]`, a **static array of exactly 8 floats** (`:1246`), with -`vk_tex_coords[8]` (`:1253`) the same. A conversion that reads six interleaved -vertices instead of four therefore reads past the end of a static array. The -expansion has to be done as an index mapping - emitting `(N-2)*3` list vertices while -reading source index `s(i)` from the original arrays - not by duplicating the -interleaved values after the fact. - -**Not started.** The change is small but lands in the middle of the frontend's draw -path, next to the shader pipeline ids (`VIDEO_SHADER_MENU` .. `_5`, which have their -own `to_menu_pipeline` mapping at `:1453`). It wants a fresh round with the console -free, and it is the first of the three refusals rather than the whole job. - -## A2's format match fails on one remap, and three refusals are all that remain - -**Measured on the console, Vulkan running.** After the corrected A1 (all four -pipeline creation sites build triangle lists) the refusal count fell from 24 to -**3**, and every `only triangle lists without primitive restart` line is gone. The -three that remain are all one cause: - - vulkan: set 0 binding 3: descriptor type 3 has no proven table entry (VK_ERROR_UNKNOWN) - -That is the storage-image descriptor written by `vulkan_copy_staging_to_dynamic`'s -compute branch, which is taken when `dynamic->format != staging->format`. - -**Why they still differ, and it is one line.** -`vulkan_create_texture` remaps a format for the *optimal* texture but not for the -staging one: - - #define VK_REMAP_TO_TEXFMT(fmt) ((fmt == VK_FORMAT_R5G6B6... ) ? R8G8B8A8 : fmt) - ... - if (remap_tex_fmt != format) { - if (type == VULKAN_TEXTURE_STREAMED) ... /* keeps the original */ - else if (type == VULKAN_TEXTURE_DYNAMIC) format = remap_tex_fmt; - } - -The macro only rewrites RGB565; every other format passes through unchanged. Patch -0027 forces `fmt = VK_FORMAT_R8G8B8A8_UNORM`, which is *not* RGB565, so nothing is -remapped: the dynamic texture is created as R8G8B8A8 and the staging texture - the -same `fmt` argument - as R8G8B8A8 too. On that reading they should match, and the -compute branch should not be taken. It is taken, so something in the chain still -diverges, and where was not determined in this round. - -**The candidate worth checking first.** The comment in `vulkan_create_texture` shows -a probe that *undoes* a remap when the device can sample the remapped format -(`remap_probe.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT` sets -`remap_tex_fmt = format`). If that probe behaves differently for the dynamic and -staging textures - one created before the other, or with different `type` - the two -end up with different formats from the same input. A one-line trace of -`dynamic->format` and `staging->format` in `vulkan_copy_staging_to_dynamic` settles -it in a single run, and that is the next step rather than another inference. - -**Also settled in this round.** An earlier round had already changed -`configuration.c` to return "vulkan", so the compiled default is Vulkan and the -objective's "CPU path stays selectable" is satisfied by `video_ps5` remaining -registered, not by it being the default. A check of the deployed image for the -`patches/series, 0027` marker returned 0 and proved nothing: the marker is inside a -comment, and comments do not survive compilation. - -## Why the GPU path was black: seven faults that all failed silently (2026-09-19) - -Every fault below produced the same three symptoms - a clean trace with no refusal, -a driver presenting ~45 frames a second, and a black screen - which is why five -successive readings of the source produced five wrong answers and the faults were -finally found by instrumenting the console and reading one frame in order. - -The measurements, from the run that has all seven fixed, in trace order: - - menu iterate 0: state=17 ret=0 - rgui render 0: blit-state reached - rgui set_texture: rgui=yes dirty=1 data=yes - vulkan set_texture_frame: rgb32=0 320x240 frame=yes - vulkan create_texture: asked=1 type=1 320x240 fmt=37 image=no buffer=yes - vulkan create_texture: asked=2 type=2 320x240 fmt=37 image=yes buffer=no - vulkan menu state: flag=1 idx=0 staging(img=0 buf=1) optimal(img=1 buf=0) - vulkan copy_staging_to_dynamic: dynamic 320x240 fmt=37 type=2, staging fmt=37 type=1, compute=0 - vulkan draw_quad 0: texture=yes image=yes layout=5 320x240 pipe=yes - -1. **The last refusal was a compute pipeline, not an upload.** libps5vk's - `ps5vk_descriptor_options` walks every set-0 binding whose stage flags match the - stage being compiled and refuses any with stride 0; a storage image's stride is - zero by design (`ps5vk_descriptor_stride`), and this frontend's shared set - declares a compute-only storage image at binding 3. Exactly one pipeline is - compiled with COMPUTE against that layout - `rgb565_to_rgba8888`, the upload - shader - so the refusal fired once per `vulkan_init_pipelines` call: at init and - again on every swapchain recreation, three times in the recorded run, every line - identical. The pipeline is unnecessary here; 0027's single-format change had - already made its branch unreachable, and the branch would fail its own - `retro_assert`. Not compiling it ends the refusals (0036). - -2. **Widgets answered true for a driver whose menu widgets do not draw.** RetroArch - chooses between two mutually exclusive branches: `gfx_widgets_init` when the - driver reports `gfx_widgets_enabled`, else `gfx_display_init_first_driver`. The - latter is the only thing that sets `p_disp->dispctx`, and `gfx_display_draw` - returns immediately without it (`gfx_display.c:643`). Answering true therefore - cost the menu its display context and every draw returned in silence (0042). - -3. **Nothing enabled the menu texture.** The driver draws the menu only inside - `if (vk->flags & VK_FLAG_MENU_ENABLE)`, and the only place in the whole frontend - that sets it true is `display_menu_libretro` - whose own comment says "Display - the libretro core's framebuffer onscreen". It is a libretro concept and it is set - when a core runs. This title launches into the menu with no content, so it was - never set. `video_ps5` never noticed because it ignores the flag and draws - whatever `set_texture_frame` handed it (0046). - -4. **The sampled image was created and never filled.** `vulkan_set_texture_frame` - fills `vk->menu.textures[]` (staging) and the draw samples - `vk->menu.textures_optimal[]`. The optimal image is written by exactly one thing, - `vulkan_copy_staging_to_dynamic`, called from `vulkan_frame`'s upload block. That - block is gated on `vk->menu.dirty[]`, which this function sets at its end for - both cases - but the function itself fills the optimal image only when the - staging texture was just created. From the second handover on it took the `else` - branch, which flushed the staging texture to the GPU and never copied it, so the - menu was drawn every frame from an untouched allocation: a valid image, a valid - pipeline, a valid draw, black pixels (0051). - -5. **A leftover file on the console ended every run.** `/app0/args.txt` is read by - the driver as well as by `src/main.cpp`, deploy never deletes anything, and a - capture file left by an earlier probe made each run capture a frame at 90, write - it, and then call `command_event(CMD_EVENT_QUIT, NULL)`. The title therefore - disappeared about 1.5 seconds after it appeared, and the console booked it as an - application crash with a coredump - a symptom that reads as "it crashes and there - is no picture". The quit is gone (0039) and `tools/run-title.sh` clears the file - before every run (0043). - -6. **The draw was instrumented in the wrong place.** `gfx_display_vk_draw` is the - display-list path used by GL-style drivers. This driver composites the menu - itself, inside `vulkan_frame`, through `vulkan_draw_quad`. A probe there reported - "no draws" for three rounds, which was true of the path being watched and false - about the driver (0053). - -7. **A probe ended the run it was measuring.** The capture block's - `CMD_EVENT_QUIT` is the mechanism in fault 5; recorded separately because the - general rule is the one worth keeping: a probe must not change the run. - -Left open, precisely: with the menu texture filled, the quad drawn every frame with -`texture=yes image=yes layout=5 320x240 pipe=yes`, and no refusal anywhere, the -screen is still black. The next question is what the driver does with that draw - -`../PS5_Vulkan`'s `ps5vk_draw.c`, the AGC stream it writes for the quad and the -viewport it programs - not any further gate on this side. - -## 2026-09-19 — Successful Vulkan calls behind the shell splash - -The diagnostic title's symbol loaders now observe every frontend call to -vkEndCommandBuffer, vkQueueSubmit and vkQueuePresentKHR. In the manually closed -run recorded in `evidence/vulkan-api-results-splash/`, the first four calls to -each returned VK_SUCCESS, including per-swapchain presentation results; the -537-line fresh startup segment contains no refusal. The quad's alpha is 1 and -its 2880x2160 viewport begins at x=480 inside the 3840x2160 swapchain. - -The owner saw the application background rather than RGUI and confirmed a manual -close. The kernel log kept SplashScreen.PPSA99169 focused until the close. -`src/display.cpp` dismisses that splash in the CPU display's open method, which -Vulkan never calls. The sibling's diagnostic title also explicitly hides the -splash before its Vulkan work. This identifies a missing title-lifecycle call; -it does not yet establish whether rendered menu pixels underneath are correct. - -The raw trace appends across launches, including after a truncated prior line. -The newest startup must therefore be selected by the last `bss check=` substring -rather than a line-anchored match. The run script's "exited on its own" message -means only that its final process query found no running title. - -## 2026-09-19 — Splash removal reveals the GPU menu and colour faults - -The title-level splash call returned 0 in the new run, and the owner confirmed -that the menu was visible with buggy colours and flicker. The first four -end/submit/present/per-image results remained VK_SUCCESS; no refusal was in the -retrieved 471-line startup. See `evidence/vulkan-splash-dismissal/`. No complete -90-second acceptance is inferred because the title was closed before the final -query. The CPU driver and sibling Vulkan project were not changed. - -The RGUI producer `argb32_to_rgba4444` returns `(r << 12) | (g << 8) | (b << 4) | a`. -Patch 0028 instead writes the blue nibble into the R8G8B8A8 texture's red byte and -red into blue, and shifts four-bit channels into only the high nibble of each -byte (15 becomes 240). This is a frontend conversion defect, independently of -the still-unexplained flicker; fixing it preserves matching 32-bit upload formats. - -## 2026-09-19 — RGUI upload and remaining visual corruption - -The RGUI producer packs R in bits 12-15, G in 8-11, B in 4-7 and A in 0-3. -Patch 0028 reversed R/B and expanded to 240 rather than 255; 0055 corrects both. -The exhaustive producer-to-upload test covers all 65,536 words. Evidence is in -`evidence/vulkan-menu-rgba/`; the owner sees green RGUI but blue flicker and fixed -black triangles remain. Successful Vulkan calls alone do not prove correct pixels. -Read-only driver findings record an earlier similar blue blending defect fixed by -FP16 exports; current code skips blend-register writes on opaque draws. This -suggests a mixed-pipeline diagnostic, not a confirmed cause of this title's issue. - -## 2026-09-19 — Fragment input aliasing explains the visible corruption - -The uploaded menu texture and six white vertex colours are correct. Captured GPU -frames are already corrupt before presentation: opaque-white source samples never -render white, and colour gradients follow the two quad triangles. The driver's -standalone compiler maps both fragment varyings to base 0 because it omits -`ac_nir_assign_fs_input_locations` before shader info. ACO therefore reads colour -from texture-coordinate components, including undefined blue/alpha. Empty input -semantics and unresolved AGC linkage accompany an otherwise successful compile. - -The user authorized driver modifications during this investigation. PS5_Vulkan -commit **6f0ce0d** fixes its compiler work copy without SDK edits. The resulting -RetroArch run has zero refusals/errors, 909/909 correct opaque-white samples, -identical consecutive frames, and owner confirmation of correct colours without -flicker or triangles. Evidence: `evidence/vulkan-fragment-inputs/`. The earlier -blend-state hypothesis did not fix the defect and is not the established cause. - -## 2026-09-19 — XMB exposes descriptor, geometry and texture assumptions - -XMB's ribbon uploads two floats (8 bytes); libps5vk currently requires whole -16-byte UBO records. Padding the upload without moving those fields clears that -refusal. Its untextured effect omits sampler writes, while the driver's metadata -validates stage-visible layout entries even when the shader does not sample -those slots. Binding the existing white texture/nearest sampler clears the second -refusal. These are frontend compatibility measures, not expanded driver coverage. - -The port's old strip converter read beyond source arrays and submitted the -unexpanded count. Its replacement emits each consecutive triple with alternating -winding (a quad becomes 0,1,2,2,1,3) and submits the expanded count for both menu -effects and icons. A host C regression exercises strips through 8,064 vertices. - -The title selects the null platform frontend. Merely defining ASSETS_DIR for -platform_unix does not initialize the asset path; setting the title default to -/app0/assets makes the shipped monochrome icons/font load. Startup logging now -records the resolved paths and whether assets/fonts are ready. - -The earlier whole-row compatibility patch widened images but retained normalized -coordinates for their logical width. Readback showed a thin background strip and -font glyph fragments. Scaling display UVs and normalizing glyph offsets by the -physical image width restores sampling of the intended content. Row alignment -also depends on format: 256-byte rows mean 64 RGBA8 texels or 256 R8 texels. - -The 256x256 icons (whose base width is already aligned) showed repeated/cropped -mip fragments. Single-level static images render correctly in the corrected -capture. The driver internals behind mipmapped corruption are not established; -this port keeps one mip level pending separate mip-chain investigation. - -Owner: "It's flawless!" after the rendering corrections. Final normal build: -45 seconds, zero refusals/API errors, successful presentations, assets loaded, -no screenshot writes. Evidence: `evidence/xmb-default/`. PS5_Vulkan was unchanged. - -### 2026-09-19 — native AudioOut behind RetroArch's PCM interface - -ProsperoLight's `src/moonlight_stream.cpp` supplies the native output ABI and -sequence: system user `0xff`, main port `0`, 256-frame grains, 48 kHz, format `1` -(signed 16-bit stereo), and already-initialized code `0x8026000e`. Its decoder -and SDL are unnecessary because RetroArch already supplies PCM. The new backend -uses those output calls directly and is registered by named patch 0067. - -RetroArch's audio header describes frame counts, but the actual producer and -existing backends exchange **bytes**. `audio_ps5` therefore exposes byte counts -and reports `use_float=false`; four bytes form one interleaved stereo frame. -Returning 48 kHz through `new_rate` makes the frontend's resampler responsible -for other rates. The worker releases its mutex around the paced native output -call, keeping the producer's queue operations independent of that call. - -The opt-in console test requested 44.1 kHz and negotiated 48 kHz, submitted -1/255/257/1000-frame chunks, exercised pause/resume and nonblocking backpressure, -and recorded 193,536 accepted and played frames, zero discarded frames/errors, -and peak queue occupancy equal to its 1,536-frame capacity. Native drain returned -positive `0x100`, close returned zero: nonnegative output results are successful, -not necessarily exactly zero. The port reopened for normal frontend audio. -The owner heard the expected left/right/left/right sequence and saw RetroArch -start. The first run was manually closed, not a spontaneous exit. - -The test's 6,656 zero-filled frames include idle output, partial grains and pauses; -this is not a measurement of uninterrupted core streaming. It proves the backend -queue and native playback path, not core A/V synchronization or long-run underrun -freedom. `evidence/native-audio/` holds the structured report and acceptance. - -### 2026-09-19 — configuration and directory browsing require a native frontend - -The user's report was correct: configuration was not reliably loaded, and the -browser offered only `/`. The null platform frontend supplied neither an -environment callback nor a drive list. task_content substituted its menu -callback at startup, reconstructed argv from uninitialized content state, and -lost `-c`. Native `frontend_ctx_ps5` now preserves the initial arguments and -supplies application-relative defaults and accessible roots (patch 0068). - -The first run restored config loading but reproduced the historical `rip=0` -crash. Symbolizing its backtrace against that exact ELF revealed -`fill_pathname_abbreviate_special -> config_set_path -> config_save_file -> -command_event_save_config -> menu_driver_init`. The application-path helper used -Unix procfs/getpid/readlink discovery; a title has a known executable path and no -such procfs contract. Patch 0069 returns `/app0/eboot.bin`. The next run saved -`/app0/config/retroarch.cfg` successfully, confirmed by both the log and owner. -This replaces the earlier suspicion that config parsing or controller setup was -the root cause: the captured fault was in path abbreviation during config saving. - -Browsing was a separate remaining failure. The shipped libc `opendir` returned -EPERM for `/`, `/app0` and `/app0/cores`, despite ordinary file I/O and directory -creation working. The public SDK's `open(O_DIRECTORY)` and `getdents` route opened -those directories. A 4 KiB read enumerated `/` but failed with EINVAL on the -mounted `/app0` filesystem. A 64 KiB read enumerated 96 entries in `/app0` and two -in its empty `cores` directory, with zero errors. The minimum acceptable size was -not measured. Patch 0070 routes RetroArch VFS directory operations through the -owned adapter, which validates records and retains native permission failures. -`/data` and `/mnt/usb0` were unavailable inside this title and are not advertised. - -The owner then confirmed: "Folders are visible, and when I select Load -configuration file I could see the .cfg file". The final build survived the -90-second window and was script-closed, with zero Vulkan refusals/API failures -and working audio/XMB initialization. The 108,188-byte saved config was identical -before and after another upload/restart. Packaged seeds and live settings now -occupy different paths, so deployment cannot replace saved preferences. -`docs/DEPLOYMENT.md` maps `/app0` to `/data/homebrew/PPSA99169/` for FTP. - -A capture-tool issue also surfaced: building the next diagnostic while an older -run completed changed the local identity header and falsely rejected its log. -The runner now snapshots the selected identity before deployment/launch. That -intermediate run remains a partial result, not final acceptance. Sanitized -captures and the failure sequence are in `evidence/native-paths/`. - -## 2026-09-19 — Managed directory modes and actual FTP writes - -The app-created folders listed as `0755`; the owner's FTP-created downloads -folder listed as `0777`. Owner identities differed, while the displayed group -matched. Applying `0775` successfully changed all seven managed directory modes, -but disposable FTP uploads still returned `550 Permission denied` in each. The -listing alone does not establish the FTP process's effective access credentials. - -The owner-authorized `0777` fallback passed actual upload, exact readback and -cleanup in `config`, `cores`, `content`, `system`, `savefiles`, `savestates` and -`playlists`. Startup calls chmod even after mkdir reports EEXIST, repairing older -folders and defeating creation-time umask restrictions. No recursive file-mode -change is needed for this verified upload case. Evidence and both outcomes: -`evidence/ftp-directory-permissions/`; reproduce using -`tools/run-title.sh --no-build --watch 30` and `python3 tools/check-ftp-write.py`. - -## 2026-09-19 — Input-only pad reads bypass menu analog and binding capture - -The old `input_ps5.input_state` returned hardcoded RetroPad buttons directly. -Upstream `input_state_wrap` calls that even without a joypad, explaining why menu -buttons worked. In contrast, `input_driver_collect_system_input` reads sticks -through `input_joypad_analog_axis(primary_joypad, ...)`, and the binding screen -calls `primary_joypad->poll/button/axis`. No native joypad was registered, leaving -those paths without input. This supersedes the earlier finding that a null -joypad was this port's normal state; the defensive null guard remains useful. - -`ps5_joypad` plus a built-in autoconfiguration profile supplies those interfaces. -The input interface must stop returning hardcoded buttons: its result is ORed -with mapped joypad input, which would otherwise keep old bindings active after -reassignment. Host tests call the upstream wrapper to prove this priority and -upstream analog helpers to prove menu stick/deadzone behavior. A zero-sample -`scePadRead` preserves the last state because binding capture can poll again -within a frame. Errors, disconnect samples and interception suppress input. - -The owner tested left-stick navigation and button/stick binding capture on build -`e6211dea9fd0bb512e83a9979a6a44c0c8c5fdf175564c753e3fd2a25067e0a9` -and replied "Everything works flawlessly!" Evidence: `evidence/native-joypad/`. - - -## 2026-09-19 — FCEUmm metadata and executable loading are separate gates - -An empty saved `libretro_info_path` overrides the new platform default. -`core_info_init_list()` then searches the core directory, so staging only -`info/fceumm_libretro.info` is insufficient for existing configurations. Staging -an identical file beside the `.so` and forcing a metadata cache refresh produced -a console cache entry with `has_info: true`, full FCEUmm name and NES extensions. -The user's provided Gambatte `.info` uses the same ordinary libretro format; -metadata does not select a different executable ABI or install a loader. - -The pinned native-SDK FCEUmm ELF exports all 25 callbacks and imports kernel_web, -libc and Posix stubs. Its final bytes survive FTP unchanged. Nonetheless native -core open returns failure and a null error string. A subsequent manual content -launch reinitializes without a core path, then dereferences a null Vulkan video -context at `vulkan_alive+0x26` (ELF 0x6b3066) reading address 0x80. The owner -confirmed the crash. This is not evidence that FCEUmm reached emulation or that -libps5vk refused GPU work. Keep loader and error-recovery acceptance open. -Evidence: `evidence/fceumm-build/`; next work: `parked/native-core-loading/`. - -## 2026-09-19: native ELF core loading and the first software-core frame - -The native title's payload-oriented dlfcn route did not open the staged FCEUmm -ELF. The local native converter also refuses application exports. A no-game -probe using public anonymous mmap, RW-to-RX mprotect and a return-42 function -succeeded (`klog/run-PPSA99169-141420.log`). This supported implementing a bounded -application ELF loader without websrv hooks, a donor SDK or kernel changes. - -The core now uses `tooling/native/ps5-core.ld` to separate 16 KiB RX/R/RW pages; -`-T` is required because the previous shared layout combined code and data. -Runtime imports are explicit title addresses generated from the core's dynamic -symbols, including this port's directory adapters. Bounds, relocations, imports -and final protection are checked before publishing a reference-counted handle. -TLS/constructors/unwind support is deliberately outside this first contract. - -Eight pre-frontend load/API/identity/unload cycles passed, but the first manual -selection failed reading the same file after XMB started. That diagnostic did -not cover the failing lifecycle stage. Replacing the single stdio/heap read with -bounded POSIX reads into mapped memory resolved the observed failure. The exact -libc/allocator cause remains unisolated; the new diagnostic loads with the full -frontend resident and tests rejection through the actual menu task functions. - -The next manual game start reached FCEUmm's video callback and aborted inside -`vulkan_copy_staging_to_dynamic`: a BGRA staging buffer differed from the sampled -RGBA image, selecting the RGB565-only compute branch. The core-frame upload now -requests RGBA8 for both textures and converts XRGB8888 channel order/alpha while -respecting row pitches. This is the existing GPU presentation route, not a switch -to CPU video. Tests exercise colours, padding and in-place writes. - -The accepted build is recorded in `evidence/native-core-loading/`. The owner -confirmed flawless gameplay and reported manual close because no return-to-menu -shortcut was set. The capture contains three current-build launches with zero -Vulkan refusals, GPU failure records or fatal signals; it is not a continuous -180-second gameplay measurement. Repeated game unload and saves remain untested. - - -## 2026-09-19 — mGBA and content transition lifetime - -mGBA needs 21 C log-category initializers. The loader now validates the bounded, -relocated callback table and every executable target before invoking any entry, -once per mapped image. Eight real-core load/unload cycles and a load with the -frontend resident passed. Native getcwd crashed in mCoreConfigPortablePath; -explicit `/app0/config/mgba` avoids that import. GBA 7z extraction then failed a -16 MiB allocation before the core received content. Routing large title/core -allocations to tracked anonymous mappings resolved that observed failure. - -The core's 32-bit renderer produces XBGR while libretro declares XRGB. A separate -callback conversion preserves the native image. Frontend menu decode/upload now -uses RGBA consistently. Refusing a writable software framebuffer prevents the -XRGB-to-RGBA upload from mutating cached source pixels during paused menu frames. -Owner confirmed colours and native Quit fixed, but Close Content still flickered -for both mGBA and FCEUmm and polluted later games. This was a distinct defect. - -The existing texture workaround widens images to 256-byte rows; uploads populate -only logical pixels. Menu/font UVs compensated, but the filter-chain quad still -sampled 0..1 across physical width. Tiny blank frames and unaligned GB/GBA widths -therefore exposed unwritten padding. Patch 0078 supplies sampled width separately, -crops both triangles, and isolates mutable VBOs by pass and retired sync slot. -Substituted image formats now determine padded width consistently. Owner confirms -“Menu and next game are clean.” The captured final transitions were GBA -> menu --> GB/GBC-sized content -> menu. This does not independently verify every shader -or a fresh FCEUmm/XMB matrix. PS5_Vulkan was not edited; linked archive hashes are -recorded because the sibling is concurrently developed. - -The old CRT return path called exit(0) then produced SIGSYS. Calling native -sceSystemServiceLoadExec("exit", nullptr) after frontend cleanup produced shell -LoadExec termination without fatal signal reports, also confirmed by the owner. -Evidence and failed iterations: `evidence/mgba-native/`; replay with -`bash tools/verify.sh evidence`. Saves, state restoration and long-run timing are -not established by these loading/transition tests. - - -## 2026-09-19 — Snes9x pixel contract and C++ core unload - -Snes9x 1.63's libretro renderer outputs RGB565 and negotiates that format at -normal and subsystem content load. The port leaves renderer arithmetic and -filter output intact, then converts at the three final video callback sites to -independent XRGB8888 storage. Both negotiation sites declare XRGB8888, matching -the existing frontend's RGBA conversion and matching-format staging/image copy. -All 65,536 RGB565 inputs pass through both actual helpers in the host test with -correct expanded R/G/B channels and alpha 255. Additional tests cover non-tight -pitch, repeated frames without source mutation, bounds, hires and NTSC widths. -Owner's console result: “Works flawlessly” for gameplay and menu transitions; -trace confirms XRGB8888 game output and return to the dummy menu. - -This core has four C++ initializers. A plain build imports __cxa_atexit, which -would register callbacks into a mapping that the native loader later frees. -The deployed core instead hides a module-local destructor registry behind the -upstream export map. Its single fini-array callback drains that list before -unmapping. Loader validates the entire bounded finalizer table, and all targets, -before constructors run; last-reference close calls it in reverse array order. -Host tests verify destruction order, reference counting, repeated reload and -rejection without side effects. Eight actual Snes9x load/unload cycles and the -resident-menu recovery test pass on the PS5. This is not TLS or general C++ -exception/unwind support. - -Source and linked archive provenance are in `evidence/snes9x-native/`. -PS5_Vulkan was not edited; FCEUmm/mGBA core binaries retain their accepted hashes. - -## 2026-09-19 — FBNeo formats, metadata ownership and catalogue allocation - -Pinned FBNeo `6bb3167a044e19e7106a5110d5531aa9c6afa96f` has native XRGB8888 -HighCol32 output and RGB565-only driver paths. Preserving those internal paths -and converting only 16-bit video callbacks to XRGB8888 works with the existing -frontend RGBA upload. Exhaustive host tests exercise every 32-bit RGB input and -RGB565 value; the accepted console log exercises both native depths, including -304x224 and 384x224 games and intervening menus. Owner: “Works flawlessly!” -No frontend video or PS5_Vulkan change was required. - -Two console failures constrained the port. First, changing the metadata version -buffer to static storage while retaining upstream free() aborted in native libc. -The old free was removed, and 10,000 sanitizer-tested calls to the patched -metadata function now check ownership and whole-archive flags. Second, the full -28,910-driver catalogue allocates three small name buffers per driver. It exhausted -the native heap during BurnLibInit, leading to NULL strcpy (return offset 0x10d82c -in the second core). A bulk name store and pointer tables use the existing -large-buffer mapping path. Validation is atomic with respect to driver pointers; -cleanup restores originals. Failure and repeated initialization tests cover -30,000 drivers. Actual maximum short/full-name lengths are 31/166 bytes. - -The native loader still does not register exception unwind tables. FBNeo's MPEG -layer 2/AMM bit-limit exception is replaced locally with setjmp/longjmp across -primitive-only frames. Differential sanitizer tests compare the pinned original -and adapted decoders on complete/truncated synthetic input; this adds no general -C++ exception support. Existing per-core destructor registration remains in use. - -The accepted logs have no GPU refusals/API failure records, kernel fatal signals -or audio backend errors. Missing-core ERROR lines are deliberate recovery tests. -Optional environment command 87 carries an endian-dependent serialization hint -which this frontend does not recognize; gameplay succeeds, but cross-platform -state compatibility is not established. See `evidence/fbneo-native/` for exact -builds, failed iterations, reports, declared geometries/rates and linked hashes. - -## 2026-09-19 — Genesis Plus GX RGB565 renderer with XRGB8888 callbacks - -Pinned Genesis Plus GX c2838c7dc4236fc2fe94e5dbd08b41486067918e uses -USE_16BPP_RENDERING and FRONTEND_SUPPORTS_RGB565 in its Unix libretro build. -Changing the renderer to 32-bit would also affect NTSC filters, cursors and Game -Gear LCD persistence. The port retains those internals and converts into a -separate callback buffer, preserving the original bitmap across cached frames. -The bitmap is 720x576 with a 1440-byte pitch; cropped SMS/NTSC viewports start at -byte offsets 16/40. The conversion checks row, input and output bounds, accounts -for that offset, and emits tightly packed 0x00RRGGBB. The existing frontend -converts it to RGBA bytes for Vulkan without modifying core-owned storage. - -`tests/test_genesis_plus_gx_video.py` checks all 65,536 values using the pinned -upstream PIXEL macro through the real upload helper, and exercises cropped, -interlaced-size and maximum-size buffers, cached frames, resolution changes and -invalid bounds. These host dimensions are not console feature acceptance. -`evidence/genesis-plus-gx-native/` records the ABI, exact source/metadata/SDK/port -hashes, driver archives, loader/recovery reports and sanitized console results. -The owner confirmed gameplay and clean Close Content/next-game transitions. -Two same-build launches report no Vulkan refusals or GPU API failures and both -quit natively with status 0. The other four core bytes and four driver archive -hashes are unchanged. - -Two ZIP extraction failures in the first frontend snapshot occurred before the -core received content. The owner identified those selections as Sega System -16/32 arcade archives, outside this core's system coverage. FBNeo has drivers -for those boards. No archive parser change is warranted by this evidence, and -no claim is made that the archive structures themselves were inspected. A later -same-build frontend snapshot has no ERROR lines. See ACTIVE for current scope. - - -## 2026-09-19 — XMB list allocation size, rather than scroll speed, triggers this capture - -The first-failure diagnostic now attributes a 7,119,712-byte native-request rise -to construction of a destination playlist, with 339 new 15,488-byte XMB nodes -accounting for 5,250,432 bytes in 23 ms. Old-node cleanup completed first; the -observed node count dropped from four to three before growth. The owner reports -natural navigation, so rapid input is not required. The console's one custom -playlist contains 7,384 entries: all nodes would request 114,363,392 bytes before -callbacks (4,784,832), strings and playlist structures. These sub-1-MiB individual -requests stay on the native allocator path. - -The later NULL write in the driver's Mesa allocation-error logger remains a -separate failure-handling defect; guarding it alone would not allocate the missing -menu nodes. XMB's non-diagnostic patch set is unchanged from its initial port, -but an older binary has not been retested with the same full playlist/config. -No historical regression verdict or exact heap-capacity claim is established. -See `evidence/xmb-playlist-allocation-crash/` for sanitized first-failure owners, -context history, source comparison and playlist counts. Raw user data remains -in ignored capture storage. - -## 2026-09-20 — the driver samples the byte order the frontend uploads - -`../PS5_Vulkan`'s `VK_FORMAT_B8G8R8A8_UNORM` entry is a sampled one -(`driver/ps5vk_image.c`: `SAMPLED_IMAGE_BIT`, the linear filter, the transfer and -blit pairs, and the `8_8_8_8_UNORM` word with the `ZYXW` selectors, which read the -memory's B, G, R, A as R, G, B, A). That project's `v0-formats` battery proves the -fetch on the console and `docs/M5_PHASE_C.md`'s round 6 records the words: -`B8G8R8A8_UNORM` takes `ZYXW`, "VideoOut's byte order", while -`A8B8G8R8_UNORM_PACK32` takes `WZYX` because the console's fetch of *that* format -reverses. The frontend uploads libretro's little-endian XRGB8888, whose four bytes -are the same B, G, R, A, so the two now agree byte for byte and no conversion -between them is owed. - -Four of this port's changes existed because that entry did not: - -- **0014** substituted `R8G8B8A8_UNORM` for any format whose entry did not - advertise `SAMPLED_IMAGE_BIT`. An application's B, G, R, A bytes then landed in - an image the driver reads as R, G, B, A: the red and blue reversal. The usage - mask in the same block is what actually mattered, and it stays. -- **0075** made `vk->tex_fmt` R8G8B8A8 for a 32-bit core frame and converted every - frame's channels on the CPU into it, because a B8G8R8A8 staging buffer beside a - substituted R8G8B8A8 image selected `vulkan_copy_staging_to_dynamic`'s - RGB565-only compute branch and aborted there. -- **0077** decoded menu images as RGBA and uploaded them as R8G8B8A8, and refused - the writable software framebuffer so that the XRGB-to-RGBA conversion could not - mutate cached source pixels while the Quick Menu paused emulation. -- **0081** is kept. It makes `supports_rgba` follow the order a caller asked for, - where upstream leaves `false` behind its own TODO. It is inert without an RGBA - request and correct with one, so it is not part of the reversal. - -The withdrawal is `tools/apply-port-patches.py`'s `WITHDRAWN` tuple: each marker -names an EDITS block whose insertion is exchanged for the upstream text it -replaced. A tree that already carries the insertion and a fresh tree end in the -same file, which is checked by applying the patcher to a copy of -`vendor/retroarch`'s `gfx/drivers/vulkan.c` and to the withdrawn `build/ra-conf` -and comparing the two (identical). Two blocks that look like the same decision -stay, for reasons that do not depend on the sample order: **0027** and **0055** -convert the menu's 16-bit RGBA4444 frame by hand into R8G8B8A8, because this -driver still refuses a view whose component mapping is not the identity and so -cannot sample the B4G4R4A4 texture with the B/R swizzle upstream asks for; and -**0015** stays for upstream's own `VK_REMAP_TO_TEXFMT`, which turns an RGB565 -request into R8G8B8A8. - -What this does not establish: the withdrawn path is the one the console accepted -earlier (owner-confirmed colours in `native-core-loading/`, `mgba-native/` and -`xmb-safe-list-run/`), and the native byte order has host evidence and the -driver's own console battery so far. The first console run after this change is -what decides the title's colours. - -A second finding, from the same round: `tools/core-imports.py` rewrites -`build/core_imports.inc`, and that file is insertion-ordered by the core list, so -running the tool by hand to read a count changes a build input and with it the -build identity. The identity does not lie about what it hashed — recomputing it -from its own inputs reproduced the printed identity — but a tree whose include was -rewritten by hand no longer describes the eboot that was built from it. - -## 2026-09-20 — the native byte order is confirmed on the console - -The owner launched the build that carries `../PS5_Vulkan` `8b311e3` and this port's -withdrawn compensation, and reports it flawless with the colours accurate. So the -pair is settled from both ends: the driver's own `v0-formats` battery proves that -`VK_FORMAT_B8G8R8A8_UNORM` fetches B, G, R, A in the order an application writes -them, and the frontend's own screen proves that keeping libretro's little-endian -XRGB8888 through the upload draws the right channels — menu, core frames and the -transitions between them. The condition that made 0014, 0075 and 0077 necessary is -therefore not merely absent from the driver's format table but absent from the -picture the console draws. - -Two limits belong with that sentence rather than after it. The acceptance is an -owner observation: no kernel log or trace was captured for the launch, so there is -no distilled record of the run itself, only of the bytes that were deployed and the -gates the build passed. And PPSSPP was not launched, so the parked first-frame -blocker is untouched and untested by this step. diff --git a/docs/GPU_PATH_CRITERIA.md b/docs/GPU_PATH_CRITERIA.md deleted file mode 100644 index fb81dbd..0000000 --- a/docs/GPU_PATH_CRITERIA.md +++ /dev/null @@ -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. diff --git a/docs/GPU_TIMING.md b/docs/GPU_TIMING.md deleted file mode 100644 index 8018b07..0000000 --- a/docs/GPU_TIMING.md +++ /dev/null @@ -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-.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-.log`, `gpu-profile-.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. diff --git a/docs/LRPS2_PORT.md b/docs/LRPS2_PORT.md deleted file mode 100644 index 8b26098..0000000 --- a/docs/LRPS2_PORT.md +++ /dev/null @@ -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. diff --git a/docs/MEMORY_DIAGNOSTICS.md b/docs/MEMORY_DIAGNOSTICS.md deleted file mode 100644 index 6db590c..0000000 --- a/docs/MEMORY_DIAGNOSTICS.md +++ /dev/null @@ -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 `. 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. diff --git a/docs/PHASE_LOG.md b/docs/PHASE_LOG.md deleted file mode 100644 index 1255da9..0000000 --- a/docs/PHASE_LOG.md +++ /dev/null @@ -1,4552 +0,0 @@ -# Phase log - -Append-only. A dated entry per landed step, newest at the end. Never rewrite an -existing entry: a correction is a new entry that says what it corrects. This -file is read by search and by its tail, never end to end. - -An entry holds, in this order: what changed, why it is right, the evidence (the -exact command or run and what it returned), and the commit. Keep the failures in -too — the runs that were wrong are what stop the next agent from repeating them. - ---- - -## 2026-09-18: The documentation contract is instantiated for PS5 RetroArch - -The repository stopped being a template. `AGENTS.md`, `docs/PLAN.md`, -`docs/ACTIVE.md`, `docs/REFERENCE.md`, `docs/TESTING.md`, -`docs/DEPLOYMENT.md`, `docs/TROUBLESHOOTING.md`, `docs/FINDINGS.md` and -`tools/verify.sh` now describe a PS5 RetroArch port: its gates, its milestone -map (M0 contract, M1 console shell, M2 video and input, M3 cores and storage, -M4 audio, mapping and release), its four invariants, its step ladder, the -pinned environment, and the evidence rules. It exists because the project has -to be defined before any code is written, and it unblocks every later step. - -**The evidence.** `grep -rno '{{[A-Z0-9_]*}}' AGENTS.md docs tools .gitignore` -returns nothing, so no template placeholder is left in the read path; -`bash -n tools/verify.sh` and `tools/verify.sh --list` return the five gate -commands; the toolchain was proven to work by cross-compiling a hello-world C -file, which produced an ELF 64-bit LSB pie executable, x86-64, version 1 -(FreeBSD), 110,712 bytes, from `prospero-clang` 22.1.8 targeting -`x86_64-sie-ps5`. The four findings of the survey are committed in -`docs/FINDINGS.md`; the environment facts are in `docs/REFERENCE.md`. - -**What was tried first.** The first draft of the plan assumed the shipped -graphics path was a Vulkan context driver through `../PS5_Vulkan` and treated -the OpenGL project as unrelated. Reading the two local checkouts showed the -opposite ordering: the OpenGL project is the only one with an installed consumer -package, a pkg-config contract and a recorded hardware acceptance run, while the -Vulkan project has driver sources and headers but no consumer package yet. -`docs/REFERENCE.md` now records both stages with the M2.1 measurement that -decides between them, instead of the assumption. Recorded here so the next step -does not rediscover it. - -**Commit.** `3dfc3dd` — Instantiate the agent documentation contract for PS5 -RetroArch. - ---- - -## 2026-09-18: The existing PS5 RetroArch payload recipe joins the repository as a baseline - -`reference/ps5-retroarch/` now holds `ps5-payload-dev/websrv`'s -`homebrew/RetroArch/` at commit `1afd476`, copied by sparse checkout and kept -read-only, with `PROVENANCE.txt` recording the source, the revision, the fetch -date and a SHA-256 for every file. It exists because that recipe already turns -the upstream RetroArch tarball into a loadable PS5 payload, so this project -starts from a known baseline instead of from nothing, and it unblocks the -packaging work: the recipe's staging step already produces the title's -`icon0.png` and its `retroarch.cfg` seed. - -**The evidence.** The clone is reproducible with -`git clone --filter=blob:none --no-checkout --depth 1 -https://github.com/ps5-payload-dev/websrv.git` followed by -`git sparse-checkout set homebrew/RetroArch`; it returned twelve files totalling -48 KB, and `sha256sum` of each is committed in the recipe's `PROVENANCE.txt`. -Reading `build.sh` shows it pins upstream **1.21.0** and configures with -`OS=BSD`, `--enable-sdl2 --enable-mmap --enable-dylib` and every GPU switch -disabled, which is what `docs/FINDINGS.md` records. - -**What was tried first.** The recipe was expected to be a drop-in build, and it -is not: `prospero-pkg-config --exists sdl2` exits 1 on this host and -`$PS5_SYSROOT/user/homebrew/` holds an empty `include/`, so the SDL2 the recipe -enables is not present. That measurement is in `docs/FINDINGS.md` and is the -first item in `docs/ACTIVE.md`'s Next list. The recipe was not modified to work -around it: it is committed as it was fetched, and any change to it is a step of -its own. - -**Commit.** `0d8a225` — Vendor the existing PS5 RetroArch payload recipe as a -baseline. - -**A correction to this entry's own landing.** It first landed as commit -`107dbae`, whose message was a stray shell fragment rather than the text above: -the command that wrote it chained a nested here-document, so the shell closed -the message early and fed it the script instead. Nothing was pushed, and the -commit was amended in place to `0d8a225` with no change to a single file. The -lesson is in the command, not in the tooling: one message per command, written -from a file, never a here-document inside a chain. - ---- - -## 2026-09-18: The baseline payload builds from cache in 34 seconds and reaches the console - -`tools/fetch-ports.sh` and `tools/build-baseline.sh` now build the Option 1 -baseline end to end, and `tools/deploy.py` publishes it to the console under this -project's own folder name. It exists because a console run of something we did -not write is what makes the later runs of our own build interpretable, and it -unblocks the move to the Vulkan driver: the frontend, its configuration seed and -its launcher are now known-good. - -**The evidence.** `tools/build-baseline.sh` returned PASS in 34 s and staged four -files in `dist/baseline/` with a digest manifest; the payload check found -`libkernel_web.sprx` in its imports and no `libkernel_sys.sprx`. A rebuild after -appending a line to `menu/menu_driver.c` returned PASS in 35 s, which is the -measurement that matters for the edit-test loop. `tools/fetch-ports.sh` verified -PacBrew v0.40.2 against its SHA-256 and resolved SDL2 2.30.12. The staged tree was -uploaded and then listed back from the console: `retroarch.elf` at 70,642,656 -bytes beside `retroarch.cfg`, `homebrew.js` and the manifest. - -**What was tried first.** Three approaches to the toolchain's prefix failed before -the fourth worked, and all four are recorded in `docs/FINDINGS.md`: rewriting -`PS5_HBROOT` before the recipe sources `prospero.sh` (it exports the value -unconditionally), a private mount namespace (`unshare -Urm` cannot create `/user` -without root), and bubblewrap (`--tmpfs /user` fails the same way). What works is -to give the build both spellings: a pkg-config of ours that answers with the host -path, and a rewrite of the generated `config.mk`. The recipe's own build script -was never edited — the changes are injected into the copy under `work/`, so -`reference/ps5-retroarch/` stays the baseline it was measured against. - -**Still open.** The launch run has not happened: the console's FTP service began -answering `550 Read-only filesystem` to writes during the last deploy, while -reads and the control payload stayed healthy. `sce_sys/icon0.png` is therefore -still at the folder root. Both are the first item in `docs/ACTIVE.md`'s Next -list, and neither is being treated as done. - -**Commit.** `1036f50` — Build and deploy the Option 1 baseline. - ---- - -## 2026-09-18: Option 1 is proved — the baseline loads on the console and draws its menu - -The vendored recipe's RetroArch 1.21.0 payload was started on the console through -its own homebrew launcher, and the menu came up. It exists because a console run -of something we did not write is the reference every later run of our own build -is read against, and it unblocks the move to the Vulkan driver: the frontend, its -configuration, its launcher manifest and the console's launcher path are all -known-good now. - -**The evidence.** `tools/console-launch.sh --capture` started -`/data/homebrew/PS5_RetroArch/retroarch.elf` through websrv's `/hbldr` endpoint -with the arguments and environment the launcher manifest declares, and the -capture is committed as `evidence/m1-baseline-loads/` with the run it came from -and the expectation `tools/evidence.py compare` replays (exit 0, one capture, -zero failures). The raw capture is `klog/launch-20260918-120546.log`. The -console's control payload reported the run as the active application — -`app=24600 pid=151` under the homebrew title — and the owner confirmed the menu -was on screen. While it ran, RetroArch wrote `retroarch.cfg` (34 KB to 110 KB) and -a `.config/retroarch/` tree into its own folder, which is independent proof that -the frontend reached its main loop with a working storage path. - -**What was tried first.** The first launch captured a single line — -`Fontconfig error: Cannot load default config file: No such file: (null)` — which -looked like a failure and is not one: it is fontconfig falling back to its -built-in defaults before any driver starts, and the menu renders anyway. It is -now in `docs/TROUBLESHOOTING.md` with its exact text, so the next reader does not -chase it. A second wrong turn is worth recording too: two deploys looked like -failures while the files had in fact arrived, because this console's FTP service -ignores the path argument of a listing command and answers deletes with 226. -Both behaviours are handled in `tools/deploy.py` and written up in -`docs/FINDINGS.md`. - -**Commit.** `d869361` — Prove the Option 1 baseline on the console. - ---- - -## 2026-09-18: The PPSA title folder, and what stands between it and eboot.bin - -`tools/stage-ppsa.sh` now assembles the PPSA title folder the objective asks -for: `dist/PPSA99005/` with the title's identity (`title/sce_sys/param.json`, -checked for a valid id, content id, version and launch intent), the 512x512 -launcher icon, `sce_module/libc.prx`, the configuration seed, the payload beside -them and a digest manifest. It exists because a payload folder and a title folder -are different things: the console lists the first only while a launcher is -running, and installs the second as an application in its own right. - -**The evidence.** `tools/stage-ppsa.sh` stages five files into `dist/PPSA99005/` -and writes `manifest.sha256`; with `eboot.bin` absent it prints that the folder is -not complete and exits 3, which is the intended state rather than a silent -success. The three converter requirements found on the way are written up with -their exact messages in `docs/FINDINGS.md`. - -**What was tried first.** Three rejections in a row from the image converter, each -one real and each one narrower than the last. The linker's default layout leaves -no room for the console's process parameters, so the link now goes through -`prospero-lld` with our `linker/ps5-pie.ld` and one extra page boundary; that was -verified on a minimal program, which converted cleanly at 121,536 bytes, before -being applied to the 68 MB payload. Weak `__dlopen` references — which clang -emits because FreeBSD's libc declares both spellings — and a `kernel_mprotect` -reference then had to be defined, which `platform/ps5_dl_stubs.c` does, including -one that forwards to `mprotect` instead of failing, because a failing stub is what -denies a dynamic recompiler executable memory. The third rejection is not ours to -fix in the build: the converter refuses to publish application exports -(`error: native converter does not yet publish application exports`), and the -payload has exports because `-rdynamic` asks for them. - -**Commit.** `4cea907` — Stage the PPSA title folder and clear two of the three converter requirements. - ---- - -## 2026-09-18: The PPSA title folder is complete, eboot.bin included - -`dist/PPSA99005/` is now a complete PPSA title folder: the signed application -image, `sce_module/libc.prx`, the title's identity and icon, the configuration -seed, the payload and a digest manifest. It exists because the objective is a -PPSA homebrew, and a payload folder is not one — the console lists a payload only -while a launcher runs, and installs a title as an application in its own right. - -**The evidence.** `tools/stage-ppsa.sh` exits 0 with six files staged. The -converter's own inspection of the image reports `container: signed, plaintext`, -`segments: 12`, `authority: 0x3100000000000002`, `program type 0x1` and -`integrity: valid`; the digests are in the folder's `manifest.sha256`, with -`eboot.bin` at 49,968,821 bytes and SHA-256 -`88b2b02ded8a4bd00b2505aa01f70c8cfcf8150c927e1ccad5d9abfd0ce974f2`. - -**What was tried first.** Four rejections from the converter, each recorded -because each one is a property of this image format rather than of RetroArch. -The layout had to leave room for the console's process parameters, which is what -`linker/ps5-pie.ld` and one page boundary fix. Linking straight to the linker -skipped the startup objects the compiler driver adds, so there was no `_start` -and the entry point stayed 0 while the converter complained about symbols that -`crt1.o` defines — a stub file written before that was understood turned out to -be redundant and was deleted rather than kept as ballast. The image's own -boundary symbols (`__bss_start`, `__bss_end`, `__image_start`, `__image_end`) -cannot come from any library, so they are defined in -`platform/ps5_image_symbols.S`. And `--exclude-libs=ALL` was needed because the -linker otherwise exports symbols pulled out of static libraries, which both -bloats the dynamic table and trips the converter's export rule. - -**Still open.** No part of this has been on the console: the folder is built and -validated here. Installing and running it is the next step and needs a console -window, which is asked for rather than taken. - -**Commit.** `c8f60e7` — Complete the PPSA title folder with a signed application image. - ---- - -## 2026-09-18: The launcher icon is generated from the project's artwork - -The title's icon now comes from `title/assets/retroarch.png` — the RetroArch -invader, 640x640 — resampled to the 512x512 a launcher tile must be. It exists -because the icon is the first thing the console shows for this title, and until -now it was whatever the vendored recipe happened to lift out of the upstream -tree: a detail of that recipe rather than a decision of ours. - -**The evidence.** `tools/stage-ppsa.sh` regenerates `build/icon0.png` on every run -and copies it into the folder; the staged `sce_sys/icon0.png` is 512x512 and its -digest is in the folder's `manifest.sha256`. `tools/build-baseline.sh` generates -the same image for the payload folder, so the two outputs cannot drift apart. The -source image was inspected at 640x640 before resampling, and the result was read -back as an image to confirm it is the intended artwork rather than a blank or -distorted tile. - -**Commit.** `162f974` — Generate the launcher icon from the project's artwork. - ---- - -## 2026-09-18: The installed title was launched, it crashed, and the log says why - -The user asked for the kernel log to be watched while `PPSA99005` was opened. -Both were done: the log was captured from the console's klog service and the -title was launched through the resident control payload. It does not start, and -the capture says exactly why. - -**The evidence.** `evidence/ppsa-99005-startup-crash/` holds the distilled record -and the raw capture (`klog/ppsa99005-122733.log`, 214 lines), and -`tools/evidence.py compare` replays it. The launch returned -`EndAppMount(0x00000018)` with no running process; the log shows the title being -mounted, then `SIGSEGV` in a thread named `eboot.bin`, a page fault at address -`0x1`, `/app0/sce_module/libc.prx` named in the crash block, and -`[Syscore App] App Crash`. The installed image is 51,870,448 bytes — a link-stage -ELF, not the converted and signed 49,968,821-byte image this repository produces. - -**What was tried first.** `tools/install-title.sh` was written to replace that -image and ran, reporting `226 File deleted` and `226 Path renamed` and verifying -nothing. Immediately afterwards the entire `/data/homebrew/PPSA99005/` directory -was gone from the console: absent from the parent listing, refused by `CWD`. The -neighbouring homebrew folders were untouched and the control payload never -faltered. That is recorded as its own finding rather than smoothed over, because -it changes the install procedure: this service may lose a directory during a -replace, so an install must write a whole folder and then verify it. - -**Still open.** The repaired run has not happened. Everything needed is local -(`dist/PPSA99005/` with a digest per file, plus the same payload on the console -under `/data/homebrew/PS5_RetroArch/`), so nothing was lost that cannot be -rebuilt. - -**Commit.** `e1c0dfa` — Watch the kernel log, launch the title, and record why it crashes. - ---- - -## 2026-09-18: The deployment path is rebuilt from the project that already solved it - -`tools/deploy-title.py` and `tools/ps5_ftp.py` now publish the PPSA folder, and -they are modelled on `../PS5_Vulkan/tools/deploy.sh` — the deployment path that -already works against this console. It exists because my own FTP client kept -reporting success while the console kept the previous bytes, and the reason was -three server quirks the sibling project had already written down: a successful -delete is answered with 226, which `ftplib.delete()` rejects; paths resolve from -the root; and a listing with a path argument behaves inconsistently. - -**The evidence.** `tools/deploy-title.py --check` reports the console's actual -state, including the image's magic bytes — `eboot.bin magic: 7f454c46 (NOT a -converted image)`. A deploy verifies every stored file's size and exits non-zero -when one does not match; that is what caught `libc.prx` reverting. Separately, -freshly generated blobs uploaded to the same folder round-trip byte-for-byte at -2 MB, 8 MB and 60 MB, which rules the server out as the limit. - -**What was tried first.** Several hand-rolled FTP paths, including one that -replaced `eboot.bin` and a `libc.prx` and appeared to succeed at every step. The -useful lesson is in the tooling, not the effort: a transfer that is not verified -by reading back or by checking the stored size is not evidence, and this server -is exactly the case where that distinction matters. - -**Still open.** The console's `eboot.bin` and `sce_module/libc.prx` are still the -other session's build; the two files in `dist/PPSA99005/` are the converted ones. -Replacing them needs either the console's owner or a moment when no other session -is publishing that title. - -**Commit.** `a80417a` — Publish the PPSA folder with the deployment path this console needs. - ---- - -## 2026-09-18: The deployment is one step from working, and the blocker is measured - -The user asked for the deployment, so it was run with the helpers taken from -`../PS5_Vulkan/tools/deploy.sh`. The result is specific: the folder is writable -and stays written, but the converted image will not take. - -**The evidence.** `sce_sys/icon0.png` (512x512) published and verified. Test blobs -of 2 MB, 8 MB and 60 MB, a 49,968,821-byte zero blob, a pattern blob of exactly -the converted image's size, and a 40 KB marker all round-tripped byte-for-byte -under `/data/homebrew/PPSA99005`. The converted image did not: written under its -own name, under an unused name, in place, and through a temporary name with a -rename, the listing and the read-back both returned 51,870,448 bytes starting -`7f454c46`. A file created under a fresh name received `eboot.bin`'s bytes. - -**What was tried first, and corrected.** The first explanation recorded in -`docs/FINDINGS.md` blamed the second session working on this console. The user -pointed out that session publishes `PPSA99988`, not this title, and the entry has -been superseded by a correction with the measurement that rules it out. The same -correction records that `/data/homebrew/PPSA99005` and -`/system_ex/app/PPSA99005` are separate trees, not one storage seen two ways — a -marker written through one is absent from the other. - -**Still open.** Placing those two files needs a route other than this FTP -service. Everything around it is ready and verified; the check that names success -is `tools/deploy-title.py --check` reporting `eboot.bin magic: 4f153d1d`. - -**Commit.** `4ad48bf` — Correct the deployment diagnosis and record what the console actually stores. - ---- - -## 2026-09-18: The route changes — a native title instead of a converted payload - -The project owner has ended the conversion approach: build RetroArch on the -pipeline that already produces working titles rather than converting a finished -payload into the title format. This entry records why that is the right call and -what the new shape is, because both are measured rather than assumed. - -**Why the conversion route ends.** A title's `eboot.bin` must be a converted -image, and conversion strips the dynamic symbols the homebrew launcher needs — the -converter refuses to publish exports and `--exclude-libs=ALL` keeps the rest -internal. So the same build cannot serve both routes, and each step of converting -the finished payload produced a further failure on the console: a null-pointer -crash inside a raw image, then `PRX_SCE_MODULE_LOAD_ERROR` from an image the -loader would not take. The lesson is the one the user reached first: sources -should be *built for* the title pipeline, not adapted into it afterwards. - -**The new foundation.** `../ps5-native-app-boilerplate-main`, and the project -built on it, `../ProsperoLight` — same author, both native PS5 applications whose -titles start on this console. Their `tools/build.sh` collects `src/**/*.{c,cc,cpp}` -only, compiles it with fixed flags (`-std=c11`, `-std=c++20`, `-O2 -Wall -Wextra --ffunction-sections -fdata-sections`), links it with the project's own -`app_crt.o`, `app_cpp_runtime.o`, stub objects and version script, and signs the -result into `eboot.bin`. Include paths and static archives arrive through -`APP_INCLUDE_PATHS` and `APP_STATIC_ARCHIVES`, which is how a tree of RetroArch's -shape can be reached without moving it file by file. - -**The blocker found by trying it.** Building that project here fails in its C++ -headers: the SDK's `math.h` defines `isnan` unconditionally while Clang 22's -libc++ headers call `std::isnan` — `error: expected unqualified-id`. Both SDK -copies on this machine share that header, so the pairing is inherent, which is -why the project pins Clang 18 (`extra/clang18 18.1.8-2` is available). Installing -it needs administrator rights, so the build waits on that. - -**Commit.** `80dc8d4` — Change route: build a native title on the pipeline that works. - ---- - -## 2026-09-18: ProsperoLight builds here, on Clang 22, and its title is staged - -The plan is now: prove the whole console path with a complete, known-good title, -then replace its sources with a fresh RetroArch. This entry records the first -half — ProsperoLight builds on this machine and produces a valid title. - -**What it took, four things, each found by a failed build.** The project's own -vendored SDK rather than the one in `$HOME`; `PS5_CLANG=/usr/bin/clang`, because -its wrapper defaults to a `clang-18` that is not installed while the sibling -project that works uses plain `clang`; a minimal, documented `jsonschema` -stand-in at `tooling/pystub/`, because mbedTLS regenerates a source file with a -script that imports it; and a verified `runtime/libc.prx`, which the project ships -only as a manifest — the boilerplate's copy carries the same digest. All four are -in `tools/build-native-app.sh`, and none of them edits the project's sources. - -**The evidence.** `make app` completed and the converter's own inspector reports -`container: signed, plaintext`, twelve segments, `integrity: valid` for -`dist/PPSA99002/eboot.bin`. The title folder is staged at `handoff/PPSA99002/` -with its identity `PPSA99002`, `UP9000-PPSA99002_00-PROSPEROLIGHT000`, -"ProsperoLight". - -**What is not proven.** That it runs on the console. Placement is the console -owner's step, because writes from this machine do not reach the console's title -folders, and a title also has to be registered with the shell before its launch is -anything but `is not registered`. - -**Commit.** `d1165a2` — Build ProsperoLight on this machine and stage its title. - ---- - -## 2026-09-18: The native pipeline is proven on the console, and the swap is scoped - -ProsperoLight, built on this machine through the native pipeline, was placed on -the console by its owner and **started flawlessly**. This is the milestone the -route change was made for: sources compiled for the title pipeline produce a title -the console runs, so the conversion fight is behind us. - -**The evidence.** The owner observed the title running. The kernel capture for the -attempt contains zero fatal signals (`klog/PPSA99002-134510.log`), and the -converter's own inspector reports `container: signed, plaintext`, twelve segments, -`integrity: valid` for `dist/PPSA99002/eboot.bin`. A note on reading those logs -honestly: an earlier pass of this session read `is not registered` lines as being -about this title, when they were the console's AutoMounter talking about other app -ids (0x2019, 0x18) — and `tools/console-run.sh` printed "still running" from a -condition that is always true. Both are corrected here; the observation that -matters is the owner's. - -**What it took, and it is now one command.** `tools/build-native-app.sh` carries -the four things that had to be right: the project's own vendored SDK, -`PS5_CLANG=/usr/bin/clang` (the wrapper default of `clang-18` is neither installed -nor needed, as the owner pointed out), the documented `jsonschema` stand-in under -`tooling/pystub/`, and a verified `runtime/libc.prx`. - -**The swap, scoped by measurement.** RetroArch 1.22.2 is cloned fresh. The -pipeline's graphics layer is not a GPU stack: its renderer drives VideoOut -directly (`sceVideoOutOpen`, `RegisterBuffers`, `SetBufferAttribute`, -`SetFlipRate`, `SubmitFlip`) from its own direct memory — enough for a CPU -framebuffer, not enough for RetroArch's menu, which draws through a GPU display -context. The backend that can serve it exists in `../PS5_Vulkan` -(`libps5vk.ps5.a`, with `ps5vk_CreateInstance` and friends defined) and its headers -live in `../ps5-opengl-sdk-0.2.0/third_party/Vulkan-Headers`. Both are reached -through `APP_INCLUDE_PATHS` and `APP_STATIC_ARCHIVES`, which is exactly how the -sibling project already consumes them. - -**Commit.** `1da48f0` — Prove the native pipeline on the console and scope the RetroArch swap. - ---- - -## 2026-09-18: The scaffold title runs on the console and presents frames - -This repository's own title — the native pipeline's scaffolding, this project's -display layer and its own entry point — deployed by FTP and launched, and stayed -up. First milestone of the RetroArch plan, and the first code of ours on the -console. - -**The evidence.** The launch was captured with the console's log delimited at the -launch (`klog/PPSA99169-135230.log`): the capture holds **zero** fatal signals and -**zero** `exit_value` lines, shows our process twice in the shell's accounting -(`[SceShellCore] 23% 700 PPSA99169 eboot.bin`, then 22%), and the control payload -reports `app=49176 title=PPSA99169 count=1 pids=228` — still running. A -present-and-wait loop is what 22–23% CPU looks like. - -**What was tried first, and cost a run each.** The first version of the display -layer guessed at constants the console does not forgive, and the title built, -launched and reported `eboot.bin calls exit() exit_value=1` because `open` had -failed. Comparing against the sibling application that had already proved them on -hardware showed five errors at once: the pixel format -(`0x8000000022000000`, 64-bit), the direct-memory size accessor (`size_t`, not -`int64_t`), the mapping protection (`0x33`), the `VideoBuffer` shape (four -pointers) and the tiled pixel addressing. The constants and the tiling are now -taken from that project with the reason written down in `src/display.cpp`, because -none of them is derivable by reasoning. - -**Commit.** `b18e203` — Run this project's own title on the console. - ---- - -## 2026-09-18: The PS5 video driver is written and compiles - -`src/video_ps5.cpp` implements both interfaces RetroArch asks of a video driver: -the `video_driver_t` itself and the `video_poke_interface_t` that RGUI needs. It -presents through `src/display.cpp`, which has already run on the console. - -**Why this is the right first target, measured rather than assumed.** RGUI -references the menu display context **zero** times; XMB references it 72 times, and -every backend in `gfx_display_ctx_drivers[]` is a GPU API — OpenGL, Vulkan, Metal, -Direct3D, or a console-specific one — with no software entry. So RGUI is the only -menu that can run over VideoOut alone, and XMB needs a GPU context over -`../PS5_Vulkan` later. - -**What the interface actually requires.** Six members: `init`, -`suppress_screensaver`, `alive`, `frame`, `ident`, `poke_interface`. Verified on -the interface report and by compiling: `suppress_screensaver` returns **bool**, not -void, and the table is filled positionally with `overlay_interface` (inside -`#ifdef HAVE_OVERLAY`) before `poke_interface`, plus `wrap_type_to_enum` after it. -`ps5_frame` prefers the menu's framebuffer when RGUI has handed one over, else the -core's frame, and scales nearest-neighbour through the display layer's tiled -addressing. - -**Evidence.** The driver compiles through the pipeline's own compiler against -RetroArch's headers: - -```text -tooling/prospero-clang18 -std=c++20 -Isrc -Ivendor/retroarch \ - -Ivendor/retroarch/libretro-common/include -c src/video_ps5.cpp -compiled: 6,056 bytes, no warnings -``` - -**Commit.** `74e22bd` — Write the PS5 video driver for the RetroArch frontend. - ---- - -## 2026-09-18: Websrv removed, and the frontend compiles from RetroArch's own build - -Two things in one step: the websrv-derived material is gone, and the frontend now -compiles from a source list that comes from RetroArch itself. - -**Removed.** `reference/ps5-retroarch/` (already empty), the Option 1 baseline -build tree under `work/baseline/`, its artefacts in `dist/baseline/` and -`dist/PPSA99005/`, the pacbrew ports cache, and the tools that existed only for -the conversion route (`build-baseline.sh`, `prospero-clang-link`, `stage-ppsa.sh`, -`fetch-ports.sh`). The intention was to start from ProsperoLight's foundation, and -none of that is part of it. - -**The source list now comes from RetroArch's own build.** The previous build script -took a 264-object list from the deleted websrv tree. That dependency is gone: -`tools/retroarch-sources.sh` runs RetroArch's own `./configure` and `make info`, -and prints exactly the objects a link needs. Its `OBJ` list is grown from 248 -conditional `OBJ +=` lines in `Makefile.common`, so only make can produce it for a -given configuration; configure runs in a copy under `build/ra-conf/`, so -`vendor/retroarch` is never touched. The configure flags are this project's — RGUI, -no graphics API, nothing that needs a library the SDK does not ship. - -**The evidence.** `tools/build-retroarch.sh` compiled **225 of 244** sources with -the pipeline's own compiler. What remains is Linux-only and correctly out of scope -for this console: `udev_joypad.c`, `udev_input.c`, `linuxraw_input.c`, -`keyboard_event_xkb.c` and `libchdr_zstd.c`. Four fixes got there, each found by a -compile rather than guessed: - -- the generated `config.h`, placed where RetroArch's relative includes - (`../config.h`, `../../config.h`, `../../../config.h`) resolve; -- `-D_GNU_SOURCE`, which RetroArch's own build passes — `_POSIX_C_SOURCE` alone - hid `strlcpy` and broke sixty more sources than it fixed; -- `-DCLOCK_REALTIME=0 -DCLOCK_MONOTONIC=4`, the header's own values, because this - SDK's `time.h` hides the clock ids under the standard the pipeline compiles with; -- RetroArch's vendored zlib (`--enable-builtinzlib`) with its compatibility headers - on the include path. - -**Still to do for RGUI on screen.** Two pieces, both now unblocked: -register `&video_ps5` in `video_drivers[]`, and reconcile the entry point, since -`retroarch.c` defines its own `main` and the pipeline's builder supplies a `_start`. -Then link with the pipeline's CRT and runtime, stage, deploy and launch. - -**Commit.** `b214c22` — Delete the websrv material and compile the frontend from RetroArch's own build. - -## 2026-09-18: The title links, signs, and carries the PS5 driver in its table - -**What was asked.** Register `&video_ps5` in RetroArch's `video_drivers[]`, -reconcile the entry point, and link the frontend with `src/` through the native -pipeline's CRT — the two items the goal table still had open. - -**Both were done, and the link is the proof.** `bash tools/build-title.sh` -compiles 225 of 225 sources, archives them into `build/ra/libretroarch.a`, links -that with `src/` through `app_crt.o` and the SDK's `_start`, signs the result and -assembles `dist/PPSA99169/`. `eboot.bin` is 8,026,239 bytes, `container: signed, -plaintext`, 12 segments, `integrity: valid`. - -**The registration is verified by relocation, not by reading the source.** -`readelf -r build/ra/obj/gfx_video_driver.c.o` shows `.rela.data.video_drivers` -holding exactly two entries, `video_ps5` then `video_null`, and -`nm build/llvm-pie.elf` shows `video_ps5`, `video_null`, `rarch_main` and a single -`main`. The entry point needed no adapter: RetroArch guards its own `main` with -`#ifndef HAVE_MAIN`, which its desktop build defines, so `-DHAVE_MAIN` removes it -and `src/main.cpp` supplies the only one. - -**Five faults were found on the way, and each was a real one.** - -1. *The build compiled the wrong tree.* `tools/build-retroarch.sh` read sources - from `vendor/retroarch` while the port's patches were applied to `build/ra-conf`. - It linked, signed and started with `video_drivers[]` holding no ps5 entry: the - file that lists the drivers came from the tree that had never heard of the - patch. Source and patch now come from the same place by construction. -2. *The feature flags were stated twice.* The script passed 36 `-DHAVE_*` flags of - its own, six of which configure had turned off — the BSV movie recorder, the - soft filters, the video filters, the translator, gfx widgets. Those compiled - code whose sources are not in the object list, so the link failed on symbols - belonging to files nobody built. `tools/retroarch-flags.sh` now asks `make -n` - for the flags it would use; the hand-written list is gone. -3. *`config.h` alone is not enough.* A source can test a feature before any header - that includes `config.h` reaches it: `libchdr_chd.c` guards its zlib code with - `#ifdef HAVE_ZLIB` near the top, so a build with config.h only compiled the CHD - reader without zlib. This is why `make` passes each enabled feature on the - command line as well, and why this project does too. -4. *Four Linux-only subsystems were in the object list.* udev, v4l2, tinyalsa and - libusb cannot compile against this SDK, and xkbcommon is enabled by this host's - pkg-config rather than by a compiler flag. All five are now switched off at - their own configure switches — `HAVE_XKBCOMMON` is declared `auto` in - `qb/config.params.sh` by the port's second patch, because upstream checks for it - without declaring it and configure therefore refuses the switch. Three further - sources (`linuxraw_input.c`, `linuxraw_joypad.c`, `linux_common.c`) are in the - object list because `Makefile.common` tests `findstring Linux,$(OS)` and - configure is given `OS=BSD`; they are dropped by `tools/retroarch-sources.sh`, - which is safe because the toolchain defines `__FreeBSD__` and not `__linux__`, - so nothing references them. -5. *Two link-time stubs the pipeline refuses.* `assert` expands to `__assert`, - which only `libc.a` defines and this pipeline deliberately does not link, so - the build is now `-DNDEBUG` — what a release build of RetroArch uses anyway. - zstd enables its tracing hooks on any x86-64 ELF and emits a weak undefined - symbol; the pipeline's stub table refuses to write one for a symbol no public - stub exports, so `-DZSTD_TRACE=0` stops it being emitted. - -**The abandoned route is fully gone.** The tools that belonged to it — -`deploy.py`, `console-launch.sh`, `check-payload.sh`, `scaffold-native.sh`, -`build-native-app.sh`, `install-title.sh` and `deploy.sh` — are deleted, and the -tools that remain are the ones `build-title.sh` and `verify.sh` call. Every tool -named by another tool now exists: `tools/fetch-retroarch.sh` was named by three -tools and three documents and had never been committed, and it now pins -RetroArch at v1.22.2 (`69a4f0e`) and checks the fetched tree against that commit. -`vendor/retroarch` carries no git directory, so upstream cannot be committed into. - -**The gates are green for the first time.** `tools/verify.sh` passes format, unit, -build, integration and evidence. Three gate scripts it named had never been -written: `tools/lint-shell.sh` and `tools/lint-format.sh` now exist, and -`tools/check-manifest.sh` verifies the built folder against its manifest. The -template's `make test-unit` built a test for `src/demo_renderer.cpp`, a file that -does not exist here; `tests/test_frontend.py` replaces it with eight checks over -what actually risks being wrong — the driver table's relocations, the frame -layout's arithmetic compiled from `src/display.cpp`, and the artifact's container. - -**A generated file stopped dirtying the tree.** `tools/build-retroarch.sh` had -been copying the configured `config.h` to the repository root; no source read it, -and it made every build show a modified tracked file. Untracked and ignored, and -the build is byte-identical without it. - -**The evidence.** `bash tools/build-title.sh` → `eboot.bin` 8,026,239 bytes, -`integrity: valid`. Two consecutive builds produce the same digest, -`71c88975040535a02b462618dd394a7a378034272c9bffa43ce0ca4b717ab9b7`. `bash -tools/check-manifest.sh` → 7 files match, none extra, `eboot.bin` magic -`4f153d1d`, and it fails as it should when one byte of the image is changed. -`tools/verify.sh` → `PASS (format unit build integration evidence)`. Staged tree -for the console: `handoff/PPSA99169/`, 8 files. - -**What is not done.** The title has not run. It is built, signed and manifested, -and the folder has to reach `/data/homebrew/PPSA99169` on the console. Writes from -this machine do not take: `tools/deploy-title.py` stored a 1,284,674-byte -`libc.prx` and read back the previous 1,335,962-byte file under the new name, -twice, and a probe uploaded under a name never used before read back those same -old bytes. The console's FTP accepts the transfer and serves something else, so -the console's owner uploads by hand. - -**Commit.** `eebba81` — Link the title and register the PS5 video driver. - -## 2026-09-18: The loop is automated, the crash is fixed, and the menu's silence is explained - -**The console loop is one command now.** `tools/run-title.sh` builds, publishes -over FTP, verifies what the console stored, starts the kernel-log listener before -launching, launches, watches, closes the title itself, and prints the title's own -trace. It exists because the hand-driven loop had produced two bad readings: runs -overlapped, so a trace from one launch was read as another's result, and a probe -build was left in place and mistaken for a finding. No round after this one -needs a person to upload anything. - -**Verification had to change to match this console.** It converts a signed fake -self into a raw ELF as it stores it, so the file it serves is a different -container from the file sent - the stored image is 8,117,072 bytes against a -signed 8,029,263 and only 12.8% of the bytes agree. A digest comparison therefore -answers the wrong question and was rejecting successful uploads. eboot.bin is now -verified by the strings that only this build carries, which is the same evidence -the debugging used. `sce_module/libc.prx` is the one path this console will not -replace - four uploads, a fresh filename and a full listing all returned the -console's own 1,335,962-byte file - so it is reported and kept, not fatal: the -title runs against the console's copy, and failing on it blocked eboot.bin from -being published at all. - -**The crash is fixed, and it was a null joypad driver.** Every joypad driver -upstream ships needs a library or header this SDK does not carry, so -`primary_joypad` is NULL, and `input_joypad_analog_axis` reads `drv->axis` without -checking `drv`. It only runs when the menu is alive, which is why the first pass -through the runloop survived and the second died: SIGSEGV, fault address 0x18, -which is `joypad_info.joy_idx` plus the `auto_binds` member. `patches/series` 0004 -returns 0 when there is no driver - the answer the function already gives when -there is no axis to read. The title now runs indefinitely: 1500+ frames in 25 -seconds, closed by the script. - -**The menu's silence is explained, and it is not the driver.** RGUI initialises, -loads its fonts from the assets now bundled under `assets/rgui/font/`, holds a -320x240 framebuffer, and `rgui_set_texture` is called every frame - but -`GFX_DISP_FLAG_FB_DIRTY` is 0 on every one of those calls, so it returns before -handing anything over and the driver reports `no-menu-source` for every frame. -That flag is set at the end of `rgui_render`, which is only reached through -`menu->driver_ctx->render` under `if (BIT64_GET(menu->state, MENU_STATE_BLIT))`. -The menu's renderer is never running; finding which condition above it is false is -the next step. - -**The evidence.** `bash tools/run-title.sh` → built, published with eboot.bin -verified by its own markers, ran 25 s, closed by the script; `/app0/trace.txt` -shows `ps5_frame 1500: no-menu-source 4x4 present=1`, i.e. a healthy loop -presenting frames and no menu pixels. `tools/verify.sh` → PASS (format unit build -integration evidence). Port changes against upstream: `retroarch.c` 10 lines, -`gfx/video_driver.{c,h}` 5, `input/input_driver.c` 6, `runloop.c` and -`menu/drivers/rgui.c` 0 - every probe removed, verified by diff. - -**One thing to check later.** `sce_sys/icon0.png` shows as modified and I cannot -account for it: the worktree file and `title/assets/retroarch.png` are both -512x512 but differently encoded, and nothing in this round touches it. It is -committed as it stands rather than reverted, and flagged here so it is not a -silent change. - -**Commit.** `4e91e35` — Automate the console loop, fix the null joypad crash, bundle the RGUI fonts. - -## 2026-09-18: The RGUI menu is on the console's screen, and the fault was a struct size - -**The milestone.** `bash tools/run-title.sh --watch 20` — one unattended command that -builds, publishes, verifies, listens, launches, watches and closes the title — ran -to completion with **no fatal signal**, and the console's owner watched RetroArch's -RGUI menu on the television during it. The title's own trace from that run is -committed as `evidence/ppsa-99169-rgui-menu-on-screen/`: the display opens at -1920x1080, the frontend is told the size, RGUI's 320x240 RGB565 framebuffer arrives -through `poke->set_texture_frame`, and the driver presents it every frame by -alternating the two registered buffers (`display: flip 1200 of buffer 1, status=0 -marker=1`). - -**Two faults had to go, and they were independent.** - -The first was that **the title and the frontend disagreed about the size of -RetroArch's driver interface struct**. `src/` was compiled with no `-DHAVE_*` flags -while the archive was compiled with fifty, so `HAVE_OVERLAY` and `HAVE_GFX_WIDGETS` -were off on one side only: `video_ps5` was 136 bytes where the frontend read 144, and -every member after `overlay_interface` was read one slot late. `poke_interface` came -back NULL, the frontend therefore never called it, and RGUI's hand-over was dropped -in silence on every frame while the driver happily presented 1500 frames of its own -probe pattern. `tools/build-title.sh` now asks `tools/retroarch-flags.sh` for the -frontend's own defines and passes them to the title's sources, and writes the -configured tree's `config.h` where RetroArch's headers look for it relative to the -repository root. `tests/test_frontend.py` gained a class that compares -`sizeof(video_driver_t)`, compiled with the frontend's defines, against the size of -`video_ps5` in the object the title links, and checks the member at the frontend's -`poke_interface` offset against the table's relocations; built without the defines it -fails with the two numbers, which was verified by doing it. - -The second was that the port initialised the **GPU command processor** -(`sceAgcInit(8)`) before opening the display, left over from the round that submitted -flips through an AGC command buffer. `../PS5_Vulkan/src/demo_renderer.cpp`, whose -output has been seen on this console, contains no `sceAgc` call at all. Removing it -produced the first pixels this port ever put on the screen. - -**A correction to the lead this round started from.** The hypothesis was that -returning into RetroArch's runloop after a flip was what undid the frame, because the -working reference blocks forever after its one flip. That is now measured false: with -the probe's frame held for 8 seconds inside `present()`, the bands appeared -immediately - before the hold could be the reason - and were still on screen twelve -seconds after `present()` had returned into the runloop, with the display's flip -status reading `marker=1` for every one of the sixteen samples taken during the hold. -The runloop does not take a presented frame back. The two candidate causes went in -together in one build, so what the run is evidence for is the pair's effect and the -elimination of the runloop; the AGC call is what remains with no other candidate. - -**What also changed in the driver, and why.** `present()` is a real driver now: it -flushes the back buffer, submits a flip to that buffer's registered index, waits a -vblank, and alternates buffers so the next frame is drawn into the one the display is -not reading. The probe paint, the readback and the hold are gone. The core's frame is -4x4 because upstream hardcodes a dummy frame when no game is loaded -(`video_driver.c` sets the cache to 4x4 for exactly that case), so the menu's -framebuffer takes precedence when it exists and the bands remain underneath as the -instrument that says "the display is alive but the menu is not drawing". - -**Verification.** `bash tools/verify.sh` → **PASS (format unit build integration -evidence)**, including the 11 host tests and the three replayed evidence records. -The probes are gone from the configured tree: `build/ra-conf` was deleted, -regenerated from `vendor/retroarch`, and diffed - only the four files -`patches/series` names differ from upstream. - -## 2026-09-18: The pad driver is written and registered, and two upstream faults stand in front of it - -**What was built.** `src/input_ps5.cpp` is a complete input driver for this console: -the pad is read with `scePadInit`/`scePadOpen`/`scePadRead` and the 120-byte sample -layout that `../ProsperoLight` verified on hardware, the pad's button words are -mapped to RetroArch's own numbering (CIRCLE is its A, so CIRCLE confirms), sticks -and triggers are reported as axes, and the table is registered in -`input_drivers[]` before `input_null`. `tests/test_frontend.py` pins the -registration with a relocation test and the button pairing by reading the map out of -the object the title links, so neither can drift quietly. - -**Two faults were found in front of it, both upstream, both fixed.** - -The first is that RetroArch's built-in test input driver is on by default -(`HAVE_TEST_DRIVERS=yes` in `qb/config.params.sh`), and with it on -`video_driver_init_input` returns immediately whenever the configured driver is not -`"test"` - so no input driver is ever initialised and every button reads 0. That is -now disabled at configure time. - -The second is that the title's `-c /app0/retroarch.cfg` never reached RetroArch: -content loading rebuilds argv from the frontend's environment and keeps only -`["retroarch", "--menu"]`. The trace shows it plainly - -`probe config_parse_file: path="(null)"` and `probe config_load: after parse -input="null" video="ext"` - which means this title has been running on compiled -defaults since the day it first built, with its config file unread and `--verbose` -dropped. A one-line guarded fix restores the title's own config path. - -**And that fix is parked, because it exposes a crash I could not finish.** With the -config actually read, the title dies on launch with `SIGSEGV`, `rip=0` - a call -through a null function pointer - before `ps5_input_init` is entered and also with -`input_driver="null"` configured. Markers through `drivers_init` and -`video_driver_init_internal` show the entire video path completing, so the crash is -after driver initialisation, in the runloop or the content task. The change is in -`parked/config-path.patch.py` with its reasoning; `config/retroarch.cfg` keeps -`input_driver = "null"` so the title continues to run and show its menu. - -**Honest position.** The pad does not work yet, the picture is still one menu frame -that only redraws when something changes (which is what input is for), and the -"frozen frame" question cannot be settled until input lands. What is solid: the -driver exists, is registered, is unit-tested, and the two faults that were silently -blocking every input path are named with their measurements. - -**Verification.** `bash tools/verify.sh` → PASS (format unit build integration -evidence), 13 host tests. `bash tools/run-title.sh --watch 12` → title runs, menu -visible, no fatal signal, `/app0/trace.txt` shows the same hand-over as before. - -## 2026-09-18: The config-path crash is reading the config, not any setting in it - -**Four runs, one change each, and the crash follows the `-c`.** With `-c` in the -argument list the title dies with `SIGSEGV`, `rip=0`; with `-c` removed it runs and -shows the menu. Dropping `--verbose` does not help, and dropping `-f` does not help, -so neither of the two settings that would newly *take effect* is the cause - it is -the config file being read at all. That is a narrower claim than the last entry -could make. - -**The config parses.** Marking `config_load` before and after shows both probes, so -defaults, file parse and `config_load_file` all return. The crash is after the read -and before the first frame: no `ps5_frame 0`, and neither `runloop_iterate` site is -reached. It is inside `retroarch_main_init`, between the config load and the first -frame. - -**What remains ruled out.** `drivers_init` completes (overlay unload/init, context -reset, display server, mouse cursor, audio init, core info all mark), and -`ps5_input_init` is never entered, so the input driver is not involved. What is left -in that stretch is the driver lookups, and that is where the next marker goes: -`audio_driver_find_driver`, `video_driver_find_driver`, `input_driver_find_driver`, -`camera_driver_find_driver`, `menu_driver_find_driver`, each indexing a table this -build has stripped to almost nothing. - -**A process note that cost this round's last attempt.** Reading a marker line into -the middle of a multi-line call split the call and produced -`undefined symbol: rarch_main` at link time. Marker placement is an edit, not a -substitution: the anchors have to be statements, and the brace and call structure -has to be checked after every insertion. The input driver stays built, registered -and unit-tested, and the title stays in its working state while this is chased. - -## 2026-09-18: The config crash is in command_event, and the log route is closed for it - -**Narrowed.** With the config-path fix applied, markers bracket the crash to -`command_event()` before its switch reaches `CMD_EVENT_CONTROLLER_INIT`: the marker -directly before that call prints, and three markers after it - the first instruction -of `command_event_init_controllers`, the case label, and the statement following the -call - never do. The register dump is identical across every config-loading run -(`rip: 0`), so it is deterministic. - -**A real guard, kept without pretending it fixed anything.** `patches/series` 0007 -null-guards the core's `retro_set_controller_port_device` callback, which upstream -calls unguarded and which only `dynamic_dummy.c`'s empty stub makes survivable. It -is verified present in the object and the crash is unchanged, so the commit message -and `docs/FINDINGS.md` both say so. - -**File logging: configured, inert, and the reason is structural.** The logger is -initialised after the config is parsed, and this crash happens before that, so the -log cannot catch it. `log_to_file`, `log_to_file_timestamp` and `log_dir` are now in -`config/retroarch.cfg` for the day the config is read - they need no build flag, -since `rarch_log_file_init` is compiled unconditionally. - -**State.** The title runs (menu up, no fatal signal, all five gates pass). The -config-path fix (0006) stays parked in `parked/config-path.patch.py`, the guard -(0007) is in `patches/series`, and `input_driver` is back to `"null"` because it is -only reachable through the config file. Two self-inflicted process faults are -recorded in `docs/FINDINGS.md`: a text-sliced patch park that emptied the parked file -and desynced the script from the tree (recovered with `git checkout`), and probe -lines inserted into a multi-line `#if` block that broke the link. - -## 2026-09-18: The pad works, the menu is live, and the pad's absence was one comparison - -**The milestone.** `bash tools/run-title.sh --watch 20` with the console owner working -the pad: the title runs, the pad is read, and the menu responds. From the title's own -trace on the shipping build: - - input: pad opened, user=515310723 handle=51119872 - input: press, pad=0x00000040 retropad=0x00000020 DOWN - input: press, pad=0x00004000 retropad=0x00000001 CROSS -> RetroPad B - input: press, pad=0x00002000 retropad=0x00000100 CIRCLE -> RetroPad A - menu: framebuffer commit 2 is a new picture (2 of 2 changed so far) - ps5_frame 600: menu commits=76 changes=14 presented=yes - -Both halves of the round's goal are answered. Input works, with the mapping a -PlayStation player expects - CIRCLE confirms, CROSS cancels. And the picture is not -one frozen frame: 76 framebuffer commits, 14 of them a different picture, against 1 -and 1 in every earlier run. The image was never frozen; the menu had nothing to -redraw for, which is exactly what input supplies. - -**The fault was one comparison.** `ps5_input_init` had never run, and the reason was -not the driver, not the registration, and not the test-driver flag alone. -`video_driver_init_input` opens with `if (*input) return true;`, which upstream means -for a video driver that pre-initialised an input driver of its own - and the tell is -`tmp`. `video_driver_init_internal` assigns `tmp = current_driver` *before* calling -`video_driver_find_driver`, so after the pre-initialisation pass selects a driver, -`tmp` is that same selection. Measured: `probe INV: entered tmp=ba41e0 *input=ba41e0 -configured="ps5"`. The early return therefore fired for a case upstream never -designed for, and the wrap below was dead code. My first attempt - clearing the -selection when `tmp == NULL` - never fired, because `tmp` is not NULL; the fix that -works is `patches/series` 0009's `if (*input != NULL && *input == tmp)`, three lines -that say what they mean. - -**The probes are kept.** They live in `tools/apply-runtime-probes.py` now, applied -with one command and reverted with `--revert`, because a rebuild wipes any probe -written into the configured tree - which is how the set that found this was lost -mid-round. They are not part of the shipping build: `tools/build-title.sh` does not -call that script. - -**Still open, and unchanged by this.** The config file is still not read: 0006 is -parked in `parked/config-path.patch.py` because reading the config crashes the launch -in `command_event`. That is the next task, and the probe set now covers the landmarks -around it. - -**Verification.** `bash tools/verify.sh` → PASS (format unit build integration -evidence). `bash tools/run-title.sh --watch 20` → pad working, menu redrawing, no -fatal signal. - -## 2026-09-18: A duplicate patch block cannot reach a build again - -**The fault this closes.** The working copy of `tools/apply-port-patches.py` had grown -to fourteen blocks against the committed ten: the controller-port guard was pasted in -twice, so `runloop.c` was patched twice, and every build from that tree died with -`rip: 0`. The block read correctly and the script reported "applied" for both, so -nothing about reading it revealed the duplicate - and the crash it produced was chased -as an unrelated bug for most of a round. - -**The check is mechanical now.** `tests/test_frontend.py` gains a `PortPatches` class -that parses `EDITS` and asserts: no two blocks share a `(file, anchor)` pair - which -is precisely what makes an edit apply twice; the patch count is the pinned ten, so an -addition or removal is a deliberate diff; and `input/input_driver.c` carries its three -distinct edits. A duplicate was planted to prove the test fails on it (11 != 10, two -failures) and removed again to prove it passes. - -**On purging the patch set: no, and the enumeration is the argument.** Of the ten -blocks, eight are load-bearing - video driver registration (2), the `main` rename, -input driver registration (2), the input-init fix, the compiled input default, and the -null-joypad guard. Purging them would remove the display and the pad. Only the -`HAVE_XKBCOMMON` declaration and the controller-port guard are dead weight, and both -are cheap. The set was instead verified from a pristine extraction: ten blocks, no -duplicate pairs, three legitimate edits to `input_driver.c` at different anchors. - -**State.** `bash tools/build-title.sh` from a wiped tree, then -`bash tools/run-title.sh --watch 12` -> title runs, no fatal signal; -`bash tools/verify.sh` -> PASS; 16 host tests. - -## 2026-09-18: The driver is linked into the title, and the link completes - -**The step.** `../PS5_Vulkan`'s driver stops being a library RetroArch loads at run -time and becomes ordinary symbols in `eboot.bin`. The alternative was measured and -closed in `0cc661d`: a PS5 title cannot dlopen a driver. This is the commit that makes -the replacement actually link. - -**What the first link said.** Twenty-four undefined symbols, and the count is only -knowable because the link line now carries `--error-limit=0`; before that lld stopped -at twenty and the tail of the list was invisible. The 24 are three unrelated problems: - -- **Four `__eh_frame_*` boundaries** (`__eh_frame_start/end`, - `__eh_frame_hdr_start/end`). Their shader compiler is C++ and links the SDK's - libunwind, which finds the unwind tables through these boundary symbols rather than - through `dl_iterate_phdr`. `../PS5_Vulkan/tooling/psbc/ps5-pie-unwind.ld` defines - them; this project's `tooling/native/ps5-pie.ld` is byte-identical to the - `ps5-pie.ld` that script includes, so the same four `PROVIDE`s were added to it - rather than adopting their `-T` path. -- **Eight Mesa utility symbols** from `u_queue.ps5.o`, `os_memory_fd.ps5.o` and - `ac_spm_config.ps5.o`: `u_thread_create`, `u_thread_setname`, - `util_barrier_init/destroy/wait`, `util_thread_get_time_nano`, - `os_create_anonymous_file`, `os_read_file`. Their - `toolchain/Makefile.opengnm-psbc-ps5` filters `src/util/{u_thread,anon_file,os_file}.c` - out of `UTIL_SRCS`, and `tools/build-psbc-ps5.sh` compiles only a five-file support - list, so nothing in that tree defines them. Their own `libvulkan.so.1` links because - a shared object may leave symbols undefined; a title's link may not. Mesa's own - definitions are compiled here (`tools/build-mesa-util.sh`) with their PS5 - configuration rather than reimplemented, because `u_thread.c`'s `util_barrier` and - its `mtx_t`/`cnd_t` come from that tree's `c11/threads.h`, the same header the - `u_queue.ps5.o` inside the archive was compiled against. Two of the three needed a - flag that configuration does not set: `HAVE_PTHREAD_NP_H` for - `pthread_set_name_np`, and for `anon_file.c` the `-D_XOPEN_SOURCE=700` drop that - `tooling/psbc/support.mk` already documents for `usleep` and `getprogname`, because - `SHM_ANON` is declared under `__BSD_VISIBLE`. `os_file.c` needed `-D__ORBIS__`, the - same switch their mak uses for `futex.ps5.o`: its FreeBSD branch walks the kernel's - file table through `sysctl(KERN_FILE)` and a `kvaddr_t` the SDK does not have. -- **Twelve `sceAgc*` entry points.** The console provides libSceAgc and - libSceAgcDriver and the SDK stubs neither. The declarations come from - `../PS5_Vulkan/vendor/ps5/sdk/stubs/agc_canary_link_stub.c`, signatures unchanged, - because those are the imports that project's own titles already record. - -**The stubs moved.** They had been edited in `vendor/ps5/sdk/stubs/`, which -`.gitignore` excludes, so the fix would have built here and vanished at the next -checkout. They are `tooling/ps5-stubs/agc_link_stub.c` and -`tooling/ps5-stubs/agc_driver_link_stub.c` now, and `tools/build.sh` reads them there. - -**Evidence.** `bash tools/build-title.sh` -> exit 0, no undefined symbols, -`dist/PPSA99169/eboot.bin` 29,859,722 bytes against 8,225,706 before: 21.6 MB of -driver and shader compiler. `bash tools/verify.sh` -> PASS (format unit build -integration evidence). `python3 -m unittest discover -s tests` -> 18 tests, two new: -every `sceAgc*` the three driver archives reference is declared by the stubs (20 -referenced, 21 declared, none missing), and the linked `build/llvm-pie.elf` defines -the four `__eh_frame_*` symbols - the check is on the image rather than on the script, -so regenerating the script from the boilerplate fails the test instead of the link. - -**Not proven.** The driver has not been on the console since it was linked. The next -run is `bash tools/run-title.sh --watch 20` and the answers are in -`/app0/retroarch.log`. - -## 2026-09-18: First console run of the linked build: it returns 1 before video init - -**What was run.** `bash tools/run-title.sh --watch 20` against the build of `17c44d8` -(the driver linked, `eboot.bin` 29,859,722 bytes). The console reports the title -running, and it is gone before the twenty-second watch ends. - -**What the console says.** Eight attempts, each exactly this and nothing else: - - main() entered; static constructors have already run - argv built: retroarch -f -c /app0/retroarch.cfg --verbose --log-file - rarch_main returned = 1 - -No `ps5_init entered`, no frame, no menu, and no fatal signal - `main` returned by -itself, which is the one failure the title's own trace can show but not explain. - -**What the frontend's own log says.** Fetched over FTP; `/app0/...` is not an FTP -path, the file is `/data/homebrew/PPSA99169/retroarch.log`, which is worth writing -down because two `RETR /app0/retroarch.log` attempts answered `550` first. It stops -after the audio fallback: - - [INFO] [Input] Found input driver: "ps5". - [INFO] [Video] Set video size to: fullscreen. - [INFO] [Video] Graphics driver did not initialize an input driver. Attempting to pick a suitable driver. - [INFO] [Video] Found display server: "null". - [ERROR] Failed to initialize audio driver. Will continue without audio. - -Two facts follow, and both narrow it. The log reaches `drivers_init` and gets past the -video block, so the video driver was found and its own init did not fail loudly; and -`rarch_main`'s setjmp handler, which logs `Fatal error received in: ""`, -never ran - so no `retroarch_fail` fired and the return is one of the silent `return 1` -paths later in `rarch_main`. The first of those is -`task_push_load_content_from_cli` (`retroarch.c:6036`), reached after `drivers_init` -returns, which is also where the parked 0006 lives: content loading is the code that -discards the title's `-c`. - -**Why `ps5_init` is absent, and it is not a regression.** The compiled video default is -`vulkan` since `6f4bd10`, and the config that names `ps5` is the config content loading -discards, so the ps5 driver is never selected. The last run that reached the menu -(`ps5_frame 600: menu commits=22 changes=2 presented=yes` in the trace at block 1714) -is the build before that default changed, which is why it looks like a step backwards -and is not one. - -**Recorded captures.** `klog/console-trace.txt` (the whole append-only trace, 1762 -lines, the eight attempts at 1741-1762) and `klog/console-retroarch.log` (24 lines, -stderr and stdout of the frontend's logger). Both stay in the ignored `klog/` tree; -what they said is transcribed here. - -**Next.** Statement-level probes, through `tools/apply-runtime-probes.py` so they stay -revertible: inside `drivers_init` after the audio block, and around `rarch_main`'s -content-load push. Which one fires decides whether this is a Vulkan-init problem or the -content-load path 0006 already describes. - -## 2026-09-19: The Vulkan driver initialises on the console, and where it stops - -**The result, in one line.** A `tools/run-title.sh` run now shows RetroArch -selecting `video_vulkan`, creating a device, a swapchain and its textures, compiling -the stock shader's SPIR-V, and then having its pipeline refused by the driver's own -console AGC call. Nothing renders yet. Everything before that refusal is new. - -**What the trace shows**, in order, from `/app0/trace.txt`: - - probe VDRV: configured="vulkan" selected="vulkan" - probe VK: init entered 960x720 rgb32=0 - probe VK: context driver=2003a58 ident="khr_display" - probe IMG: 4x4 type=1 fmt=37 tiling=0 usage=0x7 | optimal=0xdd81 - probe IMG: 512x512 type=1 fmt=37 tiling=0 usage=0x7 | optimal=0xdd81 - probe IMG: 512x512 type=1 fmt=37 tiling=0 usage=0x7 | optimal=0xdd81 - probe IMG: 1x1 type=1 fmt=37 tiling=0 usage=0x7 | optimal=0xdd81 - probe BUF: create size=128 usage=0x80 - probe BUF: bound memory=880083980 - probe REFL: entered, vertex=273 words fragment=196 words - probe CHAIN: creating vertex module, 1092 bytes of SPIR-V - probe CHAIN: both modules created; creating pipeline - probe PIPE: vkCreateGraphicsPipelines -> -13 (pipeline=0) - -**Five frontend faults stood between the link and this**, each one a request the -driver's own checks refuse, and each fixed in `patches/series` rather than in that -project (0013, 0014, 0015): - -- the swapchain asked for `TRANSFER_SRC|TRANSFER_DST|SAMPLED` beside - `COLOR_ATTACHMENT`, and the surface advertises the one bit - now masked with - `supportedUsageFlags`, which is the specification's rule anyway; -- the 4x4 blank and 1x1 default textures are created as `B8G8R8A8_UNORM`, whose - entry in that driver's table is colour-attachment-only - the create-info is now - masked to the format's advertised features, and a format that cannot be sampled - is replaced by `R8G8B8A8_UNORM`, the same image for a uniform colour; -- `vulkan_format_to_bpp()` did not know `R8G8B8A8_UNORM`, so the staging buffer - sized from it was zero bytes long. - -**One frontend file had to go.** `src/video_filters_stub.cpp` defined the whole -Vulkan filter chain as NULL-returning stubs, from when this port had video filters -off; `src/` is linked before the archive, so the stubs won and the driver read "no -chain" - no pipeline was ever created, and nothing in the log or the trace said so. -Removing it pulled in glslang and SPIRV-Cross, and with them FreeBSD's xlocale -interface: `src/locale_shims.c` provides the thirty-four `_l` functions the console -SDK does not export, each the C-locale answer its unsuffixed counterpart gives. - -**The remaining refusal is console-only.** Both stock shaders compile cleanly -through the host build of that project's compiler, with the driver's own options - -`build/host/opengnm-psbc-probe` on the extracted SPIR-V, UBO stride 16 and the -sampler at binding 2 stride 48, vertex in both `--ngg` and `--raw` modes. What the -host cannot reproduce is the next step in `ps5vk_graphics_pipeline_create` -(driver/ps5vk_pipeline.c): `sceAgcCreateShader` and `sceAgcLinkShaders` on the -console, whose failure is the `vk_errorf(..., "AGC shader creation or linking -failed: 0x%08x", result)` that returns the VK_ERROR_UNKNOWN RetroArch sees. That -driver's own log is compiled out - `src/vulkan/runtime/vk_log.c` guards it with -`#if !MESA_DEBUG` - so the `result` code has to be read from their side. - -**Instruments added, and kept.** `tools/build.sh` now links with `--error-limit=0` -and `--Map`; `tools/symbolize-crash.py` turns a console backtrace into names using -that map (this is what identified every refusal above, since `--exclude-libs` -leaves the driver, the compiler and the SDK runtime with no symbol table); -`src/main.cpp` points stderr at the trace file and installs a terminate handler -that names an uncaught exception's type. `tools/apply-runtime-probes.py` carries -the probes that measured all of it - twenty-nine, applied with one command and -reverted before the shipping build. - -## 2026-09-19: The driver initialises, the pipeline is created, and the first frame stops on an open render pass - -**Where this round started.** A `tools/run-title.sh` run showed RetroArch selecting -`video_vulkan`, creating a device, a swapchain and four textures, compiling both stock -shaders into modules - and then `vkCreateGraphicsPipelines` answering -`VK_ERROR_UNKNOWN`, with `vulkan_init` returning NULL. Nothing rendered. - -**What it took, in order.** - -- **The topology.** `../PS5_Vulkan`'s pipeline check accepts - `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST` alone (and Mesa's meta rectangle list), and its - draw path refuses a non-indexed draw whose first vertex is not zero. RetroArch's - Vulkan filter chain drew its two quads as a four-vertex triangle *strip*. Patches - 0016 converts them to six-vertex lists, says triangle list, moves the final pass's - quad to the new offset, and draws each quad's second triangle through the binding's - own offset. `vkCreateGraphicsPipelines` then returned 0 and `vulkan_init` handed back - a real pointer. -- **Samplers.** This driver refuses any sampler whose address mode is not clamp-to-edge - and leaves the output handle untouched; `CommonResources` destroys every handle that - is not `VK_NULL_HANDLE`, so the sixteen refused ones were destroyed as if they were - real samplers and the driver asserted on the first. Patch 0017 clears the array first. -- **The runloop quit before its first frame.** `rarch_main` returned 0 with no frame - drawn. The display context's `check_window` sets `quit` when the frontend's signal - handler state is non-zero, and RetroArch's khr_display context read that as "the user - asked to quit". Patch 0018 stops this context treating it as one: a console title has - no terminal and no SIGTERM sender, and the platform's own lifecycle ends the process. -- **The value behind that state was garbage, and that is the round's real find.** It - read `-285230512`, stable across runs, before any signal had been delivered - because - **this port's CRT never zeroed the BSS**. The image's writable segment is - `0x104b4` bytes in the file and `0xc7040` in memory, and `_start` - (tooling/native/app_crt.cpp) went straight from `_init_env` to the static - constructors. Every zero-initialised object in the frontend, in the driver and in the - shader compiler therefore started with whatever the memory held: a counter, a - flag, a pointer. `tooling/native/ps5-pie.ld` now marks `__bss_start`/`__bss_end` and - `_start` clears that range before anything else. A run prints - `bss check=0 (must be 0), data check=7 (must be 7)`. -- **Assertions now say what they are.** `../PS5_Vulkan`'s `__assert` prints the - expression, file and line to stderr and then calls `abort`, which does not flush - - and the console's libc buffers stderr, so every assertion arrived as a bare - `abort is called(system)`. `src/main.cpp` points stderr at `/app0/trace.txt` **and - makes it unbuffered**. The trace now reads, for the current blocker: - - probe SPAN: offscreen passes - probe SPAN: menu upload - probe SPAN: about to begin the backbuffer pass - probe REC: begin render pass - assertion failed: cmd_buffer->render_pass == NULL - (.../vulkan-runtime/src/vulkan/runtime/vk_render_pass.c:2648, - vk_common_CmdBeginRenderPass2) - -**Where it stands.** The driver initialises, both shaders compile, the pipeline is -created, the runloop runs, and the first frame records commands until -`vkCmdBeginRenderPass` - which asserts because the frame's command buffer already has -a render pass open. The marks bracket it to the span between -`vulkan_filter_chain_build_offscreen_passes` and the backbuffer pass, which is the -first-frame history/feedback clear and the menu texture upload. Nothing reaches the -screen yet. - -**Probes and their guard.** `tools/apply-runtime-probes.py` carries thirty-nine probes -and `tests/test_frontend.py` now checks the set itself: an insert that ends with its -own anchor duplicates the line it is inserted before, which broke the build three times -in this round (a duplicated `vkCreateImage`, a duplicated `switch (`, a duplicated -`vulkan_filter_chain_build_offscreen_passes(`); every probe must carry a marker, a -note and a trailing newline. All of it is checked before a probe can reach a build. - -## 2026-09-19: Two rounds of "a render pass is already open" were this port's own probe - -**The correction.** The blocker chased through round 1 and the start of round 2 - the -driver asserting `cmd_buffer->render_pass == NULL` in `vkCmdBeginRenderPass` - was not -the driver's and not RetroArch's. A probe insert had **duplicated the -`vkCmdBeginRenderPass` call**: the tool writes `insert + anchor`, the insert ended with -the anchor line, and the tree ended up with the statement twice, so the *second* call -found the pass the first had just begun. A duplicate-detector run against -`vendor/retroarch` found five such pairs - `vkCmdBeginRenderPass`, `vkCmdEndRenderPass`, -`vkEndCommandBuffer`, `vulkan_filter_chain_end_frame`, and the `if (quit)` pair from -the ALIVE probe. Restoring the file from `vendor/retroarch` and re-applying the port -patches removed all of them. - -**The guard is stronger now.** `tests/test_frontend.py`'s `ProbeSet` checked that an -insert does not *end* with its anchor; it now checks that the insert does not *contain* -the anchor at all, which is the property that matters. It caught one more of the same -mistake in the SIGNAL probe while this was written up. - -**What the real blocker is.** With the duplication gone, a run reaches the frame's -whole recording - the backbuffer pass begins, the chain binds and draws, the quad's -second triangle is drawn through the binding offset - and then `vkQueueSubmit` asserts -with `cmd_buffer->state` neither INITIAL, EXECUTABLE nor PENDING. That state is what a -**recording refusal** leaves (Mesa's `vk_command_buffer_set_error`), so the draw itself -is being refused. `../PS5_Vulkan`'s draw path refuses in three places, and two of them -are silent in a build without its log: - -- `pipeline->draw_refusal` - a string computed when the pipeline is created; -- `ps5vk_pipeline_prepare_shaders` - the AGC `sceAgcCreateShader`/`sceAgcLinkShaders` - step, which that driver runs at the **first draw**, not at pipeline creation; -- the viewport/scissor count check, which this pipeline passes (one of each is set). - -**Why the message cannot be read from here.** `vk_log.c` drops every message unless -`MESA_DEBUG` is set at compile time or the instance has debug logging on or a debug -callback installed: - - if (unlikely(!instance) || - (likely(!instance->enable_debug_logging) && - likely(list_is_empty(&instance->debug_utils.callbacks)) && - likely(list_is_empty(&instance->debug_report.callbacks)))) - return; - -and `enable_debug_logging` is never assigned anywhere in that tree. So the two ways -forward are a debug-utils messenger created from this side, or that project's own -result code. - -**Verified.** `bash tools/verify.sh` PASS (format unit build integration evidence); -20 unit tests; `strings build/llvm-pie.elf | grep -c 'probe DRAW|probe REC|...'` = 0 -after deleting `build/ra/obj`, which a probe revert needs to take effect. - -## 2026-09-19 - the Vulkan driver draws: 644 frames in one unattended run - -Run: `bash tools/run-title.sh --no-build --watch 25` -> -`VERDICT: it ran for 25s and this script closed it` (`klog/run-PPSA99169-022103.log`). -The trace (`klog/trace-r3o.txt`, fetched over FTP from `/app0/trace.txt`) counts -644 x `probe REC: frame entered`, 643 x `probe QUAD: menu quad`, 180 x `probe TRI`, and -40 driver refusals - all of them at init (21 triangle strips, 16 sampler address modes, -3 storage-image compute uploads), none inside a frame. `vkQueueSubmit`'s assertion, -which had ended every earlier run, does not appear. - -Then the probes were reverted, `build/ra/obj` removed, the frontend rebuilt (276 of 276 -sources), and the shipping image run the same way: `klog/run-shipping-r3.log` -> -`VERDICT: it ran for 20s and this script closed it`, `klog/run-PPSA99169-023553.log` -with `grep -c 'fatal signal'` = 0. `bash tools/verify.sh` -> `PASS (format unit build -integration evidence)`. - -The four patches this step needed, each one named by a refusal the driver logged once -patch 0019's `VK_EXT_debug_utils` messenger was in place: - -- 0024: `vulkan_init_samplers` asked for `VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE`, and - `../PS5_Vulkan`'s `ps5vk_CreateSampler` creates no sampler for any border colour but - transparent black, so all four display samplers stayed `VK_NULL_HANDLE`. A NULL - sampler is the descriptor write the driver refuses ("names no sampler"), and one - refused draw ends the frame. -- 0025: any sampler the driver still refuses now falls back to `vk->samplers.nearest` - instead of reaching a draw as NULL. -- 0026: the display layout declares binding 2 as a second combined image sampler (its - HDR shaders read their source there) and the driver requires a write for **every** - binding a stage's metadata names, so the quad descriptor writer writes the same image - at binding 2. `PS5VK_PUSH_CONSTANT_BINDING` is 32, not 2 - the reserved - push-constant binding was ruled out by reading `psbc_compile.h`. -- 0027/0028: the menu texture took the B4G4R4A4 path with a B/R swizzle in the view, and - the driver samples only identity component mappings. It now takes the 32-bit path the - device-without-B4G4R4A4 branch already implemented, in `R8G8B8A8_UNORM` (the format - this driver reports as sampled, so the staging texture and its destination agree and - the upload stays a copy), with the CPU conversion's channels in that order. - -One diagnostic was tried and dropped: a probe in -`gfx_ctx_khr_display_swap_buffers` (the swap-buffers/acquire path) made the run die in -`vulkan_acquire_next_image` with `abort is called(system)` - the frames had run without -it, so it was removed rather than chased (`klog/run-PPSA99169-022734.log`). - -Still refused, and named here so the next step does not rediscover them: the frontend's -first-frame clears are triangle strips, the blank texture's upload takes the compute -path (storage-image descriptor, no table entry in the driver), and the filter chain asks -for sampler address modes the driver has no word for (patch 0023 makes the chain use its -clamp-to-edge entry instead). - -## 2026-09-19 - an unattended argument file, and screenshots compiled in - -The goal's last gap is a picture of what the console was handed, and the port had no -capture path at all. Two things were missing, both now in place and both verified on the -console: - -- `src/main.cpp` reads `/app0/args.txt` when that file exists - one extra argument per - line, blank lines and `#` comments skipped, stored statically because RetroArch's - option parsing keeps pointers into argv for the whole run. Evidence: - `probe ... "argv extras from /app0/args.txt = 3"` in the trace - (`klog/trace-r4.txt`) after staging three lines over FTP; without the file nothing - changes. -- `tools/build-retroarch.sh` now defines `HAVE_SCREENSHOTS`. RetroArch's - `--max-frames=N --max-frames-ss --max-frames-ss-path=FILE` is entirely inside - `#ifdef HAVE_SCREENSHOTS`, so on this build the options did not exist and a run had - nothing to write. With the flag the frontend compiles and links 276 of 276 sources and - the title runs (`klog/run-shot-r4b.log`: "it ran for 20s and this script closed it"). - -The capture still does not fire, and the reason is precise: `runloop.h`'s -`RUNLOOP_TIME_TO_EXIT` compares `runloop_state.max_frames` against `frame_count`, which -is the **core's** frame count. This title loads no content - the menu is the whole -program - so that counter never advances and frame 200 never arrives (three runs with -`--max-frames=200` all ran the full watch window and wrote no `shot.png`). Next step: a -port block in `runloop.c` that counts the frontend's own frames for that comparison when -`--max-frames-ss` is asked for, then read the file back over FTP and look at it. - -## 2026-09-19 - the capture needs a hook the menu-only path reaches - -The screenshot still does not happen, and this round narrowed it to a single fact. The -runloop's `--max-frames` block compares `frame_count`, which is refreshed from -`video_st->frame_count` - and that counter is only incremented by `video_driver_frame`. -With no content loaded this title draws the menu through `video_driver_cached_frame`, so -the counter never advances and the block cannot be reached by a frame budget. - -A port patch was written to test exactly that (0029: a static counter of the runloop's own -iterations, folded into `frame_count` just before `RUNLOOP_TIME_TO_EXIT`), the frontend -rebuilt (276 of 276 sources) and deployed, and three console runs were made with -`--max-frames=120`/`200 --max-frames-ss --max-frames-ss-path=/app0/shot.png` staged in -`/app0/args.txt`: - -- `klog/run-shot-r5.log`, `klog/run-PPSA99169-030553.log`: "it ran for 20s and this script - closed it" - nothing exited at the budget; -- no `shot.png` in the title folder (listed over FTP); -- `/app0/retroarch.log` still the same 1200 bytes. - -So the block is not reached at all on a content-less run: the menu-only path returns -before it. The patch was **removed** rather than committed, because it had no verified -effect - the next attempt has to hook a path that runs without content. -`gfx/video_driver.c`'s `video_driver_cached_frame` is that path (it is what draws each -menu frame), and `take_screenshot` is `tasks/task_screenshot.c`'s, so the hook is a -counter there plus a call to it, with the quit the same way the max-frames path quits. - -Everything else this round is unchanged from the last: 644 frames and 643 menu draws in an -unattended run, the probe-free image alive for its whole watch window, and -`bash tools/verify.sh` PASS (format unit build integration evidence). - -## 2026-09-19 - the capture fires; the driver's readback is what fails - -Two hooks were tried for the frame budget the screenshot needs, and the difference -between them is the finding: - -- `video_driver_cached_frame` (gfx/video_driver.c) - the natural place, and **not called** - in a content-less run: the hook never ran on the console, so that patch was removed - rather than committed. -- `vulkan_frame` (gfx/drivers/vulkan.c, patch 0031) - the one path every frame takes, and - the trace now says so on every capture run: - - capture: frame 90 of 90, take_screenshot -> failed (/app0/shot.png) - -So the port's own options work end to end: `/app0/args.txt` carrying -`--ps5-capture=90` and `--ps5-capture-path=/app0/shot.png` is read by the patched -`vulkan_frame`, the count reaches the budget, `take_screenshot` is called with the path, -and the result is written to the trace either way. `src/main.cpp` keeps `--ps5-*` lines -away from RetroArch's option parser. Run: `klog/run-shot-r6b.log`, -`klog/run-PPSA99169-031642.log`. - -What fails is the readback behind `take_screenshot` - the frontend's screenshot writer -asks the video driver for the frame, and `vulkan_readback`'s synchronous path (blit the -backbuffer into a staging texture, `vkQueueWaitIdle`, map it) returns false. That is the -next step: its refusal is logged to `/app0/retroarch.log`, which is the file that never -flushes, so the first move is to get that message out - the driver's debug messenger is -already installed, so a readback refused by `../PS5_Vulkan` would name its reason in the -trace once the readback runs inside a trace-visible path. - -The options this needs are the frontend's own and stay that way: a run without -/app0/args.txt takes no picture and behaves exactly as before (verified by the same -round's runs), and `bash tools/verify.sh` is PASS (format unit build integration -evidence) with the capture patch in place. - -## 2026-09-19 - the capture fails inside the frontend's writer, not in the driver - -Three more console runs, all with the trigger working (patch 0031's line appears every -time) and all ending in `take_screenshot -> failed`: - -- `/app0/shot.png` (klog/run-shot-r7.log) - failed, no file; -- with `video_gpu_screenshot = "true"` added to `config/retroarch.cfg` and published, so - that `take_screenshot_choice` takes the viewport path (`take_screenshot_viewport` -> - the driver's `read_viewport`) - still failed, no file - (klog/run-shot-r7.log, klog/run-PPSA99169-032236.log); -- `/app0/shot.bmp` instead of `.png`, to rule out the PNG encoder - still failed, no file - (klog/run-shot-r7b.log, klog/run-PPSA99169-032522.log). - -The decisive detail is what is *absent*: the trace around the `capture:` line has no new -`vulkan:` message, and the driver's messenger writes every refusal there. So the failure -is in the frontend, before the frame is ever asked for - `take_screenshot_viewport` returns -false without a message on two of its three early paths: `video_driver_get_viewport_info` -reporting a zero width or height, and `malloc` failing. (Its third path, the driver's -`read_viewport` returning false, would have produced a driver message; the dump path -would have produced a file.) - -The config change was **reverted**: `video_gpu_screenshot = "true"` was a hypothesis about -which path is taken, and it did not change the outcome, so it does not belong in the -shipping config. - -Next step, in order: print the viewport and the dump's result from the capture patch (one -more line in the trace, which separates "no viewport" from "writer refused"), and if the -viewport is the zero one, call the driver's `read_viewport` directly and write a PPM from -the port's own code - a PPM is a header and the bytes, so no frontend writer is involved. - -## 2026-09-19 - reading the frame back in the port's own code: first attempt does not compile - -The next step from the last entry was tried: patch 0031 rewritten to call the driver's -`vulkan_read_viewport` directly from `vulkan_frame` and write a PPM (header plus bytes) -from the port's own code, with a forward declaration of the driver's static -`vulkan_read_viewport`. The intention was to take the frontend's screenshot writer out of -the path entirely, since three runs showed it bails before the driver is asked. - -It does not compile; the frontend build reports one source missing and the link fails on -`vulkan_raster_font` (the missing object's symbols): - - /home/mihawk/Desktop/PS5_Homebrews/PS5_RetroArch/build/ra-conf/gfx/drivers/vulkan.c:4695:7: error: function declared in block scope cannot have 'static' storage class - -The rewrite was reverted rather than pushed further: `tools/apply-port-patches.py` is back -to the committed 0031 (the verified `take_screenshot` trigger), the configured tree is -rebuilt from that, and `make test-unit` is green. The lesson for the next attempt is in -the error itself - the local declaration and the driver's own definition have to agree -exactly, and a `static` forward declaration inside a function body is where that attempt -went wrong. - -## 2026-09-19 - the readback patch compiles; the console left before the run - -The rewritten capture compiles and is in the image: the frontend builds 276 of 276 -sources and `strings build/llvm-pie.elf | grep -c "read_viewport ->"` is 1 -(`build/r8b-build.log`). The two errors from the first attempt - a `static` declaration -inside a function body, and `vk` referenced before its declaration - are fixed by -declaring the readback prototype at file scope (patch 0032, next to the driver's own -`vulkan_viewport_info` prototype) and by moving the capture block below -`vk_t *vk = (vk_t*)data;`. - -The run that would have produced `/app0/shot.ppm` did not happen: the console stopped -answering partway through the round - `ping` and the FTP both report "No route to host" - -and the run that was attempted first failed at its deploy step for the same reason -(`klog/run-shot-r8c.log`: "the upload did not take"). Nothing in the port is implicated: -the same image had deployed and run repeatedly earlier in the day. - -So the round ends with the patch built but the pixels still unverified, and that is how -it is committed (`949fdb9`). The next round starts by running it: - - # arm the capture, then run and read the picture back - (upload /app0/args.txt: --ps5-capture=90 --ps5-capture-path=/app0/shot.ppm) - bash tools/run-title.sh --no-build --watch 18 - (fetch /app0/shot.ppm, convert to PNG, look at it) - -The trace line to expect is `capture: frame 90 of 90, viewport WxH, read_viewport -> ok -(/app0/shot.ppm)`; if it says `failed`, the trace line now carries the viewport size, -which is the value the frontend's own writer never showed. - -## 2026-09-19 - a capture run now ends by itself (the console is still away) - -The console answered nothing this round either - `ping -c 2 -W 3` exits 1, and the FTP -attempt before it failed the same way - so the capture run could not be made. What was -built instead is the half of the objective's evidence that a killed title can never -produce: a capture run now asks the frontend to quit once it has written its trace line. - -- patch 0033 includes `command.h`, next to the `retroarch.h` the file already includes; -- patch 0031's capture calls `command_event(CMD_EVENT_QUIT, NULL)` after the trace line. - That is how the frontend itself quits, and quitting is what flushes - `/app0/retroarch.log` - the same 1200 bytes since the first round for exactly one - reason: this title has always been killed from outside, never exited by itself. - -The frontend builds 276 of 276 sources with both patches (`build/r9-build.log`), the -capture string is in `build/llvm-pie.elf`, `make test-unit` is green (37 patches pinned), -and `bash tools/verify.sh` is PASS (format unit build integration evidence). - -The run that proves it is the one the previous entry left waiting, unchanged: arm -`/app0/args.txt` with `--ps5-capture=90` and `--ps5-capture-path=/app0/shot.ppm`, run -`tools/run-title.sh --no-build --watch 20`, then read `/app0/shot.ppm` and -`/app0/retroarch.log` back. With the quit in place the verdict should read "it exited on -its own" instead of "this script closed it", and the log should finally have content. - -## 2026-09-19 - the GPU path: seven silent faults, and the black screen that is left - -Ran the acceptance criteria in `docs/GPU_PATH_CRITERIA.md` against the console with -`bash tools/run-title.sh`, reading `/app0/trace.txt` after each run. Fifteen -diagnostic marks were added to the patch set (0046-0053) because inference failed -five times in a row: every fault below produced a clean trace, a presenting driver -and a black screen. - -A4 and B1 are met. `grep -c 'vulkan: '` over a full run's trace is 0, and the trace -carries `video driver: video_vulkan init entered` with no `ps5_init entered`. - -The seven faults, each with the patch that fixed it: - -| # | Fault | Patch | -| --- | --- | --- | -| 1 | The last refusal was `vkCreateComputePipelines`, not an upload: libps5vk refuses any set-0 binding with stride 0, and this frontend's shared set declares a compute-only storage image at binding 3 | 0036 | -| 2 | `vulkan_gfx_widgets_enabled` answered true, so the frontend skipped `gfx_display_init_first_driver` and `p_disp->dispctx` was never set - `gfx_display_draw` returns immediately without it | 0042 | -| 3 | Nothing enabled `VK_FLAG_MENU_ENABLE`; the frontend's only `true` is in `display_menu_libretro`, which is a libretro concept this content-less title never reaches | 0046 | -| 4 | The optimal menu image was never copied into after the first handover, so the draw sampled an untouched allocation | 0051 | -| 5 | A stale `/app0/args.txt` made every run capture a frame and then quit itself 90 frames in, which the console books as an application crash with a coredump | 0043 | -| 6 | `gfx_display_vk_draw` was instrumented as "the menu's draw"; it is the display-list path, and the driver composites the menu itself in `vulkan_draw_quad` | 0053 | -| 7 | A capture probe that ended its own run: `command_event(CMD_EVENT_QUIT, NULL)` in the driver's capture block | 0039 | - -Measured at the moment of the draw, once per frame: - - vulkan set_texture_frame: rgb32=0 320x240 frame=yes - vulkan copy_staging_to_dynamic: dynamic 320x240 fmt=37 type=2, staging fmt=37 type=1, compute=0 - vulkan menu state: flag=1 idx=0 staging(img=0 buf=1) optimal(img=1 buf=0) - vulkan draw_quad 0: texture=yes image=yes layout=5 320x240 pipe=yes - -Nothing refuses, the copy runs, and the quad is drawn with a valid pipeline and -image in `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`. The screen is black, and the -owner has confirmed it three times. - -Not met: B2, B3, B4. Not claimed: C1-C4. The next step is the driver's own draw -path in `../PS5_Vulkan` (`ps5vk_draw.c`), because everything on this side is now -instrumented and reads correct. - -Two process notes worth keeping. The first is that the A2 mechanism recorded in the -criteria was wrong for two rounds: both "request RGB8888" and "match the menu's -format" were answers about a path that was not being taken, and the refusal named a -binding rather than an upload. The second is that a leftover file on the console -(`/app0/args.txt`, which deploy never deletes) silently changed what the title did; -`tools/run-title.sh` now clears it before every run. - -## 2026-09-19 — Vulkan API diagnostics expose a covered title - -Added three named 0054 edits and `src/vulkan_trace.cpp`: wrappers observe all -frontend command-buffer endings, submissions and presentations without changing -arguments or results. Host tests cover failed returns, per-image results, missing -symbols and repeated loading. `bash tools/verify.sh` passed all five gates with -21 tests. The initial test harness used an enum absent from the older frontend -headers; using its numeric VkResult value fixed that harness compilation failure. - -`bash tools/run-title.sh --no-build --watch 90` first failed at socket creation -under the sandbox; the approved network retry uploaded and launched. The owner -saw the app background and manually closed it. The script mislabels that as an -exit on its own. The fresh startup segment contains 537 lines and zero refusal -lines, with the first four end/submit/present/per-image results all VK_SUCCESS. -The menu alpha is 1 and its matrix/viewport are plausible. This is a diagnostic -result, not full-window GPU acceptance. Capture and expectation are committed in -`evidence/vulkan-api-results-splash/`; raw console data stays ignored. - -Reading the CPU display startup and the sibling's diagnostic title found splash -dismissal on both, but none on RetroArch's Vulkan route. The kernel log retained -the splash scene until manual closure. The next step is a title-side splash fix. -No sibling file was changed. A fresh port-patch replay also exposed repeated 0023 -sampler blocks in the configured shader source; record separately from the -display diagnosis. - -## 2026-09-19 — Dismiss the shell splash for Vulkan - -`src/main.cpp` now performs the public splash-dismissal call before rarch_main. -`bash tools/verify.sh` passed all five gates (21 tests). The staged image digest -and target results are in `evidence/vulkan-splash-dismissal/`. -`bash tools/run-title.sh --no-build --watch 90` recorded splash return 0 and -successful initial Vulkan API results, with zero refusals in the 471-line new -startup. The owner confirmed RGUI is visible, but with buggy colours and flicker. -The final process query found no title; klog shows shell closure without a fatal -signal, so the full watch window is not claimed. Visual defects remain open. - -Correction to the previous reproducibility description: 0023's marker is never -present in its replacement at all; no later edit is needed to invalidate it. A -new regression test failed on that exact condition. Its separate marker-only fix -passes all 22 host tests and makes a second patch replay byte-identical. - -The user explicitly authorized future uploads and runs without their intervention; -this supersedes the older per-run confirmation rule, while the shared-console -busy check still applies. - -The owner subsequently confirmed the second closure was manual too, and clarified -the visual defect as blue colours and black triangles appearing/disappearing. - -## 2026-09-19 — Make the sampler fallback patch idempotent - -The 0023 marker was prose that existed only in the Python comment, never in its -replacement C++. The new `test_every_patch_inserts_its_own_marker` failed on that -block before its marker was corrected to `patches/series, 0023`. All 22 host -tests then passed. The configured shader source was regenerated solely from -upstream plus named edits to remove the repeated blocks. `bash tools/verify.sh` -passed all five gates. Reapplying the full patch list leaves all 12 patched files' -SHA256 values unchanged: `evidence/frontend-patch-idempotence/` records them. -This is a host build-tool fix and makes no new claim about console rendering. - -## 2026-09-19 — Bind deployment and trace to one build identity - -Generic startup markers could accept an older transformed executable. Two host -regressions failed before the correction. `tools/build-title.sh` now embeds a -SHA256 identity of its source/archive inputs in a generated ignored header; -main logs it and deployment requires it in the executable readback. -`bash tools/verify.sh` passes all five gates with 25 tests, including the pending -colour converter's exhaustive test. The subsequent 30-second scripted launch -reported exactly the staged identity in its trace; see -`evidence/vulkan-build-identity/`. This rules out an older input set for that run -but does not prove correct pixels. The identity run closed before its final query. -No driver project files were modified and no new dependency or compiler flag was -introduced. The generated header is not committed. - -## 2026-09-19 — Correct the RGUI upload channel order and range - -Patch 0055 expands the producer's RGBA4444 nibbles to RGBA8888 bytes, preserving -A2's matching 32-bit textures and plain-copy route. Its regression test failed -before the fix and checks all 65,536 inputs after it. The five gates passed with -25 tests. The full 30-second colour run ended with a live title which the script -closed; its fresh trace has zero refusals and successful initial API results. -`evidence/vulkan-menu-rgba/` records the capture and expectations. The subsequent -identity run was verified as the intended input set. The owner's clarified report -is a green menu with blue flicker and black triangles; upload correction does not -resolve GPU corruption, and the overall goal remains open. - -## 2026-09-19 — Resolve GPU flicker in the driver compiler - -The user's detailed observation distinguished a green menu from blue flicker and -fixed-position dark triangles. An equivalent ONE/ZERO opaque-pass diagnostic -passed host gates and completed a 30-second run but left both defects unchanged; -it is retired in `parked/explicit-stock-blend/`. The user then authorized driver -changes provided they were documented and committed in PS5_Vulkan. - -The bounded readback diagnostic (`parked/gpu-readback/`) captured correct menu -texels and white vertex colours, but corrupt completed GPU frames. A host compile -of RetroArch's fragment shader showed both texture coordinates and colour at -NIR base 0; ACO read both from attr0. PS5_Vulkan now assigns fragment input -locations as RADV does before shader-info gathering. Its fix and documentation -are committed in **6f0ce0d**, with dense/sparse compiler regressions, full driver -checks, `make test`, `make lint` and `make` passing. - -`bash tools/run-title.sh --no-build --watch 30` against that compiler completed -and the script closed the title. The fresh trace has zero refusals and no observed -end/submit/present error. The owner confirms correct colours, no flicker and no -triangles. All 909 opaque-white sampled texels are correct, and frames 8 and 9 -are byte-identical. `evidence/vulkan-fragment-inputs/` records the build identity, -trace and pixel hashes; raw captures remain ignored. A1-A4 and B1-B4 are verified. - -Temporary capture hooks were then removed from the configured source and final -build. All five RetroArch gates and 25 tests pass; ELF symbols retain both video -drivers and omit the capture function. Two final deployment attempts stopped at -the pre-upload busy check (one running title). The owner-confirmed fixed diagnostic -build remains installed; no running title was interrupted. No performance or -core-execution claim is made. Criteria documentation now reflects the actual -settled upload mechanism and the user's driver-change authorization. - -## 2026-09-19 — Quiet Vulkan timing locates the severe slowdown - -Request: measure frame preparation, submission and presentation independently -before assigning the poor responsiveness to the unfinished Vulkan driver. -This is new performance work after the completed GPU menu acceptance. - -Patch 0056 adds callback boundaries and limits five formerly unbounded texture -messages to four calls each. `src/vulkan_trace.cpp` measures monotonic elapsed -wall time in memory, emits one summary per >=5-second window after four warmup -callbacks, and preserves API arguments/results and error reporting. The refined -version also measures menu texture updates outside `vulkan_frame`. No driver -change or graphics-route switch was made. Patch count grows from 68 to 77. - -Commands: `bash tools/verify.sh`; shared-console idle check; -`bash tools/run-title.sh --no-build --watch 45` for each build. Both runs completed -and their scripts closed the title. The first build -`1c14ef0e4f69b4fc559ba20d5b8bea6e6e5fff42060b0b029f029cc71da18b79` -still logged texture creation/staging asks repeatedly: seven windows fell from -35.907 to 1.630–1.985 FPS, with up to 958.226 ms outside the video callback. -The owner reported better responsiveness initially but a falling estimate. - -The final build -`38ada4be3b74b8230ec13aae97b7d3618c4635760c30e90b29a54f0008e14450` -limits those remaining logs. Eight windows hold 53.760–57.942 FPS: 2,239 frames -in 40.039 seconds, aggregate 55.920 FPS. Per-frame weighted means: preparation -5.344 ms, end 0.020, submit 3.525, present 8.075, fence/acquire 0.041, menu texture -0.153, remaining outside time 0.724; total 17.882 ms. Phase averages sum within -printed rounding. Each of the five quieted diagnostics appears exactly four times. -Both traces have zero refusals and no recorded API errors; final successful -command-end, submit and present counters reach 1,800. CPU fallback is absent. - -The owner reports "Noticeable improvement" with the estimated refresh rate -starting at 40 Hz and increasing. Source inspection confirms the estimate uses -average frame intervals, not the driver's output-mode rate. This and the timing -support a major diagnostic-I/O bottleneck in the port, not long submit/present -stalls. They do not establish a steady 60 FPS or eliminate all latency: final -frame maxima include 282 ms early and 167–184 ms in later windows. Sparse logs -and summary I/O remain possible contributors to investigate separately. - -All five gates PASS with 26 tests. The added fake-clock regression validates -warmup/window boundaries, phase accounting, unchanged dispatch and failed API -results. Applying all 77 patches to a fresh vendor copy and applying them again -produces identical hashes. No toolchain flag/dependency changed. PS5_Vulkan is -clean at 6f0ce0d. The final profiling build is installed. - -Evidence: `evidence/vulkan-quiet-timing/{capture,expectation}.json`, replay with -`python3 tools/evidence.py compare evidence/`. Private raw captures: -`klog/gpu-quiet-timing-run.log`, `klog/gpu-quiet-texture-timing-run.log` and their -`*-latest-trace.txt` extracts. Timing definitions: `docs/GPU_TIMING.md`. - -## 2026-09-19 — Keep development logs, buffer profiling, then move to XMB - -User requested important logs remain available while removing frame-loop stalls. -Patches 0057–0059 and the local profiler remove periodic successful-frame writes, -stop button-transition chatter, and make the two menu-handover probes once-only. -Startup diagnostics, driver errors/refusals and assertions remain. The frontend -logger is explicitly re-enabled after argument/config processing and writes the -build identity: the old 1,200-byte file had been stale. The run tool now captures -and verifies this log, and checks idle state before uploading and launching. - -Profiling is opt-in (one-shot gpu-profile.txt), with 120 warmup frames and a bounded -8,192-frame buffer. No summary/file writes occur during the measurement. It writes -windows and per-frame TSV after capture; tools/analyze-gpu-profile.py validates -phase sums/completeness and reports percentiles and presentation-return intervals. -These intervals are not hardware scanout timestamps; missed-interval values are -explicitly estimates. No driver or SDK code changed. - -Commands: bash tools/verify.sh; bash tools/run-title.sh --no-build --gpu-profile 60 ---watch 80; python3 tools/analyze-gpu-profile.py klog/gpu-profile-.tsv. -First buffered run (111457): 3,597 frames, 60.009771315 s, 59.940238 FPS, maximum -interval 16.905813 ms, no presentation-completion gap above 20 ms. Owner: "Very -smooth", estimate stabilizes at 59.941 Hz. With fresh frontend logging (112036): -3,582 frames, 59.690273 FPS, three ~100 ms gaps localized to texture updates. -The remaining first-four-call texture probes could occur late and were limited -to one initial call. The final run (112603), identity -a6063c61145fb42ac409a1d78026f29865673e4575e9a6afc3500e4abccc4f66, launched with -zero refusals and a current frontend log, then rarch_main returned 0 before the -profile completed. Required profile retrieval failed correctly. No final timing -claim is made, and no further performance run was started: the user requested -committing and proceeding to XMB. Existing audio/config-save errors remain visible -and outside this step's acceptance. - -All five host gates PASS, 28 tests; fresh patch replay/idempotence PASS, 86 edits. -Evidence: evidence/vulkan-buffered-logging/{capture,expectation}.json. Replay: -python3 tools/evidence.py compare evidence/. Raw logs and TSV remain in klog/. - -## 2026-09-19 — XMB default on the Vulkan path - -The user ended performance work; logging was committed as d46495d, then this step -enabled upstream XMB while retaining RGUI/video_ps5. Official monochrome assets -are pinned at 73106363e14e34c08a5854b4cfbc29f184e3b783 (120 fixed icons, M+ font, -licenses). Configure arguments now invalidate the cached configured tree before -feature flags are derived. The asset pin adds no build-time network dependency. -ASSETS_DIR uses the Unix prefix convention; patch 0063 sets the actual title asset -default because the null platform frontend never executes Unix initialization. - -Console commands were `tools/run-title.sh --no-build --watch N`; each upload was -read back and the run trace selected by its last startup marker/build identity. -Raw files below are ignored; per-run identities, counts and results are distilled -in evidence/xmb-default/capture.json: - -- xmb-first-run.log (60 s): XMB aborted on an 8-byte UBO range; EndCommandBuffer - returned -13, then Mesa asserted on submission of the invalid command buffer. - Owner reported black/background/crash. Patch 0060 pads the ribbon upload. -- xmb-corrected-run.log (60 s): the range refusal cleared; missing sampler binding - 1 caused the next -13/assertion. Owner again saw no usable menu. Patch 0061 also - fixes the independently found strip bounds/count bug; patch 0062 binds the - unused sampler slots through the existing descriptor-fill path. -- xmb-bindings-run.log (60 s): zero refusals/API errors; title survived until the - script closed it. Input worked, but the owner saw faint text and black output. -- xmb-assets-run.log (60 s): explicit /app0/assets default, startup diagnostics - report assets present/fonts ready; zero refusals and full script-owned run. - Owner still reported corrupted icons/text. This ruled out missing assets. -- xmb-capture-run.log (30 s): authorized readback on frames 120/121 reproduced the - narrow background, glyph fragments and repeated/cropped icon mip fragments. -- xmb-textures-run.log (45 s): patch 0065 fixes physical-row alignment/UVs; 0066 - uses one static texture level. Readback shows readable XMB with intact icons, - gradient and ribbon. Owner: "It's flawless!"; initial estimated refresh 25 Hz. - This diagnostic wrote large synchronous snapshots; no performance claim made. -- xmb-final-run.log (45 s): screenshot source/hook removed. Identity - 2df0e277df59faf661a37920988d1b804394c8ffe509c2b57296270f3dcaf7b3; - zero refusals, no API failures/assertion, successful presentations, current - frontend log, assets present/fonts ready, no framebuffer writes. Script closed - the still-running title. PS5_Vulkan remained unchanged at 6f0ce0d. - -Host verification: tools/verify.sh PASS (format/unit/build/integration/evidence), -31 unit tests, 278/278 frontend sources. Fresh vendor replay and second-pass -idempotence PASS for 100 edits (86 plus 14 XMB compatibility/diagnostic edits). -The geometry test executes C and checks bounds/winding through the complete -ribbon; row-alignment cases include 336/720-wide R8 atlases. Asset tests verify -pinned checksums and every fixed icon named by upstream. Evidence replay after -adding this record: 13 captures, zero failures. - -Intermediate host failures were fixed before deployment: the edit-count guard -was updated after intentional additions; the asset logger needed verbosity.h; -the temporary capture anchor was made distinct from the timing hook. Removing -that capture initially left its call in the generated source and failed linking -ps5_capture_images. Restoring that generated file from pristine vendor and -reapplying the final named edits removed it; the normal rebuild and gates passed. -The retired recipe documents this patch-removal requirement. - -Reproduce acceptance with tools/verify.sh, tools/run-title.sh --no-build --watch -45 and python3 tools/evidence.py compare evidence/. Screenshot recipe: -parked/xmb-readback/README.md. Known audio/config-save errors remain logged. -No core, steady-state XMB FPS or general mip-chain correctness is claimed. - -### 2026-09-19 — native PCM audio driver, registration and audible acceptance - -User-assigned step: adapt ProsperoLight's native output to `audio_driver_t`, -register `audio_ps5`, verify audible playback and buffering, then commit. No SDL, -Opus, core implementation or sibling driver change is part of this step. - -Implemented `src/audio_ps5.cpp`: 48 kHz S16 stereo, 256-frame AudioOut grains, -rate negotiation, bounded producer ring and paced worker, byte-based write and -capacity callbacks, blocking/nonblocking backpressure, pause/resume, error wakeup -and drain/close cleanup. Named patch 0067 adds registration/default (102 total -patch edits). The staged config agrees. Added an opt-in native tone/buffering test -and `tools/run-title.sh --audio-test` with current-build report validation. -Normal launches do not generate tones; useful development logs remain enabled. - -Validation and artifacts: - -- `python3 -m unittest discover -s tests -p test_audio_ps5.py -v`: PASS. The real - backend runs against a paced native mock: exact native arguments, rate/byte - contract, FIFO/wrap, partial/full nonblocking writes, blocked producer wakeup, - zero-filled tails, pause/resume and injected output failure. Expected injected - errors were observed and handled; no unexplained host-test failure remains. -- `tools/verify.sh`: all five gates PASS, 32 unit tests, signed PS5 title and - 143-file staged-tree manifest. Identity - `801df0ac6a5754c3e6a6ea688f1e49e8ddd2041aeb131a637b8c8b59f4030d55`. -- Fresh patch replay/idempotence: PASS, 101 edits applied plus one marker already - in upstream (`return "vulkan";`), then 102 present and bytes unchanged. An - initial scratch assertion expected 102 newly applied edits; inspection showed - the pre-existing upstream marker, and the corrected accounting passed. No - vendor source was changed. -- `tools/run-title.sh --no-build --audio-test --watch 45`: verified upload; - `klog/audio-native-run.log` and `klog/audio-test-123431.json`. The report passes: - 193,536 accepted/played frames, zero discarded frames/output errors, 1,536-frame queue - and high water, 6,144-byte partial nonblocking write. Native drain returned - `0x100` (success), close zero. Normal frontend audio reopened at 48 kHz and - initialized successfully. Zero Vulkan refusals/API failures; XMB fonts/assets - ready. Owner: "I heard left right left right, then retroarch started!" - Runner found the title closed at 45 seconds; owner: "I closed it manually". - This is not treated as an unexplained crash or a full-window survival pass. -- `tools/run-title.sh --no-build --no-deploy --watch 45`: normal follow-up, - `klog/audio-normal-run.log`, current `klog/retroarch-123631.log`. No test-tone - activation, audio initialized, XMB ready, zero refusals/API failures. Title - remained alive for the full window and the runner closed it. -- Committed evidence: `evidence/native-audio/{capture,audio-test,expectation}.json`. - `python3 tools/evidence.py compare evidence/` replays 14 records. - -ProsperoLight and PS5_Vulkan were not modified; PS5_Vulkan remains `6f0ce0d`. -The known unset configuration-save directory remains outside this audio step. -Core audio and sustained content-based A/V synchronization remain unverified; -zero-filled idle/partial grains are not claimed to measure streaming underruns. - -### 2026-09-19 — fix configuration and native directory browsing - -User reported missing config loading and browsers containing only `/`. Inspection -confirmed the null platform lacked the callbacks/defaults needed by this title. -Implemented `frontend_ctx_ps5` (0068), preserved initial argv, supplied native paths -and roots, and separated the live config from the packaged seed. Main now passes -`-c /app0/config/retroarch.cfg`; the frontend creates only its own title directories -and installs the seed only if no live configuration exists. - -Verification sequence, including failures: - -1. Initial build failed because the new menu interface reached libretro-common's - relative config include through the pristine vendor tree. The title now uses - the configured libretro-common include directory; added the missing C++ - initializer-list include. No dependency/version was added. -2. First console run, `tools/run-title.sh --no-build --watch 90`, identity - `c2d8df3799dc02af17d8f0694291f38f9cc30b1276ad8d992d6f86af5b18fa2a`, - `klog/paths-first-run.log`: seed installed; libc opendir returned EPERM for - known roots. Owner reported crash. Kernel SIGSEGV at zero, ELF-symbolized stack - reached application-path discovery during config saving, from menu init. -3. Added native executable path (0069) and public SDK getdents adapter (0070). - Second run, same command, identity - `887759ce3cfbe4baf4e5d63b9f0abcc86c3c6e15deafe9a0e954e6de308386d4`, - `klog/paths-native-run.log`: config saved; owner "It works! And it saved config". - However, owner confirmed `/app0` remained empty. Root enumerated eight entries; - 4 KiB reads on `/app0` and `cores` reported EINVAL. Runner found title alive at - 90 seconds and closed it, then falsely rejected the frontend log because the - next local diagnostic build had changed its comparison identity. The trace's - identity and independently captured frontend log match this run. Fixed runner - to snapshot identity before deployment; this partial run is not acceptance. -4. Increased the directory-read buffer to 64 KiB. Host regression explicitly - rejects small mounted-directory reads: the prior 4 KiB variant fails, current - adapter passes. Unit harness setup issues (stale linker-wrapper reference and - an incorrect hardcoded string-length assertion) were corrected before gates. -5. Final run, same command, identity - `26139a294d687ccf21c8fa779e5ad3883273b873b71ce67e1df48f48589f8f76`, - `klog/paths-large-run.log`, frontend `klog/retroarch-130100.log`: 96 entries in - `/app0`, two in the empty `cores` directory, errno zero; no frontend ERROR - lines; native audio and XMB fonts/assets ready; zero Vulkan refusals/API - failures; 90 seconds alive and script-closed. Owner: "Folders are visible, - and when I select Load configuration file I could see the .cfg file". -6. FTP readback of live config before/after the final upload/restart: 108,188 bytes, - unchanged SHA-256 `f7f2b7ba3545c0dd375004576c80d78a11b42aa2c351eb1f81349695571f15c1`. - Only selected known port settings/digest are distilled; raw files stay ignored. -7. `tools/verify.sh`: all five gates PASS, 35 unit tests, 15 evidence records. - Fresh patch replay 104 applied plus one upstream marker; second pass 105 present, - unchanged bytes. Tests exercise config preservation, startup callback, roots, - SDK directory records/errors, and executable-path discovery without procfs. - -Artifacts: `evidence/native-paths/{capture,expectation}.json`; -replay: `python3 tools/evidence.py compare evidence/`. Documentation maps `/app0` -to the FTP title folder and explains live-config preservation and content locations. -No core/ROM execution, USB availability or complete storage milestone is claimed. -PS5_Vulkan and ProsperoLight are unchanged; Vulkan and CPU/RGUI fallback registration -are preserved. Packaged seed changes after the run are comments only; the tested -binary identity is unchanged and the saved live configuration is byte-identical. - -## 2026-09-19 — Repair managed directory permissions for FTP - -Scope: the owner requested `0775` or `0777` after FTP could create downloads but -could not place files in app-created cores/content. Existing folder listings -showed `0755`. No sibling project changes or core implementation were assigned. - -- Regression: the former mkdir-only code fails the host permission assertion - with existing `0755` directories and umask `0077`. The final fixture covers - existing `0755` and `0775` plus new folders; all become `0777`, while seed and - existing configuration preservation checks still pass. -- First console run: `tools/run-title.sh --no-build --watch 30`, identity - `88367c83a15584015a71805f5599effc29875fd419a144ca042a5c59f2148a4f`. - Alive throughout, script-closed; chmod worked, all seven modes were `0775`. - `python3 tools/check-ftp-write.py` returned 1: all seven uploads denied with - `550 Permission denied`. No probes remained. This was not acceptance. -- An initial attempt to deploy the fallback refused because a title was running. - No title was interrupted; the owner closed it and confirmed availability. -- Final console run: same runner command, identity - `347655429cf4a60f3d7608c3ba4e46ec1c1972f81704cbea9f9e654c9942cfed`. - Upload readback matched, 30 seconds alive and script-closed. Zero permission - errors, Vulkan refusals, GPU API failures or frontend ERROR lines; presentation, - XMB assets/fonts and native audio initialization observed. -- Final FTP command returned 0: all seven directories `0777`, each disposable - upload/readback/cleanup passed. No existing files were overwritten by probes. -- All five `tools/verify.sh` gates PASS; 35 unit tests, 16 evidence records. - Machine-readable capture and expectations: `evidence/ftp-directory-permissions/`. - Raw captures: `klog/permissions-{run,open-run}.log`, `retroarch-131931.log`, - `ftp-write-*.json`; host/gate logs: `/tmp/permissions-*.log`. - -Only the seven managed directory modes are repaired; user file modes are not -changed recursively. Error logging remains enabled. PS5_Vulkan is unchanged at -`6f0ce0d`. Actual core/content execution remains a separate, unverified step. - -## 2026-09-19 — Native joypad fixes stick navigation and binding capture - -The owner reported working menu buttons but no left-stick navigation and binding -capture timeouts. The input-only backend bypassed the joypad callbacks used by -both paths. Added `ps5_joypad`, its built-in default profile and registration; -`input_ps5` supplies no duplicate hardcoded gamepad state. The pad is polled only -through the joypad interface. Zero new samples preserve held input for the extra -binding-screen poll; errors/interception/disconnect clear input. Existing saved -configuration is preserved, and an empty joypad selection finds the new backend. - -- Initial unit gate flagged the pinned patch counts (105 to 108, and three to - four input_driver.c edits). Updated them for the new joypad declaration, - registry entry and built-in profile. No additional dependency or SDK flag. -- Host fixture compiles the actual backend and upstream input mapping/analog - helpers. Explicit button and axis bindings suppress old mappings; raw capture, - left/right axes, triggers, deadzone, second polls and teardown/reinit pass. - ELF relocation checks verify both registration and the profile in the build. -- Fresh patch replay: 107 applied plus one existing upstream marker. Second - pass: 108 present, all bytes unchanged (`/tmp/input-patch-replay.log`). -- Interactive console command: `tools/run-title.sh --no-build --watch 120`. - Build `e6211dea9fd0bb512e83a9979a6a44c0c8c5fdf175564c753e3fd2a25067e0a9`. - Owner confirmed left-stick navigation and button/stick binding capture: - "Everything works flawlessly!" The title was gone at the 120-second check; - the runner's generic early-exit verdict was resolved by the owner's explicit - confirmation "I closed it manually". No crash was inferred from that verdict. - Raw capture `klog/input-joypad-run.log`, frontend `klog/retroarch-133020.log`. - -Rumble, multiple controllers, saved remapping after restart and actual core -execution are not claimed. PS5_Vulkan remains unchanged at `6f0ce0d`. - -Follow-up: `tools/run-title.sh --no-build --no-deploy --watch 30`, same build, -full window alive and script-closed (`klog/input-joypad-followup.log`, frontend -`klog/retroarch-133319.log`). Both runs selected/configured the PS5 joypad and -presented XMB with native audio initialization, zero GPU refusals/API errors and -no frontend ERROR lines. All five `tools/verify.sh` gates PASS: 37 unit tests, -17 evidence records. Sanitized capture/expectations: `evidence/native-joypad/`. - - -## 2026-09-19 — FCEUmm reproducible build; native core loading blocked - -Requested step: build FCEUmm with this repository's SDK and native-title pipeline. -The websrv SNES9x2010 script supplied the fetch/build/stage pattern only. Added -`tools/build-fceumm.sh`, `make fceumm`, shared-ELF ABI verification, title staging -and core metadata defaults. Pinned source/info archives are digest-checked. -Upstream source remains untouched. Link uses the compiler driver, explicit native -runtime stubs, no payload CRT/static libc, and a content-derived build ID; reasons -and paths are in `docs/REFERENCE.md`. - -Initial link experiments rejected raw-linker `-Wl` arguments/missing `-lm`, then -host Clang rejected `-nostdlibc`. The final command uses the compiler link driver, -`LIBM=` with the libc math stub, and supported `-nostdlib -nodefaultlibs`. -Clang's implicit `--build-id=uuid` changed hashes between identical builds; -`--build-id=sha1` fixed this. Multiple fresh builds now give core SHA-256 -`d1afcd2ea84de627f4a28d8ec4e81d63d23ca5b531811ba57357d98f8b31759f`. - -First console command: `tools/run-title.sh --no-build --watch 180` (manual -selection), identity `b6c7c509098a466630da0215a52bec1f3fd12e9ee1fe071bb39937faa0b5d7e4`. -Core open failed with null loader error. The user reported missing detection. -The `.info` was in `/app0/info`, but saved config had an empty info path; upstream -therefore searched `/app0/cores`. This was an actual placement compatibility bug. -That run ended early without a fatal signal; manual close was not confirmed. - -Corrected staging includes identical info beside the core and in `info/`. -Cache refresh is required because upstream can cache missing metadata. Deployment -now writes/readbacks `core_info.refresh` after info upload; the running deployment -had loaded the earlier script, so equivalent markers were uploaded/read back -explicitly before the corrected run. Unit checks cover refresh location/content -and failed readback. Core uploads require exact SHA-256, without the old driver -marker fallback. No live user settings were rewritten. - -Second command: `tools/run-title.sh --no-build --watch 120`, identity -`261d33099adec97c231205beb21d6011e1587436df099d4e6ac6f29949c50c8f`. -Full FTP core readback matches all 4,836,144 bytes and the final hash. Decoding the -console's RZIP metadata cache confirms full FCEUmm name, NES extensions and -`has_info: true`. Core open still fails. Owner: "App crashed after I started game". -The log reports missing dynamic-core path during content reinitialization; klog -records SIGSEGV at 0x80. Symbolization after subtracting image base 0x400000 shows -`vulkan_alive(NULL)` -> `runloop_iterate` -> `rarch_main` -> `main` -> `_start`. -This is a frontend failure path after a failed core load, not proof of core or -Vulkan-driver execution failure. Runtime loading/game acceptance remains FAIL. - -Host verification: `tools/verify.sh` passes all five gates, 45 tests. Artifact: -`evidence/fceumm-build/` contains the ABI report, metadata result and explicit -failed target result. `parked/native-core-loading/README.md` records the remaining -loader/recovery work and its acceptance. PS5_Vulkan unchanged at 6f0ce0d; no ROM -or BIOS is distributed. This commit completes build tooling, not a playable port. - -## 2026-09-19: native FCEUmm loader, failure guards and first gameplay - -Implemented the requested native loading/recovery step. The loader maps checked -ELF64 segments, binds 71 explicit native imports, applies 16,001 relocations and -publishes handles only after protection succeeds. FCEUmm remains a dynamically -loaded core built with this project's SDK; PS5_Vulkan remains unchanged at -6f0ce0d. Core upload uses matching RGBA8 textures with an XRGB8888 conversion. - -Runs and failures: - -- `tools/run-title.sh --no-build --core-test --watch 30`, memory probe, - `klog/run-PPSA99169-141420.log`: anonymous RW -> RX code returned 42; full window. -- Same command, `klog/run-PPSA99169-142423.log`: eight real-core load/export/API/ - identity/unload cycles passed before frontend startup; full window. -- `tools/run-title.sh --no-build --no-deploy --watch 180`, - `klog/run-PPSA99169-142526.log`: menu-time core file read failed; owner saw - No Core/archive failure and confirmed manual quit/close. Kernel exit(0) then - SIGSYS is retained, not classified as a core execution crash. -- Mapped/chunked-read diagnostic, `klog/run-PPSA99169-143443.log`: startup and - live-menu loader/rejection reports passed. Manual game start then aborted in - `vulkan_copy_staging_to_dynamic` (ELF offset 0x6b87ff, ce7176a7 build). - Symbolization: `addr2line -Cfipe klog/native-core-ce7176a7.elf 0x6b87ff - 0x6b3880 0x6b0129 0xae8f1b 0xada6fd 0x620a`. The format mismatch selected the - RGB565-only compute path; matching RGBA core textures resolve it. -- `tools/verify.sh`: PASS, all five gates, 52 tests; log - `/tmp/core-frame-verify.log`. Loader tests execute relocations/imports and - malformed-file rejection; recovery test fails on upstream's unchecked return - and passes with the guard; frame test verifies channels/alpha/pitch/in-place. -- `tools/run-title.sh --no-build --core-test --watch 180`, - `klog/native-core-game-rgba-run.log`, kernel `klog/run-PPSA99169-143921.log`: - identity 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5; - core SHA-256 fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c. - Both diagnostic JSON reports pass. Owner manually loaded the archive and - answered “Works flawlessly!” to video/audio/controls, then reported closing - manually because the menu shortcut was unset. Three launches occurred within - the capture; the runner closed a later menu instance. Zero GPU refusals/API - failure records and zero kernel fatal signals. Do not claim continuous runtime. - -Evidence: `evidence/native-core-loading/`, replay with `tools/verify.sh evidence`. -Raw gameplay log was saved before a later menu launch replaced it. No ROM or -private filename is committed. Return-to-menu shortcut, repeated game unload, -saves/states and measured performance are not included in this acceptance. - -Provenance correction for the preceding entry: this task did not edit -PS5_Vulkan, but its checkout advanced independently during the work to 8c2985a -and then 085c632. “Remains unchanged at 6f0ce0d” is therefore not a valid claim -about the sibling checkout or the linked driver revision. The accepted title -identity includes the actual archive bytes read at build time; no driver source -commit is inferred from the later checkout. Its archive changed after the -accepted title linked, so the accepted dist/ artifact has not been rebuilt -against that newer archive. Console acceptance remains tied to identity 9b287ca7. - - -## 2026-09-19 — Native mGBA archives and content lifecycle - -Added `make mgba` and the normal title build/stage integration using pinned -upstream CMake libretro source 7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6. -Both GB and GBA engines are enabled; official metadata is staged in info/ and -cores/. The core has no SDL/Qt or payload CRT. CMake is used because it is this -revision's maintained libretro target. Native directory/time adapters and 21 -validated init callbacks support its ABI. Pins and reasons for toolchain/link -flags are recorded in REFERENCE.md and the generated evidence build manifest. - -Iterations, all run with manual archive selection: - -- `656d34aa…`, `klog/run-PPSA99169-150419.log`: loader/recovery passed, but GB/GBC - crashed in mCoreConfigPortablePath at native getcwd; GBA failed before core - entry. Explicit /app0/config/mgba removed getcwd/getenv dependencies. -- `9c7baa14…`, `klog/run-PPSA99169-151335.log`: owner confirmed GB/GBC load. - Archive diagnostic isolated a null 16 MiB allocation, extraction result 2; - errno=2 was stale. Owner manually quit, exit(0) then SIGSYS. -- `773678fa…`, `klog/run-PPSA99169-152050.log`: tracked mmap allocations >=1 MiB - enabled GBA archive loading. Owner reported swapped colours, menu flicker and - Quit crash. The raw lld invocation initially rejected -Wl wrapper syntax; - corrected --wrap flags passed all gates (56 tests). Native realloc ownership - remains native; this is not an interposer inside the native libc library. -- `ef7004d3…`, `klog/run-PPSA99169-153046.log`: mGBA XBGR->XRGB callback copy, - consistent RGBA menu textures and immutable cached frames fixed colours. - Native LoadExec exit fixed Quit (zero fatal reports). Owner confirmed both; - Close Content still flickered for mGBA and FCEUmm. All gates, 57 tests. -- `6a761ad7…`, `klog/mgba-transition-capture-run.log`: diagnostic screenshot - trigger built and passed gates, but console became busy during deployment; - launch refused. Trigger was removed; owner supplied an RGUI photo/video. -- UV crop host development: format lint first required formatting; regression - initially compared double and float UVs exactly and was corrected to float32 - expectations. First crop build passed 58 tests/all gates. A follow-up made - substituted sampled format determine the physical width consistently. -- Final `82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337`: - `bash tools/verify.sh` passed all five gates, 58 tests, log - `/tmp/mgba-uv-format-final-verify.log`. mGBA SHA-256: - feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6. - `bash tools/run-title.sh --no-build --core-test=mgba --watch 180` first refused - a busy console. After owner confirmed idle, upload/readback matched and the - launch succeeded. `klog/mgba-uv-crop-run.log`, kernel - `klog/run-PPSA99169-155435.log`: one current-build launch, zero Vulkan refusals, - zero GPU API failure records and zero kernel fatal signal reports. Eight - loader cycles and actual failed-load recovery passed. Owner: “Menu and next - game are clean.” Live frontend log records GBA -> menu -> GB/GBC -> menu, - then frontend return 0/native exit before the window ended. Runner's wording - “exited on its own” is not evidence of a crash or uninterrupted gameplay. - -Patch 0078 crops core sampling to initialized logical pixels in padded images; -it uses per-pass/per-sync VBOs and preserves logical size semantics. No switch -away from Vulkan and no PS5_Vulkan edits. The sibling is independently developed; -linked archive hashes, unchanged through this build, are in the evidence. - -Evidence: `evidence/mgba-native/`, replay `bash tools/verify.sh evidence`. -Private ROM names, console credentials and raw captures are not committed. -Save/state round trips, long-run A/V sync, measured performance, Slang presets, -and a fresh FCEUmm/XMB transition matrix are not claimed. - - -## 2026-09-19 — Snes9x with explicit colour and C++ lifetime contracts - -Implemented the requested Snes9x port using pinned official libretro revision -fae2fea08f74180759ef540ee94259213f503480 (1.63), source archive SHA-256 -0d4b0c4181d66668ec0040eb17f90a559f7b7fa6cbd9198593c3bdbe79acd029. -`make snes9x` invokes the upstream libretro Makefile with this project's SDK; -no alternate SDK or payload CRT is introduced. Upstream LTO-off, no-strict- -aliasing and no-exceptions/no-RTTI choices remain. Explicit native/C++ bindings -require -z undefs for core link but are resolved by the title link. Metadata -revision/digests and all port inputs are captured in the evidence build report. -Upstream info display_version remains 1.61; the actual core reports 1.63 + pin. - -Colour handling leaves RGB565 renderer/filter arithmetic intact and converts -only final frames into a separate bounded XRGB8888 buffer. Both libretro pixel -format negotiation sites and every video callback path are patched. Host tests -exhaust all 65,536 RGB565 colours through the actual frontend RGBA upload, with -additional pitch, capacity, cached-source, hires/NTSC/4x dimension checks. - -Initial static build 81f07c1a… passed ABI but was not deployed: its imported -__cxa_atexit would retain callbacks into unloaded code. The final core includes -an export-hidden C++ destructor registry; a validated fini-array callback runs -before last-close unmap. Loader tests exercise reverse destruction order, -reference counting/reload and invalid finalizer rejection before constructors. -The host C++ fixture initially hit GNU ld's overlapping EH FDE header error with -the PS5 linker layout; --no-eh-frame-hdr fixes that host-only fixture. The target -build linked normally. TLS/legacy init/fini/unwind registration remain unsupported. - -Verification and console acceptance: - -- `bash tools/verify.sh`: PASS format, unit, build, integration, evidence; - 60 tests. Log `/tmp/snes9x-verify.log`. Final core SHA-256: - e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac, - 3,392,576 bytes, 25 exports, 4 initializers, 1 finalizer. -- Frontend identity: - 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53. - Preserved ELF `klog/snes9x-64bef2e7.elf`. -- `bash tools/run-title.sh --no-build --core-test=snes9x --watch 180`: - verified deployment, eight native loader cycles and resident-menu recovery - passed. Deliberate missing-core errors are part of that successful test. - Raw `klog/snes9x-first-run.log`, kernel `klog/run-PPSA99169-161400.log`. - One current-build launch, zero Vulkan refusals/API failure records and zero - kernel fatal signal reports. Title remained open through 180s and runner - closed it. No claim of 180s uninterrupted gameplay. -- Owner-loaded archive negotiated XRGB8888, 256x224, declared 60.10 Hz and - 32040 Hz audio, then returned to the dummy menu. Owner answered the combined - colours/sound/controls/Quick Menu/Close Content/next-content question with - “Works flawlessly.” The geometry log independently records menu -> Snes9x -> - menu; no exhaustive cross-core matrix or special-mode coverage is claimed. -- FCEUmm/mGBA binaries retain their accepted hashes. PS5_Vulkan was read only; - its exact linked archive hashes stayed unchanged during verification and are - saved in the evidence. No driver source revision is inferred. - -Evidence: `evidence/snes9x-native/`; replay `bash tools/verify.sh evidence`. -Raw logs/private filenames remain ignored. BIOS subsystems, special chips, -interlace/hires/NTSC/HD Mode 7 console tests, saves/states and long-run timing are -separate acceptance work. Build/ABI reports are static checks; their false -console_loading_verified field is superseded only by the separate console -capture, not rewritten into an unsupported claim. - -## 2026-09-19 — Native FBNeo with verified gameplay colours and transitions - -Task: add FBNeo through the existing native core pipeline, use supplied metadata -only as a reference, and verify gameplay/menu/next-game colours before committing. -Official source and core-info are pinned and hash-checked; no game/BIOS is shipped. -The full core preserves native 32-bit rendering and converts 16-bit callbacks. -The native wrapper selects SDK-compatible flags, core-local C++ cleanup and a -bounded MPEG decoder escape. Catalogue names use two bulk allocations through -the existing frontend allocator. PS5_Vulkan was not modified. - -Commands and outcomes: - -- Initial `tools/build-fbneo.sh`: MPEG throw/try failed with exceptions disabled. - Added a local non-unwinding bounds escape and differential decoder tests. -- `bash tools/verify.sh` → `/tmp/fbneo-verify.log`: all gates/62 tests passed. - Console `--no-build --core-test=fbneo --watch 240` failed: metadata patch retained - free() after changing the buffer to static. Build `a69d4ad4...`; raw - `klog/fbneo-first-run.log`, `klog/run-PPSA99169-163509.log`. Owner reported crash. -- Removed invalid free, added 10,000-query sanitizer regression. All gates passed - in `/tmp/fbneo-fixed-verify.log`; build `96ee4812...` passed eight loader cycles - and recovery. Game load failed with NULL strcpy in BurnLibInit after catalogue - small allocations exhausted libc heap. Raw `klog/fbneo-fixed-run.log` and - `klog/run-PPSA99169-164532.log`. Owner confirmed core loads but game crashes. -- Bulk catalogue storage, rollback and pointer restoration fix the second failure. - `bash tools/verify.sh` → `/tmp/fbneo-catalogue-verify.log`: all five gates pass, - 63 tests. Stress/failure/reload tests use 30,000 drivers; all 28,910 real names fit. -- `bash tools/run-title.sh --no-build --core-test=fbneo --watch 240` → - `klog/fbneo-catalogue-run.log`: verified deployment and loader/recovery PASS. - Kernel `klog/run-PPSA99169-165611.log`; live trace/frontend captured under - `klog/fbneo-catalogue-live-*`. Owner manually loaded games and confirmed - “Works flawlessly!” for colours/audio/input and menu/next-game transitions. - Both native depth 32 and 16 negotiated XRGB8888. No GPU refusals/API failure - records, kernel fatal signals or audio backend errors. Five frontend ERROR - lines are intentional recovery tests. Optional serialization hint #87 remains - unsupported, without preventing gameplay. No cross-platform save-state claim. -- Frontend returned 0 and native Quit status 0 before the watch window ended; - no claim of 240 seconds of continuous gameplay. Core rates are declared values. - -Final identity: `41cb59a98907d3c382fa7c6c2282e64740a2d079364fd7f231dd0ddef7a2ca10`. -FBNeo SHA-256: `0e4231a238af2e5cc2630568abcaec2d3fe5356d647ec87ab9d9c13e8da66e44`. -85,234,664 bytes; 25 callbacks; 7 initializers and 1 finalizer. Existing three core -hashes stayed unchanged; four linked Vulkan archive hashes stayed unchanged. -Symbol-bearing ELF: `klog/fbneo-41cb59a9-symbols.elf`. Committable evidence and -expectations: `evidence/fbneo-native/`, replay with `bash tools/verify.sh evidence`. - -## 2026-09-19 — Native Genesis Plus GX, colours and transitions accepted - -Task: add Genesis Plus GX through the existing native pipeline, use the supplied -.info as reference only, and verify gameplay/menu colours before committing. -Source c2838c7dc4236fc2fe94e5dbd08b41486067918e and official core-info -5a74858ab2f7a50cebb5a6330895bc38899531c0 are hash pinned. New script/target, -packaging/import union/build identity and loader selector are integrated. The -script preserves disc-image codecs while disabling host-detected physical CD-ROM -support and optional zstd weak tracing imports with no linked provider. The -core's RGB565 renderer remains intact; a bounded XRGB8888 adapter uses the actual -viewport offset and row pitch. update_geometry's unused return type is corrected. -Frontend video and PS5_Vulkan are unchanged. - -Commands and results: -- `python3 -m unittest discover -s tests -p test_genesis_plus_gx_video.py -v`: - three tests passed, including every RGB565 colour through upstream packing and - the frontend upload helper, cropped/resized cached views and invalid bounds. -- `bash tools/verify.sh > /tmp/genesis-verify.log 2>&1`: all five gates passed, - 66 tests, existing 22 evidence captures replayed. Build source/ABI/import/SDK - records are copied into `evidence/genesis-plus-gx-native/`. -- `bash tools/verify.sh format > /tmp/genesis-format-final.log 2>&1`: passed - after the final build-script flags. Existing core hashes match all four - accepted builds. PS5_Vulkan's four archives matched before/after the build. -- `bash tools/run-title.sh --no-build --core-test=genesis_plus_gx --watch 240 - > klog/genesis-first-run.log 2>&1`: exit 0; idle guard, full FTP readback, - eight native loader cycles/25 callbacks/API/name and failed-load/menu recovery - passed. Identity ecfcddd57febbb484c2e0724e95f43bbffb3ddd9b9c22aa9c1cf3e37af4a9445; - core 881b5118e6afe700d8de21df679b7e51a458c0bb2fe11603b96373d87f6a2fe0, - 13,388,248 bytes. Symbol-bearing ELF/map preserved under klog/genesis-ecfcddd5-*. - -Two current-build launches appear in trace, both with rarch_main/native quit 0 -before the 240-second window ended. Kernel `klog/run-PPSA99169-171959.log` has no -fatal signals. There are no Vulkan refusals or GPU API failures; eight audio -close reports have errors=0. Frontend snapshots preserve both launches because -relaunch truncates retroarch.log. They show an archive member with .md extension, -XRGB8888 callbacks, 256x192 geometry, and return to the 320x240 dummy menu. - -Failures are retained: the first snapshot has five deliberate missing-core -recovery errors plus two archive-extraction errors. The owner identified the -latter content as ARCADE - Sega System 16 & 32, which belongs with FBNeo. No -archive-structure inspection or parser fix is claimed. The second snapshot has -zero ERROR lines. Core files and private filenames remain in ignored storage; -only sanitized results are in evidence. - -Owner: “Sega Genesis works flawlessly”; explicit follow-up for Quick Menu/Close -Content/next game: “Yes, transitions and next game are clean.” This accepts the -requested colour and transition check, not every Sega system, BIOS/disc/CHD, -filter/interlace option, save mechanism or long-run A/V/performance behaviour. -Replay with `python3 tools/evidence.py compare evidence/`; the new capture and -expectation are in `evidence/genesis-plus-gx-native/`. - -## 2026-09-19 — Public README reflects the verified native port - -Replaced the initial project-definition README with the current frontend and -five-core status, a linked table of contents, scoped console verification, -software-rendering versus Vulkan-presentation explanation, and a roadmap using -✅/❌ with an explicit pending-acceptance legend. Removed obsolete claims that -nothing runs and that Vulkan/XMB must wait for driver rung 1.0. Linked Mihawk's -public PS5_Vulkan repository and explained static linkage/rebuild requirements. - -Build instructions now name the repository-local SDK, frontend fetch, required -sibling driver artifacts and initial title build before artifact-dependent tests. -Installation explains FTP versus /app0, metadata, live configuration and preserved -user folders. Credits name Mihawk, RetroArch/libretro, BlackBearReloaded's -ProsperoLight and native foundation, John Törnblom/ps5-payload-dev, core authors, -Mesa/Khronos, assets and toolchain projects, with repository links. Core author -names and license summaries were checked against pinned metadata/source notices. -The README separates executable-memory evidence from unintegrated PPSSPP JIT, -and keeps future targets distinct from console-verified features. - -Documentation-only validation: -- `python3 /tmp/check-readme-links.py > /tmp/readme-links.json`: 30 local links - and fragments resolve, including all ten TOC entries; code fences balanced. -- `bash tools/verify.sh format > /tmp/readme-format.log 2>&1`: PASS. -- `bash tools/verify.sh evidence > /tmp/readme-evidence.log 2>&1`: PASS, - 23 captures replayed, zero failures. README gameplay claims refer to those - existing committed artifacts, especially evidence/genesis-plus-gx-native/. -- `git diff --check`: PASS. No source, dependency, binary or console changes; - the accepted gameplay build remains 93a1332's recorded build identity. - -## 2026-09-19 — XMB allocation diagnostic branch, host verified only - -Owner requested a branch and diagnostic build, explicitly withholding automatic -upload/launch while working on another project. Created -`codex/xmb-allocation-diagnostics`. Added opt-in live allocation accounting to the -existing native/mapped wrappers, plus observation of `posix_memalign`, frontend -Vulkan image lifetimes and queue-idle calls. Fixed-capacity metadata and stack-only -`write` logging preserve OOM evidence without allocating. Allocation policy is -unchanged. Five-second summaries include counts, requested bytes, peaks, largest -caller groups, failure counts and explicit coverage/saturation limits. - -The prior capture (`klog/xmb-crash-20260919-181233/`) maps the null write to Mesa's -error-reporting allocation following `vk_sync_create` allocation failure in -`vkQueueWaitIdle` during XMB texture unload. That diagnoses the immediate crash, -not the reason memory allocation failed. No driver fix or console result is claimed. - -Command: `PS5_MEMORY_DIAGNOSTICS=1 bash tools/verify.sh` — PASS all five host gates. -The first unit pass had 67 tests; the added Vulkan dispatch test ran in the final -integration pass and final `bash tools/verify.sh format unit` (68 tests, PASS). -`python3 tools/check-memory-diagnostics.py` — PASS required symbols and archive -hashes. Machine-readable inspection and capture/expectation: -`evidence/xmb-memory-diagnostic/`. Raw host logs are in `klog/xmb-memory-verify.log` -and `klog/xmb-memory-final-checks.log`. Existing upstream core/compiler warnings -remain in the full build log; no diagnostic-source warning was observed. - -Build identity: `000adf3edc2538bc563f833f0ccd2e91a01053aa6dac19dd568a580cdbbbb3cb`. -Output: `dist/PPSA99169/`; matching executable, symbol ELF and map preserved under -`klog/xmb-memory-000adf3edc25/`. The current libps5vk archive differs from the -crashing baseline; the other three archives match its recorded digests. The -opt-in build snapshots all four locally to avoid mutable archive inputs while -the owner develops PS5_Vulkan. Mode and linked bytes participate in identity. -No sibling writes, console access, upload, launch or settings change occurred. -Manual protocol and limitations are in `docs/MEMORY_DIAGNOSTICS.md`. - -## 2026-09-19 — First allocation diagnostic froze; bound failure logging - -Owner authorized upload/launch of diagnostic `000adf3edc25`, then reported a -complete freeze instead of a crash and explicitly confirmed manually closing it. -Deployment was verified, klog started before launch and the allocation-log build -identity matched. Raw: `klog/xmb-memory-run-20260919-183305/`, including prelaunch -copies, deployment log, launch marker and post-freeze files. No post-launch -kernel fatal signal was observed. Console later reported count=0 after manual close. - -First observed allocation failure: 13,187 ms, 15,488 bytes at `xmb_list_insert`; -subsequent failures also include 648 bytes at `menu_entries_append` (decoded with -the exact preserved ELF and image load base). The logger wrote 12,793 failure -records and 12,793 summaries, totaling 5,404,264 bytes. This unbounded synchronous -I/O distorted the diagnostic and could aggravate a freeze. The initial allocation -failure precedes the flood; this does not prove logging caused the underlying -failure or explains the complete freeze. Observed native live requests grew from -4,559,423 bytes at the first sample to 11,627,255; mappings stayed 5,498,938. Net -frontend Vulkan image count remained 131. Foreign frees leave native coverage -incomplete; the observed errno=22 may be stale. No heap size/leak claim is made. - -Revised logging emits the first four failures immediately, then at most one -detail and summary per five seconds even if presentation stops. All failures -remain counted; failure_records and failure_suppressed expose rate limiting. -Regression test uses a controlled clock: 10,006 failures, five records, 10,001 -suppressed and <8 KiB. A supplementary 10,000-failure comparison against 971534c -produced 3,707,607 bytes before and 2,351 after. This fixes diagnostic flooding, -not XMB's allocation failure or Mesa's unsafe OOM logging. - -`PS5_MEMORY_DIAGNOSTICS=1 bash tools/verify.sh` — PASS format/unit/build/integration/ -evidence, 68 tests. `python3 tools/check-memory-diagnostics.py` — PASS. -Evidence: `evidence/xmb-memory-bounded-logging/`; raw host log: -`klog/xmb-memory-bounded-verify.log`. Revised identity: -`a4a751e58339deb55fd8b7b4cdae01c3e8479e79c22be53d717a696413ce34be`. -Matching ELF/map/executable are preserved under `klog/xmb-memory-a4a751e58339/`. -All four driver archive hashes match the first diagnostic. No PS5_Vulkan edits. -No revised console launch: further console validation awaits owner intervention. - -Revised diagnostic was uploaded under the owner's standing upload authorization -and verified by `tools/deploy-title.py` after confirming console idle. Raw: -`klog/xmb-memory-bounded-upload.log`. No launch command was sent. - -## 2026-09-19 — Bounded diagnostic reproduces original XMB SIGSEGV - -Owner authorized record/run, then reported the crash reproduced. Verified the -already-uploaded `a4a751e58339` build identity, preserved old logs, started klog -before launch, and captured memory-diagnostics.log, retroarch.log, trace.txt and -configuration. No rebuild or additional driver change preceded this run. -Raw: `klog/xmb-memory-run-20260919-184732/`. Local collector stopped after final -retrieval; no automatic relaunch. Sanitized evidence: `evidence/xmb-memory-crash/`. - -Kernel records one post-launch SIGSEGV at runtime `0x45f30e`, NULL write. Exact -preserved ELF and klog base symbolize the same chain as the original crash: -XMB wallpaper/context update -> white-texture unload -> vkQueueWaitIdle -> Mesa -allocation-error logging -> NULL dereference. First allocation failure at -6,429 ms requests 15,488 bytes at xmb_list_insert (inlined xmb_alloc_node). -Observed native requested bytes rise from 4,625,679 at 5,004 ms to 11,616,951 at -first failure. Tracked mappings stay 5,498,938 bytes; net tracked image count -stays 131; dropped records remain zero. The allocation log is only 3,217 bytes, -with four immediate failure records: later failures can be suppressed by rate -limiting. No exact heap limit, leak, fragmentation or corruption cause is proved. - -This confirms that the bounded diagnostic can capture the original crash without -the previous unbounded failure-log flood. It is not a fix or a stable-runtime -acceptance. Asked the owner which menu/list they were entering to correlate the -allocation spike; that context is pending. Evidence checks reproduce the measured -record and match its expected failing outcome. No PS5_Vulkan writes occurred. - -Owner clarification: “It was just holding a button and making the menu scroll -fast.” Reproduction does not require an explicit folder-open step in the owner's -account. Exact navigation direction/list was not specified; do not infer either. - - -## 2026-09-19 — XMB tab-switch allocation investigation - -Owner clarified that the trigger is holding left/right between XMB tabs. -The configured source shows per-entry 15,488-byte nodes (15,360 bytes of inline -path arrays), 648-byte callbacks, one copied visible outgoing list, and normal -list clearing before tab rebuilds. Missing wallpaper candidates can repeatedly -recreate the white texture, leading to the queue-wait path seen in the crash. -The captured 6,991,272-byte rise remains unattributed: the only live-caller -breakdown predates it. No claim of a node leak, heap limit or GPU-memory leak. - -`python3 tools/probe-xmb-allocations.py --expect evidence/xmb-allocation-investigation/host-probe.json` -passes 10,000 synthetic replacements using extracted upstream lifetime functions: -4,566,034 allocations, stable 259,288-byte end-of-cycle usage, 8,236,400-byte -peak, zero after cleanup. A separate check demonstrates the small console_name -ownership leak. The initial LeakSanitizer run failed because sandbox ptrace -prevents scanning; explicit accounting plus ASan/UBSan pass with leak scanning -disabled. The host harness is not a complete frontend/native heap/GPU replay. - -Evidence and limits: `evidence/xmb-allocation-investigation/` and -`docs/XMB_ALLOCATION_INVESTIGATION.md`. No title launch, deployment, application -rebuild, allocator-policy change or PS5_Vulkan edit. The next discriminating -measurement is first-failure live callers plus bounded numeric tab/list counts; -it requires another owner-authorized launch. - -Verification: `bash tools/verify.sh format evidence` passes format and all 27 -evidence replays; `git diff --check` passes. Product build/integration gates -were not rerun because this step changes only offline tooling and documentation. - - -## 2026-09-19 — Bounded first-failure owners and XMB context - -Added opt-in numeric context hooks before XMB allocation, list copy/clear, -destination selection and populate. A fixed eight-entry history records -boundary sizes and native requests; insertion updates one cached progress -snapshot. Node constructor/copy/free counters supplement the allocator table. -The first failure emits these plus up to 16 live caller groups per route, once. -No frontend callbacks or cached pointers are dereferenced by the failure logger. -Navigation writes no log bytes; ordinary failures retain their existing rate limit. - -Patch 0079 adds 11 anchors, intentionally changing the patch-count expectation -from 139 to 150. The first gate attempt failed that check, then the corrected -full run passed. PS5_MEMORY_DIAGNOSTICS now also enables the frontend hooks; this -flag is in the existing build fingerprint so switching modes recompiles the -frontend. No dependency, allocator policy, menu behavior or driver source changed. - -`PS5_MEMORY_DIAGNOSTICS=1 bash tools/verify.sh` passed all five gates, 69 tests, -278/278 frontend sources. Log: `klog/xmb-first-failure-verify-final.log`. -`python3 tools/check-memory-diagnostics.py` verified title symbols, references -from the actual XMB object and stable archive copies. The injected-clock test -captures 10,006 failures, five ordinary records, one expanded snapshot, eight -history entries, 48 owner rows and 9,404 bytes. Ring wrap, owner-route separation, -rank caps, zero navigation I/O and inert normal-build C hooks are verified. -`bash tools/verify.sh format evidence` and `git diff --check` also pass. - -Build: `e60deca9412fcedaa59162cc5254fc8e0b13df5c2b3fb4a8187e0b059b0acf00`. -Evidence: `evidence/xmb-first-failure-diagnostic/`; exact ELF/map/executable, -inspection and raw captures: `klog/xmb-first-failure/`. The linked libps5vk -archive changed from the previous run's 900d496a9eb7 prefix to 8c2a1c46a38e; -three other archive hashes match. Full hashes are in inspection.json. The -owner develops the driver concurrently; PS5_Vulkan was not modified here. - -Console was confirmed idle, previous logs/config preserved locally, and the -verified build uploaded with readback through the normal deployment function. -No title was launched or stopped. Runtime acceptance remains pending: the next -owner-authorized run should capture klog, reproduce held left/right XMB tab -switching, and preserve logs before relaunch. This is a diagnostic, not a crash fix. - - -## 2026-09-19 — First-failure capture attributes the XMB burst to a large playlist - -Owner authorized d7ad1b3's diagnostic launch. Existing logs were preserved, the -uploaded identity verified, klog started before launch, and the running -e60deca9412f session confirmed. Owner reports a crash during natural scrolling; -rapid input is not a necessary condition. The read-only title-status query -returned zero afterward. Passive collection was stopped locally after capture; -no automatic title kill or second launch occurred. - -At 6,486 ms, XMB selected custom tab 7 and freed the old list's node: live -observed node count fell 4 -> 3. It then successfully created 339 nodes in 23 ms -before xmb_list_insert failed a 15,488-byte allocation at index 339, list size -340. Native live requested bytes rose 4,497,239 -> 11,616,951. The first-failure -owner table attributes 5,265,920 bytes / 340 live requests to xmb_list_insert, -1,179,664 to playlist JSON array growth, 473,152 to file-list arrays, and 220,968 -to menu callbacks. The new nodes explain 5,250,432 bytes of that interval's -7,119,712-byte increase. This is a destination-list construction burst, not -evidence of retaining successive old tab lists. - -Read-only FTP inspection found one custom playlist with 7,384 entries. Its full -XMB node demand is 114,363,392 bytes plus 4,784,832 bytes for callback objects, -before strings and other metadata. Raw playlist content stays only in ignored -klog; committed evidence records counts, not names or game paths. The first -listing attempt used unsupported NLST and returned 502; the existing FTP helper -then enumerated successfully. No console files were modified during inspection. - -One post-launch SIGSEGV is a NULL write in __vk_log_impl, reached through the -same queue-idle allocation-error/white-texture-unload path as the prior crash. -Symbolization used the exact archived ELF and recorded load base (no return-PC -adjustment for fault RIP; minus one for caller PCs). Native accounting excludes -system-internal allocations; no exact heap-limit or fragmentation claim follows. - -XMB's non-diagnostic patch entries are identical to initial XMB commit e080d81. -The node allocator policy dates to mGBA's large-buffer mapping change; diagnostic -hooks did not change it. This does not exclude other regressions. Whether an -older binary opened this same full playlist/configuration remains unverified; -the owner was asked. No fix, policy change, new build or driver edit in this step. - -Artifacts: `evidence/xmb-playlist-allocation-crash/`; raw capture and private -playlist snapshot: `klog/xmb-first-failure-run-20260919-192044/`. Acceptance is -attribution of the observed burst, not menu stability. `bash tools/verify.sh -format evidence` replays the record; no product build was changed. - - -## 2026-09-19 — XMB list safety and normal-build acceptance - -Owner confirms the list-safety diagnostic no longer freezes or crashes and the -normal build eliminates its five-second navigation hitches. Requested landing on -main with current logging unchanged. RetroArch.log, trace and passive klog remain; -only the existing opt-in memory observer is disabled in the accepted normal build. -No PS5_Vulkan sources were modified and no agent launch/kill was issued in this step. - -XMB nodes shrink from 15,488 to 96 bytes with visible-only optional icon paths. -Reclaimable mapped slabs hold nodes/callbacks; checked list insertion and animation -copy preserve valid ownership on failure, stop failed population and permit retry -after clear. Patched headers now invalidate all frontend consumer objects. No new -SDK, dependency, compiler flag or version pin was added. See docs/XMB_LIST_SAFETY.md. - -Both `PS5_MEMORY_DIAGNOSTICS=1 bash tools/verify.sh` and -`PS5_MEMORY_DIAGNOSTICS=0 PS5_VULKAN_DIR="$PWD/build/xmb-normal-vulkan" bash tools/verify.sh` -passed all five gates, 71 tests and 278 compiled frontend sources. The first gate -attempt failed the pinned edit count (150); updating it to the actual 176 resolved -that failure. Fault-injection and 7,384-entry host tests passed ASan/UBSan with -explicit ownership/mapping accounting. GPU operations are stubbed in host tests. -Normal inspection initially found different Mesa object hashes; stripped bytes -match, and recompiling with the original source path reproduced the old hashes: -the difference is debug compilation-directory information, not runtime code. - -Diagnostic identity 88d30106ce625e783e899c97ddeab43d4ca16949ddd98cfb129a06190e0c4ec6 -has zero allocation failures, dropped records, image/idle failures, frontend ERROR -lines and GPU API failure/refusal records in the matching 55.422-second capture. -Native requested peak was 6,518,064 bytes; mapped tracking returned to zero at clean -exit. Kernel backlog collection was post-run, not full launch-to-exit coverage. -Raw capture: klog/xmb-stutter-20260919-200720/; exact diagnostic artifacts and gates: -klog/xmb-safe-lists/. Diagnostic sampling scans 131,072 slots synchronously before -presentation every five seconds, matching the reported hitch cadence. - -Normal identity 2d0743abbcf289efd0a549929d25ce7512968fc93cba8163467fdd3641c8fcf6 -uses the same four archived driver libraries; observer symbols/hooks are absent -and the safe allocator is present. Artifacts/inspection/gates: klog/xmb-normal/; -upload identity and unchanged configuration: klog/xmb-normal-upload-20260919-201708/. -The owner reported “That fixed it”; no fresh normal-run logs or measured stall -latencies are claimed. The unsafe driver logger under arbitrary OOM remains outside -this frontend fix. Sanitized evidence: evidence/xmb-safe-list-run/. Replay with -`bash tools/verify.sh format evidence`; runtime and current logging are unchanged -since the accepted normal build. - - -## 2026-09-19 — Thumbnail channel order and input after configuration reset - -Owner reported swapped save-state thumbnail colours and loss of input after Reset -to Defaults. Patch 0081 preserves the asynchronous image task's supports_rgba flag, -so decoded pixels match the Vulkan menu texture upload format. Gameplay conversion, -PNG encoding and synchronous asset loading are unchanged. Patch 0082 defaults the -joypad backend to ps5 (input already did), and asks the connected controller to -announce itself again after a live reset clears auto-binds. The next poll restores -profile discovery once without reopening the device or requiring a new pad sample. -Logging and PS5_Vulkan sources are unchanged. - -Three executable regressions were seen failing before the fixes: wrong red pixel, -null joypad default, and absent binding refresh. Focused image/default/input tests -pass afterward. Full verification used: -`PS5_MEMORY_DIAGNOSTICS=0 PS5_VULKAN_DIR="$PWD/build/xmb-normal-vulkan" bash tools/verify.sh`. -All five gates passed, with 74 tests, 278 frontend sources and 179 patch edits. The -first full attempt failed the old four-edit configuration.c inventory; it now counts -the added joypad-default edit as the fifth. Total inventory grows from 176 to 179. -No new SDK, dependency, compiler flag or version pin is introduced. The existing -archive override retains the accepted XMB driver inputs; all four archives and -three Mesa utility objects match that build. - -Identity: e11018a79a62ce91fa723deaad6939c1e48a47bace21b3527ae84f4452148451. -Exact ELF/map/title/manifest and gate/regression logs: klog/thumbnail-input-build/. -Eboot-only upload: klog/thumbnail-input-upload-20260919-205531/. Idle was checked, -runtime identity was read back, and live configuration remained byte-identical. -Saved input was ps5 with automatic joypad selection, so no config repair was needed. -No agent launch or kill was sent. Owner subsequently replied “Perfect. Commit”. -This is acceptance of the reported fixes; individual checklist results and fresh -runtime/kernel logs were not captured, so no new log-health claim is made. - -Evidence: evidence/thumbnail-input-defaults/ (source/artifact hashes, sanitized -machine deployment report, owner acceptance). Replay: `bash tools/verify.sh format evidence`. -Future manual checks and scope: docs/THUMBNAIL_INPUT_DEFAULTS.md. - -## 2026-09-19 — PPSSPP Track A: the platform branch and the cross build - -The owner asked for an implementation plan and for Track A to start. The plan is -PPSSPP_Implementation_Plan.md: software GPU core first (no PS5_Vulkan dependency), -then the Vulkan renderer once the driver work lands, one core binary and two runtime -configurations. This entry records the first step, which is verified. - -**The build.** tools/build-ppsspp.sh fetches pinned PPSSPP at -f293b10fb2d9dc0c2bc10281444ee3d3e932e6ad with its 29 submodules, resets the tree, -applies patches/ppsspp/0001-platform-ps5.patch and 0002-cross-build-ps5.patch with -`patch --fuzz=0`, configures the pinned tree directly with -tooling/ppsspp/ps5-toolchain.cmake, builds the libretro target with the SDK's own -prospero-clang wrappers, and runs tools/check-core.py. - -Result: `core ABI PASS: ppsspp_libretro.so; 25 exports; kernel_web; -sha256=038001216b59698c28268860349e0b27e8e8cb940f98dbb12650a90d063d71c6`, -18,485,448 bytes. readelf on the artifact: zero PT_TLS segments, three 16 KiB load -segments (flags 5/4/6, no writable-executable), and NEEDED limited to -libkernel_web.sprx, libSceLibcInternal.sprx and libScePosixForWebKit.sprx. -Reports: build/cores/ppsspp/abi.json and build.json (revision, 29 submodule SHAs, -seven port-input hashes). This is a host and artifact result: the core has not been -deployed or loaded on the console. - -**What the toolchain decided, with the measurements.** A TLS probe compiled and -linked with prospero-clang++ and tooling/native/ps5-core.ld showed zero PT_TLS and one -undefined import, __emutls_get_address: the SDK compiles with -femulated-tls, so -PPSSPP's thread_local caches (GPU/Software/Sampler.cpp, DrawPixel.cpp) need no source -change, contrary to the handoff plan's expectation. Two flag shims were required: --Dstatic_assert=_Static_assert, because PPSSPP's own -D_XOPEN_SOURCE=700 pins -__ISO_C_VISIBLE to 1990 on this FreeBSD-derived libc and ext/xxhash.h then calls an -undeclared C11 static_assert, and -DZSTD_TRACE=0, the same switch the frontend build -already uses for zstd's weak tracing hooks. Four libc entry points that vendored -third-party code calls are declared but not exported by the SDK stubs — swab -(ext/libpng17), nl_langinfo (ext/armips, ext/SPIRV-Cross), tmpfile (ext/lua/liolib.c) -and tmpnam (ext/lua/loslib.c) — so tooling/ppsspp/ps5-libc-shims.cpp implements them -inside the core; the two temporary-file calls refuse with NULL, which both callers -handle, because this target has no writable /tmp contract and neither is on a boot or -gameplay path. - -**The import audit.** The core has 479 undefined symbols. Cross-checked against every -SDK shared stub plus the libc++, libc++abi, libunwind and clang-builtins archives the -title links, exactly one is unresolvable: localtime_r, which tools/core-imports.py -already aliases to RetroArch's locked rtime_localtime (the mGBA precedent). The union -of the five shipped cores plus PPSSPP generates a 497-binding table for six cores with -no TLS or type conflicts; the shipped five-core table was regenerated afterwards, so -no unfinished core is in the title's import surface yet. - -**Gates.** `bash tools/verify.sh format unit` — PASS, 74 tests, 19 s. The build gate is -untouched: PPSSPP is deliberately not in core_names in tools/build-title.sh until the -title link and ABI gate pass, which is the next step. - -**Not proven.** The title link with ppsspp in core_names; console load, initialisation -and teardown; a game. No console was contacted; nothing was uploaded or launched. - -## 2026-09-19 — PPSSPP A3: the title links with the core in it - -A3 of PPSSPP_Implementation_Plan.md is the proof that the port's import surface is -complete: build the whole title with `ppsspp` in `core_names`, and let the converter -refuse any symbol no public SDK stub exports. - -**It links.** `ppsspp` joined `core_names` and the build-identity input list in -tools/build-title.sh, and `bash tools/build-title.sh` (JOBS=14) completed with exit 0: -all six cores report `core ABI PASS … 25 exports`, `tools/core-imports.py` reports -**497 explicit native bindings for 6 cores**, and the title built to 161 files with -`eboot.bin` 34,655,444 bytes at identity -ed3f78147e015696bb44550e7151352e341d333d78d0dc6d7b4315bf6fd7a9fc. Six cores are -staged in dist/PPSA99169/cores/, PPSSPP's included as 18,485,448 bytes plus its -pinned 1,793-byte metadata. - -The two imports worth naming are resolved: `__emutls_get_address` is defined in the -title (local symbol at 0x6c6ae0, pulled from the clang builtins archive the Vulkan -archives already require) and `localtime_r` goes through the existing -`rtime_localtime` alias. The title carries 465 undefined symbols, every one of them -accepted by the converter's stub check. - -**Gates.** `bash tools/verify.sh format unit` — PASS, 74 tests. `bash tools/verify.sh -integration` — PASS, including check-manifest on the rebuilt folder. The build gate -now builds PPSSPP as well, because that is what A3's acceptance means: the unfinished -core is in the shipped staging only after its ABI and link gates pass. - -**Open: the artifact digest is not reproducible across builds.** Four builds of the -same pinned revision produced four different sha256 values — 038001216b59698c and -e27177a3d4e56d80 from a /tmp checkout, then 56627f010acd6cb2 and 79647489be7b794e -from two consecutive runs against the same .deps checkout, with identical toolchain, -flags and patches. The generated version string is the same in each -(v1.20.4-1868-gf293b10fb, or "unknown" on a checkout without tags), and neither -armips — the only vendored code using __DATE__/__TIME__ — nor any date-like literal -is linked into the core, so the differing bytes are not yet localized. What is pinned -is the source revision, the 29 submodule SHAs, the two patches, the toolchain file and -the shims; what varies is the resulting byte image. The ABI report records the -artifact hash, so every console claim stays tied to one build. Localizing the -difference is a follow-up, not an A4 dependency. - -**Not proven.** Console load, initialisation, teardown and gameplay. Nothing was -uploaded or launched, and no console was contacted. - -## 2026-09-19 — PPSSPP A4 preparation: reproducible builds, and the shipped cores are untouched - -Two gaps left by A3 are closed, and the console step is armed. - -**Reproducibility: found and fixed.** Four builds had produced four hashes. Comparing -two of them byte by byte localized the difference: .rodata (40,843 bytes), .text -(3,237), .rela.dyn (971) and the build-id. Dumping both .rodata sections showed the -same size (1,793,764) with one string in the multiset differing — -" translated Sep 19 2026 22:48:28" against " translated Sep 19 2026 22:50:27" — which -is libpng's banner from ext/libpng17/pngerror.c, built from __DATE__ and __TIME__. The -large footprint comes from the linker's string tail-merging: one varying string -changes the merge layout, so ~40 KiB moves. clang honours SOURCE_DATE_EPOCH for both -macros (verified with a two-line probe compiled twice), so tools/build-ppsspp.sh now -derives the epoch from the pinned commit itself -(`git show -s --format=%ct $revision`) and exports it. Two consecutive full builds -after the change: identical, sha256 8346e010a8781f8c… over 18,501,832 bytes. The -epoch is recorded in build.json as source_date_epoch. - -**The five shipped cores are undisturbed, measured rather than assumed.** Rebuilding -fceumm and fbneo in a detached worktree at the pre-change commit b1dced9 gives -8f21cea01c018141c0e8e3493b9d73cf5e0f7534dbd5149249684f6bfbab04c1 and -8c9c402ffa82cb049d346b305294ac824178b5dd14e8cefb394a0ec882861e1e — the same two -hashes this tree produces with PPSSPP added. mgba (feb1922c…), snes9x (e8b66c5f…) and -genesis_plus_gx (881b5118…) are byte-identical to their committed evidence. The older -fceumm and fbneo hashes in klog/thumbnail-input-build/ and evidence/fbneo-native/ -predate changes to those cores' own inputs and are not a PPSSPP regression. - -**The A4 vehicle is wired.** --core-test=ppsspp is accepted by tools/run-title.sh and -by src/core_loader_probe.cpp, which loads /app0/cores/ppsspp_libretro.so eight times, -checks all 25 libretro exports, the API version and the reported library name -("PPSSPP"), and writes core-loader-test.json with the build identity. It never calls -retro_init, so this step creates no Vulkan device and measures the loader alone: an -18.5 MB module, 479 imports and a full C++ static-initialisation pass, eight times -over. - -**Gates.** `bash tools/verify.sh` — PASS on all five: format, unit (74 tests), build -(the title now builds and links with six cores), integration, evidence (31 captures -replayed, 0 failed). - -**Not proven.** Everything console-side: load, initialisation, teardown, and a game. -The launch needs the owner; the assets (system/PPSSPP/) and content are owner-supplied. - -## 2026-09-19 — PPSSPP A4: the core loads on the console, and the assets land in system/ - -**The core loads.** The console loader test passed for PPSSPP: -`{"build_identity":"build identity: 73d2aad8a13e5ad0…","core":"ppsspp","passed":true,"cycles":8, -"exports":25,"api":1,"missing_rejected":true,"unknown_symbol_rejected":true}` — eight full -load/unload cycles of the 18.1 MB core, all 25 libretro exports, the API version, and -both negative cases still rejected. The loader log names the real work: -`ran 110 initializers`, `ready symbols=19430 relocations=24812 mapped_bytes=18923520`. - -Three separate blockers had to fall before that, and each was found on the console -rather than by reading code: - -- **Unresolvable imports.** The first run failed with `unresolved native runtime - import: gai_strerror`, then `glGetString` once that was fixed. The cause is the - loader's rule that every import must have an address: the title can carry imports - it never calls, but a core cannot, so any symbol the console leaves null is fatal. - Two groups qualified. `libScePosixForWebKit` supplies `gai_strerror`, `getaddrinfo`, - `freeaddrinfo` and `isatty` — and no core this title has ever shipped imported - anything from that module, so it had never been exercised. They are now implemented - in `tooling/ppsspp/ps5-libc-shims.cpp` (resolver refuses with EAI_FAIL, isatty - answers 0, which is true). The other 83 were the OpenGL backend, which this port - can never use: `patches/ppsspp/0003-no-opengl.patch` drops `Common/GPU/OpenGL/*`, - `GPU/GLES/*`, the two libretro GL contexts and PS5's `PPSSPP_API_ANY_GL`, so the - core has **zero** gl*/egl* imports and the import table fell from 497 bindings to - 410. Every remaining import resolves from `libkernel_web` or `libSceLibcInternal`, - the two modules the frontend already proves. - -- **The thread pool.** The first successful load logged `ThreadManager::Init(compute - threads: 256, all: 512)`. PPSSPP's `Init` multiplies its two arguments - (`numRealCores * numLogicalCoresPerCpu`), so they are cores and threads-per-core, - not two totals; the platform branch had set both from `sysconf`. It now takes the - physical core count from CPUID leaf 0x80000008 and derives SMT siblings from the - online count: **16 compute / 32 total**, which is right for this console. - -- **The runtime assets.** PPSSPP resolves them as `/PPSSPP` and reported - "Core system files missing, expect bugs". `tools/build-ppsspp.sh` now stages the - **source tree's** `assets/` — the canonical, complete tree (193 files, 22 MiB) — - rather than the 187-file partial copy CMake leaves in the build directory (it omits - `cheats.json`, `knownfuncs.ini` and four others), and `tools/build-title.sh` copies - it to `dist/PPSA99169/system/PPSSPP`, which the deploy publishes. Verified on the - console: `/data/homebrew/PPSA99169/system/PPSSPP` holds the full tree including - `flash0` (18), `debugger` (12), `shaders` (54) and `lang` (48). The warning is gone - from the run log, and the tree's digest is recorded in `build.json` - (`ppsspp_assets_sha256 223c78b2…`). - -**Still open: the first frame.** With the core loading and the assets found, the run -reaches `retro_init`'s end and then the title takes a **SIGSEGV on a worker thread**: -`proc name: eboot.bin`, `thread ID: 102061` (not the main thread), `page fault (user -read data, page not present)`, `fault address 0x70`, and the backtrace's frames -(`0x80004055c`, `0x800040638`, `0x8000407fb`) all lie in `libkernel.sprx`'s text -(0x800000000–0x800044000 per the capture's own `xotext` line), i.e. inside the -console's thread machinery rather than in PPSSPP's code. The frontend log ends at -`ThreadManager::Init`, which logs *before* it creates the pool's 32 `std::thread`s — -so worker-thread creation is where it dies. `patches/ppsspp/0004-software-backend-default.patch` -is in place for the next run: on PS5 the `ppsspp_backend` default is now `"none"` -(the libretro software context) because the Vulkan renderer cannot come up against a -driver with no combined depth/stencil format, and the stalled Vulkan attempt is why -earlier runs showed no progress past core init. The console's saved core options were -cleared so the new default applies. - -**Gates.** `bash tools/verify.sh` — PASS on all five. - -## 2026-09-20 — PPSSPP relinked against rung-1.0, and what that rung does not cover - -The owner closed PS5_Vulkan's rung 1.0 (`d4e73ff`, "the depth pair proved -- rung 1.0 -closed": D16_UNORM's and D32_SFLOAT's SAMPLED_IMAGE and BLIT_SRC were the last four -unreached features) and asked for RetroArch to be built against it, then for PPSSPP to -follow through it once stability testing is done. - -**The relink.** `bash tools/verify.sh` rebuilt the frontend and title against the -sibling's current archives and passed all five gates (32 captures replayed): - - libps5vk.ps5.a d41f934b720e9ffff4c8a05b450e7a5a200abaaf9274a94eeefc1f9179704c21 - libvk_runtime.ps5.a 4106a2c56b269bc52a3976b6bc0134c00b5006ff5358a4b45355afce4a907aa7 - libpsbc_driver.ps5.a 47db73edf649023688c536e6c1a2b3e5180d582db1440c2f54dbf5bdb4fe64b2 - libpsbc_support.ps5.a e36d3e2a3fa93b0a74db15b9efce7aad4ed7ac4e024de98d5f23dc5fee90acf8 - -Title identity 735f7eb1acdc4f41a004eab0e6a86f2d4eab1ab2cf953eed0325ccbd74727c77. -The sibling's tree is being rebuilt while this work runs — `libps5vk.ps5.a` changed -twice inside this session (82055034 -> 3f816850 -> d41f934b) and its working tree -carries uncommitted vertex-format work (`driver/ps5vk_image.c`, `driver/ps5vk_pipeline.c`, -`tools/build-psbc-ps5.sh`, `tooling/psbc/patch-vertex-formats.py`), one edit of which -postdates the archive. The hashes above are the ones this link embedded; a fresh -relink is owed once the sibling's build settles. - -**The review: rung 1.0 gives PPSSPP its format matrix, not its renderer.** The rung -closed reachable format features, the depth pair (attachment, sample, blit, transfers), -wide, narrow, packed and signed colour targets, transfer coverage and the command and -limits audits — all of which PPSSPP's texture cache wants. It did not touch the six -things PPSSPP's Vulkan renderer needs, and the source still refuses each by name: - -- Combined depth/stencil: the table holds `VK_FORMAT_D16_UNORM` and - `VK_FORMAT_D32_SFLOAT` and no `S8` format at all (`driver/ps5vk_image.c:345,359`), - while PPSSPP's `VulkanContext::CreateDevice` requires one of D24_UNORM_S8_UINT, - D32_SFLOAT_S8_UINT or D16_UNORM_S8_UINT and asserts without it. This is the device - creation blocker, and it is why the earlier Vulkan-default runs stalled rather than - progressed. -- Attachments smaller than 3840x2160: `driver/ps5vk_draw.c:526,570,587,709,754` still - reject any other extent, depth, colour, render area and inherited target alike. - PPSSPP renders PSP framebuffers at 512x272, 480x272, 384x272 and more. -- Cull mode and front face (`driver/ps5vk_pipeline.c:825`), stencil test and dynamic - stencil state (`:831`, `:819`), per-channel colour write masks (`:858`) and dynamic - blend constants (`:819`) are unchanged refusals. - -**The other blocker is ours, not the driver's.** The console run after the loader test -showed the title taking a SIGSEGV on a worker thread inside `ThreadManager::Init` -(klog/run-PPSA99169-002106.log: fault address 0x70, frames in `libkernel.sprx` text). -That happens whatever the renderer is, so it is the next step: PPSSPP cannot be -"loaded through" anything until its thread pool survives creation. - -**Console evidence committed.** `evidence/ppsspp-native/` records the loader-test -result, the import and thread fixes, the asset tree and the open first-frame blocker; -`bash tools/verify.sh evidence` replays it (32 captures, 0 failed). - -## 2026-09-20 — The PPSSPP worker crash, narrowed to libkernel's module lookup - -PPSSPP dies in `ThreadManager::Init` on the console, on the first worker thread. This -entry records what the crash *is*, and what it is not, because the next attempt should -start from here rather than repeat the probes. - -**The crash, read from the console's own dump.** `klog/run-PPSA99169-002106.log` and -its successors: SIGSEGV on a thread whose id differs from the process (a worker), fault -address `0x70`, `rip 0x8000407fb`, and a backtrace of -`0x80004055c, 0x800040638, 0x20185ea6a, 0x800074d0`. - -**The console's libkernel can now be symbolized.** `/system/common/lib/libkernel.sprx` -is readable over FTP (545,752 bytes, plain ELF) and its 1,270 exports are named by -NID. SharpProspero's `NidEncoder` documents the mapping — the NID is the first eight -bytes of `SHA1(name || salt)`, byte-reversed and base-64-encoded with a custom -alphabet — and it validates against seven documented pairs (`puts=YQ0navp+YIc`, -`malloc=gQX+4GDQjpM`, …). Computing NIDs for the SDK's 4,328 libkernel/libc symbols -names 1,111 of that module's exports, which turns every console backtrace inside -libkernel into function names. That tooling is the reusable result of this round. - -**What the backtrace says.** `0x8000407fb` is `pthread_get_specificarray_np + 0x3b` -(NID `td+DYvbbDfk`), reached from the thread-creation path `pthread_create_name_np` -(`0x800074d0`). Disassembling it shows why it faults: the function walks the frame -pointer chain, takes a return address two frames up, calls a table lookup that searches -0x98-byte records for the range containing that address, and reads `[result+0x70]` -without checking for null — with `rbx = 0x8000711d0` in the register dump matching -libkernel's `__stack_chk_guard`, which confirms the reading. The address it could not -resolve, `0x20185ea6a`, is the third backtrace frame: **a return address inside this -title's core, which the loader mapped anonymously.** The core is not a registered -module, so the lookup misses. - -**What the probes ruled out.** `src/thread_probe.cpp` (opt-in, `/app0/thread-test.txt`) -now runs, and passes, every case that could be built in the title: 32 concurrent -`std::thread`s, `thread_local` access in each of them (the SDK compiles it as emulated -TLS), a heap object with `std::mutex` and `std::condition_variable` used by 1 and 32 -threads, a thread whose entry point is in an anonymous executable page, a thread whose -entry point is a *core* export, a thread that calls core code, a libkernel call whose -immediate caller is an anonymous page, and a thread-specific-data call -(`pthread_getspecific`) from an anonymous page. All passed -(`{"stage":"done","created":32,"ran":32,"joined":32,"tls_ok":32}` and successors). -PPSSPP still dies at the same instruction, so the trigger is not any of those alone. - -**Two changes stand regardless.** `patches/ppsspp/0005-ps5-thread-name.patch` stops -PPSSPP taking its `__FreeBSD__` branch — PS5 defines the macro, so every worker called -`pthread_set_name_np`, which no earlier core used — and -`patches/ppsspp/0006-diagnose-thread-pool.patch` is the temporary instrumentation that -named the failing step (`creating 0`, `created 0`, then death). - -**The next attempt.** Remove PPSSPP's `thread_local` uses (the plan's original A2 step, -now motivated by the emutls path: core TLS becomes `__emutls_get_address` → -`pthread_getspecific`, which is exactly the family in the backtrace). If that is not -enough, the fix has to make libkernel's lookup succeed for core addresses, or keep -libkernel's thread-specific-data path off every stack that contains core frames. - -## 2026-09-20 — relinked against the migrated driver with the red/blue compensation withdrawn - -**Task.** Two asks: build the frontend against `../PS5_Vulkan` now that its migration -landed (`8b311e3`), and remove this port's red/blue compensation, which existed only -because that driver could not sample the frontend's own byte order. - -**Driver review — `8b311e3`, "Migrate the driver to ps5-opengl 0.3.0's compiler fork, -and close the console SIGFPE at its owner".** The compile-side surface moved: the -driver links ps5-opengl 0.3.0's `opengnm-psbc` tree (upstream `a92a1228` + the -release's patch, tree `a27cbecc`, metadata v14) with this repository's compiler patches -on top, `patch-compute-metadata.py` is dropped, `patch-vertex-formats.py`'s anchor moved -and a new `patch-aco-min-waves.py` restores `program->workgroup_size`'s -"unknown is UINT_MAX" invariant before `calc_min_waves`. The console SIGFPE that round -15 had laid at ACO's door was the *probe's* own: earlier symbolization had missed the -PIE load base (`xotext: 0000000000400000`), and rebased, `rip 0x430b34` is `div %r13d` -in `run_sampled_format_frames`, dividing by an unfilled `std::array` -row's zero texel size; both tables now declare their rows and a `static_assert` makes a -declared-but-unfilled row a build error (console: pid 299, seven of seven cases, 1581 -PASS records, no `signal:`). Three fixes in the same commit matter here: `ps5vk_compile_stage` -dereferenced a null shader module for the NIR stages Mesa's meta operations hand it — -every meta clear, blit and resolve faulted — `ps5vk_queue_flip` recorded the wrong -submission's words, and shader compilation now runs on a driver-allocated 32 MiB stack -via `pthread_attr_setstack`, so an application thread's small stack cannot overflow the -compiler's recursion. **What the port depends on is unchanged**: `driver/ps5vk_image.c` -is not touched, `VK_FORMAT_B8G8R8A8_UNORM` still carries `SAMPLED_IMAGE_BIT` with the -`8_8_8_8_UNORM` word and the `ZYXW` selectors, and the format audit's counts are the same -(179 required, 58 reported, 0 missing, 55 conditional, split 0/0/0/0/0). - -**Withdrawn: 0014's format substitution, 0075, 0077.** `tools/apply-port-patches.py` -grew an 0083 group (the substitution goes, the usage mask stays, and 0027's and 0015's -comments are restated for the reasons they still have) and a `WITHDRAWN` tuple that -exchanges each named insertion for the upstream text it replaced, so an existing tree -and a fresh one converge — checked by patching a copy of `vendor/retroarch`'s -`gfx/drivers/vulkan.c` and the withdrawn `build/ra-conf` and comparing (identical). -`src/core_frame_ps5.cpp` no longer converts channels: the upload is a row copy of -libretro's little-endian XRGB8888 into a B8G8R8A8 image, which is the same four bytes. -Four tests now assert the core's own bytes instead of the conversion's output, and two -new `tests/test_frontend.py` cases pin the mechanism (a withdrawal must name exactly one -EDITS block, and the withdrawn set is named). The patch-count pin moved 179 → 185. - -**Build and gates.** `PS5_VULKAN_DIR=$PWD/build/frozen-driver -PS5_VULKAN_ICD=$PWD/build/frozen-driver/build/driver/ps5/libvulkan.so.1 bash -tools/verify.sh` — format PASS (1s), unit PASS (12s, 75 tests), build PASS (247s), -integration PASS (10s), evidence PASS (0s), `verify: PASS (format unit build -integration evidence)`. Title identity `f9e4ea6c…`, eboot 34,703,515 bytes sha256 -`82534b86…`, and two consecutive gate runs built it byte for byte the same. The four -driver archives were frozen into `build/frozen-driver/` before the build and re-hashed -unchanged after it, and the printed identity was recomputed from `tools/build-title.sh`'s -own input list against those hashes and matched, so the eboot's embedded identity names -the driver it carries. Record: `evidence/driver-1.0-native-byte-order/`. - -**Four runs before the green one, at the same detail.** 1) The first `tools/verify.sh` -failed at `unit`: three new 0083 markers did not appear in their own replacements — the -port pins that so a rebuild cannot append an edit twice — and the patch-count pin was -stale; both fixed (`build/verify-native-byte-order-first-run.log`). 2) The second failed -at `build` after 324 s because the frozen tree's `.deps/work` symlink was one level short -and the Mesa utility sources read as missing (`…-second-run.log`). 3) A third was -interrupted mid-build when the driver's `8b311e3` landed and the freeze was rebuilt -(`…-third-run-killed.log`). 4) The fourth passed format, unit, build and integration and -failed only at `evidence`, because the new step directory held no `capture.json` / -`expectation.json` yet (`…-gates-4-of-5.log`). - -**Also found.** `tools/core-imports.py` rewrites `build/core_imports.inc`, and that file -is insertion-ordered by the core list: running the tool by hand to read a count changes a -build input and with it the build identity. The identity does not lie about what it -hashed — recomputing it from its own inputs reproduced both printed identities — but a -tree whose include was rewritten by hand no longer describes the eboot built from it. - -**Pending, and the only thing that decides the colours: a console run.** The withdrawn -path is the one the owner accepted earlier (`native-core-loading/`, `mgba-native/`, -`xmb-safe-list-run/`), and the native byte order has host evidence plus the driver's own -console `v0-formats` battery. The command is -`JOBS=14 bash tools/run-title.sh --no-build --deploy --watch 120` with the owner -launching; the check is menu colours, core colours, and the Quick Menu over a paused -game. - -## 2026-09-20 — the native-byte-order build is published (upload only) - -`python3 tools/deploy-title.py` published the build from the previous entry and read it -back: 354 files, `eboot.bin` stored as 35,144,676 bytes (the console re-signs the fake -self; the tool re-uploaded once when the first read-back differed and then confirmed this -build's four markers), `libvulkan.so.1` at the title root and in `sce_module/` as -16,869,880 bytes with the build's library confirmed, and all six cores -SHA-256-verified. The console's own `sce_module/libc.prx` was kept, as designed. - -The console was **not idle** when the deploy started: the control endpoint answered -`count=1 title=PPSA99988`. `tools/run-title.sh` refuses an upload in that state, so this -was the owner's explicit decision to publish anyway; no title was launched or closed by -this step, and the running title's files are not touched by the title folder's paths. -Record: `evidence/driver-1.0-native-byte-order/deploy.json`; the raw deploy log is -`build/deploy-driver-8b311e3.log` (git-ignored). - -Still open, and the next thing to record: the owner's launch of this build, which is what -decides the colours. - -## 2026-09-20 — the owner accepts the native byte order on the console - -The owner launched the published build and reports it "flawless", with the colours -accurate now that the red/blue compensation is gone. That is the acceptance of this -step: the menu, the core frames and the transitions between them draw with the correct -channels while the frontend keeps libretro's own byte order, which is what the -withdrawal of 0014's substitution, 0075 and 0077 was for. - -**What kind of evidence this is, and is not.** It is an owner observation, like the -accepted baselines before it: no klog run was started for this launch, so there is no -distilled capture, no `/app0/trace.txt` and no kernel log for it, and no per-core -checklist was walked. What is machine-checked about the deployed bytes is in -`deploy.json` (sizes, read-back and the six cores' SHA-256), and what is machine-checked -about the build is in `capture.json` (the five gates, the identity, and that the identity -covers the frozen driver archives). The acceptance itself is the owner's words. - -**Not covered.** PPSSPP was not launched on this build, so its parked first-frame blocker -is unchanged and untested here; the other cores' individual behaviours were not -re-checked one by one; and there is no captured 90-second run for the build. - -The step's record is `evidence/driver-1.0-native-byte-order/`: `capture.json` (gates, -identity, driver revision), `expectation.json` (what the run must show, now carrying the -owner's words as its human check), `driver-archives.json` (which driver the link carries) -and `deploy.json` (what was published and read back). - -## 2026-09-23 — Rebuilds in seconds - -A rebuild that changed nothing took 269 s: the six cores (255 s, PPSSPP 122 s and -FBNeo 77 s of it) were extracted and compiled from scratch every time, the host -signing tool was recompiled every time (6.5 s), and the patcher rewrote files it -did not change (0075/0077's withdrawn text was inserted and withdrawn again), so -their objects recompiled. - -- tools/core-stamp.sh: each core is skipped when a hash of its build script (which - pins revision and digests), its patches and port tooling, the shared loader - files, the ABI checker and the SDK compiler wrappers matches the stamp and its - staged outputs exist. PS5_FORCE_CORES=1 builds anyway. -- ccache in tooling/prospero-clang18 (frontend and title sources) and on the - cores' make lines and CMake launchers; PS5_DISABLE_CCACHE=1 opts out. -- tools/apply-port-patches.py computes each file's final text in memory and writes - only when it differs. -- tools/build.sh rebuilds the host tool only when its sources, headers, zlib or the - host compiler change. -- tools/deploy-title.py uploads only files whose digest differs from what this - checkout last verified on the same console (build/deployed/-.json); - --all uploads and verifies everything. - -Measured on this host: no-change rebuild 269 s -> 2.2 s; a port-file edit 2.4 s; -a driver archive change 2.4 s (relink, sign, manifest); a forced FBNeo rebuild with -a warm cache 77 s -> 2.8 s; the first build after these changes 304 s (fills the -stamps and the cache). Deploy: full 74.4 s, nothing changed 0.3 s, after a -port-file edit 12.4 s (eboot.bin and the metadata). build/ is only removed by -`make clean`. - -## 2026-09-23 — 120 Hz by default, 60 Hz fallback, on the current driver - -The title declares high-frame-rate output (attribute3 0x80040) and links -../PS5_Vulkan 8577153, which offers 3840x2160 at 119880 mHz first and selects it at -swapchain creation. Patch 0084: the display context picks the largest mode, then -the highest refresh. Patch 0085: `video_refresh_rate` follows the chosen mode and -a saved swap interval of 1 becomes automatic above 100 Hz. The seed config ships -119.88 and automatic for fresh installs. main.cpp drops `--menu` when args.txt -names a core or content. - -Console, FCEUmm with 1943 for 1,800 frames through args.txt, driver profile armed: -119.88 Hz selected, `video_refresh_rate` 60.000 -> 119.880, swap interval 1 made -automatic, 1,199 presents per 10 s (8.342 ms), ~30 s for the 1,800 frames of a -60.10 FPS core (evidence/output-120hz). Same build with attribute3 0: 59.94 Hz only, -119.880 -> 59.940, 600 presents per 10 s, the same ~30 s (evidence/output-60hz- -fallback). My configuration and history were snapshotted before and restored to -their exact bytes after. tools/verify.sh PASS (five gates, 75 tests, patch count -185 -> 188 for 0084's two edits and 0085), 35 captures replay. - -## 2026-09-23 — VideoOut kept across swapchains; every core rechecked; FBNeo screenshot fixed - -Black screen on menu open/close, content close/load and video reinits: RetroArch -recreates its swapchain for each, and the driver closed VideoOut with it (restoring -60 Hz) and reopened it (selecting 119.88 Hz). ../PS5_Vulkan now keeps one VideoOut -per process: `ps5vk_display_retain(true)` (main.cpp) keeps it, its framebuffers and -the image on screen between swapchains, on any device; `false` (on quit) closes it -and restores the default mode. The same driver change configures 119.88 Hz when the -display modes are listed, so the mode is offered only once the console accepted -it, closing the edge where a refusal left RetroArch believing 119.88 Hz. - -Console, FCEUmm 1943 with a pad script (menu, Close Content, History run, menu -twice): retained, every handover kept the output with 0 black presents (menu 6-7 ms, -close content 280 ms, History 437 ms); with /app0/ps5vk-no-retain.txt the same -events opened a new output each time, 297-760 ms and a mode switch each. Quit from -the main menu: "output: default mode restored", status 0 -(evidence/display-retention). - -Core recheck at 119.88 Hz, 1,200 frames and an exit screenshot each: FCEUmm, -Snes9x, mGBA (GBA .7z, GB), FBNeo, Genesis Plus GX (MD, SMS, GG), all correct -colours, audio errors=0 discarded=0, status 0 (evidence/core-recheck-current- -driver). FBNeo's exit screenshot crashed: a new crash reporter (src/crash_report.cpp) -put the return address in scaler_ctx_scale via screenshot_dump_direct; the scaler -had failed to allocate ~97 MB of intermediate frames a same-size conversion never -uses. Patch 0086 allocates them only for scaling and fails a screenshot whose -conversion cannot be set up. The first Master System and Game Gear files were web -pages saved under ROM names; RetroArch refused them and exited cleanly, and the -replacements ran. - -vkQuake on the same driver: 119.88 Hz selected, 119.88 FPS at 4K. tools/verify.sh -PASS (patch count 188 -> 193 for 0086's five edits), 37 captures replay; driver -check PASS. - -## 2026-09-24 — God of War: Ghost of Sparta at 10x: correct picture, full speed - -The run is PPSSPP with the torture defaults (4800x2720, 16x anisotropy, auto -max-quality filtering) from a slot-0 save state, loaded with `--entryslot=0`. - -- **Picture.** The blue, banded Kratos in the menu and the brown tint with - flat-silhouette characters in gameplay were stale GPU state in - ../PS5_Vulkan (a draw left the previous draw's blend, write mask, cull and - clip words in place). With every draw recording them, both match PPSSPP's - software renderer (`klog/final-menu-ss-gow.png`, `klog/fifo-ss-ss-gow.png`). -- **Speed.** The driver's present used to wait until its flip was shown, - which gave RetroArch's doubled 120 Hz presents one vblank per frame. Its - swapchain is now a three-image FIFO queue: the audio windows went from - ~420,000 of 480,000 frames with silence filled in (`klog/pace1-trace.txt`) - to 480,000 of 480,000 with none (`klog/pace2-trace.txt`), at 1198 flips per - 10 s. Sonic on Genesis Plus GX runs at full speed with the same driver. -- **Startup.** About one launch in three aborted with `thread constructor - failed: Resource temporarily unavailable`; the core thread factory now - retries EAGAIN for up to two seconds, and the terminate handler logs an - exception's what() and code. - -Still open: 8x MSAA (the driver exposes neither renderpass2 nor depth/stencil -resolve, and supports 4x only), and a long gameplay soak. - -## 2026-09-24 — Dolphin boots Wind Waker; save states work - -libretro/dolphin (`c6630001e0`, Dolphin 2609) cross-builds with one patch and -ships in the title. In order, what stopped it on the console and what fixed it: - -1. `openat` resolved to address 0 inside the title's libc++ (`remove_all`); the - `*at` family and libc's refused `opendir` are now the title's own (`--wrap`). -2. VMA could not load `vkGetPhysicalDeviceMemoryProperties2KHR` on RetroArch's - Vulkan 1.0 instance: patch 0089 enables the extension when offered. -3. `vkCreateSampler` refused clamp-to-border: ../PS5_Vulkan R57. -4. Jit64's 200 MB code space did not fit flexible memory: 32 MiB on PS5. -5. The driver's compiler faulted in `copy_entry_create` on a NULL realloc - from libc's exhausted private heap: a refused realloc now moves the block to - direct memory (located with the driver's new opt-in SPIR-V dump; the same - modules compile on the host). -6. The fastmem logical views failed to map and flexible memory fell to 10 MB: - guest RAM is now direct memory mapped at each mirror. -7. JIT code first landed inside the GPU window, then its hint was refused: - code is mapped at 0x3_0000_0000. -8. 101 pipelines were refused for primitive restart: ../PS5_Vulkan R58. - -Wind Waker then reached its title screen with zero refusals and clean audio -(dol12). I first recorded that screenshot as correct; it is not, because the -mountain in the background is missing, which is open. - -Save states: saving crashed in the thumbnail readback, which overran its buffer -by ~1 MB after re-rendering the cached frame recomputed the viewport (patch -0090). Loading a state then crashed at exit on a NULL strdup: libc's private -heap was exhausted because the title's small blocks filled it first; they now -go to direct memory first (dolss1-3, dolld1-2). The startup popup "Failed to -load Vulkan library" was Dolphin probing a host library a libretro core does -not use; it is an info line now. - -tools/verify.sh PASS (patch count 199), 39 captures replay. - -## 2026-09-24 — Wind Waker's 3D scene: two primitive-restart faults in the driver - -Wind Waker drew its 3D scenes as a huge near-camera surface whose colours -changed every frame. The method was a FIFO log recorded on the console (debug -mode `fifo`, ten frames of Link on the ship deck) played both by the core -(single core: FIFO playback hung in `CPU::Run()` before `Core/Core.cpp`'s fix) -and by desktop Dolphin built from the same tree, then bisected by object range -(debug mode `objects A B`). - -1. Objects 0-89 matched desktop Dolphin and object 90 added the surface: the - first draw past 2048 indices, 398 short restart strips. With Dolphin drawing - lists instead of restart strips it matched. ../PS5_Vulkan R64 reduced it to a - probe: the driver turned restart off with a bare register write right after - each restart draw, and the write landed while the draw was still fetching - indices. The enable is now state, each change behind an SQ_NON_EVENT (as - RADV writes it). -2. After R64, thin triangles ran from the screen's corner to the minimap, and - the minimap's panel spilled to the left. Objects 310-319 (the minimap) - isolated them; neither the software vertex loader nor skipping line and - point draws changed anything, and lists instead of restart strips fixed - them. ../PS5_Vulkan R65: every submission a copy splits off starts with - restart off on the GPU, while the recording still held it on. - -Evidence (`evidence/dolphin-restart-strips`): the FIFO log's screenshots -against desktop Dolphin, mean difference per channel 64-71 before, 1.23-1.53 -after R64, 1.13-1.16 after R65 (resampling between the console's and the host's -resolutions). My save state on Outset Island loads and draws cleanly, with no -audio errors. - -Also: the title's stale-test-file list now covers `/app0/dolphin-debug.txt` and -`/app0/ps5vk-shader-cache-dir.txt` (a test hook a previous run had left on the -console, which redirected the driver's shader cache). The core patch keeps the -debug modes, the options override, the FIFO recorder, the object-range bisect -(debug mode only) and the single-core FIFO playback fix; the temporary host A/B -hooks are gone. - -## 2026-09-25 — Dolphin acceptance: the three games, Profile 0 - -The acceptance targets are Resident Evil 4, Super Smash Bros. Melee and Wind -Waker, each from my save state, through the profiles in -`tooling/dolphin-profiles/`. A run is summarised from its trace: audio windows -(played against silence, which is what full speed reads as), the driver's -presents and GPU time a present, hitches, errors and screenshots on SELECT, -which the GameCube pad ignores. - -1. **RE4 crashed at boot** (SIGSEGV writing JIT code at 0x3808000). Dolphin's - JIT maps 56 MB of executable memory from the title's flexible memory (about - 450 MB), and found 45 MB: RetroArch's state-load task had just read RE4's - 260 MB entry state into one buffer, and the frontend's `malloc` gave buffers - that large an anonymous mapping -- flexible memory -- before direct memory. - Large frontend buffers now take direct memory first (`src/memory_ps5.cpp`), - leaving flexible memory for what only it can hold: executable code and - thread stacks. The host test asserts the order and the fallback; its stand-in - direct heap is now locked like the real one. The core's allocator reports the - flexible memory free when an executable mapping fails. -2. **RE4 drew its frame in its haze colour.** A FIFO log of the scene plays - correctly in desktop Dolphin; desktop Dolphin reproduces the console's frame - with dual-source blending forced off. ../PS5_Vulkan R71 added dual-source - blending (the compiler already exported the second source); RE4 now draws - Leon, the car and the forest as desktop Dolphin does. -3. **Melee ran at 95-99%.** Its EFB copies split every frame into ~105 - submission steps, each a CPU wait; ../PS5_Vulkan R70 made them GPU barriers - (0.07 steps a present). Melee still shows up to 1% audio silence in some - windows, with shader-compile hitches during play; that is the next lead. - -Profile 0 on the R71 build: RE4 and Wind Waker 100% in every steady window, -correct pictures; Melee 96-100%. - -## 2026-09-25 — Profile 1; one frame per retro_run, so the display paces Dolphin - -Profile 1 (the accurate baseline: safe texture cache, scaled EFB copies, GPU -texture decoding, exact depth) needed two driver rounds and one fix in the -core's libretro glue. - -1. **RE4: "Failed to allocate 65536 bytes from texel buffer".** GPU texture - decoding streams through a texel buffer Dolphin sizes by - maxTexelBufferElements, which the driver reported as Vulkan 1.0's minimum. - ../PS5_Vulkan R72 reports the descriptor's 32-bit record count, as RADV does. -2. **Wind Waker: 32 frame periods over 40 ms in 130 s**, at full speed. The - title's CPU sampler found the driver re-reading AGC's colour-target - defaults for every target of every rendering; ../PS5_Vulkan R73 reads them - once (32 → 14). -3. **The other 14 were pacing, not work.** With the sampler's threshold just - above the game's 33 ms frame, every late frame had the CPU thread blocked in - the audio driver's write and the main thread waiting for it. libretro asks - for one video_cb per retro_run, NULL for a repeated frame; Dolphin's - presenters call it only for a frame they draw, and with duplicate XFBs - skipped (the default) a 30 FPS game's every other field drew none. Those - runs returned without presenting, so nothing waited for the display and the - audio writes alone paced them. Presenting every field (`dolphin_skip_dupe_frames` - off) left no late frame, which confirmed it. The core now hands RetroArch - NULL for a skipped duplicate at the point Dolphin would have presented it - (`Presenter::ViSwap`), and at the end of a run that output no field. - A first version sent it only at the end of the run: no late frames, but the - frontend's present then waited on the display after the frame step had - stopped the CPU thread, and the audio ran 37-7268 frames short in four of - five windows. Both are kept: `evidence/dolphin-pacing-before`, - `evidence/dolphin-pacing-end-of-run`, `evidence/dolphin-pacing-duplicates`. - -Profile 1 on title f2585b8f (`tooling/dolphin-profiles/p1-accurate.txt`, -screenshots on SELECT): RE4, Melee and Wind Waker draw as before, 1,200 -presents per 10 s, every window without a screenshot 480000 played and 0 -silent. Every late frame left is either the state load (the first 1-4) or a -screenshot: RetroArch's read-back of the 4K output costs 55 ms in the driver's -copy path, and a window with one runs ~2000 frames (0.4%) short. Under display -pacing the audio buffer is kept half full (32 ms) rather than full, so a stall -that long now empties it; before, a screenshot window was ~750 frames short in -RE4 and Wind Waker and 2000-3800 in Melee. The driver's copy throughput (the -55 ms read-back, a 218 ms copy in RE4's load) is the next lead, and matters -most for Profile 5's EFB copies to RAM. - -## 2026-09-25 — Profile 2: five swapchain images at 120 Hz - -Profile 2 (synchronous ubershaders, 1x and 6x) ran RE4 and Melee at full speed -at both resolutions, and Wind Waker at 89-99% at 1x and 80-84% at 6x with the -GPU busy 0.7 and 2.2 ms a present: slow at 1x, so CPU or presentation, not GPU -throughput. - -- The driver's new `cpu_copies` profile field showed Wind Waker reading back - 40-90 images a second (EFB copies encoded to RAM, and the EFB peeks its game - settings enable), each a wait for the GPU; ubershaders lengthen what each - wait covers. -- The CPU sampler at a 1 ms threshold found neither thread busy: the main - thread waited in vkAcquireNextImageKHR and the CPU thread between frame - steps. -- A one-shot timeline of retro_run (removed since) showed why. RetroArch - emulates a swap interval of 2 at 119.88 Hz by presenting each frame twice, - and three images leave room for two queued flips: one frame. A new frame's - run took 7 ms and queued both presents at once; the repeated field's run - then waited 13-14 ms for two vblanks inside its present, and Dolphin's GPU - thread -- the thread that waits -- cannot drain the FIFO while it does, so - the CPU thread stalled behind it. The pair of runs crossed 33.4 ms often - enough to lose 4-12%. -- A temporary driver with five images and `video_max_swapchain_images = 5` - ran the same state at 100%. - -../PS5_Vulkan R74 gives a swapchain the images it asks for, three to five, and -patch 0091 has RetroArch ask for (video_max_swapchain_images - 1) * interval + 1 -when it emulates an interval. The default of three becomes five at 120 Hz with an -interval of 2: two frames of lookahead, the same frames and the same 33 ms as three -images at 60 Hz, so the latency is what a 60 Hz display has. - -Title 5beca7cc (`evidence/dolphin-p2-swapchain-before`, `-after`): Profile 1 and -Profile 2 at 1x and 6x on all three games, 1,200 presents per 10 s and every -steady window at 100% (99% in one screenshot window). Melee's first run on a new -build still stalls 30-95 ms on each pipeline it compiles for the first time: the -driver's shader cache is per build, and Profile 8 is where that is measured. - -## 2026-09-25 — Profiles 3-7: torture, EFB, the scaling ladder and MSAA - -On title 5beca7cc (../PS5_Vulkan R74) and, for MSAA, its successors: - -- **Profile 3** (6x, ubershaders, 16x AF, forced filtering, every frame - presented), **4** (6x GPU EFB copies) and **5** (6x EFB copies to RAM): - RE4, Melee and Wind Waker at 100% in every steady window, correct pictures. - RAM EFB copies read back 270 images a second in RE4 at full speed; Wind - Waker's game settings already send its EFB copies to RAM, so its Profiles 4 - and 5 are the same run. -- **Profile 6**, 1x to 6x at the accurate baseline: all eighteen runs at 100%, - 1,200 presents per 10 s; Wind Waker's GPU time grows from 0.21 to 1.37 ms a - present, Melee's and RE4's stay under 0.2 ms. -- **Profile 7**: 4x MSAA first stopped every game at its first resolve. The - driver refused to sample a four-sample image and could not resolve into - Dolphin's resolve texture, which it stores in rows; ../PS5_Vulkan R75-R77 added - multisampled sampling, GPU resolves and rendering into rows. R78 added 2x and - 8x and moved the sample locations to the registers of every pixel of the 2x2 - quad (C8 had left the lower two at AGC's defaults). 2x, 4x and 8x at 1x: all - three games at 100%, antialiased. The maximum torture (6x, 8x MSAA, - ubershaders, 16x AF): correct and stable at 92-97% (RE4), 72-77% (Melee) and - 60-64% (Wind Waker), GPU-bound -- the uncompressed 8x surfaces at 6x - quadruple the GPU work of Profile 3. - -## 2026-09-25 — Profile 8, the shader cache; Profile 9, reloads - -**Profile 8.** Profile 1 on each game twice with the driver's shader cache in a -new, empty directory (`CACHE_DIR`), then again with the cache that run left -(title cbcf5b03). Cold, the pipelines compiled for the first time stalled the -game 88 ms in total (RE4), 124 ms (Melee) and 156 ms (Wind Waker); Melee lost -3% of one window. Warm, nothing compiled, every steady window was at 100% and -Melee's late frames fell from 19 to 6. The test caches were removed afterwards; -my own cache was not touched. - -**Profile 9.** Four reloads of each game in one process: the menu opened and -closed ten times, three state loads, a screenshot, then Close Content through -the Quick Menu and the content loaded again with its core, a state load and a -screenshot after each (`<seconds> RELOAD`, a new pad-script action that pushes -the same load the menu's history does, with the core and content from -`/app0/args.txt`). No crash, no refused call, the right picture after each load, -and 100% in each window without a menu or a load in it. - -- A first pad-script action that closed the content itself crashed Dolphin: it - unloaded the core from the input poll, which runs inside retro_run, and the - core's next step read freed state. That is the test aid's fault, not the - core's -- a menu close happens between runs -- so the action was removed and - the script closes through the Quick Menu as a player does. -- The driver's new `direct_live` field showed each reload keeping 13 more - direct-memory mappings (title 26c9daa0). This port's patch 0021 was the - cause: it creates wide default and blank textures after upstream's 1x1 and - 4x4 ones and stores them in the same slots, so the small ones -- an image, - its memory and its view each -- were never destroyed, on every swapchain - change for the default and every video init for the blank. Patch 0092 - releases each small texture before its slot is reused. -- The same count was wrong in one direction: destroying a graphics pipeline's - zeroed compute mapping released direct memory at offset 0 and took a mapping - off the count. ../PS5_Vulkan now releases and counts only a mapping with - bytes. -- With both (title 9c623566; `evidence/dolphin-p9-reload-before`, `-after`), - the count grows by 2 a reload (182, 184, 186, 188 - at each reload's first state load) and the driver's bytes stay at 592 MiB; before, - its bytes rose from 592 to 593 MiB over the same four. Flexible memory drops - 2 MiB once, after the first reload, and then holds. The two mappings a reload - are small -- under the MiB the profile resolves -- and still unidentified. - -Two side effects of my earlier test runs remain on the console: RetroArch's -content history and runtime logs recorded the test loads, and the FTP account -may not change `playlists/` or the core's state directory, so I cannot clean -them from here. Every run now turns history and runtime logging off in its -configuration, and Profile 10 keeps its states in a directory of its own. - -## 2026-09-25 — The title's folders and files are reachable over FTP - -FTP answered `550 Permission denied` for `savestates/dolphin-emu`, so my save -states could not be copied off the console. The title runs as its own user and -the FTP server as another, so only the bits for everyone else count. The -frontend gave its seven top-level folders 0777, but RetroArch creates every -folder below them 0750 (libretro-common's `retro_vfs_mkdir_impl`), its -unbuffered files with mode 0, and the rest under the process umask. - -`src/permissions_ps5.cpp` wraps `mkdir`, `open` and `fopen` at link time (a -core's imports of them bind to the same wrappers), and each wrapper gives what -it creates 0777 for a folder and at least 0666 for a file. A first build that -cleared the umask instead jumped to address 0 at start: the console's libraries -do not resolve `umask`. At each start a thread walks /app0 and gives every -folder and file the bits it lacks. Only modes change; no contents are touched. -From inside the title every entry's owner reads as uid 0, so the walk cannot -skip what FTP uploaded and covers all 17,551 entries: 389 ms, which is why it -runs beside the frontend's start, not before it (141 ms on a warm cache). - -The first start repaired 42 folders and files, none refused. FTP now lists -`savestates/dolphin-emu` (`drwxrwxrwx`) and its states (`-rw-rw-rw-`); the next -start, after a full Profile 10 run that wrote states, screenshots and logs, -found nothing to repair. - -## 2026-09-25 — Profile 10: save-state stress - -Each game from a copy of my entry state in a test directory, `/app0/p10-states` -(the title makes the copy, `/app0/state-copy.txt`, because FTP could not read my -states then): 15 saves and 15 loads three seconds apart, then 6 of each 0.6 s -apart, and screenshots between them. RetroArch saved into the entry slot, so the -test states replaced only the copy; my own states were read and never written. -The test directory was emptied afterwards. - -Title 718b41d7 (`evidence/dolphin-p10-state-stress`): Melee, RE4 and Wind Waker -each save 21 times and load 22 times with no error and no crash, and every -screenshot after a load shows the loaded scene correctly. Windows with no state -operation run at 100%. Windows with three or four operations run at 96-100% -(Melee), 97-100% (RE4) and 94-98% (Wind Waker), because each load stalls the game. -Flexible memory holds at 243,712 KiB throughout. The driver's direct mappings level -off in RE4 (256 to 265) and Wind Waker (346 to 382). In Melee they rise from 266 -to 333 and were still rising when the run ended, so Profile 11's soak is where that -is decided. - -Where a load's stall goes (Wind Waker, a bounded timing capture in the core, -removed since): RetroArch's serialize takes 13-20 ms and unserialize 25-76 ms. -The two fields after a load then take 130-245 ms and 80-170 ms, because Dolphin -clears its JIT cache on every load and recompiles what runs: 6,000-9,400 blocks, -about 20 µs each, 124-219 ms of the first field. The driver's part of those -frames is a few milliseconds. So a load costs 250-400 ms and a save about 20 ms, -the same cause desktop Dolphin has. The CPU sampler first pointed at an INI file -written on the main thread; that was Dolphin's configuration saved at boot, -outside the stress. - -## 2026-09-25 — Profile 11 so far: Melee 30 minutes, RE4 18 - -Title 718b41d7, the accurate profile, from my entry states, a screenshot every -five minutes, driver profile armed: - -- **Melee, 30 minutes:** no crash; 184 of 185 windows at 99% or better, the - exception being the first (boot and the entry state). The driver's direct - mappings reach 356 within the first seven minutes and hold there to the end, - so Melee's rise in Profile 10 was a plateau being reached, not a leak. - Flexible memory holds at 243,712 KiB. Each screenshot costs its 55 ms read - of the 4K frame; three stalls in Dolphin (129, 86 and 87 ms) took their windows - to 99.0-99.1%. -- **RE4, 18 minutes** (stopped to free the console for other testing): no crash, - every window after the first at 99.6% or better, direct mappings 261-262 and - flexible memory flat. - -Still to run: RE4 and Wind Waker for 30 minutes and one game for an hour. - -## 2026-09-25 — Mario Kart Wii's black scenery; Rogue Leader's FIFO desync - -**Mario Kart Wii** (my save state, Luigi Circuit): the grass bank and palm tree -left of the track drew black. Dolphin asks for samplers biased -2.1875 to --3.1875; the driver reported a limit of 2 and refused them (three refusals in -100 s, title ad98b960). ../PS5_Vulkan R79 reports the +/-16 its sampler word -holds, proved by a console probe; the scenery draws (klog/mkw-base-*, mkw-r79-*). - -**Rogue Leader** crashed for a tester after about three minutes. From my save -state (Death Star Attack) it plays without error; from boot, the attract -sequence fails at the same point every run: a burst of "Negative -fifo.CPReadWriteDistance" alerts -- 221,472 in one run -- then commands decoded -from stale FIFO words, then a fault in LoadIndexedXF, the tester's crash. It -needs dual core: single core runs past that point with no alert, and without -fastmem the desync still happens, earlier and milder. - -A bounded one-shot capture (removed since) recorded the FIFO register writes and -the GPU loop's starts and stops, dumped at the first negative distance: the GPU -loop resumed at a field with one chunk still queued, the game -- resuming at the -same moment -- switched GP reads off and wrote 0 to the distance, and the loop, -already inside that chunk, subtracted 32 from the zero. A negative distance -reads as more to fetch, so the loop fetched the FIFO's old words as commands. -Standalone Dolphin has the same unguarded window, but its GPU thread is almost -always idle when a game resets its FIFO; the libretro core restarts the loop -with a backlog at every field, exactly when the game runs again. - -The fix (patches/dolphin/ps5-port.patch): the CPU's writes to the read pointer -and the distance are counted before they land, and the dual-core GPU loop -commits a chunk -- read pointer past it, distance a chunk shorter -- by -compare-and-swap, only if no such write came while the chunk was in flight. The -CPU's values win, as a CPU write does on the hardware; a gather-pipe burst, which -only adds, is retried. Title 08e8ed4c, twice from boot for seven minutes (368 -emulated seconds, past the old failure and through a second attract loop): no -alert, no unknown opcode, no crash. - -## 2026-09-25 — Profile 9's last two mappings: the HDR uniform buffer - -Profile 9 left two direct-memory mappings a reload unidentified. A bounded -one-shot registry in ../PS5_Vulkan's allocator (removed since; title 1ecbfd3a) -recorded the caller of every live VkDeviceMemory. After each reload, one entry grew -by 2 (1, 3, 5, 7, 9 over four reloads): 256 bytes of host-visible memory from -`vulkan_create_buffer`, called by `vulkan_init` for `vk->hdr.ubo`. RetroArch -creates that uniform buffer whenever the build has `VULKAN_HDR_SWAPCHAIN`, but -`vulkan_free` destroyed it only when the context has HDR support, which this -console's surface never reports. The video driver is initialised twice a reload, -so two buffers leaked each time. - -Patch 0093 releases the buffer whenever it exists; the HDR images stay with the -support flag that creates them. Title 9a84b116, the same four-reload Melee run -(`evidence/dolphin-p9-reload-flat`): every reload returns to `direct_live=180` -and `memory:152`, the count the first boot held. Before 0093, memory grew 152, -154, 156, 158, 160. The stage and table mappings return to the same counts each -cycle. There were no crashes, and each steady window ran at 97-100%. - -## 2026-09-25 — Rogue Leader's slow attract sequence: which layer owns it - -A tester reported Rogue Leader stuttering at first and then lagging in video and -audio. From my save state (Death Star Attack) it runs at 100% once the shaders -it needs are compiled. From boot, the attract sequence's in-engine fly-bys drop -to between 52% and 81% for a minute at a time. I profiled two runs from boot -with the title's sampler on the main thread (Dolphin's GPU loop and the driver) -and Dolphin's CPU thread, both on title 9a84b116 (klog/rl-prof-*, klog/rl-prof-warm-*). - -- **Cold** (the first run after a driver build; the driver's shader cache is - one directory per driver build, so every rebuild starts empty): 392 shader - compiles land in the hitch reports, 4.6 s in all. In the worst window (52%) - the main thread spends 45% of its samples compiling and 9% opening cache - files that are not there yet. -- **Warm** (the same sequence straight after, the cache filled): 20 compiles - and 0.25 s, but the same stretch still runs at 65-81%. Dolphin's CPU thread - is idle 97% of the time, waiting for its next frame. The main thread is the - limit: about 30% of it is the driver's `ps5vk_queue_run_step` spinning until - the GPU finishes a step, so that a copy the driver still performs on the CPU - can run; 11-14% is `ps5vk_flush_cpu_cache`, the driver flushing the CPU's - cache lines for every draw's descriptor tables, registers and constants. - -So the owning layer is the driver. The first run's stutter is shader compilation -(inherent, and cached from the second run). The lasting slowdown is the -driver's CPU cost on the thread that runs Dolphin's GPU emulation: CPU-side -copies that split a submission and wait for the GPU, and a cache flush per -draw. Neither is Dolphin's, and both will cost PS2 workloads more, so they are -in ../PS5_Vulkan's round plan for the LRPS2 port (docs/LRPS2_GAPS.md) and not -fixed here. - -The warm run is itself an interrupted test. I stopped its harness mid-launch, -the title ran by itself until its frame limit, and I fetched its trace and -screenshots afterwards and removed its test inputs from the console. - -## 2026-09-25 — Profile 11 complete: RE4 and Wind Waker 30 minutes, Wind Waker an hour - -Title 9a84b116 (driver 2f1e447, patch 0093), the accurate profile, from my -entry states with no input, the driver profile armed, a screenshot every five -minutes (every ten in the hour-long run): - -| Run | Windows | After the first | Direct mappings | Flexible memory | -|---|---|---|---|---| -| RE4, 30 min | 185 | all at 99.5% or better | 256 → 264 (666-668 MiB), flat after the first quarter | 245,760 KiB throughout | -| Wind Waker, 30 min | 185 | all at 99.6% or better | 349 (680 MiB) start to end | 245,760 KiB throughout | -| Wind Waker, 60 min | 365 | all at 99.6% or better | 349 (680 MiB) start to end | 243,712 KiB throughout | - -None crashed. The first window of each is the boot and the state load (72-83%): -its hitches are the state load (1.0-1.1 s) and, in Wind Waker, the first -shader compiles (116 in 1.5 s at 30 minutes; 408 ms at 60, the cache warmer). -Every later hitch is a screenshot's read of the 4K frame (55-64 ms), which -leaves its window 0.4% short, and one 40 ms stall in the hour. The last frame -of the hour shows Outset Island correctly. With Melee's 30 minutes (above), -Profile 11 passes for all three games, and nothing grows: the mappings each -game holds level off within minutes and flexible memory does not move -(`evidence/dolphin-p11-soak30-re4`, `-soak30-ww`, `-soak60-ww`). - -## 2026-09-25 — LRPS2's first boots on the console - -LRPS2 (my fork, ../PS5_LRPS2, branch ps5-port) is built by tools/build-lrps2.sh -at its pinned revision with patches/lrps2/ps5-port.patch and staged in the -title. BIOS-only runs, from my test options and test folders -(/app0/lrps2-test), BIOS 2.30 (USA): - -- **First load:** the core loader stopped on `fstatat`, which the SDK links - and the console resolves to nothing. The core's libretro-common now stats - directory entries by joined path on the PS5, as it does on the PS4. -- **Second:** SIGSEGV in libretro-common's process barrier, whose x86 page - flip faulted on its write after mprotect. The PS5 now takes the barrier's - NONE tier, like the other multi-core consoles. -- **Software renderer:** the BIOS reaches its main menu and runs 150 s with no - crash and every audio window at 99.6% or better. Sampled every 10 s, the - menu is right from 10 s to 100 s; at 110 s and 120 s the whole picture sits - lower in the frame. I have not yet established whether that is the BIOS's - own idle behaviour or an emulation fault: my BIOS never leaves the console, - so there is no desktop reference for this frame, and it is the first thing - the hardware renderer's run will be compared on. -- **Vulkan renderer:** the core loaded libvulkan itself and ignored the - vkGetInstanceProcAddr the frontend passes it; it now uses the frontend's - when no library loads. With ../PS5_Vulkan R81-R84 it creates its 1.1 - device and GS resources; its draws then met the driver's dynamic line width - refusal (R85, fixed) and then its refusal of a non-indexed draw with a - first vertex, the next driver round. - -Everything the runs created is reachable over FTP (0777/0666). The shared -memory cards and empty per-core folders they left outside the test folder -were removed afterwards; my own saves, cards and configurations were not -touched. - -## 2026-09-26 — The payload SDK fork and its platform layer, adopted - -The title and every project now build with my fork of the payload SDK -(../PS5_PayloadSDK, pinned in tools/setup-native-dependencies.sh): the v0.42 -release binaries with the fork's headers and its platform layer installed over -them (docs/PLATFORM_FIRMWARE_ANALYSIS.md has what the layer rests on). What -moved into it, and out of the title and the cores: - -- **The machine context.** The fork's `sys/_ucontext.h` declares the console's - 48 extra bytes, so `uc_mcontext.mc_rip` is the faulting instruction. The - crash reporter, the sampler and the Dolphin, PPSSPP and LRPS2 ports read the - header's fields; the six-word offset each carried is gone. The probe built - with the fork's header finds rip at its field. -- **The libc gaps.** `src/ps5_directory.cpp` and most of `src/libc_shims.c` - are the platform's `ps5_*` functions now. `src/platform_wraps.c` binds the - title's `--wrap` links to them, and tools/core-imports.py binds a core's - imports (the whole `*at` family, `utimensat`, `futimens`, - `clock_nanosleep`, `arc4random*`, `statvfs`/`fstatvfs`). The directory - fixture test moved to the fork with the code. The platform's `statvfs` - refuses a path that does not exist, as a desktop build sees it; the title's - answered for any path. -- **Executable code.** Dolphin's `AllocateExecutableMemory`, PPSSPP's, and - LRPS2's code area come from the platform's executable direct memory, near - each core's own code, instead of anonymous flexible memory. The code caches - are upstream's sizes again: Dolphin's near and far caches 128 + 64 MiB (32 - + 16 before), LRPS2's recompilers 305 MiB (161 before). Dolphin frees its - code blocks through `FreeMemoryPages` off iOS, so that returns platform - memory too. -- **Guest memory.** Dolphin's `MemArena`, PPSSPP's arena (a shared-memory - object before, every view charged to flexible memory) and LRPS2's memshm are - the platform's direct-memory objects, views and reserved ranges. PPSSPP maps - its VRAM view 80 KiB into its guest memory; the platform first refused a view - off the 64 KiB allocation unit (PPSSPP's memory init failed). The probe then - measured a view at a 16 KiB page offset mapping and aliasing correctly, and - the platform now takes page offsets. -- **Reporting.** A profiled run's memory line is followed by the platform's: - `memory: platform exec=<regions>/<MiB> shm=<objects>/<MiB> views=<n> - ranges=<n>/<MiB>`. - -Measured on the console with this build, against the runs before it: - -| | before | now | -|---|---|---| -| Dolphin, flexible memory free in game (RE4, Melee, Wind Waker) | 243,712 KiB | 303,104 KiB | -| Dolphin, JIT code | 48 MiB of flexible memory | one 200 MiB direct region (288 MiB with the MMU: Rogue Leader) | -| LRPS2, code area | 161 MiB, 163,840 KiB flexible left | 305 MiB, 328,704 KiB flexible left | -| PPSSPP, guest memory | a shared-memory object, views in flexible memory | 144 MiB of direct memory, 19 views | - -- **Dolphin, Profiles 0 and 1** on Melee, RE4 and Wind Waker: no crash, every - steady window at 100% but one Melee window at 98% in each profile. -- **Dolphin, Profile 9** (Melee, four reloads): no crash; the platform holds - one code region, one guest-memory object and one arena after each reload, - so each reload returns them; flexible memory drops 2 MiB once and holds; the - driver's direct mappings are flat at the reloads (180 each). -- **Rogue Leader** from boot: no crash, no FIFO alert, no unknown opcode; the - attract sequence's windows are as before (its heavy section read a few - points lower on this run). -- **PPSSPP, God of War: Ghost of Sparta** from its state: full speed (480,000 - of 480,000 frames every window after the first), 1,200 presents per 10 s, - no crash. -- **LRPS2** boots its BIOS with the new code area; the hardware renderer still - stops at the driver's first-vertex refusal, the next driver round. -- When content closes, the platform's counters go back to zero. - -Still to run on this build: Profile 10 (save-state stress), whose test harness -I have to rebuild, and a second Rogue Leader run for its heavy section. - -## 2026-09-26 — LRPS2's hardware renderer at 4K: God of War II, FFX and San Andreas at full speed - -The Vulkan renderer now draws games on the console at 6x (2880x2160, 4:3 at -4K height). What it took after the platform adoption: - -- **Driver R89** (../PS5_Vulkan): a non-indexed draw's first vertex and a first - instance, the refusal the renderer stopped at. -- **The upscale ladder.** LRPS2's option listed 1x, 2x, 4x and 8x. The fork - adds 3x (~1080p), 5x (~1800p) and 6x (~2160p/4K) to the core option; the - renderer took any integer already. -- **Driver R90**: vkCmdClearColorImage and vkCmdClearDepthStencilImage of an - attachment are a draw on the GPU instead of the CPU's write at a submission - split point, each of which drained the GPU. LRPS2 clears its render targets - and its D32_SFLOAT_S8_UINT depth that way every frame. R90 also ends every - submission step with the depth caches flushed, which a CPU copy after a - depth write needed (it read stale depth before), and R91 fixes the CPU's - eight-byte tile map and one-byte copies (../PS5_Vulkan, docs/M5_PHASE_C.md). -- **MTVU.** VU1 on its own thread. The fork makes it the PS5's default, as the - arm64 build already does; the game database still turns it off for the games - it breaks (`mtvu: 0`, applied on the console as on the desktop). - -God of War II's combat at 6x, the same four-minute run each time: - -| | MTVU off | MTVU on | -|---|---|---| -| before R90 | 47-61% | 70-78% | -| R90 | 65-70% | 99% (the fork's default, no option set) | - -The EE thread was the limit: VU1 on its own thread and the clears off the CPU -each lift it, and together they reach full speed. - -Measured on the console with the title as committed (driver a1ad7c6, R91; the -fork's f6bc86ecb, whose defaults turn MTVU on; 6x set, nothing else): runs from -boot of four minutes (five for San Andreas), Cross pressed every 2.5 s, a -screenshot every 10 s. Speed is the audio produced per 10-second window against -real time. - -| Game | Windows | Speed | Driver per frame | -|---|---|---|---| -| God of War II demo (NTSC, 60 fps) | 23 | 99% in every window after the boot, combat included | 0.5 split steps, queue 0.13 ms | -| Final Fantasy X demo (PAL, 50 fps) | 23 | 99% in every window: the opening FMV and the scenes after it | 1.0 split steps, queue 0.38 ms | -| GTA San Andreas (NTSC, 60 fps) | 29 | 98-100% in every window after the boot: intros, loading, the first mission in Ganton | no split steps, queue 0.13 ms | - -No crash, no refusal, no device loss. The pictures are right: God of War II's -colossus fight, FFX's opening, San Andreas' Grove Street alley with its HUD -(the haze below aside). Memory is flat in every run: 323,584 KiB of flexible memory free, -one 305 MiB code region, two shared-memory objects (324 MiB), 4 GiB of reserved -ranges. Most hitches are the screenshots (a 4K frame read, 55-65 ms); the rest -are pipeline compiles and loads at scene starts, none over 130 ms. - -Two things to look at, neither a stall: - -- **FFX presents at 50 Hz, not on the 120 Hz output's cadence.** A PAL game's - frames do not divide the refresh, so the pacer's plan for 50 fps on the - 120 Hz output (docs/LRPS2_PORT.md, Content) is next. -- **San Andreas' radiosity haze is doubled at 6x.** The game database's fix - (`halfPixelOffset: 4`, applied on the console) only "helps align radiosity - closer to native", so this may be upstream's own limit at high scales. Phase - 3's comparison with desktop LRPS2 at the same settings is where it is - settled. - -The Wind Waker 30-minute soak on the adoption build (before this entry's -changes, Profile 11's recipe) was flat: 185 windows, every one after the first -at 99.6% or better, 349 direct mappings (680 MiB) start to end, 301,056 KiB of -flexible memory free throughout (245,760 before the adoption), no crash. - -## 2026-09-26 — The committed title on every core, and LRPS2's save states - -The title as committed (build ad1207c6: driver R91, whose step end now flushes -every GPU cache, the SDK fork's platform layer, LRPS2 with MTVU on) was run -through the regressions the earlier builds passed: - -- **Dolphin, Profiles 0 and 1**, RE4, Melee and Wind Waker from my entry states: - no crash; every window after the first at 100%, but one Melee window at 98% - in each profile, as before. -- **PPSSPP, God of War: Ghost of Sparta** from its state: 100% in every window - after the first, 1,200 presents per 10 s. - -**LRPS2, Profile 10** (God of War II's demo at 6x, from boot): states in the test -directory `/app0/lrps2-test/states`, never mine; save state on L3 and load -state on R3, alternating 3 s apart fifteen times each, then six of each 0.6 s -apart, screenshots after loads. Each state is 50,599,352 bytes. - -- Every save and load that ran succeeded: no state error, no crash, flexible - memory at 323,584 KiB throughout. The screenshots after loads show the game - going on from the loaded moment, with the frontend's "loaded state from slot - 0" notice. -- About half the presses produced an operation: 11 of 21 loads ran (the - frontend backs up the running state to "RAM" only for a load it applies), - and 10 of 21 saves (a bounded timing capture in the frontend, removed since, - logged each save that finished). The frontend runs one save or load at a - time: `task_queue_push` skips a blocking task while another is running ("user - must try again later"), and `content_load_state` ignores that answer, so a - press that arrives while the last operation is still going does nothing. Its - "Saving state" and "Loading state" lines do not show this: the first is - written before the task is pushed, and the second also when a save reads the - old file into its undo buffer. The Dolphin Profile 10 entry above counted - those lines, so how many of Dolphin's operations ran is not known from it. -- A save holds that slot 2.1-2.6 s with state compression on (50.6 MB - compressed to about 18 MB) and 2.7-2.9 s with it off: the title's file writes - run at about 18 MB/s, and writing in 4 MiB chunks instead of RetroArch's - 100 KB changes nothing (2.4 and 2.8 s). What limits the writes is the next - thing to measure, below RetroArch. -- A load stalls the game up to 320 ms; windows with state operations run at - 93-100%, the others at 100%. -- A first run with a full-size thumbnail per save (2880x2160 PNG) and a new - slot for each wrote only 5 of 21 saves, 12-15 s apart. The profile runs - without thumbnails, as Dolphin's did. The test states were deleted after - each run. - -Memory cards: each game's run created its own card in `/app0/lrps2-test/saves` -(the per-game cards the core options select), beside the test states and away -from mine. - -**LRPS2, 30-minute soaks** at 6x (God of War II's demo from boot, the driver -profile armed, a screenshot every five minutes): - -| Run | Windows | After the first | Direct mappings | Flexible memory | -|---|---|---|---|---| -| idle after the menus: Kratos dies at about five minutes and the game waits on "You are dead" | 185 | all at 99.2% or better | 228 → 318 (1,616 MiB) by the first quarter, flat after | 323,584 KiB throughout | -| Cross every 20 s: fighting, dying and restarting from the checkpoint for the whole half hour | 184 | two windows at 98.9%, the rest 99.6% or better | 228 → 315 at the first quarter, 346 at the end (1,616 → 1,618 MiB) | 323,584 KiB throughout | - -Neither crashed; the hitches are the screenshots and scene loads (17 in the -active run, the worst 128 ms). The mappings the active run adds after the first -quarter are small ones -- 31 of them, 2 MiB -- as new fights bring new -pipelines. - -## 2026-09-26 — Save states written without stdio (patch 0094) - -The payload SDK fork's file probe (its docs/PROBE.md, "Files") found this -console's `fwrite()` at 3-13 MiB/s, slower the larger its buffer, where -`write()` of a megabyte or more runs at 150-230 MiB/s. RetroArch writes save -states through stdio. Patch 0094 gives a state the raw descriptor on this -console: uncompressed in 4 MiB chunks, compressed (rzip) in 1 MiB ones. Small -writers such as configuration files keep stdio. - -LRPS2's 50.6 MB God of War II state, measured with a bounded timing capture -removed since: - -| | before | 0094 | -|---|---|---| -| uncompressed | 2.7-2.9 s | 0.28-0.29 s | -| compressed (the default) | 2.1-2.6 s | 1.8 s, now mostly zlib at upstream's level | - -The Profile 10 stress on the committed build (compressed, the default): no state -error, no crash, the game at 93-100% in windows with state operations. With -saves this short, the uncompressed run completed 15 of 21 saves and 16 of 21 -loads (10 and 11 before). The ones still skipped come in the burst 0.6 s apart, -where each operation overlaps the last. - -The title also pins the fork at 66f658e, whose platform probe takes the word -`files` (`/app0/platform-probe.txt`) to run the file probe in `/app0`. - -## 2026-09-26 — A tester's trace: half speed with V-Sync on, and two crashes - -A tester sent their trace.txt from build ad98b960 (2026-09-25): Zelda: A Link -to the Past (snes9x), Pokemon Pinball: Ruby & Sapphire (mGBA) and Ratchet & -Clank: Size Matters (PPSSPP), with V-Sync turned on. Three problems, all fixed -here and in ../PS5_Vulkan R93. - -**Half speed with V-Sync on.** Every audio window of the V-Sync session played -240,000 of its 480,000 frames, in all three cores; mGBA and PPSSPP played all of -them with V-Sync off earlier in the same trace. Each swapchain handover came -15-17 ms after the previous present, where mine mostly come within 8 ms. The -console had accepted 119.88 Hz and the display went on refreshing at 60 Hz, so -RetroArch, told 119.88 Hz, showed each frame twice at 60 Hz. The driver now -times six vblanks after configuring 119.88 Hz and restores 59.94 Hz when they -are more than 12.5 ms apart. Mine: `119.88 Hz selected, a vblank every 8.345 -ms`, and Zelda with V-Sync on plays 480,000 of 480,000. The fallback has not -run on a display that stays at 60 Hz yet; the tester's next trace will say. - -**A crash on closing content.** The tester's eboot still holds its code, so I -matched the bytes at each crash address against this build's symbolised ELF. -The first crash is `vk_enumerate_instance_extension_properties` reading the -count it was passed, from `vulkan_context_create_instance_wrapper` while the -video driver was re-created after content closed: the count's address lay -66 KB above the stack pointer and was not mapped. Patch 0019 held 256 extension -records (66,560 bytes) on the stack beside upstream's 128 layer records, a -133 KB frame. The payload SDK's new thread probe measured the stacks: 2 MiB for -the main thread, 64 KiB for a thread created with no attributes -- which is how -RetroArch creates its own -- and the tester's stack pointer was on a thread of -the second kind, not the main thread: threaded video. - -To check it rather than infer it, I built this title with only 0096 (below) -withdrawn and ran Zelda with threaded video and V-Sync on: twice it died at its -first video init, before the trace's buffer reached the file. The kernel -reported a write fault one page below a 64 KiB thread's stack in -`vulkan_context_create_instance_wrapper`, called from `vulkan_init` in -`video_thread_loop`: upstream's layer list alone is 66,560 bytes. Patch 0096 -gives RetroArch's own threads 2 MiB, as upstream does for Apple and this title -does for the cores' threads; 0095 moves 0019's list to the heap. With both, the -same run closes content through the Quick Menu at 25 s, re-creates the driver on -the video thread and goes on presenting, with threaded video and without -(klog/r93-zelda-threaded-fixed, r93-zelda-plain-fixed; the A/B run is -klog/r93-zelda-threaded-no0096b). - -**Two crashes in PPSSPP.** Both in `vkDestroyBuffer`, one from XMB's font free, -one from the core, each following a `next` field of 0x2 or 0x80 on the -driver's list of live buffers. The driver changed that list, and two others, -without a lock, while buffers may be created and destroyed from any thread. -R93 locks them; its host test (four threads, 16,000 buffers) spun forever on -the old driver. - -The title pins the payload SDK fork at a110320 (the thread probe, "threads" in -platform-probe.txt) and links ../PS5_Vulkan 060c5ac. The unit gate's patch -count moves 203 -> 212: 0094's seven blocks, which its commit (44d6fa5) left -out of the pinned count, and 0095 and 0096; 0019's old block joins the -withdrawn ones. tools/verify.sh PASS. - -## 2026-09-26 — A release without the crashes: the stability checks - -Before a release, the classes of the tester's crashes were looked for -everywhere, not only where they struck. - -- **Thread stacks.** Every thread the title starts for itself -- the log - flusher, the audio worker, the permission repair, the sampler, the - core-thread factory -- was created with no attributes, 64 KiB here; they ask - for 256 KiB now (src/title_threads.hpp), as the driver's shader-cache writer - and blit workers do (../PS5_Vulkan 40e9d30). RetroArch's threads have 2 MiB - (0096), the cores' 2 MiB. -- **Device state changed from several threads.** Beyond R93's lists, the - driver's writable globals are atomic or locked. One more was not: each draw - claimed a slot of the device's debug descriptor-table array with a check and - an increment, so two recording threads -- a core's and the frontend's with - threaded video -- could write an entry past it. It is an atomic increment now. - -The release candidate (build 870bd1bb; LRPS2 at its pinned revision, the -upstream port and 8x staying out until they are validated) on the console, V-Sync -on, my saves, configs and history kept out of the way (a test save folder, -core-options file and no history): - -- every core with a game -- FCEUmm (Super Mario Bros.), snes9x (Zelda: A Link - to the Past), mGBA (Mario Pinball Land), Genesis Plus GX (Sonic), FBNeo - (Metal Slug), PPSSPP (God of War: Ghost of Sparta), Dolphin (Wind Waker), - LRPS2 (God of War II's demo) -- through its title screens, the menu opened - and closed, then a Quick Menu action, threaded video on for four of them: no - crash, no fatal signal. The script's first DOWN was dropped for snes9x and - Dolphin, which restarted the content instead of closing it; closing snes9x - was already run on this code with threaded video on and off. -- PPSSPP for ten minutes with threaded video, the menu opened and closed every - 20 s (25 times): no crash, every audio window 98-100%. - -tools/verify.sh PASS; ../PS5_Vulkan tools/check-driver.sh PASS. - -## 2026-09-26 — Dolphin at its best settings: the PS5 defaults, and what Rogue Leader still needs - -Dolphin's defaults were upstream's: 1x, Synchronous shader compilation, no -anisotropic filtering. Profile 12 (`tooling/dolphin-profiles/p12-ps5-best.txt`) -is the best picture that holds full speed, every accuracy setting at Dolphin's -default: 6x, Async (UberShaders) -- exact ubershaders draw while the specialised -shaders compile in the background, so nothing is skipped and nothing stalls -- -16x anisotropic filtering and texture decoding on the GPU. From my save states, -Resident Evil 4, Melee, Wind Waker and Mario Kart Wii held 100% in every window -after the one that boots and loads the state (title 870bd1bb). - -The port now makes these the defaults (patches/dolphin/ps5-port.patch, -`Ps5ApplyDefaultProfile`), and like the PPSSPP port it sets an options file an -earlier core saved aside once, as `dolphin-emu.opt.before-ps5-profile`, because -RetroArch would otherwise keep its values over the new defaults. It makes the -options folder first, open to FTP, so the marker (`ps5-default-profile-v1`) exists -even where Dolphin's options were never saved. On my console (title b8d43519) my -file was set aside, RetroArch wrote the new one with the four profile values and -every other option as mine had it, and Wind Waker ran at 100% after boot. - -Rogue Leader is the exception, and it is the driver's. Its attract sequence runs -at 73-85% for stretches and its gameplay (my Death Star Attack state) dips to -90-97%, with every shader cached as much as cold. Sampled, Dolphin's GPU thread -waits on its CPU thread and the other way round, and on the GPU thread the -driver's transfers done by the CPU at submission split points -- uploads into -tiled images, then image copies once texture decoding is on the GPU, each after -waiting for the GPU -- are the largest cost that is not Dolphin's own. That is -../PS5_Vulkan R95: those transfers on the GPU. R94 (compiles on several threads -at once, ../PS5_Vulkan 6078307) removed the GPU thread's waits behind the -compiler's lock; cold, Rogue Leader's first window went from 77% to 82%. - -## 2026-09-26 — Rogue Leader after R94 and R95: what is left is Dolphin's - -With ../PS5_Vulkan R95 (tiled uploads a run at a time, c1f82d8) the GPU thread's -samples in uploads fell from 4,897 to 1,350 over Rogue Leader's 200 s attract -sequence (title db2f6363, texture decoding on the CPU), and with Dolphin's -default GPU decoding its CPU work is image copies between two row-stored images -instead. Neither moved the game's speed: the attract sequence stays at 71-87% -for stretches and the gameplay from my state dips to 93-96%. Dolphin's own -settings for the game (GameSettings/GSW.ini) require full MMU emulation ("MMU = -True -- strictly required") and turn on CPU culling; in the slow stretches both -of Dolphin's threads are partly idle, waiting on each other, and the CPU thread -runs the MMU's slow memory path. On this console's 16 KiB pages, Dolphin's -page-table fastmem maps a host page only where four guest pages are contiguous. -That is where the next Rogue Leader work lies -- in Dolphin, not the driver. - -## 2026-09-26 — Rogue Leader: what does and does not move its slow stretches - -Rogue Leader's attract sequence from boot, 200 s, Dolphin's PS5 defaults, the -10 s audio windows' speed (the slow stretches are windows 1-2 and 12-13): - -| Change | Windows 1, 2 / 12, 13 | -| --- | --- | -| none (title db2f6363) | 74, 85 / 72, 87 | -| CPU culling off (a user GSWE64.ini, removed after) | 74, 85 / 72, 87 | -| single core | 69, 59 / 78, 49 (but 100 where dual core gave 94) | -| emulated CPU at 75% (title fb60ec12) | 85, 97 / 83, 100 | - -The payload SDK fork's thread probe (58f7765) now times a hand-off between two -threads on the console: a condition-variable round trip 7.6 us on average, -27.7 us at worst over 2,000, a spinning flag's 0.9 us -- so the threads' waits -are not wake-up latency. Nor is it the refresh: in the slow windows the frames -themselves take 20-32 ms (the present periods), and the frontend's share per -present rises from 5.7 ms to 10 ms. The heavy stretches need about 40% more per -frame than a 60 Hz frame holds, in Dolphin's CPU emulation (full MMU, "strictly -required" by its game settings) and its GPU emulation, which single core shows -do not fit one core. Underclocking the emulated CPU is the only setting that -moves them, and it changes the game's own timing, so it is not a default. - -## 2026-09-28 — RetroArch on RADV: the menu, PPSSPP, Dolphin and LRPS2 run - -First console runs of this branch's title (PS5_VULKAN_DRIVER=radv; ../PS5_Vulkan's -release archive at ps5-port 884f954, SDK 489467e) against the installed ps5vk -build (fb60ec12), each launched from `/app0/args.txt` with `/app0/test-run.txt` -and a capture at a fixed frame. The 10 s audio windows give the speed: 480,000 -samples and no silence is 100%. - -| Run | RADV | ps5vk | -| --- | --- | --- | -| Menu (frame 600) | renders; no fault | — | -| PPSSPP, God of War: Ghost of Sparta (frame 3600) | the same title menu as ps5vk; every window full after boot with the shader cache filled | the same | -| Dolphin, Wind Waker (frame 3600) | title screen; every window full after boot | — | -| LRPS2, GTA San Andreas (frame 3600) | the logo, 0.1% of pixels differing from ps5vk's by more than 16 (the video's timing); two late windows 99.8% and 98.9% | one late window 99.4% | - -- **PPSSPP faulted at first,** calling a null vkCreateRenderPass2 3.6 s in. It - took the physical device's API version (1.4 on RADV) as the version it could - use, so it treated render pass 2 as core and never enabled the extension, - while the frontend created the instance for 1.1, and a device offers no core - command above its instance's version. ps5vk reports 1.1, and PPSSPP's MSAA - needs render pass 2, which ps5vk does not offer, so the path never ran there. - The port patch now caps the device's version at the instance's - (VulkanContext::CreateDevice, which PPSSPP's own allocator setup already did) - and asks the frontend for 1.1, the version RetroArch creates anyway: on ps5vk - nothing changes (1.1 and 1.1), and on RADV render pass 2 and the others are - enabled as extensions. -- **With an empty shader cache** PPSSPP on RADV lost 0.7% of one window to - pipeline compiles (3,277 samples); with the cache filled the run matched - ps5vk's window for window. Compiles must not cost speed, so that is open. -- **A capture run does not quit** under my configuration, on either driver: - `quit_press_twice = "true"` turns the capture's quit request into the first of - two presses. The runs ended at their watchdogs. - -The installed title was put back afterwards: the main checkout's build -redeployed (its record's five changed files dropped first), a capture run showed -build fb60ec12 presenting at 119.88 Hz, the capture files and RADV's shader -cache folder removed, and my configuration, core options, history playlist and -trace compared with the copies taken before (the history playlist restored). - -## 2026-09-28 — The release battery on RADV - -The checks the release candidate passed on ps5vk (2026-09-26), run on this -branch's RADV title (15ea4b2's PPSSPP fix included). My configuration, saves -and history were kept out of the way: `--appendconfig` with a test file that -turns history off, sends saves and states to a test folder and does not save -the configuration on exit. The test folder and files were removed afterwards. - -- **Every core with a game** -- FCEUmm (Super Mario Bros.), snes9x (A Link to - the Past), mGBA (Mario Pinball Land), Genesis Plus GX (Sonic), FBNeo (Metal - Slug), PPSSPP (Ghost of Sparta), Dolphin (Wind Waker), LRPS2 (God of War - II's demo), threaded video on for FCEUmm, mGBA, PPSSPP and LRPS2. A pad - script opened and closed the menu twice, closed the content through the - Quick Menu, then loaded the same core and content again (RELOAD). No crash, - no fatal signal. Each run initialised the Vulkan driver three times (start, - close, reload), and the windows after the reload were full, except PPSSPP's - third (99.2%) and Dolphin's second (99.2%), both with the shader cache empty. -- **PPSSPP for ten minutes** with threaded video, the menu opened and closed - every 20 s (25 times): no crash, every audio window 98-100%, as on ps5vk. - -The installed title was put back as before: main's build fb60ec12 presenting -at 119.88 Hz, and my configuration, core options, history and trace identical -to the copies taken first. - -## 2026-09-28 — Shader compiles on RADV: the heap's lock, and arenas - -With an empty shader cache PPSSPP lost 0.7% of a window to pipeline compiles -on RADV. RADV's own timings on the console, for PPSSPP's God of War from an -empty cache: 45 ms a pipeline (SPIR-V to NIR 15 ms, ACO 14 ms, the rest NIR), -where the same pipelines, captured from the console, compile in 7.4 ms on one -host thread. Dolphin's pipelines compile as fast on the console as on the -host, and Dolphin compiles on few threads; PPSSPP compiles on many. On the host, one -lock over every allocation doubled the time a pipeline took at eight threads -(19.6 ms median against 9.3), and the compiler allocates constantly. - -This title's overflow heap, where small blocks go, was one locked dlmalloc -mspace. It is now the payload SDK's title heap (95c08f2): up to eight arenas, -one for each allocating thread, with each block going back to its own. On the -console, RADV and an empty cache: - -| | Before | Arenas | -| --- | --- | --- | -| PPSSPP compiles | 5,480 ms, median 37.4 ms, p90 105 ms | 2,043 ms, median 17.4 ms, p90 28 ms | -| PPSSPP windows | 94, 100, 99, 100, 100 | 96, 100, 100, 100, 100 | -| Dolphin windows | 82, 91, 99, 100, 100 | 83, 92, 100, 100, 100 | - -Dolphin's first two windows are its boot, which compiles its ubershaders. -The release battery (every core, close and reload, threaded video on half) -and PPSSPP's ten-minute soak ran again on RADV with the arenas: no crash, -the soak's windows 98-100%. The installed title was put back as before. - -## 2026-09-28 — RADV's shader cache reads, and the battery again - -With the cache filled, PPSSPP's pipelines still took a median 11.3 ms: the -reads of Mesa's cache database queued behind each other's per-access reopen, -relock and write-back. ../PS5_Vulkan 5d8f37d links RADV with the database's -exclusive mode (ps5-port cedb774), the default on the PS5. On the console, -PPSSPP with a filled cache: pipeline creation 1,419 ms to 29 ms (median -0.3 ms). Dolphin with a filled cache: 87% and 99% for its first two windows, -as ps5vk from an empty cache (86% and 99%). The release battery (every core, -close and reload, threaded video on half) passes again with it: no crash. -The installed title was put back as before. - -## 2026-09-28 — LRPS2 pinned on the fork's main, without a patch - -../PS5_LRPS2's port moved from its `ps5-port` branch to `main`, pushed to -github.com/mihawk-99/PS5_LRPS2. `tools/build-lrps2.sh` now pins that branch's -revision, `9c2eea4`, and builds it as it is; `patches/lrps2/ps5-port.patch` -(the branch's diff against `6d14775`) is gone. The patch was byte-for-byte -`git diff 6d14775 9c2eea4`, with no binary hunks, so the source tree is the -same. Built both ways in this checkout, the two `pcsx2_libretro.so` are -identical (23,105,064 bytes, no byte differs). All five host gates pass on -`main` with it (title build `e30b3650`). The release, `f96bdb0e`, was built -in the PS5_RetroArch-radv checkout, and its LRPS2 core carries that path in -1,427 strings where this build carries PS5_RetroArch's. The installed title -is unchanged. - -## 2026-09-28 — Seven more systems: PS1, N64, Saturn, C64, MAME, DS and 3DS - -One core a system, each chosen for its fit with RADV and for how far it -upscales, built from my fork of it and pinned by revision (`tools/core-fork.sh` -and a `tools/build-<core>.sh` each). The defaults are the most each core offers -that holds full speed in the games I tested: - -| System | Core | PS5 defaults | On the console | -| --- | --- | --- | --- | -| PlayStation | Beetle PSX HW | Vulkan, 16×, 32-bit colour, no dithering, PGXP, the disc precached | Crash Bandicoot, full speed | -| Nintendo 64 | Mupen64Plus-Next | ParaLLEl-RDP at 8×, ParaLLEl-RSP, both JITs | Mario Kart 64, full speed after boot | -| Saturn | Beetle Saturn | as upstream | loads, then needs a BIOS (`mpr-17933.bin` or `sega_101.bin`) | -| Commodore 64 | VICE x64sc | as upstream | a `.d64` game, full speed | -| Arcade | MAME 0.289 | native renderer for raster screens, 4K alternate renderer for vector ones | Metal Slug 3, full speed | -| Nintendo DS | DeSmuME | JIT, 5× (1280×960), eight rasterizer threads | Pokémon Diamond, full speed; 6× 93–95%, 8× 60% | -| Nintendo 3DS | Azahar | Vulkan, 18×, asynchronous shaders, JIT | Mario & Luigi: Superstar Saga + Bowser's Minions, full speed after boot | - -The 16× Beetle renderer allows no MSAA, so that stays 1×. Reading the disc in -synchronous 2 KiB pieces cost up to 4%, so the image is now read into memory at -load. The JITs (ParaLLEl-RSP, new_dynarec, DeSmuME's, dynarmic's code cache) -take their code memory from `ps5_exec_allocate`. They had used the title's -flexible pool, which a JIT's cache exhausted. - -The loader grew to fit the larger cores. MAME's image is 413 MB, so a core's -file and image now come from direct memory, up to 1 GiB each. Azahar has 2,476 -initializers (the cap is now 65,536). A load that misses imports lists every -one, not only the first. Each image's `.eh_frame` is registered with the -title's unwinder, because MAME throws. The platform layer gained the libc -functions the cores import: `getcwd` (libc's calls a function only -libkernel_sys has), `tmpfile`, `mkstemp`, `strcasestr`, `getnameinfo` and the -rest. The title's aligned `operator new` now comes from the core heap. - -The loader's move to direct memory broke unloading: a reload faulted in every -core. Unlike an anonymous mapping, direct memory is not zeroed. A reloaded -core's `.bss`, the part of the image the file does not fill, held its -predecessor's bytes, and a static mutex that should have been zero was not. -Moving each image to a new address did not help. Zeroing the image when it is -reserved fixed it. - -Metal Slug 3 stuttered at 69%: MAME's alternate renderer scales a raster game's -bitmap on the CPU, and at 4K that took a third of the frame. The fork's new -"Vector Screens Only" mode, the PS5 default, uses the alternate renderer, -cropped at 3840×2160, only for a machine with a vector screen. Raster games keep -their native size and RetroArch scales them on the GPU. Metal Slug 3 now plays -at 99.4% (the rest is the window's share of boot). I had no vector game to test -the 4K path with. - -The battery (a game, the menu twice, Close Content, the same game again) passes -with no crash for all seven, with full speed after the reload. Saturn stops at -its missing BIOS and leaves the menu usable. - -**Load Content's two roots (patch 0097).** Load Content, and the file browser's -top level, list only INTERNAL (`/app0`, the title's folder) and EXTERNAL -(`/mnt`). Parent Directory from either goes back to the two, not to the -sandbox's `/`. A pad script went Load Content → INTERNAL → Parent Directory → -INTERNAL → content → Nintendo DS → the game, and DeSmuME loaded it. EXTERNAL is -empty for now: inside the title, `/mnt` is ENOENT, because the app sandbox hides -the console's mounts. Reading USB and extended storage needs the title -jailbroken, for example through etaHEN's jailbreak call (127.0.0.1:9028, with -etaHEN's Legacy CMD server enabled), which I have not used. - -The installed title is this build (`4be9e814`). It launched and passed every -run above. - -## 2026-09-28 — Executable memory with no limit: Rogue Leader's crash leaving a mission - -A tester's Rogue Leader crashed going from a mission back to the main menu -(v0.5.0-alpha.5). Their trace ends with `dolphin ps5: no executable memory for -4096 bytes`, and the kernel's record with a write fault at address 0. Dolphin -takes 4 KiB of code for each vertex format a game draws with. The platform -layer's `ps5_exec_allocate` kept its regions in a table of 128, and the trace's -last count was 127. The next allocation returned NULL, and Dolphin filled the -null block with breakpoints. The same table served every JIT on that call: -Dolphin, PPSSPP, Mupen64Plus (new_dynarec and ParaLLEl-RSP), DeSmuME and -Azahar's dynarmic. LRPS2 and the core loader use the region form, which had no -table. - -The SDK fork (d3e8625, probe 588998e) has no limit now, as mmap has none. A -request of 64 KiB or more is a region of its own, in a list. A smaller one is a -block of whole 16 KiB pages in a shared 4 MiB arena within reach of its anchor, -zeroed when handed out. Whole pages mean a core's protection change touches -only its own block (PPSSPP takes execute away before it frees one), and a freed -block is made read, write and execute again. Near regions are asked for just -past the last one, so they pack instead of taking a 64 MiB step each. - -On the console, the platform probe inside the title (build ac6e64c2): - -| | Result | -| --- | --- | -| 2,000 blocks of 4 KiB near the code | all handed out, each runs its own code, in 8 arenas, 1.3 µs each | -| 300 regions of 64 KiB | all handed out, each runs, 31 µs each | -| release | 25 µs each (a freed block's protection is restored) | -| a block freed read-write, handed out again | the same page, and its code runs | -| direct memory and the heap around the step | unchanged | - -An allocation costs microseconds, where a frame has 16.7 ms, so it was never -behind the audio stutter the tester also reported. That comes from Dolphin's -speed at transitions, the next piece of work. The battery (a game, the menu -twice, Close Content, the same game again) passes for Dolphin, PPSSPP, -Mupen64Plus, DeSmuME, Azahar and LRPS2 on this build, with full speed after -the reload. - -The probe's own last check had failed on the console by 6 MiB. Its four -threads leave their stacks in the platform's stack cache, which the check -predates. It now allows for the stacks left cached. The title's build identity -now includes the SDK's revision: a change to the platform layer alone had -kept the old identity. - -## 2026-09-28 — Rogue Leader's slow windows on RADV: where the time goes (paused) - -A tester found Rogue Leader smoother on v0.5.0-alpha.5, but still stuttering -at transitions. I profiled its attract sequence from boot with the sampler -on the main thread (Dolphin's GPU loop and RetroArch), Dolphin's emulation -thread, its four shader-compiler threads, its DVD thread and its work queue -(stall threshold 20 ms). The 10 s windows ran (%): 83 78 96 100 95 100 100 96 -90 100 100 94 76 98 100 95 100 100 96, and the next two runs gave the same -within a point or two. - -- **Not shader compiles.** In the mid-game dips (windows 9 and 13) all four - compiler threads were idle, waiting for work. -- **The main thread waits for frames.** 85-90% of its stalled samples are a - timed wait inside Dolphin: the GPU loop waiting for the emulation thread's - commands. -- **The emulation thread spins in `sched_yield`** for about half its stalled - samples. I found that from the addresses of the exported waiting functions, - which the sampler now logs at start: `sched_yield` is where those samples - sit. It is called from libkernel code just past `pthread_rwlock_wrlock` and - before `nanosleep`, by a caller that keeps no frame chain. libc++'s - `shared_mutex` does not use rwlocks, and Dolphin's throttle is off under - libretro, so this is neither Dolphin's config lock nor its pacing. The - sampler now scans the stack for callers when a sample is inside a system - library, to name the Dolphin code that ends up there; I stopped before that - scan ran on the console. -- **The other half** is the MMU's slow path (`ReadFromHardware`, - `TranslatePageAddr`, `Memcheck`), as on 2026-09-26. -- **At boot**, 69% of the main thread's stalled samples in the first window - are Dolphin saving an INI file (`IniFile::Save`, `fflush`) on the thread - that runs the GPU loop. - -Frame pointers are already this target's default: Dolphin built with -`-fno-omit-frame-pointer` came out byte-identical, so chains stop only at JIT -code and the system libraries. The sampler also logs every core thread's -start address when a thread is created, so a run can name the ones to sample. - -The thread probe (SDK 9a57ba7) measured how the console schedules the title's -threads, for the priority work that comes next. Threads run at policy 1, -priority 700, in a range of 256 (highest) to 767. A priority set on a running -thread reads back. Sixteen spinning threads ran on CPUs 0-12, thirteen in all. -`pthread_getaffinity_np` with FreeBSD's `cpuset_t` returns ERANGE. - -Next for Dolphin, when I come back to it: run the stack scan and name what -the emulation thread spins on, then the compile-thread priority, the boot INI -save, the GPU-thread cost and the MMU fast path. The installed title is -build 6abfd89d, and the Dolphin battery passes on it. - -## 2026-09-28 — RPCS3: the core builds, loads, installs the firmware and runs a PS3 program (ladder steps 1, 3, 4 and 5) - -**Build (steps 1 and 3).** `tools/build-rpcs3.sh` builds `rpcs3_libretro.so` -from my fork PS5_RPCS3 (github.com/mihawk-99/PS5_RPCS3, `main`, pinned in the -script) with the pinned dependencies: - -| Dependency | Script | Result | -| --- | --- | --- | -| LLVM 22.1 (my fork PS5_LLVM, X86 only, libraries only) | `tools/build-llvm.sh` | 105 archives, 223 MB | -| FFmpeg 8.1.1 (signed tarball, RPCS3's component list) | `tools/build-ffmpeg.sh` | 12 MB of archives | -| GNU libiconv 1.18 (signed tarball) | `tools/build-libiconv.sh` | 1.1 MB | - -The core is about 105 MB. `tools/check-core.py` passes, and it now also fails -any core that makes a system call itself: the first RPCS3 link carried 233 -system-call stubs from the SDK's static payload libc, which asmjit's CMake had -linked by name (`-lc`). The console killed the title the first time one ran -(`fstatat` in `Emu.Init`). asmjit links no C library by name in the fork now, -and no other core had any. - -**Load (step 4).** On the console the loader maps the core (1,344 static -initialisers; RPCS3's 56 GiB guest layout reserved from 64 GiB up), and -`retro_init` runs `Emu.Init`. What it took, in the order the console showed it: - -- *Imports the console resolves to nothing* at run time although the SDK's - stubs link them: `gai_strerror`, then `statfs`, `fstatfs`, `umask`, `fork`, - `setsid` and `wait4` (only libkernel_sys has them). With the static libc - gone: `syscall` (Abseil's raw logging), `getpwnam_r`, `posix_madvise` and - `strsignal`. All are in the platform layer now (SDK a9fbb6a), aliased in - `tools/core-imports.py`. -- *Weak references* (thread-local initialisation routines, gcov's hooks) are - bound weakly in the title's import table, so they stay null. -- *RetroArch loads the core once to read its information* and unloads it - without `retro_init`: RPCS3's log listeners now live from `retro_init` to - `retro_deinit`, since one with static storage outlived RPCS3's logger. -- *Thread exit.* libkernel runs a thread's key destructors in one pass and in - no set order, and emulated TLS frees a thread's storage from its own key, so - a C++ `thread_local` destructor could run on freed storage (the first thread - to exit crashed in the heap). The platform's `pthread_create` wrapper runs a - thread's `thread_local` destructors when its start routine returns, as glibc - does; the probe's check reads the thread's value in its destructor. -- *Direct memory is handed out again as it was*: 64 KiB released and - allocated again read back its old bytes. The platform zeroes what it - promises fresh (shared objects, committed units, executable regions), and - the title heap's large `calloc` clears again (dlmalloc's `MMAP_CLEARS` had - been on, a latent bug for every core). -- *CPUs:* `sysconf` says 16, a title runs on 13; `ps5_sysconf` answers 13, - and `pthread_getaffinity_np` accepts sets of 8 and 16 bytes only (FreeBSD's - 32-byte `cpuset_t` gets ERANGE), so the affinity calls go through the - exported 64-bit mask. -- *Emulated TLS* costs 4.7 ns a read against 1.3 ns for a global. - -**Firmware and a program (step 5).** `PS3UPDAT.PUP` from `system/RPCS3/` -installs inside `retro_load_game` (the TAR loader writes `dev_flash`; about -9 s). RetroArch brings up Vulkan through the core's negotiation, and RPCS3's -own device code makes the device on the PS5's GPU (RADV). Then: - -- VMA needs volk's entry points handed to it (Vulkan is loaded at run time). -- RSX starts the PPU only if it finds the emulator starting when its thread - begins; the renderer set up faster than the boot, so the program stayed in - `starting`. The core's frame now waits for the boot, which is the order the - Qt application gets by timing. -- RPCS3's shader interpreter compiles 6,650 base pipelines before the program - starts (about 100 s the first time, RADV's cache after that), without a - loading screen yet. -- LLVM's JIT asks for `dlopen(NULL)`; the console refuses it and `dlerror()` - is then NULL. Cores' `dl*` imports go through the title's loader now, which - answers the process handle from the core import table and is otherwise the - console's. -- Every LLVM compile reserved 768 MiB and kept it; the firmware's modules ran - out the area reservations were placed in. The platform places them on up to - 1 TiB, and on the PS5 the fork releases the range when the compiler goes. -- The console's kqueue does not wake a waiter for a user event re-added with - `NOTE_TRIGGER` (measured): RSX audio's thread never stopped, so every - shutdown timed out. The fork triggers it the canonical way. -- The image handed to RetroArch was a stack copy, which RetroArch reads again - to repeat a frame or take a screenshot (a crash in `radv_UpdateDescriptorSets`). - -With all of that, RPCS3's own test program `gs_gcm_hello_world.elf` boots -with the LLVM recompilers: 71 firmware modules compile (about 50 s the first -time), the program runs, and its "Hello World!" is on RetroArch's screenshot -(3840×2160, of RPCS3's 720p output). cellAudio's output reaches RetroArch at -full speed (10 s windows at 100%), the program exits on RetroArch's quit, and -the core unloads cleanly. The CTS run was paused for these runs and resumed -afterwards. - -Next: God of War HD from its package and licence, first at 100%. - -## 2026-09-28 — RPCS3: God of War HD installs behind a loading screen and boots to its SPU compiles (ladder step 6, begun) - -**The package installs.** RPCS3's installer normalises its path with -`std::filesystem::weakly_canonical`, which libc++ builds on `realpath`, and the -console refuses a title's `realpath` with EPERM, even for `/app0` (the files -probe, measured). The platform answers it now (`ps5_realpath`, SDK 8160289): -the path made absolute from `ps5_getcwd`, `.` and `..` resolved by name, each -component checked with `stat`. The title links it too (`--wrap=realpath`), and -the cores' imports are aliased to it. - -God of War HD's 6.3 GB package then installed in 31.5 minutes: 13 MB/s for the -first 2.7 GB, 2.2 MB/s after that. The sampler put the writing thread in -`write()` for almost every sample, not in AES (AES-NI) or the reads. The SDK's -new sustained write probe (`writes`, SDK c769422) measured why: a title writes -242 MiB/s for about 1.3 GiB, even with `O_DIRECT`, then 2.0-2.1 MiB/s however -it writes, while the FTP server wrote the same folder at 22 MiB/s at the same -moment. The storage is not the limit; a title's writes are (the SDK's -docs/PROBE.md, "Sustained writes"). Installing a package's large files without -copying them, read and decrypted from the package the way RPCS3 reads an ISO, -would remove both the wait and the second copy; that is the next step for -installs. - -**A loading screen.** The install ran inside `retro_load_game`, so RetroArch -showed a black screen for as long as it took. The fork (78dfa8f) installs the -firmware and packages on a worker thread and shows a loading screen until -RPCS3's first frame (docs/RPCS3_PORT.md, "Content loading and the loading -screen"): the game's own art tinted violet, its icon, the title in Inter, and a -violet-to-orchid bar with the sizes, speed and time left, then "Starting" with -RPCS3's compile progress, or what failed. RetroArch's screenshot on the console, -one minute into the install, showed it at 36% (2.32 GB of 6.32 GB). Along the -way: - -- the screen called `vkCreateImage` before RPCS3 had loaded any device - function through volk (a jump to 0); it loads them for RetroArch's device; -- a package header's `title_id` is the content ID (`UP9000-NPUA80490_00-...`), - so the check for an installed game never matched and each load installed the - package again, overwriting the installed game (which then had to be - installed again); the title ID now comes from its PARAM.SFO, and a finished - install is recorded in `system/RPCS3/libretro/installed/`; -- closing content during the install waited for the whole 6 GB file: RPCS3's - installer saw an abort only between files. It checks after every block now, - and a file left part way counts the package as aborted. - -The resolution scale became a core option (100% to 300%, 300% by default). - -**The boot.** With the game installed, the boot got through the PPU executable -and then stopped at RPCS3's boot music: the overlay plays SND0.AT3 through the -frontend's video source, and the core made none. It makes a silent one now. -Next, the shader interpreter's 6,650 pipelines (115 s the first time, a few -seconds from RADV's cache after that), then SPU compiles, where every run -crashed within seconds in LLVM's `MachineInstr::hasOrderedMemoryRef`, reading -`0x2_xxxx_xxxx`. The crash report now prints every register and the heap words -around them, which showed the instruction's two memory operands with the -second one's upper 32 bits replaced by 2: the next allocation's first field. -LLVM's `TrailingObjects` takes its alignment from an empty `alignas` base, -which the PS4/PS5 ABI ignores, so on the console such classes were allocated -4 bytes short (docs/RPCS3_PORT.md, "LLVM"). The LLVM fork (98b45cc) fixes it; -the PS5 target now lays such a class out as the host does. - -**To gameplay.** Four more stops, each found from the console's own report: - -- *The game's folder lost its first letter.* Emulator::Load cuts the folder - name after the resolved `<hdd0>/game` plus a separator of its own, and the - core had left the path callbacks at their default, which returns a path as - given, trailing separator and all: the game booted as - `/dev_hdd0/game/PUA80490/` and found none of its files. The core resolves - paths as the Qt application does (`QFileInfo::canonicalFilePath`), through - `std::filesystem` on the platform's `realpath`. -- *Three frontend callbacks were unset* (`get_scaled_image`, `get_image_info`, - `get_photo_path`). The first overlay with an icon called an empty - `std::function`, and a core built without exceptions terminates on the - `std::bad_function_call`. They are stb_image's now. RPCS3's terminate - handler also names an uncaught exception and prints its message, and the - title's crash report lists the code addresses on the crashing stack, up to - its top: the throw site survives, since nothing unwinds before terminate. -- *"mutex lock failed: Resource deadlock avoided".* On FreeBSD and the PS5 - the atomic wait engine waits on a `std::condition_variable`, and the waiting - thread held its own mutex while running its wait callback; an SPU thread's - callback notifies lv2 objects, the notification locks every waiter's mutex, - its own among them, and the console's default mutex checks for that - (EDEADLK). The fork releases the mutex around the callback; a notification - meanwhile sets `sync` first, which is checked again under the mutex. -- *No button reached the game.* The core named the RetroPads in the input - configuration but left RPCS3's default bindings, which are empty. Each pad - gets the RetroPad handler's own now (`pad_thread::InitPadConfig`). - -With those, God of War HD boots through its logo video to its menu, and a -scripted Cross on "New Game" and the difficulty reaches the first level, the -ship in the Aegean, with RPCS3's Vulkan renderer on RADV: - -| Scale | Evidence | 10 s windows after the boot | -| --- | --- | --- | -| 100% (720p) | gameplay screenshot, 3840×2160 | 17 of 17 at 100% | -| 300% (2160p) | gameplay screenshot, 3840×2160 | 17 of 17 at 100% | - -The two windows after the 300% run's last (7%, 0%) are RetroArch encoding its -5.8 MB screenshot and quitting after the frame limit, when the core no longer -runs. - -**Ten minutes at 300%.** A twelve-minute run with a scripted Square and Cross -every 5 s after the menus (124 presses) ran to its end without a crash: 71 of -71 windows after the boot at 100%. The script is no fighter: Kratos died and -restarted from checkpoints throughout, so the run covered combat, deaths and -checkpoint loads, and ended on the game's "You Are Dead" screen. - -**Close Content and reload.** The battery (the game to its first level, the -menu opened and closed twice, Close Content through the Quick Menu, the same -core and content again, New Game once more) passes: no crash, every window at -100% but the two boots', and the second boot reaches the game's "Three Weeks -Earlier" card. Two things were needed: - -- *The core never unloaded.* The loader counts a core thread as running until - its start routine returns, and RPCS3's threads end in `pthread_exit`: 122 - were "still running" at every close, so the core stayed mapped and the - reload mapped a second copy. A core thread now counts itself finished from - its first-registered `thread_local` destructor, which runs last of them, - whether it returns or calls `pthread_exit`; a start routine that returns - ends through `ps5_pthread_exit`, so its destructors run while its TLS is - whole rather than from libkernel's key destructors; and the loader allows - 20 ms after the count reaches zero for the key destructors that follow. RPCS3 - now unloads at Close Content and reloads at the same base. PPSSPP's battery - (Ghost of Sparta) still passes, the core unloaded at each close. -- *RetroArch forgets `--max-frames` when a core closes*, so a run that reloads - never took its screenshot or ended. The pad script has a `STOP` action, - which ends the run as the frame limit does, screenshot included. - -A 4K screenshot costs a run its last windows: RetroArch encodes and writes -it after the last frame (5.5-5.8 MB of PNG, held to the title's write budget), -and the core does not run meanwhile, so the windows then read 0-70%. - -## 2026-09-29 — Licences, notices and source for every build (licence audit) - -A licence audit of the published releases found that none of the five ZIPs -carries the licence texts its parts require (GPLv3, GPLv2, MPL-2.0, the Snes9x, -FBNeo and Genesis Plus GX licences, MIT notices, OFL for PPSSPP's fonts), and that -the tags of v0.1.0-alpha.1, v0.3.0-alpha.3 and v0.5.0-alpha.5 name commits older -than the ones built (docs/releases/SOURCE_CORRESPONDENCE.md). This step changes -what every build produces; the published ZIPs are unchanged. - -- `tooling/notices/components.json`: every part of the title, its licence, the - texts that travel with it and where its source is. -- `tools/stage-notices.py`, run by `tools/build-title.sh` before the manifest: - writes `licenses/` into the title folder (texts, `components.json` tying every - executable to its source revision, `README.txt`). A core whose digest is not the - one its `build/cores/<core>/build.json` records, or a missing licence text, - fails the build. -- `tools/check-notices.py`, in the integration gate: every executable and `.info` - belongs to a listed part with its digest; verbatim texts unchanged; `--release` - also requires committed, published source. -- `tools/source-bundle.py`: one source archive per part at the recorded revision, - submodules included, with `SHA256SUMS` and `SOURCES.txt`. -- `LICENSE` (GPLv3), `docs/RELEASING.md`, README licence and trademark sections, - `docs/RPCS3_PORT.md` licence section (RPCS3 stays out of releases), and - `runtime/README.md` (what libc.prx contains, as verified). - -Evidence: `python3 tools/stage-notices.py` on a copy of the 2026-09-29 16-core -`dist/PPSA99169`: 28 parts, every core's digest equal to its build report, the -RADV archive equal to its PROVENANCE; `tools/check-notices.py` passes on it and -`--release` fails on the uncommitted tree, as it should. `tools/source-bundle.py` -built 10 archives (PPSSPP with 29 submodules, Azahar with 52) whose `SHA256SUMS` -verify. `make test-unit`: 83 tests OK (7 new in tests/test_notices.py). -`tools/lint-shell.sh`: PASS. libc.prx rebuilt from the committed files alone is -byte-identical to the one in every release (`e6ff45d1…`). The full build was not -rerun (the build tree was in use); a `dist/` built before this change fails the -integration gate until it is rebuilt. - -## 2026-09-29 — RPCS3: GTA IV 1.00 boots, saves and loads states, runs at 50 fps at 4K; fast installs; frame-rate patches - -GTA IV (BLES00229 1.00) replaced God of War HD as the RPCS3 core's test game, -with 4K at a stable 60 fps as the goal (docs/RPCS3_PORT.md, "GTA IV"). - -- It stopped at its first RSX commands ("Dead FIFO commands queue state", - RPCS3 issue #14194): its libgcm puts the FIFO in local memory (system mode - 0x210). The fork now reads GET, PUT and jump targets as local offsets in that - mode, and takes the ZCULL late-fault fix of issue #19610 (16K pages). -- Its first start installs 3.3 GB to `dev_hdd0/game/BLES00229`; held to the - title's write budget, that took two runs (25 minutes at 0.7 MiB/s, then the - rest). The budget is per process and on what reaches the storage (`write()`, - `O_DIRECT`, a shared mapping and 1-8 threads alike); the console's FTP - server, another process, writes about 7 MiB/s a connection and 22 MiB/s in - all (the SDK fork's docs/PROBE.md). RPCS3's files in `/app0` now go through - it once the budget is spent: deleting 1414 MiB of the install with the budget - spent, GTA IV put it back in 4 minutes (about 6 MiB/s), byte for byte. -- Save states: made (134 MiB, 1.1 s) and resumed; resuming mounted the wrong - folder (a savestate's boot path is `dev_hdd0//game/...`), and loading over a - resumed game stopped on the stack block's page flags. Both fixed in the fork; - the baseline state is saved at the mission's start. -- Frame rate: RPCS3's patch database (815 executables, 502 of them with a - frame-rate patch) is in the fork and the title, and every game starts with - its frame-rate patch (a core option, on by default). It has none for GTA IV - 1.00; the render thread's limiter, found in a dump of the running game, reads - a frame lock (0x0113b9f4, 2) and skips itself at 0, which the fork's - `BLES00229_patch.yml` sets. From the baseline state at 300%: 50.4-51.5 fps - (30 before), 53.4-54.8 at 100%, loading screens 59.8, audio at full speed. -- Where the rest goes: the guest RSX 98% busy (RPCS3's overlay); the title's - sampler with every thread (its frame walk now bounded by each thread's stack, - which had faulted) shows one SPU thread 85-89% busy in recompiled code and - five at 30-55% on `vm::writer_lock`, which RPCS3's accurate SPU reservations - take for every atomic write. - -Evidence: runs gta3-gta22 (klog, traces, RPCS3 logs and screenshots in my -scratchpad; game data and the executable dump stay there and on the console). -Probes: `routes` and `offload` in the platform probe (docs/PROBE.md tables). diff --git a/docs/PLAN.md b/docs/PLAN.md deleted file mode 100644 index b1b0e3c..0000000 --- a/docs/PLAN.md +++ /dev/null @@ -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. diff --git a/docs/PLATFORM_FIRMWARE_ANALYSIS.md b/docs/PLATFORM_FIRMWARE_ANALYSIS.md deleted file mode 100644 index cc69482..0000000 --- a/docs/PLATFORM_FIRMWARE_ANALYSIS.md +++ /dev/null @@ -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. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md deleted file mode 100644 index 8f4ca11..0000000 --- a/docs/REFERENCE.md +++ /dev/null @@ -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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md deleted file mode 100644 index e69ee0a..0000000 --- a/docs/RELEASING.md +++ /dev/null @@ -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. diff --git a/docs/RPCS3_PORT.md b/docs/RPCS3_PORT.md deleted file mode 100644 index 1188718..0000000 --- a/docs/RPCS3_PORT.md +++ /dev/null @@ -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. diff --git a/docs/TESTING.md b/docs/TESTING.md deleted file mode 100644 index 01c398e..0000000 --- a/docs/TESTING.md +++ /dev/null @@ -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. diff --git a/docs/THUMBNAIL_INPUT_DEFAULTS.md b/docs/THUMBNAIL_INPUT_DEFAULTS.md deleted file mode 100644 index 6bcae9f..0000000 --- a/docs/THUMBNAIL_INPUT_DEFAULTS.md +++ /dev/null @@ -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. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md deleted file mode 100644 index d8b33f1..0000000 --- a/docs/TROUBLESHOOTING.md +++ /dev/null @@ -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 | diff --git a/docs/XMB_ALLOCATION_INVESTIGATION.md b/docs/XMB_ALLOCATION_INVESTIGATION.md deleted file mode 100644 index f560421..0000000 --- a/docs/XMB_ALLOCATION_INVESTIGATION.md +++ /dev/null @@ -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`. diff --git a/docs/XMB_LIST_SAFETY.md b/docs/XMB_LIST_SAFETY.md deleted file mode 100644 index bea4810..0000000 --- a/docs/XMB_LIST_SAFETY.md +++ /dev/null @@ -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/`. diff --git a/docs/releases/SOURCE_CORRESPONDENCE.md b/docs/releases/SOURCE_CORRESPONDENCE.md deleted file mode 100644 index 668d617..0000000 --- a/docs/releases/SOURCE_CORRESPONDENCE.md +++ /dev/null @@ -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>. -``` diff --git a/docs/releases/v0.4.0-alpha.4.md b/docs/releases/v0.4.0-alpha.4.md deleted file mode 100644 index 1f468b4..0000000 --- a/docs/releases/v0.4.0-alpha.4.md +++ /dev/null @@ -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. diff --git a/docs/releases/v0.5.0-alpha.5.md b/docs/releases/v0.5.0-alpha.5.md deleted file mode 100644 index 87d72cf..0000000 --- a/docs/releases/v0.5.0-alpha.5.md +++ /dev/null @@ -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. diff --git a/examples/ACTIVE.filled.md b/examples/ACTIVE.filled.md deleted file mode 100644 index 889937f..0000000 --- a/examples/ACTIVE.filled.md +++ /dev/null @@ -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. diff --git a/examples/PHASE_LOG.filled.md b/examples/PHASE_LOG.filled.md deleted file mode 100644 index 1405f71..0000000 --- a/examples/PHASE_LOG.filled.md +++ /dev/null @@ -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. diff --git a/handoff/PPSA99002/ui/README.md b/handoff/PPSA99002/ui/README.md deleted file mode 100644 index 6eb66da..0000000 --- a/handoff/PPSA99002/ui/README.md +++ /dev/null @@ -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. diff --git a/handoff/PPSA99002/ui/fonts/lvgl-bitmap/README.md b/handoff/PPSA99002/ui/fonts/lvgl-bitmap/README.md deleted file mode 100644 index f997176..0000000 --- a/handoff/PPSA99002/ui/fonts/lvgl-bitmap/README.md +++ /dev/null @@ -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. diff --git a/handoff/PPSA99002/ui/icons/README.md b/handoff/PPSA99002/ui/icons/README.md deleted file mode 100644 index 60238ee..0000000 --- a/handoff/PPSA99002/ui/icons/README.md +++ /dev/null @@ -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. diff --git a/parked/explicit-stock-blend/README.md b/parked/explicit-stock-blend/README.md deleted file mode 100644 index ec9c11b..0000000 --- a/parked/explicit-stock-blend/README.md +++ /dev/null @@ -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. diff --git a/parked/gpu-readback/README.md b/parked/gpu-readback/README.md deleted file mode 100644 index a906ffb..0000000 --- a/parked/gpu-readback/README.md +++ /dev/null @@ -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. diff --git a/parked/xmb-readback/README.md b/parked/xmb-readback/README.md deleted file mode 100644 index 6574b30..0000000 --- a/parked/xmb-readback/README.md +++ /dev/null @@ -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/`. diff --git a/runtime/README.md b/runtime/README.md deleted file mode 100644 index b599bae..0000000 --- a/runtime/README.md +++ /dev/null @@ -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`. diff --git a/tooling/README.md b/tooling/README.md deleted file mode 100644 index d1b2618..0000000 --- a/tooling/README.md +++ /dev/null @@ -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. diff --git a/tooling/dolphin-profiles/README.md b/tooling/dolphin-profiles/README.md deleted file mode 100644 index 9a183c9..0000000 --- a/tooling/dolphin-profiles/README.md +++ /dev/null @@ -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.