The scaffold title - the native pipeline's scaffolding, this repository's display layer and its own entry point - was deployed by FTP, launched, and stayed up. Evidence: the delimited capture klog/PPSA99169-135230.log holds zero fatal signals and zero exit_value lines, shows the process twice in the shell's accounting ([SceShellCore] 23% 700 PPSA99169 eboot.bin), and the control payload reports app=49176 title=PPSA99169 count=1 pids=228 while it runs. The first version of src/display.cpp guessed five constants the console does not forgive and cost a run: the title built, launched, and reported "eboot.bin calls exit() exit_value=1" because open had failed. Comparing with the sibling application that had proved them on hardware showed all five - the 64-bit pixel format, the direct-memory accessor's type, the mapping protection, the four- pointer VideoBuffer, and the tiled pixel addressing. All are now taken from that project with the reason recorded, since none is derivable by reasoning. Also in this commit: tools/scaffold-native.sh (builds the scaffold, merging the pipeline's tools into this repository's rather than replacing them - an earlier version deleted this project's scripts), tools/fetch-retroarch.sh (pinned clone plus recorded revision), and tools/console-run.sh corrections - the mount wait removed as it is a one-time step, and the verdict no longer reports "still running" from a condition that is always true.
27 KiB
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. {{SHA}} — Run this project's own title on the console.