mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Build pinned mGBA with this PS5 SDK, official metadata and both GB/GBA engines.
Support validated C initializer arrays, native directory/time imports and an
explicit config path. Map large title/core allocations so GBA archives load.
Correct mGBA callback colours and cached frame ownership, crop padded core
textures during presentation, and use the native shell exit after cleanup.
Verified: bash tools/verify.sh (format, unit, build, integration, evidence;
58 tests), then bash tools/run-title.sh --no-build --core-test=mgba --watch 180.
Owner confirms GB/GBC/GBA loading, correct colours/native Quit, and clean menu
and next game. Final capture has zero Vulkan refusals, GPU failure records or
kernel fatal-signal reports; normal frontend exit occurred before window end.
Evidence: evidence/mgba-native/{capture,expectation,iterations,build,abi}.json,
loader/recovery reports and exact linked-driver hashes. Replay with
bash tools/verify.sh evidence. Final build identity:
82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337.
Patch count is deliberately 139: archive diagnostics, menu/cache ownership and
padded-source sampling edits. PS5_Vulkan was not modified. Saves/states,
long-run timing and a fresh FCEUmm/XMB transition matrix remain unverified.
2131 lines
130 KiB
Markdown
2131 lines
130 KiB
Markdown
# 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: "<error_string>"`,
|
||
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-<stamp>.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.
|