Add reproducible PS5 FCEUmm build and record native loader blocker

Build pinned source and core-info with this project's SDK, validate the ELF,
and stage both metadata paths for existing saved configs. Use a deterministic
build ID and require exact core readback; refresh stale metadata caches.

Verified: tools/verify.sh (five host gates, 45 tests, 18 evidence records).
Artifact: evidence/fceumm-build/{abi,capture,expectation}.json.
Target: tools/run-title.sh --no-build --watch 120, manual core/game selection.
Metadata discovery and exact core bytes pass; dynamic core open fails, then
content startup crashes in frontend vulkan_alive(NULL). This commit verifies
build tooling only, not playable FCEUmm. Park native loader/recovery acceptance
in parked/native-core-loading/README.md. PS5_Vulkan is unchanged.
This commit is contained in:
Mihawk committed 2026-09-19 14:05:29 -04:00
1 parent dae9006c37
commit d337e2c11b
19 files changed
+718 -57

No files matched your search

+5
View File
@@ -45,6 +45,10 @@ HOST_UNIT_TEST := build/tests/demo_renderer_tests
all: app
build: app
.PHONY: fceumm
fceumm:
@bash tools/build-fceumm.sh
init:
@printf '%s\n' '==> [init] Configuring the application identity in sce_sys/param.json'
@bash tools/init-project.sh sce_sys/param.json
@@ -161,6 +165,7 @@ help:
'make test-unit Run host-native GoogleTest application tests' \
'make test-integration Run host tooling integration tests' \
'make deps Fetch native dependencies into .deps/' \
'make fceumm Build and ABI-check the pinned PS5 FCEUmm core' \
'make pacbrew Fetch the pinned PacBrew ports sysroot' \
'make pacbrew-list List PacBrew pkg-config module names' \
'make assets-check Validate the current presentation assets' \
+65 -56
View File
@@ -4,70 +4,79 @@ _Updated: 2026-09-19_
## Now
**Left-stick menu navigation and input binding capture work on the PS5.** The
owner tested both on the native joypad build and replied: "Everything works
flawlessly!" This follows the new input issue rather than continuing the previous
permission step. No driver work or core implementation was needed.
**FCEUmm build and metadata staging pass; native core loading is blocked.**
The user requested a core build script using this project's SDK, with manual
core/game selection for console testing. This follows that request rather than
continuing the earlier input step. No PS5_Vulkan changes were needed.
The old input-only backend bypassed RetroArch's joypad interface. Menu sticks and
the binding screen therefore had nothing to poll, although direct menu buttons
worked. `ps5_joypad` now provides raw buttons/axes and a built-in controller
profile; normal RetroArch binding/deadzone logic handles the resulting input.
`input_ps5` no longer ORs hardcoded controls back into remapped input.
`make fceumm` fetches SHA-256-verified source and metadata, builds with this
project's cross SDK, checks the shared ELF and stages the `.so` and `.info`.
`tools/build-title.sh` includes them in the title. Metadata is placed in both
`info/` and `cores/`: the owner's saved empty core-info path uses the latter.
Deployment refreshes the metadata cache and strictly checks core SHA-256.
## Verified build and evidence
- Identity: `e6211dea9fd0bb512e83a9979a6a44c0c8c5fdf175564c753e3fd2a25067e0a9`.
- Command: `tools/run-title.sh --no-build --watch 120`; the owner confirmed
left-stick navigation and button/stick binding capture on this build.
- Follow-up: `tools/run-title.sh --no-build --no-deploy --watch 30` stayed alive
for the full window and was script-closed. Both runs selected/configured the PS5
joypad; Vulkan presentation, XMB and audio startup passed with zero GPU errors.
The owner manually closed the interactive run, explaining its early-exit verdict.
- All five gates PASS; 37 unit tests; 17 evidence records.
- Host tests use actual upstream input mapping and analog helpers with mocked
native samples. They cover button/axis bindings, deadzones, trigger scaling,
extra binding-screen polls, interception/disconnect and teardown/reinit.
- Fresh patch replay: 107 applied + one upstream marker; second pass 108 present,
bytes unchanged. Three new edits register the joypad and built-in profile.
- Evidence: `evidence/native-joypad/`; raw console capture stays in ignored
`klog/input-joypad-run.log`. Verification: `tools/verify.sh`.
- PS5_Vulkan remains unchanged at **6f0ce0d**. No sibling project was modified.
- Frontend identity: `261d33099adec97c231205beb21d6011e1587436df099d4e6ac6f29949c50c8f`.
- FCEUmm upstream revision: `236ccdfc911e84c60fea6b9d0699c2d440a8de14`.
- Core SHA-256: `d1afcd2ea84de627f4a28d8ec4e81d63d23ca5b531811ba57357d98f8b31759f`.
- Core size 4,836,144 bytes; ELF64 x86-64 ET_DYN FreeBSD ABI, 25 callbacks,
16 KiB-compatible segments, native libc/Posix/kernel_web imports.
- Repeated fresh builds give the same hash after replacing Clang's random
PS5 build UUID with `--build-id=sha1`. FTP returns exactly those core bytes.
- Console command: `tools/run-title.sh --no-build --watch 120`; owner manually
selected core/content. The decoded console cache reports FCEUmm's full name,
`fds|nes|unif|unf` and `has_info: true`. Metadata discovery is verified.
- All five host gates pass; 45 tests. Evidence: `evidence/fceumm-build/` includes
ABI report and the failed runtime result. This is not playable-core acceptance.
- Raw logs: `klog/fceumm-metadata-run.log`, `klog/retroarch-135742.log`,
`klog/run-PPSA99169-135742.log`. User filenames remain in ignored captures.
- PS5_Vulkan remains unchanged at **6f0ce0d**.
The previous step verified actual FTP upload/readback/cleanup in all seven managed
folders at `0777`; `0775` was insufficient (`evidence/ftp-directory-permissions/`).
Startup still repairs those directory modes. Configuration saving and browsing
remain documented in `evidence/native-paths/`; deployments preserve live settings.
## Open target blockers
1. **Core open fails:** both manual tests log `Failed to open libretro core`
for `/app0/cores/fceumm_libretro.so`, followed by `Error(s): (null)`.
Correct metadata and a byte-identical upload do not resolve native loading.
2. **Frontend recovery crashes:** after the owner starts content, initialization
reports that the dynamic core path is unset. The main loop then calls
`vulkan_alive` with null context and faults reading address `0x80`.
RIP `0xab3066`, image base `0x400000`, ELF offset `0x6b3066`; symbolized with
`addr2line -Cfipe build/llvm-pie.elf 0x6b3066 0xae4774 0xad7db3 0x3df4 0x190`.
The owner explicitly confirmed this crash; it was not a manual close.
These failures are recorded rather than waived as core success. Build/staging is
verified, but M3 core execution remains incomplete. No game frames/audio/saves
were verified. Implementation and acceptance plan: `parked/native-core-loading/`.
Do not substitute a static core, donor SDK or CPU video route to hide the blocker.
## Preserved platform functionality
Native joypad/menu analog navigation and binding capture were confirmed by the
owner in `evidence/native-joypad/`. Native audio and buffering are verified in
`evidence/native-audio/`; real-core audio and long-run A/V sync remain unverified.
XMB stays default with statically linked Vulkan; `video_ps5` and RGUI remain
registered/selectable. Single-level menu textures remain the verified path.
Useful frontend/core/audio/error logs are retained without frame-success chatter.
Config saving and directory browsing are verified in `evidence/native-paths/`.
Managed directories are repaired to `0777`; FTP upload/readback/cleanup was
verified in `evidence/ftp-directory-permissions/`. Deployments preserve live config
and user content. Input remains limited to the initial user's single controller;
rumble, multiple controllers and persistent remapping need separate acceptance.
## User file locations
FTP base is `/data/homebrew/PPSA99169/`; `/app0` is its in-title mount name.
Use `cores/` for PS5-compatible cores, `content/` for ROMs/content, `system/` for
BIOS files, and `config/retroarch.cfg` for live settings. Save directories are
`savefiles/` and `savestates/`. Ordinary updates preserve these user files.
See `docs/DEPLOYMENT.md`; no cores or ROMs were installed by this step.
## Preserved functionality and limits
Native `audio_ps5` is default; audible left/right tones, bounded buffering and
port lifecycle were verified in `evidence/native-audio/`. Real-core audio,
long-run A/V sync and content-based underruns remain unverified.
XMB stays default on the statically linked Vulkan path. Single-level menu textures
remain the tested compatibility path; general mip chains are unverified.
`video_ps5` and RGUI stay registered/selectable. Screenshots remain opt-in via the
parked recipe; GPU profiling stays opt-in. Useful startup/frontend/core/audio/error
logs remain enabled without routine frame-success chatter.
Input currently supports the initial user’s single controller. Rumble, multiple
controllers and saved remapping across a console restart remain unverified. USB
access and real core loading require separate verification on available mounts
and compatible core artifacts.
FTP base: `/data/homebrew/PPSA99169/`; in-title mount: `/app0`.
Use `cores/` for compatible cores, `content/` for games, `system/` for BIOS,
`config/retroarch.cfg` for settings, and `savefiles/` / `savestates/` for saves.
The build now ships experimental FCEUmm and metadata, but no game or BIOS.
See `docs/DEPLOYMENT.md` for build/deploy procedures and file locations.
## Operating notes
The user authorized console uploads/runs without further permission and necessary
driver fixes with documentation plus a PS5_Vulkan commit. Never interrupt an
existing title. The runner checks idle state before upload and launch. For appended
trace files, select the last `bss check=` substring and verify its build identity.
Never substitute the CPU route to conceal a GPU refusal.
The user authorized uploads/runs without further permission and necessary driver
fixes with documentation plus a PS5_Vulkan commit. Never interrupt an existing
title. The runner checks idle before upload and launch. Core/game selection is
manual at the user's request; collect retroarch.log afterward. For appended trace
files, select the last `bss check=` substring and verify the build identity.
+16
View File
@@ -178,3 +178,19 @@ older `0755`/`0775` directories.
`python3 tools/check-ftp-write.py` verifies upload/readback/cleanup in every managed
folder with uniquely named disposable files and records the result under `klog/`.
It does not overwrite cores, ROMs, saved settings or other existing files.
## Built-in core artifacts
The title build stages FCEUmm under `cores/fceumm_libretro.so` and its metadata
under `info/fceumm_libretro.info`, with an identical copy beside the core for
saved configurations whose core-info path is empty. The manifest and FTP
readback cover all three files.
`make fceumm` builds the core alone without uploading or running anything.
Runtime acceptance remains separate from the artifact checks; see
`docs/ACTIVE.md` for the current console result. No game or BIOS is bundled.
After uploading each `.info`, deployment writes and reads back RetroArch's
`core_info.refresh` marker in the same directory. RetroArch consumes it when
rebuilding its metadata cache; it is not a shipped manifest file. This also
refreshes entries previously cached as having no metadata.
+20
View File
@@ -2653,3 +2653,23 @@ within a frame. Errors, disconnect samples and interception suppress input.
The owner tested left-stick navigation and button/stick binding capture on build
`e6211dea9fd0bb512e83a9979a6a44c0c8c5fdf175564c753e3fd2a25067e0a9`
and replied "Everything works flawlessly!" Evidence: `evidence/native-joypad/`.
## 2026-09-19 — FCEUmm metadata and executable loading are separate gates
An empty saved `libretro_info_path` overrides the new platform default.
`core_info_init_list()` then searches the core directory, so staging only
`info/fceumm_libretro.info` is insufficient for existing configurations. Staging
an identical file beside the `.so` and forcing a metadata cache refresh produced
a console cache entry with `has_info: true`, full FCEUmm name and NES extensions.
The user's provided Gambatte `.info` uses the same ordinary libretro format;
metadata does not select a different executable ABI or install a loader.
The pinned native-SDK FCEUmm ELF exports all 25 callbacks and imports kernel_web,
libc and Posix stubs. Its final bytes survive FTP unchanged. Nonetheless native
core open returns failure and a null error string. A subsequent manual content
launch reinitializes without a core path, then dereferences a null Vulkan video
context at `vulkan_alive+0x26` (ELF 0x6b3066) reading address 0x80. The owner
confirmed the crash. This is not evidence that FCEUmm reached emulation or that
libps5vk refused GPU work. Keep loader and error-recovery acceptance open.
Evidence: `evidence/fceumm-build/`; next work: `parked/native-core-loading/`.
+50
View File
@@ -1967,3 +1967,53 @@ full window alive and script-closed (`klog/input-joypad-followup.log`, frontend
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.
+39
View File
@@ -360,3 +360,42 @@ axis-threshold handling. A zero-sample read preserves the previous sample (the
binding screen may poll twice); errors/disconnects and shell interception suppress
input. No per-frame or per-button logging is added. Rumble and multiple controllers
remain unsupported in this backend.
## FCEUmm core build
`make fceumm` (or `tools/build-fceumm.sh`) cross-builds the pinned upstream
FCEUmm revision and its pinned core-info metadata. The script verifies both
input SHA-256 digests, extracts fresh sources into `build/cores/fceumm`, and
uses this project's `.deps/native/ps5-payload-sdk` wrappers for CC/CXX/AR/LD.
It does not modify the fetched upstream source. `platform=unix` selects upstream
source/features, while explicit compiler and linker make variables prevent a
host build. The linker is the compiler driver because upstream passes `-Wl`
options; `-nostdlib -nodefaultlibs` excludes payload CRT/static libc defaults.
The `--build-id=sha1` override replaces Clang's random PS5 UUID with a
content-derived ELF ID so repeated builds have stable hashes.
Math and libc imports come from the native runtime stubs, with `libkernel_web`
rather than the payload `libkernel_sys`. No SDL or decoder is added.
Outputs are `build/cores/stage/cores/fceumm_libretro.so` and
`build/cores/stage/info/fceumm_libretro.info`. `tools/build-title.sh` builds and
copies both after assembling the title, before recording its manifest. Native
core metadata defaults to `/app0/info`. The same `.info` file is also staged
beside the core because older saved configs set `libretro_info_path = ""`;
upstream then searches the core directory. Existing settings are preserved.
`build/cores/fceumm/build.json` records
source/metadata pins, hashes, ELF imports, exported callbacks and compiler-wrapper
identity. First use requires network access; verified cached inputs allow offline
rebuilds. `JOBS` controls build parallelism.
The reference was john-tornblom's websrv
[core build script](https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/RetroArch/build-snes9x2010.sh).
Only its fetch/build/stage pattern applies here: this application uses the native
title pipeline and its own SDK. A shared ELF passing the ABI gate is not proof
that the native title's dynamic loader can load it. FCEUmm renders NES frames in
software; RetroArch presents those frames through the existing Vulkan driver.
After uploading each `.info`, deployment writes and reads back RetroArch's
`core_info.refresh` marker in the same directory. RetroArch consumes it when
rebuilding its metadata cache; it is not a shipped manifest file. This also
refreshes entries previously cached as having no metadata.
+26 -1
View File
@@ -23,7 +23,7 @@ step. A step is never closed by a mock of the console.
| --- | --- | --- |
| format | `tools/verify.sh format` | `tools/lint-shell.sh` parses every script in `tools/` (`bash -n`) and every python tool (`ast.parse`), rejects CRLF and byte-order marks, and refuses a committed console address or credential; `tools/lint-format.sh` then runs `clang-format --dry-run --Werror` over `src/`, `tests/` and `tooling/native/` against the repository's `.clang-format`. Formatters are not installed for shell or python on this machine, so this gate is syntax and policy rather than style. |
| unit | `tools/verify.sh unit` | `make test-unit` runs `tests/test_frontend.py`. Four kinds of check, none of which needs a console: the built `gfx_video_driver.c.o` really lists `video_ps5` before `video_null` in `video_drivers[]` (read from its relocations); the title and the frontend agree on `video_driver_t`, by compiling RetroArch's header with the frontend's own defines from `tools/retroarch-flags.sh` and comparing `sizeof` and the `poke_interface` offset against the object the title links - the fault that kept the menu off the screen, see `docs/FINDINGS.md`; the frame-layout function from `src/display.cpp` is compiled from the source by the host compiler, compared against `../PS5_Vulkan`'s own addressing over all 2,073,600 pixels, and pinned against a table of values so a change to it cannot be silent; and the built `dist/<TITLE_ID>/` is a fake self wrapping a 64-bit x86-64 ELF whose `param.json` names its own folder. |
| build | `tools/verify.sh build` | `bash tools/build-title.sh` fetches nothing, compiles RetroArch's sources with the cross toolchain, archives them, compiles `src/` with the same `-D` flags the archive was compiled with, links it with the pipeline's CRT, signs the result and records the folder's manifest. It is not bare `make app`: the things the link needs, and the defines the two sides must share, are set by that script and by nothing else. |
| build | `tools/verify.sh build` | `bash tools/build-title.sh` fetches verified core inputs on first use, compiles RetroArch's sources with the cross toolchain, archives them, compiles `src/` with the same `-D` flags the archive was compiled with, links it with the pipeline's CRT, signs the result and records the folder's manifest. It is not bare `make app`: the things the link needs, and the defines the two sides must share, are set by that script and by nothing else. |
| integration | `tools/verify.sh integration` | `make test-integration` (the same tests as `unit`, from the other side of the build) followed by `tools/check-manifest.sh` on the built tree: every recorded file present and unchanged, nothing extra, `eboot.bin` a fake self, and `param.json` naming the folder it sits in. |
| evidence | `tools/verify.sh evidence` | `tools/evidence.py compare evidence/` replays every committed capture against its expectation and exits non-zero on a difference. It never contacts the console. |
@@ -156,3 +156,28 @@ ELF relocations verify joypad registration and the built-in profile in the shipp
frontend. On console, confirm left-stick menu navigation, button/axis capture in
Settings > Input > RetroPad Binds > Port 1 Controls, and saved bindings after a
restart. Preserve the owner's live configuration when deploying.
## Libretro core artifacts
`tools/check-core.py <core.so> --report <report.json>` requires an ELF64 x86-64
FreeBSD/PS5 shared object, 16 KiB-compatible load segments, native runtime imports
including `libkernel_web.sprx`, and all 25 libretro callbacks in the dynamic symbol
table. It rejects Linux ELF ABI, payload kernel imports and absent callbacks.
The report deliberately keeps `console_loading_verified` false: ELF structure
cannot establish runtime loading. The title manifest check also checks the
staged FCEUmm artifact and requires identical `.info` metadata in `info/` and
`cores/`, covering both explicit and legacy empty core-info paths.
`tests/test_core_abi.py` builds real host fixture ELFs, marking only test fixtures
with the PS5 OSABI. Positive and negative cases exercise the parser through
readelf: missing callback, wrong runtime library, wrong ABI, 4 KiB segments and
truncated header. These fixtures are never staged or run on the console.
For the target test, upload a verified build, open RetroArch normally, and let
the owner select FCEUmm under Load Core and then a game under Load Content.
Collect `retroarch.log`, trace and kernel log with the build identity. Record
core load, content initialization and presented frames separately; a visible
core entry or `.info` name is not proof of loaded executable code. Owner game
paths remain in ignored logs. Keep normal XMB, Vulkan and audio startup working
when no core is selected.
+131
View File
@@ -0,0 +1,131 @@
{
"sha256": "d1afcd2ea84de627f4a28d8ec4e81d63d23ca5b531811ba57357d98f8b31759f",
"bytes": 4836144,
"elf": "ELF64 x86-64 ET_DYN FreeBSD/PS5",
"needed": [
"libkernel_web.sprx",
"libSceLibcInternal.sprx",
"libScePosixForWebKit.sprx"
],
"libretro_exports": [
"retro_api_version",
"retro_cheat_reset",
"retro_cheat_set",
"retro_deinit",
"retro_get_memory_data",
"retro_get_memory_size",
"retro_get_region",
"retro_get_system_av_info",
"retro_get_system_info",
"retro_init",
"retro_load_game",
"retro_load_game_special",
"retro_reset",
"retro_run",
"retro_serialize",
"retro_serialize_size",
"retro_set_audio_sample",
"retro_set_audio_sample_batch",
"retro_set_controller_port_device",
"retro_set_environment",
"retro_set_input_poll",
"retro_set_input_state",
"retro_set_video_refresh",
"retro_unload_game",
"retro_unserialize"
],
"undefined_symbols": [
"__error",
"__isthreaded",
"atan",
"atof",
"atoi",
"calloc",
"close",
"closedir",
"cos",
"exp",
"fclose",
"ferror",
"fflush",
"fileno",
"fopen",
"fread",
"free",
"fseeko",
"ftello",
"ftruncate",
"fwrite",
"isspace",
"isxdigit",
"ldexp",
"localtime",
"log",
"lseek",
"malloc",
"mbstowcs",
"memchr",
"memcmp",
"memcpy",
"memmove",
"memset",
"mkdir",
"open",
"opendir",
"pow",
"qsort",
"read",
"readdir",
"realloc",
"remove",
"rename",
"setlocale",
"setvbuf",
"sin",
"sincos",
"snprintf",
"sprintf",
"sscanf",
"stat",
"strchr",
"strcmp",
"strdup",
"strftime",
"strlen",
"strncmp",
"strncpy",
"strrchr",
"strstr",
"strtod",
"strtok",
"strtoul",
"time",
"tolower",
"toupper",
"vsnprintf",
"wcslen",
"wcstombs",
"write"
],
"load_segments": [
{
"flags": 7,
"alignment": 16384
},
{
"flags": 6,
"alignment": 16384
},
{
"flags": 6,
"alignment": 16384
}
],
"abi_passed": true,
"console_loading_verified": false,
"source_revision": "236ccdfc911e84c60fea6b9d0699c2d440a8de14",
"source_archive_sha256": "dd002cde9b5271979e0394bb9e696bd37e149ced473ff1e3629cc7fed502381f",
"info_revision": "5a74858ab2f7a50cebb5a6330895bc38899531c0",
"info_sha256": "eda6fbfdc1fda5ea80c2caed205345b0cbbfa9427acf3626e0cffd6ee5a96721",
"sdk_compiler_wrapper_sha256": "3dcf3139662db7b0f796c2073bc8f673a5557e48bc24d2e413896d33847a69db"
}
+34
View File
@@ -0,0 +1,34 @@
{
"step": "fceumm-build",
"recorded": "2026-09-19",
"revision": "4ac3bce + FCEUmm build step",
"command": "make fceumm; tools/verify.sh; tools/run-title.sh --no-build --watch 120 (owner loads core/game manually)",
"raw_capture": "klog/fceumm-metadata-run.log",
"raw_lines": 7865,
"lines": [
"frontend build identity: 261d33099adec97c231205beb21d6011e1587436df099d4e6ac6f29949c50c8f",
"FCEUmm source revision: 236ccdfc911e84c60fea6b9d0699c2d440a8de14",
"core bytes: 4836144; sha256: d1afcd2ea84de627f4a28d8ec4e81d63d23ca5b531811ba57357d98f8b31759f",
"ELF: x86-64 ET_DYN FreeBSD; 25 libretro exports; libkernel_web.sprx; no libkernel_sys.sprx",
"Repeated fresh extraction/build: identical core SHA-256 after --build-id=sha1 override",
"Console core readback: 4836144 bytes; same full SHA-256; header 7f454c4602010109",
"Core info readback SHA-256: eda6fbfdc1fda5ea80c2caed205345b0cbbfa9427acf3626e0cffd6ee5a96721",
"Saved configuration: libretro_info_path empty; upstream falls back to /app0/cores",
"Metadata staged identically in /app0/info and /app0/cores; cache refresh markers read back",
"[INFO] [Core info] Wrote to cache file: \"/app0/cores/core_info.cache\".",
"Console cache decoded: display_name=Nintendo - NES / Famicom (FCEUmm); core_name=FCEUmm; supported_extensions=fds|nes|unif|unf; has_info=true",
"[ERROR] [Core] Failed to open libretro core: \"/app0/cores/fceumm_libretro.so\"",
"[ERROR] [Core] Error(s): (null)",
"[INFO] [Core] Using content: \"<private content>\".",
"[ERROR] [Core] Frontend is built for dynamic libretro cores, but path is not set. Cannot continue.",
"[ERROR] Fatal error received in: \"init_libretro_symbols()\"",
"Kernel SIGSEGV: read address 0x80; RIP 0xab3066; title base 0x400000; ELF offset 0x6b3066",
"Symbolized fault: vulkan_alive; null rdi; instruction reads 0x80(%rdi)",
"Backtrace: vulkan_alive -> runloop_iterate -> rarch_main -> main -> _start",
"Owner: App crashed after I started game",
"Build/ABI/staging/metadata: PASS; runtime core loading and game execution: FAIL (open blocker)"
],
"runtime_acceptance_passed": false,
"build_abi_passed": true,
"metadata_cache_verified": true
}
+15
View File
@@ -0,0 +1,15 @@
{
"step": "fceumm-build",
"note": "Build and metadata evidence with an explicit unresolved native-loader/recovery failure; this is not a playable-core acceptance.",
"must_contain": [
"core bytes: 4836144; sha256: d1afcd2ea84de627f4a28d8ec4e81d63d23ca5b531811ba57357d98f8b31759f",
"Console cache decoded: display_name=Nintendo - NES / Famicom (FCEUmm); core_name=FCEUmm; supported_extensions=fds|nes|unif|unf; has_info=true",
"[ERROR] [Core] Failed to open libretro core: \"/app0/cores/fceumm_libretro.so\"",
"Kernel SIGSEGV: read address 0x80; RIP 0xab3066; title base 0x400000; ELF offset 0x6b3066",
"Build/ABI/staging/metadata: PASS; runtime core loading and game execution: FAIL (open blocker)"
],
"must_not_contain": [
"runtime core loading and game execution: PASS"
],
"human_check": "Owner attempted game launch and reported a crash. No game frames, audio, saves or controller gameplay were verified."
}
+31
View File
@@ -0,0 +1,31 @@
# Native dynamic core loading: unresolved
FCEUmm is cross-built and its metadata is discovered. It is not a working core
port yet. Evidence: `evidence/fceumm-build/`. No loader implementation or untested
crash fix is included here. The failed target gate remains open.
The native title cannot currently open the generated shared core. Both manual
runs log `Failed to open libretro core` and `Error(s): (null)`. The final ELF is
byte-identical on FTP readback, so this is not a stale upload or missing `.info`.
The later content attempt has no core path after the failed selection, tears down
the frontend, fails initialization, then reaches `vulkan_alive(NULL)` and faults
on the video-width read at address 0x80. This is a frontend lifecycle crash;
it does not establish a Vulkan driver failure or execution inside FCEUmm.
## Next implementation and acceptance
1. Add native dynamic-loader diagnostics around failed open/symbol resolution,
retaining the core path and error status without printing user content paths.
Establish whether a native module/export conversion or an application ELF
loader is the supported route. The current native module converter explicitly
rejects application exports; the websrv SDK environment is not interchangeable.
2. Fix failed-core selection/content startup so it returns to a valid menu or
exits cleanly before polling a destroyed video context. Reproduce the current
failure in a regression test. A null check alone must not conceal a broken
frontend reinitialization or select a fallback video driver.
3. Test load, all required libretro symbols, API version and FCEUmm identity on
console, then the owner's manually selected game. Capture current frontend
identity, exact core SHA-256, retroarch.log, trace and klog. Require no crash.
4. Confirm visible gameplay, audio, controls, repeated load/unload and preserved
menu. Saves/states are separate acceptance. No static-only core substitution
or donor-SDK swap is implied by this build step.
+1
View File
@@ -75,6 +75,7 @@ void initialize(void *)
set_directory(DEFAULT_DIR_MENU_CONFIG, "/app0/config");
set_directory(DEFAULT_DIR_MENU_CONTENT, "/app0");
set_directory(DEFAULT_DIR_CORE, "/app0/cores");
set_directory(DEFAULT_DIR_CORE_INFO, "/app0/info");
set_directory(DEFAULT_DIR_CORE_ASSETS, "/app0/content");
set_directory(DEFAULT_DIR_SYSTEM, "/app0/system");
set_directory(DEFAULT_DIR_SRAM, "/app0/savefiles");
+83
View File
@@ -0,0 +1,83 @@
"""Exercise the core gate with real ELFs, including incompatible artifacts."""
import importlib.util
from pathlib import Path
import shutil
import struct
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location('core_abi', ROOT / 'tools/check-core.py')
core_abi = importlib.util.module_from_spec(spec)
spec.loader.exec_module(core_abi)
class CoreABI(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.folder = Path(self.tmp.name)
self.cc = shutil.which('clang') or shutil.which('cc')
if not self.cc:
self.skipTest('host C compiler unavailable')
def build(self, soname='libkernel_web.sprx', omit=None):
stub = self.folder / 'stub.so'
subprocess.run([self.cc, '-x', 'c', '-', '-shared', '-nostdlib',
'-Wl,-soname,' + soname, '-o', str(stub)],
input='void stub(void) {}', text=True, check=True,
capture_output=True)
path = self.folder / 'core.so'
source = '\n'.join('void ' + name + '(void) {}'
for name in sorted(core_abi.REQUIRED) if name != omit)
subprocess.run([self.cc, '-x', 'c', '-', '-x', 'none', str(stub),
'-shared', '-nostdlib', '-Wl,-z,max-page-size=16384',
'-o', str(path)], input=source, text=True, check=True,
capture_output=True)
data = bytearray(path.read_bytes())
data[7] = 9 # Test fixture only: mark its ELF OSABI as FreeBSD.
path.write_bytes(data)
return path
def test_valid_shape_does_not_claim_console_verification(self):
report = core_abi.inspect(self.build())
self.assertTrue(report['abi_passed'])
self.assertFalse(report['console_loading_verified'])
self.assertEqual(len(report['libretro_exports']), 25)
def test_reject_host_abi(self):
path = self.build()
data = bytearray(path.read_bytes())
data[7] = 0
path.write_bytes(data)
with self.assertRaisesRegex(ValueError, 'not a host library'):
core_abi.inspect(path)
def test_reject_payload_kernel_import(self):
with self.assertRaisesRegex(ValueError, 'unexpected PS5 core imports'):
core_abi.inspect(self.build(soname='libkernel_sys.sprx'))
def test_reject_missing_callback(self):
with self.assertRaisesRegex(ValueError, 'retro_run'):
core_abi.inspect(self.build(omit='retro_run'))
def test_reject_four_kib_segment(self):
path = self.build()
data = bytearray(path.read_bytes())
phoff = struct.unpack_from('<Q', data, 32)[0]
size, count = struct.unpack_from('<HH', data, 54)
for i in range(count):
offset = phoff + i * size
if struct.unpack_from('<I', data, offset)[0] == 1:
struct.pack_into('<Q', data, offset + 48, 4096)
break
path.write_bytes(data)
with self.assertRaisesRegex(ValueError, '16 KiB'):
core_abi.inspect(path)
def test_reject_truncated_header(self):
path = self.folder / 'truncated.so'
path.write_bytes(b'\x7fELF')
with self.assertRaisesRegex(ValueError, 'expected ELF64'):
core_abi.inspect(path)
+17
View File
@@ -4,6 +4,7 @@ import importlib.util
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock
ROOT = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location("deploy_title", ROOT / "tools/deploy-title.py")
@@ -28,3 +29,19 @@ class DeployIdentity(unittest.TestCase):
self.assertIn(marker, required)
old_image = b"main() entered\0build identity: " + b"b" * 64 + b"\0"
self.assertTrue(any(value not in old_image for value in required))
def test_metadata_refresh_uses_info_parent_and_checks_readback(self):
ftp = Mock()
stored = {}
ftp.storbinary.side_effect = lambda cmd, stream: stored.update({cmd: stream.read()})
ftp.retrbinary.side_effect = lambda cmd, write, **kw: write(b'\0')
deploy.refresh_core_info(ftp, '/title/cores/fceumm_libretro.info')
self.assertEqual(stored, {'STOR /title/cores/core_info.refresh': b'\0'})
self.assertEqual(ftp.retrbinary.call_args.args[0],
'RETR /title/cores/core_info.refresh')
def test_metadata_refresh_refuses_failed_readback(self):
ftp = Mock()
ftp.retrbinary.side_effect = lambda cmd, write, **kw: write(b'wrong')
with self.assertRaisesRegex(SystemExit, 'refresh marker readback failed'):
deploy.refresh_core_info(ftp, '/title/info/fceumm_libretro.info')
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Cross-build pinned FCEUmm with this title's SDK. No host compiler fallback.
# Output: build/cores/stage/{cores,info}/; build-title.sh stages these in /app0.
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$root"
[[ $# == 0 ]] || { echo "usage: ${0##*/}" >&2; exit 2; }
sdk="$root/.deps/native/ps5-payload-sdk"
[[ -x $sdk/bin/prospero-clang ]] || { echo "error: bootstrap this project's SDK first" >&2; exit 2; }
export PS5_PAYLOAD_SDK="$sdk"
export PS5_CLANG=/usr/bin/clang
# Both environment and make variables are explicit: upstream assigns CC/LD.
export CC="$sdk/bin/prospero-clang" CXX="$sdk/bin/prospero-clang++"
export AR="$sdk/bin/prospero-ar" LD="$CC"
revision=236ccdfc911e84c60fea6b9d0699c2d440a8de14
source_sha=dd002cde9b5271979e0394bb9e696bd37e149ced473ff1e3629cc7fed502381f
info_revision=5a74858ab2f7a50cebb5a6330895bc38899531c0
info_sha=eda6fbfdc1fda5ea80c2caed205345b0cbbfa9427acf3626e0cffd6ee5a96721
cache="$root/.deps/downloads"
mkdir -p "$cache"
fetch() {
local url=$1 target=$2 digest=$3
if [[ ! -f $target ]]; then
curl --fail --location --retry 3 "$url" -o "$target.download"
printf '%s %s\n' "$digest" "$target.download" | sha256sum --check --status || {
rm -f -- "$target.download"; echo 'error: download digest mismatch' >&2; exit 1;
}
mv -- "$target.download" "$target"
fi
printf '%s %s\n' "$digest" "$target" | sha256sum --check --status || {
echo "error: cached input digest mismatch: $target" >&2; exit 1;
}
}
archive="$cache/fceumm-$revision.tar.gz"
info="$cache/fceumm_libretro.info"
fetch "https://codeload.github.com/libretro/libretro-fceumm/tar.gz/$revision" "$archive" "$source_sha"
fetch "https://raw.githubusercontent.com/libretro/libretro-core-info/$info_revision/fceumm_libretro.info" "$info" "$info_sha"
work="$root/build/cores/fceumm"
stage="$root/build/cores/stage"
# Fresh extraction ensures stale objects or modified upstream sources never ship.
rm -rf -- "$work"
mkdir -p "$work" "$stage/cores" "$stage/info"
tar -xzf "$archive" --strip-components=1 -C "$work"
# Keep upstream source/feature selection. Invoke the compiler as link driver:
# SHARED contains -Wl options, which raw prospero-lld must not receive.
# -nodefaultlibs avoids the SDK's static libc/CRT; this title uses runtime imports.
# LIBM is empty because the public libc stub exports the math functions.
# Clang's PS5 default is --build-id=uuid; override it for repeatable artifacts.
make -C "$work" -f Makefile.libretro -j"${JOBS:-8}" platform=unix \
CC="$CC" CXX="$CXX" AR="$AR" LD="$LD" \
GIT_VERSION="\" ${revision:0:7}\"" LIBM= \
LIBS='-lkernel_web -lSceLibcInternal -lScePosixForWebKit' \
LDFLAGS='-nostdlib -nodefaultlibs -Wl,--build-id=sha1'
python3 tools/check-core.py "$work/fceumm_libretro.so" --report "$work/abi.json"
cp -- "$work/fceumm_libretro.so" "$stage/cores/fceumm_libretro.so"
cp -- "$info" "$stage/info/fceumm_libretro.info"
python3 - "$work" "$revision" "$source_sha" "$info_revision" "$info_sha" <<'PY'
import hashlib, json, pathlib, sys
work = pathlib.Path(sys.argv[1])
report = json.loads((work / 'abi.json').read_text())
report.update(source_revision=sys.argv[2], source_archive_sha256=sys.argv[3],
info_revision=sys.argv[4], info_sha256=sys.argv[5])
report['sdk_compiler_wrapper_sha256'] = hashlib.sha256(
pathlib.Path('.deps/native/ps5-payload-sdk/bin/prospero-clang').read_bytes()).hexdigest()
(work / 'build.json').write_text(json.dumps(report, indent=2) + '\n')
PY
printf '==> [fceumm] built and ABI-checked revision %s; console loading is a separate gate\n' "$revision"
+6
View File
@@ -51,6 +51,7 @@ sdk="$root/.deps/native/ps5-payload-sdk"
echo "==> [title] step 1/3: the frontend"
"$root/tools/build-retroarch.sh"
bash "$root/tools/build-fceumm.sh"
# The title's own sources are compiled with the same feature defines as the
# frontend, because the two share a header full of #ifdefs and a struct whose
@@ -188,6 +189,11 @@ dist="$root/dist/$title_id"
# title, because naming a driver whose library is missing makes RetroArch fail to
# initialise and the title exit 1 saying nothing. See config/retroarch.cfg.
cp -a -- "$root/config/retroarch.cfg" "$dist/retroarch.cfg"
mkdir -p "$dist/cores" "$dist/info"
cp -- "$root/build/cores/stage/cores/fceumm_libretro.so" "$dist/cores/"
cp -- "$root/build/cores/stage/info/fceumm_libretro.info" "$dist/info/"
# Older saved configs have an empty info path: upstream then searches cores/.
cp -- "$root/build/cores/stage/info/fceumm_libretro.info" "$dist/cores/"
# The Vulkan driver, beside the title, when it exists.
#
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Check a PS5 libretro shared ELF without confusing it with a host library."""
import argparse
import hashlib
import json
from pathlib import Path
import re
import struct
import subprocess
REQUIRED = {
'retro_api_version', 'retro_init', 'retro_deinit', 'retro_run',
'retro_get_system_info', 'retro_get_system_av_info', 'retro_load_game',
'retro_load_game_special', 'retro_unload_game', 'retro_reset',
'retro_set_environment', 'retro_set_video_refresh', 'retro_set_audio_sample',
'retro_set_audio_sample_batch', 'retro_set_input_poll', 'retro_set_input_state',
'retro_set_controller_port_device', 'retro_serialize_size', 'retro_serialize',
'retro_unserialize', 'retro_get_region', 'retro_get_memory_data',
'retro_get_memory_size', 'retro_cheat_reset', 'retro_cheat_set',
}
ALLOWED_IMPORTS = {'libkernel_web.sprx', 'libSceLibcInternal.sprx',
'libScePosixForWebKit.sprx'}
def inspect(path):
data = Path(path).read_bytes()
if len(data) < 64 or data[:8] != b'\x7fELF\x02\x01\x01\x09':
raise ValueError('expected ELF64 little-endian FreeBSD/PS5 ABI, not a host library')
kind, machine = struct.unpack_from('<HH', data, 16)
if kind != 3 or machine != 62:
raise ValueError('expected x86-64 ET_DYN shared object')
dynamic = subprocess.check_output(['readelf', '-dW', str(path)], text=True)
needed = re.findall(r'\(NEEDED\).*\[([^\]]+)\]', dynamic)
if 'libkernel_web.sprx' not in needed or set(needed) - ALLOWED_IMPORTS:
raise ValueError(f'unexpected PS5 core imports: {needed}')
symbols = subprocess.check_output(['readelf', '--dyn-syms', '-W', str(path)], text=True)
exported, undefined = set(), set()
for line in symbols.splitlines():
fields = line.split()
if len(fields) < 8 or not fields[0].endswith(':'):
continue
if fields[6] == 'UND':
undefined.add(fields[7])
elif fields[3:6] == ['FUNC', 'GLOBAL', 'DEFAULT']:
exported.add(fields[7])
missing = REQUIRED - exported
if missing:
raise ValueError(f'missing dynamic libretro exports: {sorted(missing)}')
phoff = struct.unpack_from('<Q', data, 32)[0]
phsize, phcount = struct.unpack_from('<HH', data, 54)
if phsize != 56 or phoff + phsize * phcount > len(data):
raise ValueError('invalid program header table')
loads = []
for i in range(phcount):
ptype, flags, offset, address, _, filesz, memsz, align = struct.unpack_from(
'<IIQQQQQQ', data, phoff + i * phsize)
if ptype != 1:
continue
if (align < 0x4000 or offset % 0x4000 != address % 0x4000
or filesz > memsz or offset + filesz > len(data)):
raise ValueError('invalid PS5 16 KiB load segment')
loads.append({'flags': flags, 'alignment': align})
if not loads:
raise ValueError('no loadable segments')
return {'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'elf': 'ELF64 x86-64 ET_DYN FreeBSD/PS5', 'needed': needed,
'libretro_exports': sorted(REQUIRED), 'undefined_symbols': sorted(undefined),
'load_segments': loads, 'abi_passed': True,
'console_loading_verified': False}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('core', type=Path)
parser.add_argument('--report', type=Path)
args = parser.parse_args()
try:
report = inspect(args.core)
except (OSError, ValueError, subprocess.CalledProcessError) as error:
parser.exit(1, f'core ABI check failed: {error}\n')
if args.report:
args.report.write_text(json.dumps(report, indent=2) + '\n')
print(f'core ABI PASS: {args.core.name}; {len(REQUIRED)} exports; '
f'kernel_web; sha256={report["sha256"]}')
if __name__ == '__main__':
main()
+4
View File
@@ -94,6 +94,10 @@ else
fi
echo "==> [manifest] the two files the console reads"
python3 tools/check-core.py "$folder/cores/fceumm_libretro.so" || bad "FCEUmm core ABI check failed"
[[ -s $folder/info/fceumm_libretro.info ]] || bad "FCEUmm core info is missing"
cmp -s "$folder/info/fceumm_libretro.info" "$folder/cores/fceumm_libretro.info" ||
bad "FCEUmm metadata fallback differs or is missing"
for required in eboot.bin sce_sys/param.json sce_sys/icon0.png; do
[[ -f $folder/$required ]] || bad "$required is missing"
done
+20
View File
@@ -93,6 +93,15 @@ def remote_bytes(ftp, path: str) -> bytes:
return buffer.getvalue()
def refresh_core_info(ftp, remote_info: str) -> None:
"""Invalidate cached missing/stale metadata after its verified upload."""
import io
refresh = join(remote_info.rsplit('/', 1)[0], 'core_info.refresh')
ftp.storbinary(f"STOR {refresh}", io.BytesIO(b'\0'))
if remote_bytes(ftp, refresh) != b'\0':
raise SystemExit('core metadata refresh marker readback failed')
def sizes(ftp, path: str) -> dict[str, int]:
previous = ftp.pwd()
ftp.cwd(path)
@@ -238,6 +247,13 @@ def do_deploy(settings: dict, tid: str) -> int:
print(f" {relative:28} {len(served):>10,} bytes stored; all "
f"{len(markers)} of this build's markers present ok")
continue
if relative.startswith('cores/') and relative.endswith('.so'):
# The FCEUmm ELF is served unchanged on this native-title route.
# Never accept a stale core via the legacy driver marker fallback.
if digest != expected:
raise SystemExit(f'{relative}: core SHA-256 readback mismatch')
print(f" {relative:28} {size:>10,} bytes; full core SHA-256 verified ok")
continue
if relative.endswith(".so") or relative.endswith(".so.1"):
# A shared object is re-signed by the console on write, so it is
# checked the same way the program image is: a byte string this
@@ -279,6 +295,10 @@ def do_deploy(settings: dict, tid: str) -> int:
f"{digest[:16]}, the file here is {local.stat().st_size} bytes "
f"with {expected[:16]}")
print(f" {relative:28} {size:>10,} bytes {digest[:16]} ok")
if relative.endswith('.info'):
# RetroArch caches missing metadata too. Ask it to re-read the
# newly uploaded info on next startup without changing settings.
refresh_core_info(ftp, remote)
present_root = list_names(ftp, remote_root)
present_sys = list_names(ftp, join(remote_root, "sce_sys"))
for required, where in (("eboot.bin", present_root), ("param.json", present_sys)):