Load native FCEUmm cores safely and present software core frames

Replace payload dlfcn hooks with a bounded ELF loader and explicit native
runtime bindings. Separate core RX/R/RW segments, reject unsupported ELF
features, and release failed loads. Guard failed menu selection before
frontend teardown and stop polling drivers after failed initialization.

Read core files through mapped buffers and bounded POSIX reads. Convert
libretro XRGB8888 frames to matching RGBA8 staging/sampled textures so real
content uses the verified Vulkan copy path rather than the RGB565 compute
branch. Keep video_ps5 selectable and preserve frontend/audio/error logs.

Validation: tools/verify.sh passed all five gates (52 tests). Console command:
tools/run-title.sh --no-build --core-test --watch 180, with manual core/game
selection. Eight ELF load/unload cycles and live-menu failure recovery passed;
owner confirmed flawless gameplay. Zero GPU refusals/API failure records and
kernel fatal signals across captured launches. Return-to-menu shortcut and
repeated game unload/reload remain unverified; owner closed/reopened the app.

Evidence: evidence/native-core-loading/{capture,expectation,loader,recovery,
abi,iterations}.json; replay with tools/verify.sh evidence. Accepted title
identity 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5.
No PS5_Vulkan edits by this task; its independently advancing checkout is
not used as a substitute for the accepted title's archive-bound identity.
This commit is contained in:
Mihawk committed 2026-09-19 14:47:09 -04:00
1 parent d337e2c11b
commit 63a440414a
31 files changed
+1443 -92

No files matched your search

+68 -55
View File
@@ -4,79 +4,92 @@ _Updated: 2026-09-19_
## Now
**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.
**Native FCEUmm loading and first gameplay are verified.** The owner answered
“Works flawlessly!” to the manual video/audio/controls test. This implements the
requested native core loading and failed-load recovery step. No PS5_Vulkan
changes were made by this task. The sibling advanced independently during the
work (currently **085c632**); the accepted title identity includes its exact
linked archive bytes and is not attributed to an assumed driver commit.
`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.
The title loads this pipeline's shared ELF through a bounded in-process loader,
with explicit native runtime imports and separate RX/R/RW segments. Missing or
unsupported cores return an error. Menu selection checks success before tearing
down the frontend; a late initialization failure stops driver polling safely.
FCEUmm's XRGB8888 frames are converted to matching RGBA8 staging/sampled textures
and presented through the existing Vulkan path.
## Verified build and evidence
- 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**.
- Frontend identity: `9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5`.
- FCEUmm source revision: `236ccdfc911e84c60fea6b9d0699c2d440a8de14`.
- Core SHA-256: `fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c`.
- Core size: 4,836,368 bytes; 25 callbacks; 71 native bindings; 16,001
relocations; mapped image 9,076,736 bytes. No payload loader hooks/static core.
- All five host gates pass, with 52 tests. `evidence/native-core-loading/`
contains loader/recovery reports, ABI report, failed iterations and acceptance.
- Final command: `tools/run-title.sh --no-build --core-test --watch 180`.
Eight load/export/API/identity/unload cycles passed. A second load with XMB
resident passed. Missing-core selection and content startup were rejected
through actual task functions while preserving the initialized menu context.
- The owner manually loaded an NES archive. The core negotiated XRGB8888,
256x224, 60.10 Hz and 48 kHz audio. The declared rate is not a speed measurement.
- Current-build trace: three launches, zero Vulkan refusals, zero GPU API failure
records; kernel capture has zero fatal signals. The owner closed/reopened the
title, and the runner closed a later menu instance. Do not describe this as
180 seconds of uninterrupted gameplay.
- Raw captures: `klog/native-core-game-rgba-run.log`,
`klog/run-PPSA99169-143921.log`, `klog/core-rgba-live-retroarch.log` and
`klog/core-rgba-final-trace.txt`. The live frontend log preserves the gameplay
session; later menu launches replaced `/app0/retroarch.log`.
## Open target blockers
## Limits and remaining acceptance
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.
- Return from gameplay to XMB and repeated game unload/reload are **not verified**:
the owner had not configured a menu shortcut and manually closed the app.
Repeated ELF load/unload is verified separately by the diagnostic.
- Save RAM, save states, long-run A/V sync and measured core performance remain
separate milestones. RGB565 content, other cores, runahead and multiple
controllers are not covered by this FCEUmm acceptance.
- The loader explicitly rejects TLS, constructors/finalizers, unsupported
relocation types and additional shared dependencies. It is not a general
system dynamic linker; a faulty core can still crash the shared process.
- Failed-core guards cover the tested menu selection/new-content paths. The
late-initialization safeguard exits instead of polling freed drivers; recovery
from every possible content/core callback failure is not claimed.
- Tolerated log: `GET_VARIABLE: fceumm_hdpacks - Not implemented.` The optional
HD-pack query returns false and upstream uses its default before checking for
a pack. No pack is shipped/tested. Core-option/HD-pack support needs separate
validation; this message did not prevent the accepted standard NES game.
- The old stdio/heap file read failed after XMB initialized. Bounded POSIX reads
into mapped memory fixed the observed failure; the precise libc/allocator
cause was not isolated. Earlier failed runs remain in the evidence history.
## 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.
Native joypad/menu analog navigation and binding capture are verified in
`evidence/native-joypad/`. Native audio/buffering are in `evidence/native-audio/`.
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.
registered/selectable. Useful frontend/core/audio/error logging is retained.
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.
Managed directories are repaired to `0777`; FTP upload/readback/cleanup is in
`evidence/ftp-directory-permissions/`. Deployments preserve live config/content.
## User file locations
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.
The build ships FCEUmm and `.info` metadata in both `info/` and `cores/`, with
cache refresh on deployment. No game or BIOS is shipped. `make fceumm` builds
only the core; see `docs/DEPLOYMENT.md` for procedures.
## Operating notes
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.
Uploads/runs are authorized without further permission. Never interrupt an
existing title; the runner checks idle before upload/launch. Core/game selection
is manual. Keep private filenames and console details in ignored captures.
Trace appends across launches: separate `bss check=` sections and verify identity.
A manual close/relaunch may replace the frontend log during a capture window;
save the live gameplay log before it is lost. Do not infer gameplay duration
from the runner's final process count.
+8
View File
@@ -194,3 +194,11 @@ After uploading each `.info`, deployment writes and reads back RetroArch's
`core_info.refresh` marker in the same directory. RetroArch consumes it when
rebuilding its metadata cache; it is not a shipped manifest file. This also
refreshes entries previously cached as having no metadata.
For a no-game native loader/recovery diagnostic, use
`tools/run-title.sh --no-build --core-test --watch 30` after the host gates pass.
For manual core/game testing, launch with `--no-build --watch 180` (also
`--no-deploy` if the same build is already verified on the console). The owner
selects the game; collect `retroarch.log`, `trace.txt` and the kernel capture.
The loader's supported ELF/runtime contract is in `docs/REFERENCE.md`.
+35
View File
@@ -2673,3 +2673,38 @@ context at `vulkan_alive+0x26` (ELF 0x6b3066) reading address 0x80. The owner
confirmed the crash. This is not evidence that FCEUmm reached emulation or that
libps5vk refused GPU work. Keep loader and error-recovery acceptance open.
Evidence: `evidence/fceumm-build/`; next work: `parked/native-core-loading/`.
## 2026-09-19: native ELF core loading and the first software-core frame
The native title's payload-oriented dlfcn route did not open the staged FCEUmm
ELF. The local native converter also refuses application exports. A no-game
probe using public anonymous mmap, RW-to-RX mprotect and a return-42 function
succeeded (`klog/run-PPSA99169-141420.log`). This supported implementing a bounded
application ELF loader without websrv hooks, a donor SDK or kernel changes.
The core now uses `tooling/native/ps5-core.ld` to separate 16 KiB RX/R/RW pages;
`-T` is required because the previous shared layout combined code and data.
Runtime imports are explicit title addresses generated from the core's dynamic
symbols, including this port's directory adapters. Bounds, relocations, imports
and final protection are checked before publishing a reference-counted handle.
TLS/constructors/unwind support is deliberately outside this first contract.
Eight pre-frontend load/API/identity/unload cycles passed, but the first manual
selection failed reading the same file after XMB started. That diagnostic did
not cover the failing lifecycle stage. Replacing the single stdio/heap read with
bounded POSIX reads into mapped memory resolved the observed failure. The exact
libc/allocator cause remains unisolated; the new diagnostic loads with the full
frontend resident and tests rejection through the actual menu task functions.
The next manual game start reached FCEUmm's video callback and aborted inside
`vulkan_copy_staging_to_dynamic`: a BGRA staging buffer differed from the sampled
RGBA image, selecting the RGB565-only compute branch. The core-frame upload now
requests RGBA8 for both textures and converts XRGB8888 channel order/alpha while
respecting row pitches. This is the existing GPU presentation route, not a switch
to CPU video. Tests exercise colours, padding and in-place writes.
The accepted build is recorded in `evidence/native-core-loading/`. The owner
confirmed flawless gameplay and reported manual close because no return-to-menu
shortcut was set. The capture contains three current-build launches with zero
Vulkan refusals, GPU failure records or fatal signals; it is not a continuous
180-second gameplay measurement. Repeated game unload and saves remain untested.
+52
View File
@@ -2017,3 +2017,55 @@ Host verification: `tools/verify.sh` passes all five gates, 45 tests. Artifact:
failed target result. `parked/native-core-loading/README.md` records the remaining
loader/recovery work and its acceptance. PS5_Vulkan unchanged at 6f0ce0d; no ROM
or BIOS is distributed. This commit completes build tooling, not a playable port.
## 2026-09-19: native FCEUmm loader, failure guards and first gameplay
Implemented the requested native loading/recovery step. The loader maps checked
ELF64 segments, binds 71 explicit native imports, applies 16,001 relocations and
publishes handles only after protection succeeds. FCEUmm remains a dynamically
loaded core built with this project's SDK; PS5_Vulkan remains unchanged at
6f0ce0d. Core upload uses matching RGBA8 textures with an XRGB8888 conversion.
Runs and failures:
- `tools/run-title.sh --no-build --core-test --watch 30`, memory probe,
`klog/run-PPSA99169-141420.log`: anonymous RW -> RX code returned 42; full window.
- Same command, `klog/run-PPSA99169-142423.log`: eight real-core load/export/API/
identity/unload cycles passed before frontend startup; full window.
- `tools/run-title.sh --no-build --no-deploy --watch 180`,
`klog/run-PPSA99169-142526.log`: menu-time core file read failed; owner saw
No Core/archive failure and confirmed manual quit/close. Kernel exit(0) then
SIGSYS is retained, not classified as a core execution crash.
- Mapped/chunked-read diagnostic, `klog/run-PPSA99169-143443.log`: startup and
live-menu loader/rejection reports passed. Manual game start then aborted in
`vulkan_copy_staging_to_dynamic` (ELF offset 0x6b87ff, ce7176a7 build).
Symbolization: `addr2line -Cfipe klog/native-core-ce7176a7.elf 0x6b87ff
0x6b3880 0x6b0129 0xae8f1b 0xada6fd 0x620a`. The format mismatch selected the
RGB565-only compute path; matching RGBA core textures resolve it.
- `tools/verify.sh`: PASS, all five gates, 52 tests; log
`/tmp/core-frame-verify.log`. Loader tests execute relocations/imports and
malformed-file rejection; recovery test fails on upstream's unchecked return
and passes with the guard; frame test verifies channels/alpha/pitch/in-place.
- `tools/run-title.sh --no-build --core-test --watch 180`,
`klog/native-core-game-rgba-run.log`, kernel `klog/run-PPSA99169-143921.log`:
identity 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5;
core SHA-256 fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c.
Both diagnostic JSON reports pass. Owner manually loaded the archive and
answered “Works flawlessly!” to video/audio/controls, then reported closing
manually because the menu shortcut was unset. Three launches occurred within
the capture; the runner closed a later menu instance. Zero GPU refusals/API
failure records and zero kernel fatal signals. Do not claim continuous runtime.
Evidence: `evidence/native-core-loading/`, replay with `tools/verify.sh evidence`.
Raw gameplay log was saved before a later menu launch replaced it. No ROM or
private filename is committed. Return-to-menu shortcut, repeated game unload,
saves/states and measured performance are not included in this acceptance.
Provenance correction for the preceding entry: this task did not edit
PS5_Vulkan, but its checkout advanced independently during the work to 8c2985a
and then 085c632. “Remains unchanged at 6f0ce0d” is therefore not a valid claim
about the sibling checkout or the linked driver revision. The accepted title
identity includes the actual archive bytes read at build time; no driver source
commit is inferred from the later checkout. Its archive changed after the
accepted title linked, so the accepted dist/ artifact has not been rebuilt
against that newer archive. Console acceptance remains tied to identity 9b287ca7.
+42
View File
@@ -374,6 +374,9 @@ host build. The linker is the compiler driver because upstream passes `-Wl`
options; `-nostdlib -nodefaultlibs` excludes payload CRT/static libc defaults.
The `--build-id=sha1` override replaces Clang's random PS5 UUID with a
content-derived ELF ID so repeated builds have stable hashes.
`-T tooling/native/ps5-core.ld` separates executable, read-only and writable
segments on 16 KiB boundaries; the loader never maps writable executable pages.
The core checker rejects a writable executable load segment.
Math and libc imports come from the native runtime stubs, with `libkernel_web`
rather than the payload `libkernel_sys`. No SDL or decoder is added.
@@ -399,3 +402,42 @@ After uploading each `.info`, deployment writes and reads back RetroArch's
`core_info.refresh` marker in the same directory. RetroArch consumes it when
rebuilding its metadata cache; it is not a shipped manifest file. This also
refreshes entries previously cached as having no metadata.
## Native in-process core loader
`src/core_loader_ps5.cpp` supplies the dynamic-library operations used by the
frontend. It loads this pipeline's ELF64 x86-64 FreeBSD-ABI shared cores into
anonymous memory, resolves their imports against explicit title bindings,
applies checked RELA relocations, then protects each segment as RX, R or RW.
File reads use bounded POSIX reads into a separate mapped buffer. Failed opens
release allocations and report the failing operation; successful handles are
reference counted and unmapped on the final close.
`tools/core-imports.py` derives the required native bindings from the staged
FCEUmm ELF; those bindings and the core bytes participate in the title build
identity. Directory imports use this port's existing directory adapters. The
runtime dependencies are limited to the public kernel_web, libc and Posix stubs.
This route does not depend on websrv's payload loader hooks or publish native
module exports through the title converter.
The initial supported contract is deliberately narrower than a general dynamic
linker: no TLS, ELF interpreter, constructors/finalizers, C++ unwind registration,
REL/RELR, or additional dependent shared libraries. Unsupported imports and
relocations fail explicitly. Adding another core requires reviewing this
contract and extending both host tests and target diagnostics as needed.
Core selection checks load success before content startup tears down drivers.
The main loop also refuses to poll drivers after frontend initialization fails.
This protects against load failures; it cannot isolate a fault inside arbitrary
core code running in the same process.
Software core frames in libretro XRGB8888 are converted by
`src/core_frame_ps5.cpp` to RGBA8 bytes with opaque alpha, respecting both row
pitches and in-place framebuffers. The Vulkan frontend requests RGBA8 for both
staging and sampled core textures. This extends the menu's matching-format
upload rule to actual content, avoiding the RGB565-only compute branch. Frame
scaling and presentation remain on the Vulkan GPU path; `video_ps5` stays
registered as the selectable fallback. RGB565 core frames retain their existing
path and require separate content acceptance.
+31
View File
@@ -181,3 +181,34 @@ core load, content initialization and presented frames separately; a visible
core entry or `.info` name is not proof of loaded executable code. Owner game
paths remain in ignored logs. Keep normal XMB, Vulkan and audio startup working
when no core is selected.
## Native core loader diagnostics
`tests/test_core_loader.py` compiles and executes the real loader against a small
host ELF fixture. It tests relocation/import execution, reference counts,
reloads, missing imports, malformed header/table bounds, forbidden segment flags,
TLS, and invalid relocation targets/types. The fixture's OSABI byte is adjusted
only for this host test; shipped cores must pass the actual cross-build checker.
`tests/test_core_recovery.py` runs the upstream core-selection function before
and after the patch and checks rejection without marking the core as selected.
`tools/run-title.sh --no-build --core-test --watch 30` arms an opt-in no-game
console diagnostic. It executes eight FCEUmm load/symbol/API/identity/unload
cycles, rejects a missing core/export, and loads the core again with the frontend
resident. It then attempts missing-core selection/content startup through the
actual task functions and checks that the initialized menu context survives.
The runner collects `core-loader-test.json` and `core-recovery-test.json`, checks
their build identity and pass status, captures logs, and closes the title. Normal
runs clear the control file; diagnostics never run without the flag.
These diagnostics do not establish gameplay. The owner selects the core and
content manually in a separate captured run and confirms video, audio, controls
and return to XMB. Keep user content paths in ignored captures, not evidence.
`tests/test_core_frame.py` executes the actual XRGB8888 upload conversion with
red/green/blue and mixed colours, checks opaque alpha, distinct input/output row
pitches, untouched padding and in-place conversion. Real-core acceptance also
requires a trace with no Vulkan refusals or failed command buffers, rather than
inferring correctness from the core's successful initialization.
+126
View File
@@ -0,0 +1,126 @@
{
"sha256": "fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c",
"bytes": 4836368,
"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": 5,
"alignment": 16384
},
{
"flags": 4,
"alignment": 16384
},
{
"flags": 6,
"alignment": 16384
}
],
"abi_passed": true,
"console_loading_verified": false
}
+47
View File
@@ -0,0 +1,47 @@
{
"step": "native-core-loading",
"recorded": "2026-09-19",
"revision": "7af1081 + native loader/recovery/core upload",
"command": "tools/verify.sh; tools/run-title.sh --no-build --core-test --watch 180 (owner selects FCEUmm and NES archive manually)",
"raw_capture": "klog/native-core-game-rgba-run.log",
"raw_lines": 8363,
"lines": [
"frontend build identity: 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5",
"core SHA-256: fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c",
"ELF64 x86-64 ET_DYN FreeBSD; 25 libretro exports; kernel_web; separate RX/R/RW segments",
"core loader test: passed=1 cycles=8 exports=25 api=1 name=FCEUmm",
"core recovery test: passed=1 selection_rejected=1 content_rejected=1 menu_context_preserved=1 menu_core_load=1",
"[INFO] [Environ] SET_PIXEL_FORMAT: XRGB8888.",
"[INFO] [Core] Geometry: 256x224, Aspect: 1.306, FPS: 60.10, Sample rate: 48000.00 Hz.",
"[libretro INFO] Player 1: Gamepad",
"[libretro INFO] Player 2: Gamepad",
"current-build launches captured: 3; Vulkan refusals: 0; GPU API failure records: 0; kernel fatal signals: 0",
"owner: Works flawlessly! I manually closed as I didnt set the shortcut to close a game and return to menu",
"Runner used a 180-second window; owner closed/reopened the title, and runner closed a later menu instance. This is not 180 seconds of uninterrupted gameplay.",
"Gameplay acceptance: owner-confirmed; return-to-XMB hotkey and repeated game unload/reload: NOT VERIFIED",
"Host verification: PASS format unit build integration evidence; 52 tests",
"PS5_Vulkan not edited by this task; sibling advanced independently; title identity binds the linked archive bytes"
],
"loader": {
"build_identity": "build identity: 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5",
"passed": true,
"cycles": 8,
"exports": 25,
"api": 1,
"missing_rejected": true,
"unknown_symbol_rejected": true
},
"recovery": {
"build_identity": "build identity: 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5",
"passed": true,
"selection_rejected": true,
"content_rejected": true,
"menu_context_preserved": true,
"menu_core_load": true
},
"gpu_refusals": 0,
"gpu_api_failure_records": 0,
"kernel_fatal_signals": 0,
"core_sha256": "fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c",
"return_to_menu_verified": false
}
@@ -0,0 +1,15 @@
{
"step": "native-core-loading",
"note": "Native FCEUmm loading and first owner-confirmed gameplay; failed selection preserves live frontend. No claim of game hotkey/unload or sustained-rate acceptance.",
"must_contain": [
"frontend build identity: 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5",
"core SHA-256: fec7dc4eb7ec6cae937ae778f0b59365d8bd7570927a73353424345e13589d9c",
"core loader test: passed=1 cycles=8 exports=25 api=1 name=FCEUmm",
"core recovery test: passed=1 selection_rejected=1 content_rejected=1 menu_context_preserved=1 menu_core_load=1",
"[INFO] [Core] Geometry: 256x224, Aspect: 1.306, FPS: 60.10, Sample rate: 48000.00 Hz.",
"current-build launches captured: 3; Vulkan refusals: 0; GPU API failure records: 0; kernel fatal signals: 0",
"Host verification: PASS format unit build integration evidence; 52 tests"
],
"must_not_contain": [],
"human_check": "Owner answered the video/audio/controls question with \u201cWorks flawlessly!\u201d and reported manually closing the app because no return-to-menu shortcut was set."
}
@@ -0,0 +1,26 @@
[
{
"raw_capture": "klog/run-PPSA99169-141420.log",
"stage": "executable mapping probe",
"build_identity": null,
"outcome": "RW anonymous memory -> RX; executed return value 42; title survived 30s"
},
{
"raw_capture": "klog/run-PPSA99169-142423.log",
"stage": "pre-frontend loader test",
"build_identity": "4dfefb6b14414a24c164fdbbe10d364897aa8b7e0058745ac53e8e8b2dcca513",
"outcome": "8 load/export/API/identity/unload cycles passed; title survived 30s"
},
{
"raw_capture": "klog/run-PPSA99169-142526.log",
"stage": "manual selection",
"build_identity": "4dfefb6b14414a24c164fdbbe10d364897aa8b7e0058745ac53e8e8b2dcca513",
"outcome": "core file read failed after frontend initialization; owner confirmed manual quit/close; exit(0) followed by SIGSYS"
},
{
"raw_capture": "klog/run-PPSA99169-143443.log",
"stage": "mapped file reads and live recovery",
"build_identity": "ce7176a7e6688563b2b59cfa52808f403d5021299308e6038ea4f64d67fd661d",
"outcome": "loader/recovery reports passed; manually loaded content reached video callback; aborted in vulkan_copy_staging_to_dynamic"
}
]
+9
View File
@@ -0,0 +1,9 @@
{
"build_identity": "build identity: 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5",
"passed": true,
"cycles": 8,
"exports": 25,
"api": 1,
"missing_rejected": true,
"unknown_symbol_rejected": true
}
@@ -0,0 +1,8 @@
{
"build_identity": "build identity: 9b287ca7e0d4a04721c68d30fb88b678a62cc568c1f2d44b75716b464c21b6d5",
"passed": true,
"selection_rejected": true,
"content_rejected": true,
"menu_context_preserved": true,
"menu_core_load": true
}
-31
View File
@@ -1,31 +0,0 @@
# 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.
+11
View File
@@ -77,3 +77,14 @@
# 0071 expose PS5 pad through the binding/menu joypad interface and auto profile
0071-register-native-ps5-joypad
# Refuse failed core selection before frontend teardown; stop a failed runloop.
0072-native-core-failure-recovery
# Route libretro dynamic loading through the native ELF core loader.
0073-native-elf-core-loader
# Opt-in native loader diagnostic verifies rejected loads preserve the live menu.
0074-native-core-recovery-diagnostic
0075-native-core-rgba-upload
+19
View File
@@ -0,0 +1,19 @@
/* Convert libretro's little-endian XRGB8888 to the sampled RGBA8 upload format. */
#include <cstddef>
#include <cstdint>
extern "C" void ps5_core_frame_rgba(void *output, size_t stride, const void *input, size_t pitch,
unsigned width, unsigned height)
{
auto *dst = static_cast<uint8_t *>(output);
auto *src = static_cast<const uint8_t *>(input);
for (unsigned y = 0; y < height; ++y, dst += stride, src += pitch)
for (unsigned x = 0; x < width; ++x)
{
const uint8_t blue = src[4 * x], green = src[4 * x + 1], red = src[4 * x + 2];
dst[4 * x] = red;
dst[4 * x + 1] = green;
dst[4 * x + 2] = blue;
dst[4 * x + 3] = 255;
}
}
+2
View File
@@ -0,0 +1,2 @@
/* Public runtime addresses are relocated by the native title builder. */
#include "../build/core_imports.inc"
+106
View File
@@ -0,0 +1,106 @@
/* Opt-in native loader diagnostics: no content is loaded or executed. */
#include <cstdio>
#include <cstring>
#include <libretro.h>
extern "C"
{
const char *ps5_frontend_build_identity();
void *ps5_core_dlopen(const char *, int);
void *ps5_core_dlsym(void *, const char *);
int ps5_core_dlclose(void *);
char *ps5_core_dlerror();
}
namespace
{
bool recovery_pending = false;
}
extern "C" bool ps5_core_recovery_test_pending()
{
bool pending = recovery_pending;
recovery_pending = false;
return pending;
}
extern "C" void ps5_core_loader_test_if_requested()
{
FILE *control = std::fopen("/app0/core-loader-test.txt", "r");
if (!control)
return;
std::fclose(control);
std::remove("/app0/core-loader-test.txt");
recovery_pending = true;
const char *exports[] = {"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"};
bool missing = ps5_core_dlopen("/app0/cores/__missing_loader_test__.so", 0) == nullptr &&
ps5_core_dlerror() != nullptr;
unsigned cycles = 0, found = 0, api = 0;
char name[64] = {}, error[512] = {};
bool unknown_symbol = false;
for (unsigned i = 0; i < 8; ++i)
{
void *core = ps5_core_dlopen("/app0/cores/fceumm_libretro.so", 0);
if (!core)
{
std::snprintf(error, sizeof(error), "%s", ps5_core_dlerror());
break;
}
found = 0;
for (const char *symbol : exports)
found += ps5_core_dlsym(core, symbol) != nullptr;
if (found == 25)
{
auto version =
reinterpret_cast<unsigned (*)()>(ps5_core_dlsym(core, "retro_api_version"));
auto info = reinterpret_cast<void (*)(retro_system_info *)>(
ps5_core_dlsym(core, "retro_get_system_info"));
retro_system_info system{};
api = version();
info(&system);
std::snprintf(name, sizeof(name), "%s", system.library_name ? system.library_name : "");
unknown_symbol = ps5_core_dlsym(core, "__missing_export__") == nullptr &&
ps5_core_dlerror() != nullptr;
}
if (ps5_core_dlclose(core) || found != 25 || api != RETRO_API_VERSION ||
std::strcmp(name, "FCEUmm"))
break;
++cycles;
}
const bool passed = missing && unknown_symbol && cycles == 8;
std::fprintf(stderr,
"core loader test: passed=%d cycles=%u exports=%u api=%u name=%s error=%s\n",
passed, cycles, found, api, name, error);
if (FILE *out = std::fopen("/app0/core-loader-test.json", "w"))
{
std::fprintf(out,
"{\"build_identity\":\"%s\",\"passed\":%s,\"cycles\":%u,\"exports\":%u,"
"\"api\":%u,\"missing_rejected\":%s,\"unknown_symbol_rejected\":%s}\n",
ps5_frontend_build_identity(), passed ? "true" : "false", cycles, found, api,
missing ? "true" : "false", unknown_symbol ? "true" : "false");
std::fclose(out);
}
}
+454
View File
@@ -0,0 +1,454 @@
/* PS5 RetroArch - bounded in-process ELF loader for libretro cores.
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
* Runtime imports are bound to the title's public SDK imports, not websrv hooks.
*/
#include <elf.h>
#include <cerrno>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#ifndef R_X86_64_JUMP_SLOT
#define R_X86_64_JUMP_SLOT R_X86_64_JMP_SLOT
#endif
extern "C" void *ps5_core_import(const char *name);
namespace
{
constexpr size_t page = 0x4000;
constexpr size_t max_file = 128 * 1024 * 1024;
constexpr size_t max_image = 512 * 1024 * 1024;
struct Module
{
Module *next;
unsigned references;
char path[1024];
unsigned char *file;
size_t file_size;
size_t file_span;
unsigned char *base;
size_t span;
const Elf64_Phdr *ph;
size_t ph_count;
const Elf64_Sym *symbols;
size_t symbol_count;
const char *strings;
size_t string_size;
};
Module *modules = nullptr;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
thread_local char error_text[512];
bool fail(const char *format, ...)
{
va_list args;
va_start(args, format);
std::vsnprintf(error_text, sizeof(error_text), format, args);
va_end(args);
std::fprintf(stderr, "core loader: %s\n", error_text);
return false;
}
bool range(uint64_t offset, uint64_t size, uint64_t limit)
{
return offset <= limit && size <= limit - offset;
}
template <typename T> const T *records(const Module *m, uint64_t offset, uint64_t count)
{
if (offset % alignof(T) || count > m->file_size / sizeof(T) ||
!range(offset, count * sizeof(T), m->file_size))
return nullptr;
return reinterpret_cast<const T *>(m->file + offset);
}
const char *string_at(const Module *m, uint64_t offset)
{
if (offset >= m->string_size || !std::memchr(m->strings + offset, 0, m->string_size - offset))
return nullptr;
return m->strings + offset;
}
bool mapped(const Module *m, uint64_t address, uint64_t bytes, unsigned flags = 0)
{
for (size_t i = 0; i < m->ph_count; ++i)
{
const auto &p = m->ph[i];
if (p.p_type == PT_LOAD && (p.p_flags & flags) == flags && address >= p.p_vaddr &&
range(address - p.p_vaddr, bytes, p.p_memsz))
return true;
}
return false;
}
void release(Module *m)
{
if (m->base)
munmap(m->base, m->span);
if (m->file)
munmap(m->file, m->file_span);
std::free(m);
}
bool symbol_address(Module *m, size_t index, uintptr_t &address)
{
if (index >= m->symbol_count)
return fail("relocation symbol index out of range");
const auto &s = m->symbols[index];
const char *name = string_at(m, s.st_name);
if (!name)
return fail("unterminated symbol name");
const unsigned type = ELF64_ST_TYPE(s.st_info);
if (type != STT_NOTYPE && type != STT_FUNC && type != STT_OBJECT && type != STT_SECTION)
return fail("unsupported symbol type %u for %s", type, name);
if (s.st_shndx == SHN_UNDEF)
{
address = reinterpret_cast<uintptr_t>(ps5_core_import(name));
if (!address && ELF64_ST_BIND(s.st_info) != STB_WEAK)
return fail("unresolved native runtime import: %s", name);
return true;
}
if (s.st_shndx == SHN_ABS)
{
address = s.st_value;
return true;
}
if (s.st_shndx >= SHN_LORESERVE || !mapped(m, s.st_value, s.st_size ? s.st_size : 1))
return fail("symbol outside loaded segments: %s", name);
address = reinterpret_cast<uintptr_t>(m->base) + s.st_value;
return true;
}
bool load(Module *m)
{
const int fd = open(m->path, O_RDONLY);
if (fd < 0)
return fail("open failed errno=%d", errno);
const off_t size = lseek(fd, 0, SEEK_END);
if (size < off_t(sizeof(Elf64_Ehdr)) || size > off_t(max_file) || lseek(fd, 0, SEEK_SET) < 0)
{
const int error = errno;
close(fd);
return fail("invalid core file size/seek: size=%lld errno=%d", (long long)size, error);
}
m->file_size = size_t(size);
m->file_span = (m->file_size + page - 1) & ~(page - 1);
void *buffer =
mmap(nullptr, m->file_span, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (buffer == MAP_FAILED)
{
const int error = errno;
close(fd);
return fail("core file buffer mapping failed: bytes=%zu errno=%d", m->file_span, error);
}
m->file = static_cast<unsigned char *>(buffer);
size_t done = 0;
while (done < m->file_size)
{
const size_t remaining = m->file_size - done;
const ssize_t count = read(fd, m->file + done, remaining < 65536 ? remaining : 65536);
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
{
const int error = count < 0 ? errno : 0;
close(fd);
return fail("core read failed: offset=%zu size=%zu count=%lld errno=%d", done,
m->file_size, (long long)count, error);
}
done += size_t(count);
}
close(fd);
const auto *eh = records<Elf64_Ehdr>(m, 0, 1);
const unsigned char expected[] = {0x7f, 'E', 'L', 'F', 2, 1, 1, 9};
if (!eh || std::memcmp(eh->e_ident, expected, sizeof(expected)) || eh->e_type != ET_DYN ||
eh->e_machine != EM_X86_64 || eh->e_version != EV_CURRENT || eh->e_ehsize != sizeof(*eh) ||
eh->e_phentsize != sizeof(Elf64_Phdr) || eh->e_shentsize != sizeof(Elf64_Shdr) ||
!eh->e_phnum || eh->e_phnum > 32 || !eh->e_shnum || eh->e_shnum > 4096)
return fail("expected PS5 ELF64 shared core with program/section headers");
m->ph = records<Elf64_Phdr>(m, eh->e_phoff, eh->e_phnum);
m->ph_count = eh->e_phnum;
const auto *sections = records<Elf64_Shdr>(m, eh->e_shoff, eh->e_shnum);
if (!m->ph || !sections)
return fail("truncated ELF header tables");
const Elf64_Phdr *dynamic = nullptr;
size_t loads = 0, sym_index = 0;
for (size_t i = 0; i < m->ph_count; ++i)
{
const auto &p = m->ph[i];
if (p.p_type == PT_TLS || p.p_type == PT_INTERP)
return fail("TLS and ELF interpreters are not supported by this core loader");
if (p.p_type == PT_DYNAMIC)
{
if (dynamic)
return fail("duplicate dynamic segment");
dynamic = &p;
}
if (p.p_type != PT_LOAD)
continue;
if (!p.p_memsz)
{
if (p.p_filesz)
return fail("nonempty file segment without memory");
continue;
}
if (p.p_filesz > p.p_memsz || !range(p.p_offset, p.p_filesz, m->file_size) ||
!range(p.p_vaddr, p.p_memsz, max_image - page) || p.p_vaddr % page ||
p.p_offset % page || p.p_align < page || (p.p_align & (p.p_align - 1)) ||
(p.p_flags & (PF_W | PF_X)) == (PF_W | PF_X))
return fail("invalid, unaligned or writable-executable load segment");
const uint64_t end = (p.p_vaddr + p.p_memsz + page - 1) & ~(page - 1);
for (size_t j = 0; j < i; ++j)
{
const auto &q = m->ph[j];
const uint64_t qend = (q.p_vaddr + q.p_memsz + page - 1) & ~(page - 1);
if (q.p_type == PT_LOAD && q.p_memsz && p.p_vaddr < qend && q.p_vaddr < end)
return fail("overlapping load segment pages");
}
if (end > m->span)
m->span = end;
++loads;
}
if (!loads || !dynamic || !m->span)
return fail("missing load/dynamic segments");
for (size_t i = 0; i < eh->e_shnum; ++i)
{
const auto &s = sections[i];
if (s.sh_type != SHT_DYNSYM)
continue;
if (m->symbols || s.sh_entsize != sizeof(Elf64_Sym) || s.sh_size % sizeof(Elf64_Sym) ||
s.sh_link >= eh->e_shnum || s.sh_size / sizeof(Elf64_Sym) > 100000)
return fail("invalid dynamic symbol table");
m->symbol_count = s.sh_size / sizeof(Elf64_Sym);
m->symbols = records<Elf64_Sym>(m, s.sh_offset, m->symbol_count);
sym_index = i;
const auto &str = sections[s.sh_link];
if (!m->symbols || !m->symbol_count || str.sh_type != SHT_STRTAB ||
!range(str.sh_offset, str.sh_size, m->file_size))
return fail("invalid symbol strings");
m->strings = reinterpret_cast<const char *>(m->file + str.sh_offset);
m->string_size = str.sh_size;
}
if (!m->symbols)
return fail("missing dynamic symbols");
const auto *dt =
records<Elf64_Dyn>(m, dynamic->p_offset, dynamic->p_filesz / sizeof(Elf64_Dyn));
if (!dt || dynamic->p_filesz % sizeof(Elf64_Dyn))
return fail("invalid dynamic entries");
bool terminated = false;
for (size_t i = 0; i < dynamic->p_filesz / sizeof(Elf64_Dyn); ++i)
{
auto tag = dt[i].d_tag;
auto value = dt[i].d_un.d_val;
if (tag == DT_NULL)
{
terminated = true;
break;
}
if (tag == DT_NEEDED)
{
const char *name = string_at(m, value);
if (!name || (std::strcmp(name, "libkernel_web.sprx") &&
std::strcmp(name, "libSceLibcInternal.sprx") &&
std::strcmp(name, "libScePosixForWebKit.sprx")))
return fail("unsupported core dependency");
}
// Constructors, TLS and C++ unwind registration need a later loader extension.
if (tag == DT_TEXTREL || ((tag == DT_INIT || tag == DT_FINI || tag == DT_INIT_ARRAYSZ ||
tag == DT_FINI_ARRAYSZ || tag == DT_PREINIT_ARRAYSZ ||
tag == DT_RELSZ || tag == 35 /* DT_RELRSZ */) &&
value))
return fail("unsupported core initialization/relocation feature");
}
if (!terminated)
return fail("unterminated dynamic entries");
// Reserve aligned memory without ever making a page writable and executable.
void *allocation =
mmap(nullptr, m->span + page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (allocation == MAP_FAILED)
return fail("core mmap failed errno=%d", errno);
const uintptr_t address = reinterpret_cast<uintptr_t>(allocation);
const uintptr_t aligned = (address + page - 1) & ~(page - 1);
const size_t prefix = aligned - address;
const size_t suffix = page - prefix;
if (prefix)
munmap(allocation, prefix);
if (suffix)
munmap(reinterpret_cast<void *>(aligned + m->span), suffix);
m->base = reinterpret_cast<unsigned char *>(aligned);
for (size_t i = 0; i < m->ph_count; ++i)
if (m->ph[i].p_type == PT_LOAD && m->ph[i].p_filesz)
std::memcpy(m->base + m->ph[i].p_vaddr, m->file + m->ph[i].p_offset, m->ph[i].p_filesz);
// Resolve every import before publishing the handle or executing code.
for (size_t i = 1; i < m->symbol_count; ++i)
{
uintptr_t unused;
if (!symbol_address(m, i, unused))
return false;
}
size_t relocations = 0;
for (size_t i = 0; i < eh->e_shnum; ++i)
{
const auto &s = sections[i];
if (!(s.sh_flags & SHF_ALLOC))
continue;
if (s.sh_type == SHT_REL || s.sh_type == 19 /* SHT_RELR */)
return fail("unsupported relocation encoding");
if (s.sh_type != SHT_RELA)
continue;
if (s.sh_link != sym_index || s.sh_entsize != sizeof(Elf64_Rela) ||
s.sh_size % sizeof(Elf64_Rela))
return fail("invalid relocation section");
const size_t count = s.sh_size / sizeof(Elf64_Rela);
const auto *rel = records<Elf64_Rela>(m, s.sh_offset, count);
if (!rel)
return fail("truncated relocation section");
for (size_t j = 0; j < count; ++j)
{
const auto &r = rel[j];
const unsigned type = ELF64_R_TYPE(r.r_info);
if (type == R_X86_64_NONE)
continue;
if (!mapped(m, r.r_offset, sizeof(uint64_t), PF_W))
return fail("relocation destination outside writable segment");
uintptr_t value = 0;
if (type == R_X86_64_RELATIVE)
{
if (ELF64_R_SYM(r.r_info) || r.r_addend < 0 || !mapped(m, uint64_t(r.r_addend), 1))
return fail("invalid relative relocation");
value = reinterpret_cast<uintptr_t>(m->base) + r.r_addend;
}
else if (type == R_X86_64_64 || type == R_X86_64_GLOB_DAT || type == R_X86_64_JUMP_SLOT)
{
if (!symbol_address(m, ELF64_R_SYM(r.r_info), value))
return false;
value += uintptr_t(r.r_addend);
}
else
return fail("unsupported x86-64 relocation %u", type);
std::memcpy(m->base + r.r_offset, &value, sizeof(value));
++relocations;
}
}
if (mprotect(m->base, m->span, PROT_NONE))
return fail("core protect reserve failed errno=%d", errno);
for (size_t i = 0; i < m->ph_count; ++i)
{
const auto &p = m->ph[i];
if (p.p_type != PT_LOAD || !p.p_memsz)
continue;
int protection = ((p.p_flags & PF_R) ? PROT_READ : 0) |
((p.p_flags & PF_W) ? PROT_WRITE : 0) |
((p.p_flags & PF_X) ? PROT_EXEC : 0);
const size_t length = (p.p_memsz + page - 1) & ~(page - 1);
if (mprotect(m->base + p.p_vaddr, length, protection))
return fail("core segment mprotect flags=%u failed errno=%d", p.p_flags, errno);
}
std::fprintf(stderr, "core loader: ready symbols=%zu relocations=%zu mapped_bytes=%zu\n",
m->symbol_count, relocations, m->span);
return true;
}
} // namespace
extern "C" void *ps5_core_dlopen(const char *path, int)
{
pthread_mutex_lock(&mutex);
error_text[0] = 0;
if (!path || !*path || std::strlen(path) >= sizeof(Module::path))
{
fail("invalid core path (no process-global lookup)");
pthread_mutex_unlock(&mutex);
return nullptr;
}
unsigned count = 0;
for (auto *m = modules; m; m = m->next)
{
++count;
if (!std::strcmp(path, m->path))
{
++m->references;
pthread_mutex_unlock(&mutex);
return m;
}
}
auto *m = count < 8 ? static_cast<Module *>(std::calloc(1, sizeof(Module))) : nullptr;
if (!m)
fail("core handle capacity/allocation exhausted");
if (m)
{
std::strcpy(m->path, path);
if (!load(m))
{
release(m);
m = nullptr;
}
else
{
m->references = 1;
m->next = modules;
modules = m;
}
}
pthread_mutex_unlock(&mutex);
return m;
}
extern "C" void *ps5_core_dlsym(void *handle, const char *name)
{
pthread_mutex_lock(&mutex);
error_text[0] = 0;
void *answer = nullptr;
for (auto *m = modules; m; m = m->next)
if (m == handle && name)
for (size_t i = 1; i < m->symbol_count; ++i)
{
const auto &s = m->symbols[i];
const unsigned bind = ELF64_ST_BIND(s.st_info);
if (s.st_shndx == SHN_UNDEF || (bind != STB_GLOBAL && bind != STB_WEAK) ||
ELF64_ST_VISIBILITY(s.st_other) == STV_HIDDEN ||
std::strcmp(string_at(m, s.st_name), name))
continue;
uintptr_t address;
if (symbol_address(m, i, address))
answer = reinterpret_cast<void *>(address);
break;
}
if (!answer && handle)
fail("core export not found: %s", name ? name : "<null>");
pthread_mutex_unlock(&mutex);
return answer;
}
extern "C" int ps5_core_dlclose(void *handle)
{
pthread_mutex_lock(&mutex);
error_text[0] = 0;
for (Module **at = &modules; *at; at = &(*at)->next)
if (*at == handle)
{
Module *m = *at;
if (--m->references == 0)
{
*at = m->next;
release(m);
}
pthread_mutex_unlock(&mutex);
return 0;
}
fail("invalid core handle on close");
pthread_mutex_unlock(&mutex);
return -1;
}
extern "C" char *ps5_core_dlerror()
{
return error_text[0] ? error_text : nullptr;
}
+68
View File
@@ -0,0 +1,68 @@
/* Opt-in refused-load test against the actual menu/task pipeline. No ROM runs. */
extern "C"
{
#include <runloop.h>
#include <paths.h>
#include <gfx/video_driver.h>
#include <tasks/task_content.h>
bool ps5_core_recovery_test_pending();
void *ps5_core_dlopen(const char *, int);
void *ps5_core_dlsym(void *, const char *);
int ps5_core_dlclose(void *);
const char *ps5_frontend_build_identity();
}
#include <cstdio>
#include <cstring>
extern "C" void ps5_core_recovery_test_if_requested()
{
if (!ps5_core_recovery_test_pending())
return;
auto *runloop = runloop_state_get_ptr();
auto *video = video_state_get_ptr();
void *context = video->data;
bool passed = false;
bool selection = true, content = true;
bool menu_core_load = false;
if (context && (runloop->flags & RUNLOOP_FLAG_IS_INITED) &&
runloop->current_core_type == CORE_TYPE_DUMMY)
{
char old_paths[3][PATH_MAX_LENGTH];
const rarch_path_type slots[] = {RARCH_PATH_CORE, RARCH_PATH_CORE_LAST, RARCH_PATH_CONTENT};
for (unsigned i = 0; i < 3; ++i)
std::snprintf(old_paths[i], sizeof(old_paths[i]), "%s", path_get(slots[i]));
// Exercise allocation, I/O and execution with the full frontend resident.
if (void *core = ps5_core_dlopen("/app0/cores/fceumm_libretro.so", 0))
{
auto api = reinterpret_cast<unsigned (*)()>(ps5_core_dlsym(core, "retro_api_version"));
menu_core_load = api && api() == RETRO_API_VERSION;
menu_core_load = ps5_core_dlclose(core) == 0 && menu_core_load;
}
content_ctx_info_t info{};
const char *missing = "/app0/cores/__missing_loader_test__.so";
selection =
task_push_load_new_core(missing, nullptr, &info, CORE_TYPE_PLAIN, nullptr, nullptr);
content = task_push_load_content_with_new_core_from_menu(
missing, "/app0/__not_loaded_test__.nes", &info, CORE_TYPE_PLAIN, nullptr, nullptr);
passed = menu_core_load && !selection && !content && context == video->data &&
(runloop->flags & RUNLOOP_FLAG_IS_INITED) &&
runloop->current_core_type == CORE_TYPE_DUMMY;
for (unsigned i = 0; i < 3; ++i)
path_set(slots[i], old_paths[i]);
}
std::fprintf(stderr,
"core recovery test: passed=%d selection_accepted=%d content_accepted=%d "
"menu_context_preserved=%d menu_core_load=%d\n",
passed, selection, content, context && context == video->data, menu_core_load);
if (FILE *out = std::fopen("/app0/core-recovery-test.json", "w"))
{
std::fprintf(out,
"{\"build_identity\":\"%s\",\"passed\":%s,\"selection_rejected\":%s,\"content_"
"rejected\":%s,\"menu_context_preserved\":%s,\"menu_core_load\":%s}\n",
ps5_frontend_build_identity(), passed ? "true" : "false",
selection ? "false" : "true", content ? "false" : "true",
context && context == video->data ? "true" : "false",
menu_core_load ? "true" : "false");
std::fclose(out);
}
}
+2
View File
@@ -47,6 +47,7 @@ extern "C" int sceSystemServiceHideSplashScreen();
extern "C" void ps5_vulkan_profile_init();
extern "C" void ps5_audio_test_if_requested();
extern "C" void ps5_core_loader_test_if_requested();
extern "C" const char *ps5_frontend_build_identity()
{
return PS5_RETROARCH_BUILD_ID;
@@ -219,6 +220,7 @@ int main()
argv_with_extras[base_count + extra_count] = nullptr;
ps5_audio_test_if_requested();
ps5_core_loader_test_if_requested();
const int status =
rarch_main(static_cast<int>(base_count + extra_count), argv_with_extras, nullptr);
+31
View File
@@ -0,0 +1,31 @@
"""Verify core upload channel order, alpha, row pitch and in-place writes."""
import ctypes
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
class CoreFrame(unittest.TestCase):
def test_xrgb_upload_with_padding_and_in_place(self):
with tempfile.TemporaryDirectory() as td:
lib = Path(td) / 'frame.so'
subprocess.run(['c++', '-shared', '-fPIC', '-Wall', '-Wextra', '-Werror',
str(ROOT / 'src/core_frame_ps5.cpp'), '-o', str(lib)], check=True)
convert = ctypes.CDLL(str(lib)).ps5_core_frame_rgba
convert.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p,
ctypes.c_size_t, ctypes.c_uint, ctypes.c_uint]
source = bytes([0, 0, 255, 0, 0, 255, 0, 128, 9, 9, 9, 9,
255, 0, 0, 0, 0x56, 0x34, 0x12, 1, 9, 9, 9, 9])
expected = bytes([255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 255, 255, 0x12, 0x34, 0x56, 255])
src = ctypes.create_string_buffer(source)
dst = ctypes.create_string_buffer(bytes([0xAA]) * 32)
convert(dst, 16, src, 12, 2, 2)
self.assertEqual(dst.raw[:8] + dst.raw[16:24], expected)
self.assertEqual(dst.raw[8:16] + dst.raw[24:32], bytes([0xAA]) * 16)
convert(src, 12, src, 12, 2, 2)
self.assertEqual(src.raw[:8] + src.raw[12:20], expected)
self.assertEqual(src.raw[8:12] + src.raw[20:24], bytes([9]) * 8)
+125
View File
@@ -0,0 +1,125 @@
"""Execute the real loader on small relocated ELFs; reject malformed input."""
import ctypes
from pathlib import Path
import struct
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
class CoreLoader(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.tmp.cleanup)
cls.folder = Path(cls.tmp.name)
c = cls.folder / 'core.c'
c.write_text('''extern int native_add(int);
int counter;
int *pointer = &counter;
int (*call_import)(int) = native_add;
int run(void) { return call_import(++*pointer); }
''')
cls.core = cls.folder / 'fixture.so'
subprocess.run(['cc', '-shared', '-nostdlib', '-fPIC', str(c),
'-Wl,-z,max-page-size=16384', '-Wl,-T,' + str(ROOT / 'tooling/native/ps5-core.ld'),
'-o', str(cls.core)], check=True, capture_output=True)
data = bytearray(cls.core.read_bytes())
data[7] = 9
cls.core.write_bytes(data)
cls.original = bytes(data)
wrapper = cls.folder / 'runtime.cpp'
wrapper.write_text('''#include <cstring>
static bool allow = true;
extern "C" void allow_import(bool value) { allow = value; }
static int native_add(int value) { return value + 40; }
extern "C" void *ps5_core_import(const char *name) {
return allow && !std::strcmp(name, "native_add") ? reinterpret_cast<void *>(&native_add) : nullptr;
}
''')
harness = cls.folder / 'loader.so'
subprocess.run(['c++', '-std=c++20', '-shared', '-fPIC', '-pthread', '-O1', '-g',
'-Wall', '-Wextra', '-Werror', str(ROOT / 'src/core_loader_ps5.cpp'),
str(wrapper), '-o', str(harness)], check=True, capture_output=True)
cls.lib = ctypes.CDLL(str(harness))
cls.lib.ps5_core_dlopen.argtypes = [ctypes.c_char_p, ctypes.c_int]
cls.lib.ps5_core_dlopen.restype = ctypes.c_void_p
cls.lib.ps5_core_dlsym.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
cls.lib.ps5_core_dlsym.restype = ctypes.c_void_p
cls.lib.ps5_core_dlclose.argtypes = [ctypes.c_void_p]
cls.lib.ps5_core_dlerror.restype = ctypes.c_char_p
cls.lib.allow_import.argtypes = [ctypes.c_bool]
def setUp(self):
self.lib.allow_import(True)
def open(self, path):
return self.lib.ps5_core_dlopen(str(path).encode(), 0)
def reject(self, data, reason):
path = self.folder / 'invalid.so'
path.write_bytes(data)
handle = self.open(path)
if handle:
self.lib.ps5_core_dlclose(handle)
self.assertFalse(handle)
self.assertIn(reason, self.lib.ps5_core_dlerror().decode())
def test_relocations_imports_reference_count_and_reload(self):
for _ in range(8):
a = self.open(self.core)
self.assertTrue(a, self.lib.ps5_core_dlerror())
b = self.open(self.core)
self.assertEqual(a, b)
run = ctypes.CFUNCTYPE(ctypes.c_int)(self.lib.ps5_core_dlsym(a, b'run'))
self.assertEqual(run(), 41)
self.assertEqual(self.lib.ps5_core_dlclose(a), 0)
self.assertEqual(run(), 42)
self.assertFalse(self.lib.ps5_core_dlsym(b, b'absent'))
self.assertEqual(self.lib.ps5_core_dlclose(b), 0)
self.assertEqual(self.lib.ps5_core_dlclose(1), -1)
def test_missing_import_is_rejected_then_valid_load_recovers(self):
self.lib.allow_import(False)
self.assertFalse(self.open(self.core))
self.assertIn(b'unresolved native runtime import: native_add', self.lib.ps5_core_dlerror())
self.lib.allow_import(True)
handle = self.open(self.core)
self.assertTrue(handle)
self.lib.ps5_core_dlclose(handle)
def test_bad_header_and_header_table_bounds(self):
self.reject(b'not an ELF', 'file size')
data = bytearray(self.original)
data[7] = 0
self.reject(data, 'expected PS5 ELF64')
data = bytearray(self.original)
struct.pack_into('<Q', data, 32, 2**64 - 8)
self.reject(data, 'header tables')
def test_bad_segments_and_unsupported_tls(self):
phoff = struct.unpack_from('<Q', self.original, 32)[0]
data = bytearray(self.original)
struct.pack_into('<I', data, phoff + 4, 7)
self.reject(data, 'writable-executable')
data = bytearray(self.original)
struct.pack_into('<Q', data, phoff + 8, len(data) + 0x4000)
self.reject(data, 'load segment')
data = bytearray(self.original)
struct.pack_into('<I', data, phoff, 7) # PT_TLS
self.reject(data, 'TLS')
def test_bad_relocation_target_and_encoding(self):
shoff = struct.unpack_from('<Q', self.original, 40)[0]
count = struct.unpack_from('<H', self.original, 60)[0]
offset = next(struct.unpack_from('<Q', self.original, shoff + i * 64 + 24)[0]
for i in range(count)
if struct.unpack_from('<I', self.original, shoff + i * 64 + 4)[0] == 4)
data = bytearray(self.original)
struct.pack_into('<Q', data, offset, 2**64 - 8)
self.reject(data, 'relocation destination')
data = bytearray(self.original)
struct.pack_into('<I', data, offset + 8, 5) # R_X86_64_COPY
self.reject(data, 'unsupported x86-64 relocation')
+61
View File
@@ -0,0 +1,61 @@
"""Run actual upstream core-selection code against refused/successful loads."""
import importlib.util
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
class CoreRecovery(unittest.TestCase):
def test_refused_selection_never_changes_running_core_type(self):
source = (ROOT / 'vendor/retroarch/tasks/task_content.c').read_text()
spec = importlib.util.spec_from_file_location('patches', ROOT / 'tools/apply-port-patches.py')
patches = importlib.util.module_from_spec(spec)
spec.loader.exec_module(patches)
patched = source
for path, anchor, replacement, marker in patches.EDITS:
if path == 'tasks/task_content.c' and marker not in patched:
self.assertIn(anchor, patched)
patched = patched.replace(anchor, replacement, 1)
fixture = r'''
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#define HAVE_DYNAMIC 1
#define RARCH_PATH_CORE 0
#define RARCH_PATH_CORE_LAST 1
#define CMD_EVENT_LOAD_CORE 2
enum rarch_core_type { CORE_TYPE_DUMMY, CORE_TYPE_PLAIN };
typedef void content_ctx_info_t;
typedef void (*retro_task_callback_t)(void);
static const char *paths[2] = {"", ""};
static bool accepted;
static int loads, changes;
static void path_set(int key, const char *path) { paths[key] = path; }
static const char *path_get(int key) { return paths[key]; }
static bool string_is_equal(const char *a, const char *b) { return !strcmp(a,b); }
static bool command_event(int command, void *data) { ++loads; return accepted; }
static void runloop_set_current_core_type(enum rarch_core_type type, bool explicit_type) { ++changes; }
'''
main = r'''
int main(void) {
accepted = false;
if (task_push_load_new_core("bad.so", "", NULL, CORE_TYPE_PLAIN, NULL, NULL)) return 1;
if (loads != 1 || changes != 0) return 2;
accepted = true;
if (!task_push_load_new_core("good.so", "", NULL, CORE_TYPE_PLAIN, NULL, NULL)) return 3;
return loads == 2 && changes == 1 ? 0 : 4;
}
'''
with tempfile.TemporaryDirectory() as td:
for name, text, expected in [('before', source, 1), ('after', patched, 0)]:
body = re.search(r'bool task_push_load_new_core\(.*?\n\}', text, re.S)[0]
path = Path(td) / (name + '.c')
path.write_text(fixture + body + main)
binary = Path(td) / name
subprocess.run(['cc', '-std=c99', str(path), '-o', str(binary)], check=True)
result = subprocess.run([str(binary)], check=False)
self.assertEqual(result.returncode, expected)
+1 -1
View File
@@ -300,7 +300,7 @@ class PortPatches(unittest.TestCase):
change deliberately, in a commit that says why.
"""
self.assertEqual(
len(self.blocks()), 108,
len(self.blocks()), 116,
"the patch count changed: if a block was added or removed on purpose, "
"update this number in the same commit and say why in its message")
+33
View File
@@ -0,0 +1,33 @@
/* PS5 libretro shared cores: distinct 16 KiB code, read-only and data pages. */
OUTPUT_FORMAT("elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
PHDRS {
text PT_LOAD FLAGS (5);
ro PT_LOAD FLAGS (4);
data PT_LOAD FLAGS (6);
dynamic PT_DYNAMIC FLAGS (6);
}
SECTIONS {
.text 0 : ALIGN(CONSTANT(MAXPAGESIZE)) { *(.text .text.*) *(.plt .plt.*) } : text
. = ALIGN(CONSTANT(MAXPAGESIZE));
.rodata : { *(.rodata .rodata.*) } : ro
.eh_frame_hdr : { KEEP(*(.eh_frame_hdr)) } : ro
.eh_frame : { KEEP(*(.eh_frame)) } : ro
.gcc_except_table : { *(.gcc_except_table .gcc_except_table.*) } : ro
.note.gnu.build-id : { *(.note.gnu.build-id) } : ro
.dynsym : { *(.dynsym) } : ro
.gnu.hash : { *(.gnu.hash) } : ro
.hash : { *(.hash) } : ro
.dynstr : { *(.dynstr) } : ro
.rela.dyn : { *(.rela.dyn) } : ro
.rela.plt : { *(.rela.plt) } : ro
. = ALIGN(CONSTANT(MAXPAGESIZE));
.data : { *(.data .data.*) } : data
.data.rel.ro : { *(.data.rel.ro .data.rel.ro.*) } : data
.got : { *(.got) *(.got.plt) } : data
.preinit_array : { KEEP(*(.preinit_array)) } : data
.init_array : { KEEP(*(.init_array .init_array.* .ctors .ctors.*)) } : data
.fini_array : { KEEP(*(.fini_array .fini_array.* .dtors .dtors.*)) } : data
.dynamic : { *(.dynamic) } : data : dynamic
.bss : { *(.bss .bss.*) *(COMMON) } : data
}
+8
View File
@@ -30,6 +30,14 @@ from pathlib import Path
# (file, anchor, inserted-before-anchor, already-present-marker)
EDITS = [
('gfx/drivers/vulkan.c', ' vk->tex_fmt = video->rgb32 ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R5G6B5_UNORM_PACK16;', ' /* patches/series, 0075: matching sampled RGBA staging and dynamic core frames. */\n vk->tex_fmt = video->rgb32 ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_R5G6B5_UNORM_PACK16;', 'patches/series, 0075: matching'),
('gfx/drivers/vulkan.c', ' if (frame != chain->texture.mapped)\n {', ' /* patches/series, 0075: libretro XRGB pixels become RGBA upload bytes. */\n if (vk->video.rgb32)\n {\n extern void ps5_core_frame_rgba(void *, size_t, const void *, size_t, unsigned, unsigned);\n ps5_core_frame_rgba(chain->texture.mapped, chain->texture.stride,\n frame, pitch, frame_width, frame_height);\n }\n else if (frame != chain->texture.mapped)\n {', 'patches/series, 0075: libretro XRGB'),
('retroarch.c', ' settings = config_get_ptr();\n\n ui_companion_driver_init_first(', ' /* patches/series, 0074: opt-in refused-core test against the live menu. */\n {\n extern void ps5_core_recovery_test_if_requested(void);\n ps5_core_recovery_test_if_requested();\n }\n settings = config_get_ptr();\n\n ui_companion_driver_init_first(', 'patches/series, 0074: opt-in refused-core test'),
('libretro-common/dynamic/dylib.c', '#include <dlfcn.h>', '#include <dlfcn.h>\n/* patches/series, 0073: native title core loading, not payload dlfcn hooks. */\nextern void *ps5_core_dlopen(const char *, int);\nextern void *ps5_core_dlsym(void *, const char *);\nextern int ps5_core_dlclose(void *);\nextern char *ps5_core_dlerror(void);\n#define dlopen ps5_core_dlopen\n#define dlsym ps5_core_dlsym\n#define dlclose ps5_core_dlclose\n#define dlerror ps5_core_dlerror', 'patches/series, 0073: native title core loading'),
('tasks/task_content.c', ' /* Load core */\n command_event(CMD_EVENT_LOAD_CORE, NULL);\n\n /* Load content */', ' /* patches/series, 0072: keep the live menu when core selection fails. */\n if (!command_event(CMD_EVENT_LOAD_CORE, NULL))\n {\n RARCH_ERR("[PS5] Core selection failed; preserving the running frontend.\\n");\n ret = false;\n goto end;\n }\n\n /* Load content */', 'patches/series, 0072: keep the live menu'),
('tasks/task_content.c', ' /* Load core */\n command_event(CMD_EVENT_LOAD_CORE, NULL);\n\n#ifndef HAVE_DYNAMIC', ' /* patches/series, 0072: do not mark a refused core as selected. */\n if (!command_event(CMD_EVENT_LOAD_CORE, NULL))\n return false;\n\n#ifndef HAVE_DYNAMIC', 'patches/series, 0072: do not mark'),
('tasks/task_content.c', ' command_event(CMD_EVENT_LOAD_CORE, NULL);\n\n runloop_set_current_core_type(CORE_TYPE_PLAIN, true);', ' /* patches/series, 0072: reject contentless core failure before teardown. */\n if (!command_event(CMD_EVENT_LOAD_CORE, NULL))\n {\n ret = false;\n goto end;\n }\n\n runloop_set_current_core_type(CORE_TYPE_PLAIN, true);', 'patches/series, 0072: reject contentless'),
('retroarch.c', ' ret = runloop_iterate();', ' /* patches/series, 0072: failed reinitialization cannot poll freed drivers. */\n if (!(runloop_st->flags & RUNLOOP_FLAG_IS_INITED))\n {\n RARCH_ERR("[PS5] Frontend initialization failed; leaving the main loop safely.\\n");\n break;\n }\n ret = runloop_iterate();', 'patches/series, 0072: failed reinitialization'),
# 0071: native joypad discovery, binding capture and built-in defaults.
('input/input_driver.h', 'extern input_device_driver_t ps4_joypad;', 'extern input_device_driver_t ps4_joypad;\nextern input_device_driver_t ps5_joypad;', 'extern input_device_driver_t ps5_joypad;'),
('input/input_driver.c', ' &null_joypad,\n NULL,\n};', ' &ps5_joypad,\n &null_joypad,\n NULL,\n};', ' &ps5_joypad,'),
+1 -1
View File
@@ -50,7 +50,7 @@ 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'
LDFLAGS="-nostdlib -nodefaultlibs -Wl,--build-id=sha1 -Wl,-T,$root/tooling/native/ps5-core.ld"
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"
+2
View File
@@ -52,6 +52,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"
python3 "$root/tools/core-imports.py"
# 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
@@ -153,6 +154,7 @@ root = pathlib.Path(sys.argv[1])
inputs = sorted(p for p in (root / "src").rglob("*") if p.is_file())
inputs += [root / name for name in (
"build/ra/libretroarch.a", "build/ra-conf/config.h", "tools/build-title.sh",
"build/core_imports.inc", "build/cores/stage/cores/fceumm_libretro.so",
"tools/build.sh", "tools/retroarch-flags.sh")]
inputs += [pathlib.Path(name) for name in sys.argv[2:]]
digest = hashlib.sha256()
+1 -1
View File
@@ -56,7 +56,7 @@ def inspect(path):
'<IIQQQQQQ', data, phoff + i * phsize)
if ptype != 1:
continue
if (align < 0x4000 or offset % 0x4000 != address % 0x4000
if (align < 0x4000 or flags & 3 == 3 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})
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Generate direct native-runtime bindings for the built core's imports."""
from pathlib import Path
import re
import subprocess
ROOT = Path(__file__).resolve().parent.parent
core = ROOT / 'build/cores/stage/cores/fceumm_libretro.so'
symbols = subprocess.check_output(['readelf', '--dyn-syms', '-W', str(core)], text=True)
imports = {}
for line in symbols.splitlines():
fields = line.split()
if len(fields) < 8 or fields[6] != 'UND':
continue
name, kind = fields[7], fields[3]
if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name) or kind not in ('FUNC', 'OBJECT'):
raise SystemExit(f'Unsupported native core import: {name} ({kind})')
imports[name] = kind
lines = ['// Generated by tools/core-imports.py; never edit.', '#include <cstring>']
aliases = {'opendir': 'ps5_opendir', 'readdir': 'ps5_readdir', 'closedir': 'ps5_closedir'}
for index, (name, kind) in enumerate(sorted(imports.items())):
target = aliases.get(name, name)
declaration = f'void core_import_{index}()' if kind == 'FUNC' else f'char core_import_{index}[]'
lines.append(f'extern "C" {declaration} asm("{target}");')
lines.extend(['extern "C" void *ps5_core_import(const char *name)', '{'])
for index, name in enumerate(sorted(imports)):
lines.append(f' if (!std::strcmp(name, "{name}")) return reinterpret_cast<void *>(&core_import_{index});')
lines.extend([' return nullptr;', '}'])
(ROOT / 'build/core_imports.inc').write_text('\n'.join(lines) + '\n')
print(f'core imports: {len(imports)} explicit native bindings generated')
+20 -3
View File
@@ -40,14 +40,16 @@ deploy=1
watch=30
profile=0
audio_test=0
core_test=0
while (( $# )); do
case "$1" in
--no-build) build=0 ;;
--no-deploy) deploy=0 ;;
--watch) shift; watch=${1:?--watch needs seconds} ;;
--audio-test) audio_test=1 ;;
--core-test) core_test=1 ;;
--gpu-profile) shift; profile=${1:?--gpu-profile needs seconds} ;;
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test]" >&2; exit 2 ;;
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test]" >&2; exit 2 ;;
esac
shift
done
@@ -149,7 +151,7 @@ fi
# appeared. A run that does not ask for extras must not inherit them, so the file
# is removed on every run - before the launch, because deleting it afterwards
# would leave it for the next one if this run dies.
python3 - "$title_id" "$profile" "$audio_test" <<'PY'
python3 - "$title_id" "$profile" "$audio_test" "$core_test" <<'PY'
import importlib.util, sys
spec = importlib.util.spec_from_file_location("dt", "tools/deploy-title.py")
dt = importlib.util.module_from_spec(spec); spec.loader.exec_module(dt)
@@ -161,6 +163,13 @@ with connect(**dt.load_settings()) as ftp:
print(" cleared a leftover args.txt from the console")
except Exception:
pass
control = f"/data/homebrew/{sys.argv[1]}/core-loader-test.txt"
remove_if_present(ftp, control)
if int(sys.argv[4]):
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/core-loader-test.json")
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/core-recovery-test.json")
ftp.storbinary(f"STOR {control}", io.BytesIO(b"native core loader test\n"))
print(" armed native core loader test (no game)")
control = f"/data/homebrew/{sys.argv[1]}/gpu-profile.txt"
remove_if_present(ftp, control)
if int(sys.argv[2]):
@@ -233,7 +242,7 @@ except Exception as error:
PY
# --- preserve development logs and optional buffered timing ------------------
python3 - "$title_id" "$profile" "$stamp" "$audio_test" "$expected_identity" <<'PY'
python3 - "$title_id" "$profile" "$stamp" "$audio_test" "$expected_identity" "$core_test" <<'PY'
import importlib.util, json, sys
from pathlib import Path
sys.path.insert(0, "tools")
@@ -243,7 +252,10 @@ dt = importlib.util.module_from_spec(spec); spec.loader.exec_module(dt)
with connect(**dt.load_settings()) as ftp:
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/gpu-profile.txt")
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/audio-test.txt")
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/core-loader-test.txt")
names = ["retroarch.log"]
if int(sys.argv[6]):
names.extend(["core-loader-test.json", "core-recovery-test.json"])
if int(sys.argv[4]):
names.append("audio-test.json")
if int(sys.argv[2]):
@@ -257,6 +269,11 @@ with connect(**dt.load_settings()) as ftp:
if name == "retroarch.log":
if f"build identity: {expected}" not in target.read_text(errors="replace"):
raise SystemExit("RetroArch log is stale or logging failed: current build identity absent")
if name in ("core-loader-test.json", "core-recovery-test.json"):
report = json.loads(target.read_text())
if report.get("build_identity") != f"build identity: {expected}" or not report.get("passed"):
raise SystemExit("Native core loader test failed or has stale identity")
print(" native core loader test PASS")
if name == "audio-test.json":
report = json.loads(target.read_text())
checks = (