mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
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:
1 parent
dae9006c37
commit
d337e2c11b
19 files changed
+718
-57
No files matched your search
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/`.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
Executable
+67
@@ -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"
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
Executable
+88
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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)):
|
||||
|
||||
Reference in new issue
Block a user