mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
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:
1 parent
d337e2c11b
commit
63a440414a
31 files changed
+1443
-92
No files matched your search
+68
-55
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Public runtime addresses are relocated by the native title builder. */
|
||||
#include "../build/core_imports.inc"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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})
|
||||
|
||||
@@ -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
@@ -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 = (
|
||||
|
||||
Reference in new issue
Block a user