From d2a91b395be28c24d3e2022cdfca27107ef5cfda Mon Sep 17 00:00:00 2001 From: Mihawk <163614687+mihawk-99@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:00:01 -0400 Subject: [PATCH] Port native mGBA and fix core/menu lifecycle rendering Build pinned mGBA with this PS5 SDK, official metadata and both GB/GBA engines. Support validated C initializer arrays, native directory/time imports and an explicit config path. Map large title/core allocations so GBA archives load. Correct mGBA callback colours and cached frame ownership, crop padded core textures during presentation, and use the native shell exit after cleanup. Verified: bash tools/verify.sh (format, unit, build, integration, evidence; 58 tests), then bash tools/run-title.sh --no-build --core-test=mgba --watch 180. Owner confirms GB/GBC/GBA loading, correct colours/native Quit, and clean menu and next game. Final capture has zero Vulkan refusals, GPU failure records or kernel fatal-signal reports; normal frontend exit occurred before window end. Evidence: evidence/mgba-native/{capture,expectation,iterations,build,abi}.json, loader/recovery reports and exact linked-driver hashes. Replay with bash tools/verify.sh evidence. Final build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337. Patch count is deliberately 139: archive diagnostics, menu/cache ownership and padded-source sampling edits. PS5_Vulkan was not modified. Saves/states, long-run timing and a fresh FCEUmm/XMB transition matrix remain unverified. --- Makefile | 5 + docs/ACTIVE.md | 138 ++++++++---------- docs/DEPLOYMENT.md | 9 ++ docs/FINDINGS.md | 36 +++++ docs/PHASE_LOG.md | 59 ++++++++ docs/REFERENCE.md | 109 +++++++++++++- docs/TESTING.md | 45 +++++- evidence/mgba-native/abi.json | 124 ++++++++++++++++ evidence/mgba-native/build.json | 138 ++++++++++++++++++ evidence/mgba-native/capture.json | 71 +++++++++ .../mgba-native/console-observations.json | 36 +++++ evidence/mgba-native/driver-archives.json | 6 + evidence/mgba-native/expectation.json | 19 +++ evidence/mgba-native/iterations.json | 32 ++++ evidence/mgba-native/loader.json | 1 + evidence/mgba-native/recovery.json | 1 + patches/mgba/native-config-path.patch | 31 ++++ patches/mgba/native-locale-type.patch | 78 ++++++++++ patches/mgba/native-xrgb-output.patch | 32 ++++ patches/series | 6 + src/core_frame_ps5.cpp | 13 ++ src/core_loader_probe.cpp | 40 ++++- src/core_loader_ps5.cpp | 46 +++++- src/core_recovery_test.cpp | 12 +- src/main.cpp | 16 ++ src/memory_ps5.cpp | 119 +++++++++++++++ src/ps5_directory.cpp | 14 ++ src/ps5_directory.h | 1 + tests/memory_ps5_test.cpp | 78 ++++++++++ tests/ps5_directory_test.cpp | 22 +++ tests/test_core_frame.py | 36 +++++ tests/test_core_imports.py | 40 +++++ tests/test_core_loader.py | 53 +++++++ tests/test_frontend.py | 2 +- tests/test_memory_ps5.py | 17 +++ tests/test_mgba_video.py | 26 ++++ tooling/mgba/CMakeLists.txt | 25 ++++ tooling/mgba/ps5-toolchain.cmake | 16 ++ tooling/mgba/ps5-video.h | 10 ++ tools/apply-port-patches.py | 24 +++ tools/build-mgba.sh | 74 ++++++++++ tools/build-title.sh | 23 ++- tools/core-imports.py | 70 ++++++--- tools/run-title.sh | 18 ++- 44 files changed, 1635 insertions(+), 136 deletions(-) create mode 100644 evidence/mgba-native/abi.json create mode 100644 evidence/mgba-native/build.json create mode 100644 evidence/mgba-native/capture.json create mode 100644 evidence/mgba-native/console-observations.json create mode 100644 evidence/mgba-native/driver-archives.json create mode 100644 evidence/mgba-native/expectation.json create mode 100644 evidence/mgba-native/iterations.json create mode 100644 evidence/mgba-native/loader.json create mode 100644 evidence/mgba-native/recovery.json create mode 100644 patches/mgba/native-config-path.patch create mode 100644 patches/mgba/native-locale-type.patch create mode 100644 patches/mgba/native-xrgb-output.patch create mode 100644 src/memory_ps5.cpp create mode 100644 tests/memory_ps5_test.cpp create mode 100644 tests/test_core_imports.py create mode 100644 tests/test_memory_ps5.py create mode 100644 tests/test_mgba_video.py create mode 100644 tooling/mgba/CMakeLists.txt create mode 100644 tooling/mgba/ps5-toolchain.cmake create mode 100644 tooling/mgba/ps5-video.h create mode 100755 tools/build-mgba.sh diff --git a/Makefile b/Makefile index a052319..21e905b 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,10 @@ HOST_UNIT_TEST := build/tests/demo_renderer_tests all: app build: app +.PHONY: mgba +mgba: + @bash tools/build-mgba.sh + .PHONY: fceumm fceumm: @bash tools/build-fceumm.sh @@ -165,6 +169,7 @@ help: 'make test-unit Run host-native GoogleTest application tests' \ 'make test-integration Run host tooling integration tests' \ 'make deps Fetch native dependencies into .deps/' \ + 'make mgba Build and ABI-check the pinned PS5 mGBA core' \ 'make fceumm Build and ABI-check the pinned PS5 FCEUmm core' \ 'make pacbrew Fetch the pinned PacBrew ports sysroot' \ 'make pacbrew-list List PacBrew pkg-config module names' \ diff --git a/docs/ACTIVE.md b/docs/ACTIVE.md index 792ed30..1ab7649 100644 --- a/docs/ACTIVE.md +++ b/docs/ACTIVE.md @@ -4,92 +4,82 @@ _Updated: 2026-09-19_ ## Now -**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. - -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. +**mGBA GB/GBC/GBA archive loading and clean content transitions are verified.** +Owner confirms colours, native Quit, and the final “menu and next game are clean” +result. This closes the mGBA port step; no next core is selected by this file. ## Verified build and evidence -- 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`. +- Frontend identity: `82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337`. +- mGBA source: `7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6`. +- mGBA SHA-256: `feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6`. + Core size 3,255,056 bytes; 25 libretro callbacks; 21 bounded init callbacks. + Build/ABI/source/metadata hashes are in `evidence/mgba-native/`. +- All five host gates pass, 58 tests. Final build log: + `/tmp/mgba-uv-format-final-verify.log`. ELF: `klog/mgba-82024b89.elf`. +- Console command: `tools/run-title.sh --no-build --core-test=mgba --watch 180`. + Eight load/export/API/identity/unload cycles and live-menu recovery pass. + Deliberate missing-core errors belong to the successful recovery test. +- Final owner confirmation: “Menu and next game are clean.” Captured sequence: + dummy 320x240 -> GBA 240x160 -> dummy -> GB/GBC 160x144 -> dummy. Earlier owner + tests individually confirmed GB, GBC and GBA archives load, correct colours, + and working native Quit. Do not infer measured FPS from core-declared rates. +- Raw logs: `klog/mgba-uv-crop-run.log`, `klog/run-PPSA99169-155435.log`, + `klog/mgba-transition-live-retroarch.log`, `klog/mgba-transition-live-trace.txt`. + Frontend returned status 0 and called native exit before the runner's window + ended; this is not 180 seconds of uninterrupted gameplay. +- PS5_Vulkan was only read, never edited by this task. The owner develops it + concurrently. `evidence/mgba-native/driver-archives.json` records the actual + archive hashes, unchanged through this build; no source revision is inferred. -## Limits and remaining acceptance +## What changed -- 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. +- `make mgba`: pinned upstream CMake libretro build, native SDK, GB/GBC/GBA, + official metadata in info/ and cores/, no SDL/Qt or payload CRT in the core. +- Native loader validates relocated C init arrays before running callbacks; + generated imports cover both cores, native directory rewind and local time. +- Explicit `/app0/config/mgba` avoids the crashing native getcwd import. + Mapped allocations >=1 MiB allow the previously failing 16 MiB 7z decode. +- mGBA native XBGR is converted to declared XRGB without changing its renderer + buffer. Vulkan menu textures consistently use RGBA; writable software + framebuffer sharing is disabled to preserve cached source pixels. +- Patch 0078 crops core UVs to initialized pixels in padded textures, using + per-sync VBOs. It fixes the observed stale image/flicker after Close Content. +- Native Quit calls `sceSystemServiceLoadExec("exit", nullptr)` after cleanup. + Failed iterations and their evidence are retained in `evidence/mgba-native/`. -## Preserved platform functionality +## Limits -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. Useful frontend/core/audio/error logging is retained. +- Save RAM/state round trips, RTC correctness, long-run A/V sync and measured + performance are not verified. Native-owned realloc remains on the native heap. +- mGBA's 16-bit-only colour correction/interframe blending paths are not enabled + in this XRGB8888 build. Arbitrary Slang presets/history and linear-filter edge + behaviour are separate acceptance work. +- The final transition capture uses mGBA and RGUI; it does not independently + establish a fresh FCEUmm/XMB transition matrix. FCEUmm remains included with + its earlier verified core unchanged (`016a47d`, `evidence/native-core-loading/`). +- Loader rejects TLS, legacy init/fini, nonempty finalizer/preinit arrays, + unsupported relocations and additional dependencies. It is not a general + dynamic linker; a faulty core can still crash the shared process. -Config saving and directory browsing are verified in `evidence/native-paths/`. -Managed directories are repaired to `0777`; FTP upload/readback/cleanup is in -`evidence/ftp-directory-permissions/`. Deployments preserve live config/content. +## Preserved platform and files -## User file locations +XMB remains default, RGUI selectable; Vulkan remains the active GPU route and +`video_ps5` remains the selectable CPU fallback. Native audio, analog menu +navigation/binding capture, config persistence and directory browsing remain. +Useful frontend/core/audio/error logs are retained. Managed directories use 0777; +deployment preserves live settings and games. No ROM or BIOS is shipped. -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 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. +FTP base `/data/homebrew/PPSA99169/` corresponds to in-title `/app0`. +Use cores/, content/, system/, config/retroarch.cfg, savefiles/ and savestates/. +See `docs/DEPLOYMENT.md` for the build/upload procedure. ## Operating notes 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. +existing title; the runner checks idle before deployment and launch. Core/game +selection is manual. Keep private filenames and console details in ignored logs. 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. +Preserve live frontend logs before relaunch replaces them. Owner can supply +images; a matching camera cadence can hide flicker. Do not infer duration or +manual closure solely from the runner's final process count. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index c6e1517..b84444f 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -202,3 +202,12 @@ 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`. + + +`make mgba` builds and checks the mGBA shared core without uploading it. The title +build includes `mgba_libretro.so` alongside FCEUmm and installs its `.info` in both +metadata locations. Deploy the rebuilt title with the new core: its import table +now covers both cores. Use `--core-test=mgba` for the optional loader diagnostic. +Place user GB/GBC/GBA games in `content/` (or another browsable user folder), select +mGBA manually, and capture the gameplay log before closing/reopening the title. +No ROM or BIOS is included in the build. diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index 52c2046..d6048ca 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -2708,3 +2708,39 @@ 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. + + +## 2026-09-19 — mGBA and content transition lifetime + +mGBA needs 21 C log-category initializers. The loader now validates the bounded, +relocated callback table and every executable target before invoking any entry, +once per mapped image. Eight real-core load/unload cycles and a load with the +frontend resident passed. Native getcwd crashed in mCoreConfigPortablePath; +explicit `/app0/config/mgba` avoids that import. GBA 7z extraction then failed a +16 MiB allocation before the core received content. Routing large title/core +allocations to tracked anonymous mappings resolved that observed failure. + +The core's 32-bit renderer produces XBGR while libretro declares XRGB. A separate +callback conversion preserves the native image. Frontend menu decode/upload now +uses RGBA consistently. Refusing a writable software framebuffer prevents the +XRGB-to-RGBA upload from mutating cached source pixels during paused menu frames. +Owner confirmed colours and native Quit fixed, but Close Content still flickered +for both mGBA and FCEUmm and polluted later games. This was a distinct defect. + +The existing texture workaround widens images to 256-byte rows; uploads populate +only logical pixels. Menu/font UVs compensated, but the filter-chain quad still +sampled 0..1 across physical width. Tiny blank frames and unaligned GB/GBA widths +therefore exposed unwritten padding. Patch 0078 supplies sampled width separately, +crops both triangles, and isolates mutable VBOs by pass and retired sync slot. +Substituted image formats now determine padded width consistently. Owner confirms +“Menu and next game are clean.” The captured final transitions were GBA -> menu +-> GB/GBC-sized content -> menu. This does not independently verify every shader +or a fresh FCEUmm/XMB matrix. PS5_Vulkan was not edited; linked archive hashes are +recorded because the sibling is concurrently developed. + +The old CRT return path called exit(0) then produced SIGSYS. Calling native +sceSystemServiceLoadExec("exit", nullptr) after frontend cleanup produced shell +LoadExec termination without fatal signal reports, also confirmed by the owner. +Evidence and failed iterations: `evidence/mgba-native/`; replay with +`bash tools/verify.sh evidence`. Saves, state restoration and long-run timing are +not established by these loading/transition tests. diff --git a/docs/PHASE_LOG.md b/docs/PHASE_LOG.md index 29d724b..ec057ba 100644 --- a/docs/PHASE_LOG.md +++ b/docs/PHASE_LOG.md @@ -2069,3 +2069,62 @@ 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. + + +## 2026-09-19 — Native mGBA archives and content lifecycle + +Added `make mgba` and the normal title build/stage integration using pinned +upstream CMake libretro source 7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6. +Both GB and GBA engines are enabled; official metadata is staged in info/ and +cores/. The core has no SDL/Qt or payload CRT. CMake is used because it is this +revision's maintained libretro target. Native directory/time adapters and 21 +validated init callbacks support its ABI. Pins and reasons for toolchain/link +flags are recorded in REFERENCE.md and the generated evidence build manifest. + +Iterations, all run with manual archive selection: + +- `656d34aa…`, `klog/run-PPSA99169-150419.log`: loader/recovery passed, but GB/GBC + crashed in mCoreConfigPortablePath at native getcwd; GBA failed before core + entry. Explicit /app0/config/mgba removed getcwd/getenv dependencies. +- `9c7baa14…`, `klog/run-PPSA99169-151335.log`: owner confirmed GB/GBC load. + Archive diagnostic isolated a null 16 MiB allocation, extraction result 2; + errno=2 was stale. Owner manually quit, exit(0) then SIGSYS. +- `773678fa…`, `klog/run-PPSA99169-152050.log`: tracked mmap allocations >=1 MiB + enabled GBA archive loading. Owner reported swapped colours, menu flicker and + Quit crash. The raw lld invocation initially rejected -Wl wrapper syntax; + corrected --wrap flags passed all gates (56 tests). Native realloc ownership + remains native; this is not an interposer inside the native libc library. +- `ef7004d3…`, `klog/run-PPSA99169-153046.log`: mGBA XBGR->XRGB callback copy, + consistent RGBA menu textures and immutable cached frames fixed colours. + Native LoadExec exit fixed Quit (zero fatal reports). Owner confirmed both; + Close Content still flickered for mGBA and FCEUmm. All gates, 57 tests. +- `6a761ad7…`, `klog/mgba-transition-capture-run.log`: diagnostic screenshot + trigger built and passed gates, but console became busy during deployment; + launch refused. Trigger was removed; owner supplied an RGUI photo/video. +- UV crop host development: format lint first required formatting; regression + initially compared double and float UVs exactly and was corrected to float32 + expectations. First crop build passed 58 tests/all gates. A follow-up made + substituted sampled format determine the physical width consistently. +- Final `82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337`: + `bash tools/verify.sh` passed all five gates, 58 tests, log + `/tmp/mgba-uv-format-final-verify.log`. mGBA SHA-256: + feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6. + `bash tools/run-title.sh --no-build --core-test=mgba --watch 180` first refused + a busy console. After owner confirmed idle, upload/readback matched and the + launch succeeded. `klog/mgba-uv-crop-run.log`, kernel + `klog/run-PPSA99169-155435.log`: one current-build launch, zero Vulkan refusals, + zero GPU API failure records and zero kernel fatal signal reports. Eight + loader cycles and actual failed-load recovery passed. Owner: “Menu and next + game are clean.” Live frontend log records GBA -> menu -> GB/GBC -> menu, + then frontend return 0/native exit before the window ended. Runner's wording + “exited on its own” is not evidence of a crash or uninterrupted gameplay. + +Patch 0078 crops core sampling to initialized logical pixels in padded images; +it uses per-pass/per-sync VBOs and preserves logical size semantics. No switch +away from Vulkan and no PS5_Vulkan edits. The sibling is independently developed; +linked archive hashes, unchanged through this build, are in the evidence. + +Evidence: `evidence/mgba-native/`, replay `bash tools/verify.sh evidence`. +Private ROM names, console credentials and raw captures are not committed. +Save/state round trips, long-run A/V sync, measured performance, Slang presets, +and a fresh FCEUmm/XMB transition matrix are not claimed. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 3019e91..258b1e7 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -414,16 +414,22 @@ 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 +`tools/core-imports.py` derives the union of required native bindings from the +explicitly shipped FCEUmm and mGBA ELFs; those bindings and both cores participate in the title build +identity. Directory imports use this port's directory adapters, including `rewinddir`. +The `localtime_r` binding uses RetroArch's existing locked `rtime_localtime` +helper, initialized before gameplay. 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 +linker: no TLS, ELF interpreter, legacy DT_INIT/DT_FINI, finalizers, C++ unwind +registration, REL/RELR, or additional dependent shared libraries. Bounded +DT_INIT_ARRAY callbacks are supported: every relocated pointer must target this +module's executable segment, and all callbacks are checked before any run. +Initializers run once per new mapping after protection, before exposing the +handle; a reference-counted reopen does not rerun them. Unsupported imports and relocations fail explicitly. Adding another core requires reviewing this contract and extending both host tests and target diagnostics as needed. @@ -441,3 +447,96 @@ 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. + + +## mGBA core build + +`make mgba` runs `tools/build-mgba.sh` with the same SDK and ELF linker layout as +FCEUmm. The pinned source is the [official libretro mGBA tree](https://github.com/libretro/mgba/tree/7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6). +Source and `.info` inputs have verified SHA-256 digests; a fresh extraction goes +to `build/cores/mgba/`. CMake is required by this source revision. The wrapper in +`tooling/mgba/` selects only the shared libretro target, both GB and GBA engines, +software XRGB8888 output, and native kernel_web/libc/Posix imports. External +frontend/media dependencies are disabled: RetroArch supplies presentation, +audio, archive extraction and content services. No SDL, Qt or OpenGL backend is +added. This core uses Vulkan for frontend presentation, not hardware emulation. + +The toolchain restricts header/library/package searches to the SDK. Function +probes link against the actual native stubs without a CRT and are never executed; +static-library probes falsely reported unavailable locale functions. The SDK +provides locale types/headers but lacks several locale runtime functions, so the +small `patches/mgba/native-locale-type.patch` uses a private formatting type for +upstream's string-locale fallback; `HAVE_XLOCALE` is removed from this target. +The patch is applied with zero fuzz to the fresh source. `HAVE_LOCALTIME_R` names +the frontend adapter, avoiding upstream's unimplemented fallback. `-z undefs` +permits that frontend-provided import; the generated binding table must still +resolve every core import when the title links. + +`--build-id=sha1` keeps output repeatable. `GIT_CEILING_DIRECTORIES` prevents +upstream's version generator from discovering RetroArch's parent Git checkout; +the archive reports its own 0.11.0 version, with the exact source revision in +`build/cores/mgba/build.json`. That report also contains the SDK wrapper, source, +metadata and port-input hashes. No source checkout or environment is modified. + +Outputs are `build/cores/stage/cores/mgba_libretro.so` and +`build/cores/stage/info/mgba_libretro.info`. The title build stages both cores, +with metadata in both `info/` and `cores/` for existing saved configurations. +Changing the shipped core list requires rebuilding the frontend's native import +bindings. Console loading and manual gameplay remain separate target checks. + + +`MGBA_PS5` selects `/app0/config/mgba` for mGBA's optional standalone config +and disables `portable.ini` working-directory discovery. The zero-fuzz +`native-config-path.patch` avoids native `getcwd`/Unix home discovery; core +options continue to come from RetroArch's libretro environment. + +## Large application allocations + +`src/memory_ps5.cpp` maps allocations of at least 1 MiB with the native kernel +and keeps smaller requests on libc. The title linker wraps `malloc`, `calloc`, +`realloc` and `free` together, including the generated core binding references. +This flag is required because a 16 MiB 7z extraction allocation returned null +through the native libc import. The donor also uses mappings for large buffers; +this implementation tracks mappings in a mutex-protected list and never reads +headers before unknown pointers. Buffers allocated inside native libraries +remain libc-owned and are forwarded to native free/realloc. Resize of a mapped +buffer preserves its contents and leaves the old allocation intact on failure. +Resize of a libc-owned buffer remains a libc operation, including growth beyond +the threshold. Do not pass a mapped pointer to an API that privately frees or +reallocates it inside an unwrapped native library. + +Patch 0076 reports 7z allocation/header/extraction failures without filenames +and checks allocation before copying extracted content. It logs errors only. + + +The mGBA libretro callback converts native XBGR to its declared XRGB8888 format +using `ps5-video.h` and a separate bounded video buffer. It leaves the renderer's +pixels intact for duplicate/cached frames. Upstream's 16-bit-only colour +correction/interframe-blending routines are not enabled by this 32-bit build. + +Patch 0077 sets RGBA menu image decoding on every Vulkan context initialization +and requests RGBA textures for those decoded pixels. This is separate from core +XRGB frames. The Vulkan software-framebuffer callback returns false: callers +must retain their own original pixels, since exposing RGBA staging memory as +writable XRGB would let upload conversion mutate cached frames while paused. +FCEUmm uses its own buffer when that optional callback is unavailable. + +On normal frontend return, `catchReturnFromMain` requests +`sceSystemServiceLoadExec("exit", nullptr)` and waits for asynchronous shell +termination on success. RetroArch has already cleaned up drivers and saved its +configuration. A failed request is logged and falls back to the existing CRT +termination path; it must not be described as a successful clean exit. + +### Padded core presentation + +Patch 0078 extends the existing 256-byte texture-row workaround to software +core presentation. The filter chain carries logical width separately from the +sampled image width and crops both triangle UVs to that initialized region. +Each pass owns one small vertex buffer per retired sync slot, so CPU updates do +not overwrite another in-flight frame. Swapchain reconstruction releases these +buffers. Hardware-provided images and intermediate pass outputs keep their own +extents; sampled format substitution is applied before computing padded width. +The host regression covers FCEUmm, the blank menu frame, GBA and GB widths across +repeated transitions. Console flicker acceptance is recorded in `evidence/mgba-native/`. This is +not validation of arbitrary Slang presets, history sampling or linear-filter +edge behaviour; those remain separate milestones. diff --git a/docs/TESTING.md b/docs/TESTING.md index 8f4a4e2..8265a9c 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -187,7 +187,8 @@ when no core is selected. `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, +reloads, once-per-mapping initializers, invalid initializer tables/callbacks, +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 @@ -212,3 +213,45 @@ 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. + + +For mGBA use `tools/run-title.sh --no-build --core-test=mgba --watch 180`. +`--core-test` without a value retains the FCEUmm diagnostic. Reports contain the +selected core name, checked along with build identity and pass status. Only these +two names are accepted. The owner selects content manually after startup. + +`tests/test_core_imports.py` checks union/deduplication across two cores, object +and untyped imports, directory/time adapters, and conflicting or TLS import +rejection. The directory adapter fixture also checks rewind after EOF, rewind of +a partially consumed batch and failed seek without discarding state. The loader +constructor test was observed failing before support was added, then passing; +it also rejects callbacks into writable data without executing them. + +Record GB, GBC and GBA gameplay separately when tested. Core compilation and +metadata support for a system do not substitute for console gameplay. RTC, +optional BIOS files, saves/states, sensors, rumble and sustained performance +require their own acceptance; do not infer them from one successful game. + + +The native memory test covers 16/32 MiB allocation, zeroing, preserved data on +resize, overflow/failure keeping the original buffer, mapped-to-small resize, +foreign libc ownership and concurrent allocation/free. Target acceptance must +also load a GBA archive larger than the native heap could previously allocate; +a host allocation test alone does not prove native content loading. + + +For core lifecycle acceptance, test coloured content, open Quick Menu with the +core paused, close content to XMB, reload another game, and choose Quit. Inspect +both the live frontend log and kernel capture. Native exit(0) followed by SIGSYS +is a shutdown failure even when the user intentionally chose Quit. Check core +XBGR-to-XRGB conversion and repeated immutable cached-frame uploads on the host; +console colour/flicker confirmation remains required. + +For patch 0078, manually exercise game -> Quick Menu -> Close Content -> another +core/game, repeating with FCEUmm and mGBA. Check menu and gameplay for flicker +and stale image regions at each transition, including the RGUI configuration +in the owner's reproduction. Owner observation is required: a recording at a +matching refresh cadence may hide alternating corrupted frames. Preserve the +live frontend log before a relaunch replaces it; do not accept a clean loader +probe alone as visual acceptance. Record hashes of linked driver archives when +the sibling Vulkan repository is being developed concurrently. diff --git a/evidence/mgba-native/abi.json b/evidence/mgba-native/abi.json new file mode 100644 index 0000000..fb699bd --- /dev/null +++ b/evidence/mgba-native/abi.json @@ -0,0 +1,124 @@ +{ + "sha256": "feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6", + "bytes": 3255056, + "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", + "abort", + "calloc", + "cbrtf", + "close", + "closedir", + "div", + "exp2f", + "fclose", + "fgets", + "fopen", + "free", + "fstat", + "fsync", + "ftruncate", + "futimes", + "gettimeofday", + "hypotf", + "isspace", + "localtime", + "localtime_r", + "lseek", + "malloc", + "memcmp", + "memcpy", + "memmove", + "memset", + "mkdir", + "mktime", + "mmap", + "msync", + "munmap", + "open", + "opendir", + "printf", + "putchar", + "qsort", + "read", + "readdir", + "realloc", + "realpath", + "remove", + "rewinddir", + "setlocale", + "sincosf", + "snprintf", + "sprintf", + "sscanf", + "stat", + "strcasecmp", + "strcat", + "strchr", + "strcmp", + "strcpy", + "strdup", + "strftime", + "strlcpy", + "strlen", + "strncat", + "strncmp", + "strncpy", + "strrchr", + "strtof", + "strtol", + "strtoul", + "time", + "vprintf", + "vsnprintf", + "write" + ], + "load_segments": [ + { + "flags": 5, + "alignment": 16384 + }, + { + "flags": 4, + "alignment": 16384 + }, + { + "flags": 6, + "alignment": 16384 + } + ], + "abi_passed": true, + "console_loading_verified": false +} diff --git a/evidence/mgba-native/build.json b/evidence/mgba-native/build.json new file mode 100644 index 0000000..76baf6a --- /dev/null +++ b/evidence/mgba-native/build.json @@ -0,0 +1,138 @@ +{ + "sha256": "feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6", + "bytes": 3255056, + "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", + "abort", + "calloc", + "cbrtf", + "close", + "closedir", + "div", + "exp2f", + "fclose", + "fgets", + "fopen", + "free", + "fstat", + "fsync", + "ftruncate", + "futimes", + "gettimeofday", + "hypotf", + "isspace", + "localtime", + "localtime_r", + "lseek", + "malloc", + "memcmp", + "memcpy", + "memmove", + "memset", + "mkdir", + "mktime", + "mmap", + "msync", + "munmap", + "open", + "opendir", + "printf", + "putchar", + "qsort", + "read", + "readdir", + "realloc", + "realpath", + "remove", + "rewinddir", + "setlocale", + "sincosf", + "snprintf", + "sprintf", + "sscanf", + "stat", + "strcasecmp", + "strcat", + "strchr", + "strcmp", + "strcpy", + "strdup", + "strftime", + "strlcpy", + "strlen", + "strncat", + "strncmp", + "strncpy", + "strrchr", + "strtof", + "strtol", + "strtoul", + "time", + "vprintf", + "vsnprintf", + "write" + ], + "load_segments": [ + { + "flags": 5, + "alignment": 16384 + }, + { + "flags": 4, + "alignment": 16384 + }, + { + "flags": 6, + "alignment": 16384 + } + ], + "abi_passed": true, + "console_loading_verified": false, + "source_revision": "7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6", + "source_archive_sha256": "5cbf639e527fb586bf33e14d59037eab35f78f45154e0b79c784fff474c3bc37", + "info_revision": "5a74858ab2f7a50cebb5a6330895bc38899531c0", + "info_sha256": "64444beb8268d3a57d53a45564c55049f39ff621315401fef86e82f3849042d6", + "sdk_compiler_wrapper_sha256": "3dcf3139662db7b0f796c2073bc8f673a5557e48bc24d2e413896d33847a69db", + "port_inputs_sha256": { + "tooling/mgba/CMakeLists.txt": "71662780c9e21aa33f50a64d3302380fd5aef18e98f4881777fef9a38545250e", + "tooling/mgba/ps5-toolchain.cmake": "13d926e1c03d22eb111e7edc435cebbeeeb0405b43342a762d0023d94edb787c", + "tooling/native/ps5-core.ld": "5ffe0f0e99e23de5245afb46fa6dfda6dd860b53d942a5fe2f06eaf9e520df42", + "patches/mgba/native-locale-type.patch": "0090a7e1370e07fc795d429e812675bb7384a6234c7c8ab8f43169148c416ee9", + "patches/mgba/native-config-path.patch": "aff4c2795949269c788843406c334859c26967b1e64a34897ae852b42f663ac9", + "patches/mgba/native-xrgb-output.patch": "3e81cf749efa9a5e4a20ce7abce86e839425064d9a7250bdabe1a2e7c466666d", + "tooling/mgba/ps5-video.h": "df613d43609e52dca3d37b20803f316a3588fa2f9d7ef3221d9b3e4d49cba326" + } +} diff --git a/evidence/mgba-native/capture.json b/evidence/mgba-native/capture.json new file mode 100644 index 0000000..9c0d8f2 --- /dev/null +++ b/evidence/mgba-native/capture.json @@ -0,0 +1,71 @@ +{ + "step": "mgba-native", + "recorded": "2026-09-19", + "revision": "016a47d + mGBA native core and content lifecycle fixes", + "command": "bash tools/verify.sh; bash tools/run-title.sh --no-build --core-test=mgba --watch 180 (owner loads archives and closes content manually)", + "raw_capture": "klog/mgba-uv-crop-run.log", + "raw_lines": 10189, + "lines": [ + "frontend build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337", + "mGBA SHA-256: feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6", + "core loader test: passed=1 cycles=8 exports=25 api=1 name=mGBA", + "core recovery test: passed=1 selection_rejected=1 content_rejected=1 menu_context_preserved=1 menu_core_load=1", + "core loader: ran 21 initializers", + "[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz.", + "[INFO] [Core] Geometry: 240x160, Aspect: 1.500, FPS: 59.73, Sample rate: 65536.00 Hz.", + "[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz.", + "[INFO] [Core] Geometry: 160x144, Aspect: 1.111, FPS: 59.73, Sample rate: 131072.00 Hz.", + "[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz.", + "gpu result: vkEndCommandBuffer calls=1 failures=0 result=0", + "gpu result: vkQueueSubmit calls=1 failures=0 result=0", + "gpu result: vkEndCommandBuffer calls=2 failures=0 result=0", + "gpu result: vkQueueSubmit calls=2 failures=0 result=0", + "gpu result: vkEndCommandBuffer calls=3 failures=0 result=0", + "gpu result: vkQueueSubmit calls=3 failures=0 result=0", + "gpu result: vkEndCommandBuffer calls=4 failures=0 result=0", + "gpu result: vkQueueSubmit calls=4 failures=0 result=0", + "gpu result: vkQueuePresentKHR calls=1 failures=0 result=0", + "gpu result: swapchain image calls=1 failures=0 result=0", + "gpu result: vkQueuePresentKHR calls=2 failures=0 result=0", + "gpu result: swapchain image calls=2 failures=0 result=0", + "gpu result: vkQueuePresentKHR calls=3 failures=0 result=0", + "gpu result: swapchain image calls=3 failures=0 result=0", + "gpu result: vkQueuePresentKHR calls=4 failures=0 result=0", + "gpu result: swapchain image calls=4 failures=0 result=0", + "current-build launches: 1; Vulkan refusals: 0; GPU API failure records: 0; kernel fatal signal reports: 0", + "rarch_main returned = 0", + "native quit: frontend status = 0", + "owner: Menu and next game are clean", + "Earlier owner confirmations: GB and GBC load; GBA now loads; App native quit fixed. Colors fixed.", + "Final observed sequence: GBA -> menu -> GB/GBC-sized content -> menu; frontend called native exit before runner window ended.", + "This is not 180 seconds of continuous gameplay; no fresh FCEUmm/XMB transition matrix is claimed.", + "Host verification: PASS format unit build integration evidence; 58 tests", + "PS5_Vulkan read only; linked archive hashes unchanged through verification; driver-archives.json records bytes, not an inferred source commit." + ], + "loader": { + "build_identity": "build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337", + "core": "mgba", + "passed": true, + "cycles": 8, + "exports": 25, + "api": 1, + "missing_rejected": true, + "unknown_symbol_rejected": true + }, + "recovery": { + "build_identity": "build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337", + "core": "mgba", + "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_signal_reports": 0, + "return_to_menu_verified": true, + "next_content_clean_owner_confirmed": true, + "continuous_gameplay_seconds": null, + "build_report_note": "abi.json/build.json console_loading_verified=false is the static build report; console results are in loader.json/recovery.json and this capture." +} diff --git a/evidence/mgba-native/console-observations.json b/evidence/mgba-native/console-observations.json new file mode 100644 index 0000000..025c59e --- /dev/null +++ b/evidence/mgba-native/console-observations.json @@ -0,0 +1,36 @@ +{ + "build_identity": "82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337", + "current_build_launches": 1, + "geometry": [ + "[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz.", + "[INFO] [Core] Geometry: 240x160, Aspect: 1.500, FPS: 59.73, Sample rate: 65536.00 Hz.", + "[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz.", + "[INFO] [Core] Geometry: 160x144, Aspect: 1.111, FPS: 59.73, Sample rate: 131072.00 Hz.", + "[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz." + ], + "lines": [ + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader test: passed=1 cycles=8 exports=25 api=1 name=mGBA error=", + "core loader: ran 21 initializers", + "core recovery test: passed=1 selection_accepted=0 content_accepted=0 menu_context_preserved=1 menu_core_load=1", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "core loader: ran 21 initializers", + "rarch_main returned = 0", + "native quit: frontend status = 0" + ] +} diff --git a/evidence/mgba-native/driver-archives.json b/evidence/mgba-native/driver-archives.json new file mode 100644 index 0000000..35504d3 --- /dev/null +++ b/evidence/mgba-native/driver-archives.json @@ -0,0 +1,6 @@ +{ + "build/driver/ps5/libps5vk.ps5.a": "270adafeb70648c5ebc6c92d1ffda57931a1afabd34ed025675becb7a5da9883", + ".deps/native/vulkan-runtime/lib/libvk_runtime.ps5.a": "4106a2c56b269bc52a3976b6bc0134c00b5006ff5358a4b45355afce4a907aa7", + "build/driver/ps5/libpsbc_driver.ps5.a": "a7605370c28895a3b5a13d0718ebfea2abf1b9cf842633de07533775745b21b8", + ".deps/native/psbc/lib/libpsbc_support.ps5.a": "e36d3e2a3fa93b0a74db15b9efce7aad4ed7ac4e024de98d5f23dc5fee90acf8" +} diff --git a/evidence/mgba-native/expectation.json b/evidence/mgba-native/expectation.json new file mode 100644 index 0000000..c5ec6b4 --- /dev/null +++ b/evidence/mgba-native/expectation.json @@ -0,0 +1,19 @@ +{ + "step": "mgba-native", + "note": "Native mGBA archive loading, corrected colours, clean menu/next-content transitions and native Quit; no saves/states/performance claim.", + "must_contain": [ + "frontend build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337", + "mGBA SHA-256: feb1922c9fe9dd3424b42f538a80362d0574b4dfa0134980d3fd14c52593cff6", + "core loader test: passed=1 cycles=8 exports=25 api=1 name=mGBA", + "core recovery test: passed=1 selection_rejected=1 content_rejected=1 menu_context_preserved=1 menu_core_load=1", + "core loader: ran 21 initializers", + "[INFO] [Core] Geometry: 240x160, Aspect: 1.500, FPS: 59.73, Sample rate: 65536.00 Hz.", + "[INFO] [Core] Geometry: 160x144, Aspect: 1.111, FPS: 59.73, Sample rate: 131072.00 Hz.", + "current-build launches: 1; Vulkan refusals: 0; GPU API failure records: 0; kernel fatal signal reports: 0", + "native quit: frontend status = 0", + "owner: Menu and next game are clean", + "Host verification: PASS format unit build integration evidence; 58 tests" + ], + "must_not_contain": [], + "human_check": "Owner confirmed GB/GBC/GBA archive loading and corrected colours/native Quit in earlier runs, then clean menu and next game in the final texture-cropping run. Final capture shows mGBA GBA and GB/GBC dimensions." +} diff --git a/evidence/mgba-native/iterations.json b/evidence/mgba-native/iterations.json new file mode 100644 index 0000000..2561b28 --- /dev/null +++ b/evidence/mgba-native/iterations.json @@ -0,0 +1,32 @@ +[ + { + "identity": "656d34aa57e6172c08bfe03106468a832013a5cca37af924553410e2d9889389", + "raw": "klog/run-PPSA99169-150419.log", + "result": "Loader/recovery pass; GB/GBC SIGSEGV at native getcwd in mCoreConfigPortablePath; GBA archive fails before core entry." + }, + { + "identity": "9c7baa1430ba8b149a01ee6c25c2bd642e38ed257a5bd1243790629ea279434d", + "raw": "klog/run-PPSA99169-151335.log", + "result": "Explicit /app0/config/mgba removes getcwd/getenv; owner confirms GB/GBC load. 7z 16777216-byte allocation returns null, extract result 2. Owner manually quit; exit(0) then SIGSYS is a shutdown defect. errno=2 in allocator diagnostic was stale, not the cause." + }, + { + "identity": "773678fae328d01c5ba38e2e999a1d3ed77e45cbe8a862e2620e1ba39c1ec724", + "raw": "klog/run-PPSA99169-152050.log", + "result": "Mapped large allocations allow GBA archives to load. Owner reports swapped core/menu colours, Quick Menu flicker and native Quit crash. 56 host tests pass." + }, + { + "identity": "ef7004d32ed14ec1b4a288e8ec549fe67c068bf96722d135c131df725dc7d80a", + "raw": "klog/run-PPSA99169-153046.log", + "result": "Owner confirms colours and native Quit fixed. Zero kernel fatal signal reports, Kill for LoadExec. Close Content flicker remains on mGBA and FCEUmm. 57 host tests pass." + }, + { + "identity": "6a761ad7738f99ba28d019bcee159c694342eab21b0cd4452c231c8052ed0445", + "raw": "klog/mgba-transition-capture-run.log", + "result": "Host checks pass; console became busy during deployment, so runner refused launch. Unexercised screenshot trigger removed; owner supplied image/video." + }, + { + "identity": "82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337", + "raw": "klog/mgba-uv-crop-run.log", + "result": "Initial attempt refused upload/launch because another title was running. Owner freed console; subsequent run uploaded and verified identical bytes. Cropped source UVs and sampled-format width fix; owner confirms menu and next game clean. Final capture follows." + } +] diff --git a/evidence/mgba-native/loader.json b/evidence/mgba-native/loader.json new file mode 100644 index 0000000..35bc478 --- /dev/null +++ b/evidence/mgba-native/loader.json @@ -0,0 +1 @@ +{"build_identity":"build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337","core":"mgba","passed":true,"cycles":8,"exports":25,"api":1,"missing_rejected":true,"unknown_symbol_rejected":true} diff --git a/evidence/mgba-native/recovery.json b/evidence/mgba-native/recovery.json new file mode 100644 index 0000000..bd60f3d --- /dev/null +++ b/evidence/mgba-native/recovery.json @@ -0,0 +1 @@ +{"build_identity":"build identity: 82024b89e74bbad4ad2ce2e2916d1a49baf26ac325fef20fc71a2c168b862337","core":"mgba","passed":true,"selection_rejected":true,"content_rejected":true,"menu_context_preserved":true,"menu_core_load":true} diff --git a/patches/mgba/native-config-path.patch b/patches/mgba/native-config-path.patch new file mode 100644 index 0000000..7478d59 --- /dev/null +++ b/patches/mgba/native-config-path.patch @@ -0,0 +1,31 @@ +--- a/src/core/config.c ++++ b/src/core/config.c +@@ -233,6 +233,11 @@ + } + + void mCoreConfigDirectory(char* out, size_t outLength) { ++#ifdef MGBA_PS5 ++ /* Native titles have no Unix home/working-directory configuration. */ ++ strlcpy(out, "/app0/config/mgba", outLength); ++ mkdir(out, 0777); ++#else + char portableDir[PATH_MAX]; + mCoreConfigPortablePath(portableDir, sizeof(portableDir)); + if (portableDir[0]) { +@@ -282,6 +287,7 @@ + snprintf(out, outLength, "%s/.config/%s", home, binaryName); + mkdir(out, 0755); + #endif ++#endif + } + + void mCoreConfigPortableIniPath(char* out, size_t outLength) { +@@ -297,7 +303,7 @@ + } + WideCharToMultiByte(CP_UTF8, 0, wpath, -1, out, outLength, 0, 0); + StringCchCatA(out, outLength, PATH_SEP "portable.ini"); +-#elif defined(PSP2) || defined(GEKKO) || defined(__SWITCH__) || defined(__3DS__) ++#elif defined(PSP2) || defined(GEKKO) || defined(__SWITCH__) || defined(__3DS__) || defined(MGBA_PS5) + UNUSED(outLength); + out[0] = '\0'; + #else diff --git a/patches/mgba/native-locale-type.patch b/patches/mgba/native-locale-type.patch new file mode 100644 index 0000000..702a3a8 --- /dev/null +++ b/patches/mgba/native-locale-type.patch @@ -0,0 +1,78 @@ +--- a/include/mgba-util/formatting.h ++++ b/include/mgba-util/formatting.h +@@ -14,14 +14,19 @@ + + #ifdef HAVE_XLOCALE + #include +-#elif !defined(HAVE_LOCALE) +-typedef const char* locale_t; + #endif + +-int ftostr_l(char* restrict str, size_t size, float f, locale_t locale); ++/* The SDK can declare locale_t without providing the locale runtime API. */ ++#ifdef HAVE_LOCALE ++typedef locale_t mLocale; ++#else ++typedef const char* mLocale; ++#endif ++ ++int ftostr_l(char* restrict str, size_t size, float f, mLocale locale); + + #ifndef HAVE_STRTOF_L +-float strtof_l(const char* restrict str, char** restrict end, locale_t locale); ++float strtof_l(const char* restrict str, char** restrict end, mLocale locale); + #endif + + int ftostr_u(char* restrict str, size_t size, float f); +--- a/src/util/formatting.c ++++ b/src/util/formatting.c +@@ -7,11 +7,11 @@ + + #include + +-int ftostr_l(char* restrict str, size_t size, float f, locale_t locale) { ++int ftostr_l(char* restrict str, size_t size, float f, mLocale locale) { + #ifdef HAVE_SNPRINTF_L + return snprintf_l(str, size, locale, "%.*g", FLT_DIG, f); + #elif defined(HAVE_USELOCALE) +- locale_t old = uselocale(locale); ++ mLocale old = uselocale(locale); + int res = snprintf(str, size, "%.*g", FLT_DIG, f); + uselocale(old); + return res; +@@ -27,9 +27,9 @@ + } + + #ifndef HAVE_STRTOF_L +-float strtof_l(const char* restrict str, char** restrict end, locale_t locale) { ++float strtof_l(const char* restrict str, char** restrict end, mLocale locale) { + #ifdef HAVE_USELOCALE +- locale_t old = uselocale(locale); ++ mLocale old = uselocale(locale); + float res = strtof(str, end); + uselocale(old); + return res; +@@ -47,9 +47,9 @@ + + int ftostr_u(char* restrict str, size_t size, float f) { + #ifdef HAVE_LOCALE +- locale_t l = newlocale(LC_NUMERIC_MASK, "C", 0); ++ mLocale l = newlocale(LC_NUMERIC_MASK, "C", 0); + #else +- locale_t l = "C"; ++ mLocale l = "C"; + #endif + int res = ftostr_l(str, size, f, l); + #ifdef HAVE_LOCALE +@@ -60,9 +60,9 @@ + + float strtof_u(const char* restrict str, char** restrict end) { + #ifdef HAVE_LOCALE +- locale_t l = newlocale(LC_NUMERIC_MASK, "C", 0); ++ mLocale l = newlocale(LC_NUMERIC_MASK, "C", 0); + #else +- locale_t l = "C"; ++ mLocale l = "C"; + #endif + float res = strtof_l(str, end, l); + #ifdef HAVE_LOCALE diff --git a/patches/mgba/native-xrgb-output.patch b/patches/mgba/native-xrgb-output.patch new file mode 100644 index 0000000..43b1fb9 --- /dev/null +++ b/patches/mgba/native-xrgb-output.patch @@ -0,0 +1,32 @@ +--- a/src/platform/libretro/libretro.c ++++ b/src/platform/libretro/libretro.c +@@ -58,6 +58,10 @@ + + static retro_environment_t environCallback; + static retro_video_refresh_t videoCallback; ++#ifdef MGBA_PS5 ++#include "ps5-video.h" ++static uint32_t ps5Video[VIDEO_WIDTH_MAX * VIDEO_HEIGHT_MAX]; ++#endif + static retro_audio_sample_batch_t audioCallback; + static retro_input_poll_t inputPollCallback; + static retro_input_state_t inputCallback; +@@ -1699,7 +1703,18 @@ + videoCallback(ppOutputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + } else + #endif ++#ifdef MGBA_PS5 ++ { ++ /* Preserve the renderer's native buffer for repeated/cached frames. */ ++ for (unsigned y = 0; y < height; ++y) ++ for (unsigned x = 0; x < width; ++x) ++ ps5Video[y * VIDEO_WIDTH_MAX + x] = ++ ps5_mgba_xrgb(outputBuffer[y * VIDEO_WIDTH_MAX + x]); ++ videoCallback(ps5Video, width, height, VIDEO_WIDTH_MAX * sizeof(uint32_t)); ++ } ++#else + videoCallback(outputBuffer, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); ++#endif + } else { + videoCallback(NULL, width, height, VIDEO_WIDTH_MAX * sizeof(mColor)); + } diff --git a/patches/series b/patches/series index c2827f5..8d64a04 100644 --- a/patches/series +++ b/patches/series @@ -88,3 +88,9 @@ 0074-native-core-recovery-diagnostic 0075-native-core-rgba-upload + +0076-archive-read-error-diagnostics + +0077-vulkan-menu-colour-and-cached-frame-ownership + +0078-crop-padded-core-source diff --git a/src/core_frame_ps5.cpp b/src/core_frame_ps5.cpp index cdd041c..26c60e1 100644 --- a/src/core_frame_ps5.cpp +++ b/src/core_frame_ps5.cpp @@ -17,3 +17,16 @@ extern "C" void ps5_core_frame_rgba(void *output, size_t stride, const void *inp dst[4 * x + 3] = 255; } } + +/* Two triangle-list quads, with UVs restricted to the logical source image. + * The caller uses a buffer belonging to the current, fence-retired sync slot. */ +void ps5_core_source_quad(float *vertices, unsigned width, unsigned physical_width) +{ + const float right = physical_width > width ? float(width) / physical_width : 1.0f; + const float quad[] = { + -1, -1, 0, 0, -1, 1, 0, 1, 1, -1, right, 0, 1, -1, right, 0, -1, 1, 0, 1, 1, 1, right, 1, + 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, right, 0, 1, 0, right, 0, 0, 1, 0, 1, 1, 1, right, 1, + }; + for (unsigned i = 0; i < 48; ++i) + vertices[i] = quad[i]; +} diff --git a/src/core_loader_probe.cpp b/src/core_loader_probe.cpp index 41e0a2d..549c762 100644 --- a/src/core_loader_probe.cpp +++ b/src/core_loader_probe.cpp @@ -15,7 +15,19 @@ extern "C" namespace { bool recovery_pending = false; +const char *test_core = "fceumm"; +const char *test_path = "/app0/cores/fceumm_libretro.so"; +const char *test_name = "FCEUmm"; +} // namespace +extern "C" const char *ps5_core_loader_test_path() +{ + return test_path; } +extern "C" const char *ps5_core_loader_test_name() +{ + return test_core; +} + extern "C" bool ps5_core_recovery_test_pending() { bool pending = recovery_pending; @@ -28,8 +40,21 @@ extern "C" void ps5_core_loader_test_if_requested() FILE *control = std::fopen("/app0/core-loader-test.txt", "r"); if (!control) return; + char selection[32] = {}; + const int fields = std::fscanf(control, "%31s", selection); std::fclose(control); std::remove("/app0/core-loader-test.txt"); + if (fields == 1 && !std::strcmp(selection, "mgba")) + { + test_core = "mgba"; + test_path = "/app0/cores/mgba_libretro.so"; + test_name = "mGBA"; + } + else if (fields != 1 || std::strcmp(selection, "fceumm")) + { + std::fprintf(stderr, "core loader test: unsupported selection\n"); + return; + } recovery_pending = true; const char *exports[] = {"retro_api_version", "retro_init", @@ -63,7 +88,7 @@ extern "C" void ps5_core_loader_test_if_requested() bool unknown_symbol = false; for (unsigned i = 0; i < 8; ++i) { - void *core = ps5_core_dlopen("/app0/cores/fceumm_libretro.so", 0); + void *core = ps5_core_dlopen(test_path, 0); if (!core) { std::snprintf(error, sizeof(error), "%s", ps5_core_dlerror()); @@ -86,7 +111,7 @@ extern "C" void ps5_core_loader_test_if_requested() ps5_core_dlerror() != nullptr; } if (ps5_core_dlclose(core) || found != 25 || api != RETRO_API_VERSION || - std::strcmp(name, "FCEUmm")) + std::strcmp(name, test_name)) break; ++cycles; } @@ -96,11 +121,12 @@ extern "C" void ps5_core_loader_test_if_requested() 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::fprintf( + out, + "{\"build_identity\":\"%s\",\"core\":\"%s\",\"passed\":%s,\"cycles\":%u,\"exports\":%u," + "\"api\":%u,\"missing_rejected\":%s,\"unknown_symbol_rejected\":%s}\n", + ps5_frontend_build_identity(), test_core, passed ? "true" : "false", cycles, found, api, + missing ? "true" : "false", unknown_symbol ? "true" : "false"); std::fclose(out); } } diff --git a/src/core_loader_ps5.cpp b/src/core_loader_ps5.cpp index 04dfdf4..b407e09 100644 --- a/src/core_loader_ps5.cpp +++ b/src/core_loader_ps5.cpp @@ -244,6 +244,8 @@ bool load(Module *m) if (!dt || dynamic->p_filesz % sizeof(Elf64_Dyn)) return fail("invalid dynamic entries"); bool terminated = false; + uint64_t init_array = 0, init_bytes = 0; + bool have_init_array = false, have_init_size = false; for (size_t i = 0; i < dynamic->p_filesz / sizeof(Elf64_Dyn); ++i) { auto tag = dt[i].d_tag; @@ -261,15 +263,35 @@ bool load(Module *m) 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)) + if (tag == DT_INIT_ARRAY) + { + if (have_init_array) + return fail("duplicate initializer array"); + have_init_array = true; + init_array = value; + } + if (tag == DT_INIT_ARRAYSZ) + { + if (have_init_size) + return fail("duplicate initializer array size"); + have_init_size = true; + init_bytes = value; + } + // Legacy init functions, finalizers, TLS and C++ unwinding remain unsupported. + if (tag == DT_TEXTREL || + ((tag == DT_INIT || tag == DT_FINI || 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"); + if (init_bytes && + (!have_init_array || init_array % sizeof(uint64_t) || init_bytes % sizeof(uint64_t) || + init_bytes > 1024 * sizeof(uint64_t) || !mapped(m, init_array, init_bytes, PF_R))) + return fail("invalid initializer array range/size"); + if (have_init_array && !have_init_size) + return fail("initializer array has no size"); // 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); @@ -338,6 +360,16 @@ bool load(Module *m) ++relocations; } } + // Validate every relocated callback before executing any initializer. + const auto *initializers = + init_bytes ? reinterpret_cast(m->base + init_array) : nullptr; + const size_t initializer_count = init_bytes / sizeof(uintptr_t); + for (size_t i = 0; i < initializer_count; ++i) + { + const uintptr_t base = reinterpret_cast(m->base); + if (initializers[i] < base || !mapped(m, initializers[i] - base, 1, PF_X)) + return fail("initializer callback outside executable segment"); + } 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) @@ -352,6 +384,10 @@ bool load(Module *m) if (mprotect(m->base + p.p_vaddr, length, protection)) return fail("core segment mprotect flags=%u failed errno=%d", p.p_flags, errno); } + for (size_t i = 0; i < initializer_count; ++i) + reinterpret_cast(initializers[i])(); + if (initializer_count) + std::fprintf(stderr, "core loader: ran %zu initializers\n", initializer_count); std::fprintf(stderr, "core loader: ready symbols=%zu relocations=%zu mapped_bytes=%zu\n", m->symbol_count, relocations, m->span); return true; diff --git a/src/core_recovery_test.cpp b/src/core_recovery_test.cpp index acecfd0..39388b4 100644 --- a/src/core_recovery_test.cpp +++ b/src/core_recovery_test.cpp @@ -6,6 +6,8 @@ extern "C" #include #include bool ps5_core_recovery_test_pending(); + const char *ps5_core_loader_test_path(); + const char *ps5_core_loader_test_name(); void *ps5_core_dlopen(const char *, int); void *ps5_core_dlsym(void *, const char *); int ps5_core_dlclose(void *); @@ -32,7 +34,7 @@ extern "C" void ps5_core_recovery_test_if_requested() 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)) + if (void *core = ps5_core_dlopen(ps5_core_loader_test_path(), 0)) { auto api = reinterpret_cast(ps5_core_dlsym(core, "retro_api_version")); menu_core_load = api && api() == RETRO_API_VERSION; @@ -57,10 +59,12 @@ extern "C" void ps5_core_recovery_test_if_requested() if (FILE *out = std::fopen("/app0/core-recovery-test.json", "w")) { std::fprintf(out, - "{\"build_identity\":\"%s\",\"passed\":%s,\"selection_rejected\":%s,\"content_" + "{\"build_identity\":\"%s\",\"core\":\"%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", + ps5_frontend_build_identity(), ps5_core_loader_test_name(), + passed ? "true" : "false", selection ? "false" : "true", + content ? "false" : "true", context && context == video->data ? "true" : "false", menu_core_load ? "true" : "false"); std::fclose(out); diff --git a/src/main.cpp b/src/main.cpp index c01803c..423f33c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include @@ -229,3 +230,18 @@ int main() return status; } + +/* RetroArch has already closed its drivers and written configuration here. + * Native title shutdown goes through the shell; kernel exit(0) raises SIGSYS + * for this application instead of returning cleanly to the home screen. */ +extern "C" int sceSystemServiceLoadExec(const char *, const char *const *); +extern "C" void catchReturnFromMain(int status) +{ + ps5::debug::mark_value("native quit: frontend status", status); + std::fflush(nullptr); + const int result = sceSystemServiceLoadExec("exit", nullptr); + ps5::debug::mark_value("native quit: system service result", result); + if (result >= 0) + for (;;) + usleep(100000); /* Shell termination is asynchronous. */ +} diff --git a/src/memory_ps5.cpp b/src/memory_ps5.cpp new file mode 100644 index 0000000..25a3df3 --- /dev/null +++ b/src/memory_ps5.cpp @@ -0,0 +1,119 @@ +/* Large application buffers must not exhaust SceLibcInternal's private heap. + * Wrap only title/core references; native library allocations stay libc-owned. + * A locked list identifies mappings without reading before foreign pointers. */ +#include +#include +#include +#include +#include +#include + +extern "C" +{ + void *__real_malloc(size_t); + void *__real_calloc(size_t, size_t); + void *__real_realloc(void *, size_t); + void __real_free(void *); + void *__wrap_malloc(size_t); + void __wrap_free(void *); +} + +namespace +{ +constexpr size_t threshold = 1024 * 1024; +constexpr size_t page = 0x4000; +struct alignas(std::max_align_t) Mapping +{ + Mapping *next; + size_t requested; + size_t span; +}; +pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +Mapping *mappings = nullptr; + +Mapping **find(void *pointer) +{ + Mapping **entry = &mappings; + while (*entry && static_cast(*entry + 1) != pointer) + entry = &(*entry)->next; + return entry; +} +} // namespace + +extern "C" void *__wrap_malloc(size_t size) +{ + if (size < threshold) + return __real_malloc(size ? size : 1); + if (size > SIZE_MAX - sizeof(Mapping) - (page - 1)) + { + errno = ENOMEM; + return nullptr; + } + const size_t span = (sizeof(Mapping) + size + page - 1) & ~(page - 1); + void *memory = mmap(nullptr, span, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (memory == MAP_FAILED) + return nullptr; + auto *entry = static_cast(memory); + entry->requested = size; + entry->span = span; + pthread_mutex_lock(&lock); + entry->next = mappings; + mappings = entry; + pthread_mutex_unlock(&lock); + return entry + 1; +} + +extern "C" void __wrap_free(void *pointer) +{ + if (!pointer) + return; + pthread_mutex_lock(&lock); + Mapping **slot = find(pointer); + Mapping *entry = *slot; + if (entry) + *slot = entry->next; + pthread_mutex_unlock(&lock); + if (entry) + munmap(entry, entry->span); + else + __real_free(pointer); +} + +extern "C" void *__wrap_calloc(size_t count, size_t size) +{ + if (size && count > SIZE_MAX / size) + { + errno = ENOMEM; + return nullptr; + } + const size_t bytes = count * size; + if (bytes < threshold) + return bytes ? __real_calloc(count, size) : __real_calloc(1, 1); + /* Anonymous mappings are zero-initialized by the kernel. */ + return __wrap_malloc(bytes); +} + +extern "C" void *__wrap_realloc(void *pointer, size_t size) +{ + if (!pointer) + return __wrap_malloc(size); + if (!size) + { + __wrap_free(pointer); + return nullptr; + } + pthread_mutex_lock(&lock); + Mapping *entry = *find(pointer); + const size_t old_size = entry ? entry->requested : 0; + pthread_mutex_unlock(&lock); + /* Native buffers retain their allocator: their usable size is not part of + * this ABI. Never guess it or read a private libc allocation header. */ + if (!entry) + return __real_realloc(pointer, size); + void *replacement = __wrap_malloc(size); + if (!replacement) + return nullptr; + std::memcpy(replacement, pointer, old_size < size ? old_size : size); + __wrap_free(pointer); + return replacement; +} diff --git a/src/ps5_directory.cpp b/src/ps5_directory.cpp index 1658742..873aa13 100644 --- a/src/ps5_directory.cpp +++ b/src/ps5_directory.cpp @@ -102,6 +102,20 @@ extern "C" struct dirent *ps5_readdir(DIR *opaque) return nullptr; } +extern "C" void ps5_rewinddir(DIR *opaque) +{ + auto *directory = reinterpret_cast(opaque); + if (!directory) + { + errno = EBADF; + return; + } + if (lseek(directory->fd, 0, SEEK_SET) < 0) + return; + directory->offset = directory->bytes = 0; + directory->finished = false; +} + extern "C" int ps5_closedir(DIR *opaque) { auto *directory = reinterpret_cast(opaque); diff --git a/src/ps5_directory.h b/src/ps5_directory.h index e6474a7..2e3dbe0 100644 --- a/src/ps5_directory.h +++ b/src/ps5_directory.h @@ -7,6 +7,7 @@ extern "C" #endif DIR *ps5_opendir(const char *path); struct dirent *ps5_readdir(DIR *directory); + void ps5_rewinddir(DIR *directory); int ps5_closedir(DIR *directory); #ifdef __cplusplus } diff --git a/tests/memory_ps5_test.cpp b/tests/memory_ps5_test.cpp new file mode 100644 index 0000000..88f15c2 --- /dev/null +++ b/tests/memory_ps5_test.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include +#include + +extern "C" +{ + void *__wrap_malloc(size_t); + void *__wrap_calloc(size_t, size_t); + void *__wrap_realloc(void *, size_t); + void __wrap_free(void *); + // Host build binds these to libc, preserving genuinely foreign pointers. + void *__real_malloc(size_t n) + { + return std::malloc(n); + } + void *__real_calloc(size_t n, size_t s) + { + return std::calloc(n, s); + } + void *__real_realloc(void *p, size_t n) + { + return std::realloc(p, n); + } + void __real_free(void *p) + { + std::free(p); + } +} +int main() +{ + constexpr size_t large = 16 * 1024 * 1024; + auto *p = static_cast(__wrap_malloc(large)); + assert(p && uintptr_t(p) % alignof(std::max_align_t) == 0); + std::memset(p, 0xA5, large); + assert(!__wrap_realloc(p, SIZE_MAX)); + assert(p[0] == 0xA5 && p[large - 1] == 0xA5); + p = static_cast(__wrap_realloc(p, large * 2)); + assert(p && p[0] == 0xA5 && p[large - 1] == 0xA5); + p = static_cast(__wrap_realloc(p, 13)); + assert(p && p[12] == 0xA5); + __wrap_free(p); + p = static_cast(__wrap_calloc(2, large)); + assert(p); + for (size_t i = 0; i < 2 * large; ++i) + assert(p[i] == 0); + __wrap_free(p); + assert(!__wrap_calloc(SIZE_MAX, 2) && errno == ENOMEM); + assert(!__wrap_malloc(SIZE_MAX) && errno == ENOMEM); + // strdup/native library buffers must still be freed by libc. + char *foreign = strdup("native allocation"); + foreign = static_cast(__wrap_realloc(foreign, 128)); + assert(foreign && !strcmp(foreign, "native allocation")); + __wrap_free(foreign); + __wrap_free(nullptr); + void *empty = __wrap_calloc(0, SIZE_MAX); + assert(empty); + __wrap_free(empty); + assert(!__wrap_realloc(__wrap_malloc(large), 0)); + std::vector workers; + for (int t = 0; t < 4; ++t) + workers.emplace_back( + [] + { + for (int i = 0; i < 50; ++i) + { + void *buffer = __wrap_malloc(1024 * 1024); + assert(buffer); + memset(buffer, i, 1024 * 1024); + __wrap_free(buffer); + } + }); + for (auto &worker : workers) + worker.join(); +} diff --git a/tests/ps5_directory_test.cpp b/tests/ps5_directory_test.cpp index fb7f33a..c198234 100644 --- a/tests/ps5_directory_test.cpp +++ b/tests/ps5_directory_test.cpp @@ -9,6 +9,7 @@ namespace std::vector> batches; size_t batch = 0; int closed = 0; +bool seek_fails = false; std::vector record(uint32_t inode, unsigned type, const std::string &name) { uint16_t length = uint16_t((8 + name.size() + 1 + 3) & ~3u); @@ -37,6 +38,17 @@ extern "C" int __wrap_close(int fd) ++closed; return 0; } +extern "C" off_t lseek(int fd, off_t offset, int whence) +{ + assert(fd == 0 && offset == 0 && whence == SEEK_SET); + if (seek_fails) + { + errno = EINVAL; + return -1; + } + batch = 0; + return 0; +} extern "C" int getdents(int fd, char *buffer, int size) { assert(fd == 0); @@ -69,6 +81,16 @@ int main() assert(entry && entry->d_type == DT_REG && std::string(entry->d_name) == "Example ROM.bin"); errno = 0; assert(!ps5_readdir(dir) && errno == 0 && !ps5_readdir(dir)); + seek_fails = true; + ps5_rewinddir(dir); + assert(errno == EINVAL && !ps5_readdir(dir)); + seek_fails = false; + ps5_rewinddir(dir); + entry = ps5_readdir(dir); + assert(entry && std::string(entry->d_name) == "cores"); + ps5_rewinddir(dir); // Reset a partially consumed batch too. + entry = ps5_readdir(dir); + assert(entry && std::string(entry->d_name) == "cores"); assert(ps5_closedir(dir) == 0 && closed == 1); // Truncated headers, oversized/zero records and missing name terminators fail closed. auto oversized = record(1, DT_DIR, "x"); diff --git a/tests/test_core_frame.py b/tests/test_core_frame.py index 8aa895e..4c1cc81 100644 --- a/tests/test_core_frame.py +++ b/tests/test_core_frame.py @@ -26,6 +26,42 @@ class CoreFrame(unittest.TestCase): 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) + # Quick Menu repeats the cached core frame into alternating images. + # The source remains XRGB, so repeated uploads must be identical. + self.assertEqual(src.raw[:len(source)], source) + for _ in range(4): + convert(dst, 16, src, 12, 2, 2) + self.assertEqual(dst.raw[:8] + dst.raw[16:24], expected) + self.assertEqual(src.raw[:len(source)], source) 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) + + def test_padded_source_sampling_across_core_transitions(self): + with tempfile.TemporaryDirectory() as td: + wrapper = Path(td) / 'quad.cpp' + wrapper.write_text('#include "' + str(ROOT / 'src/core_frame_ps5.cpp') + '"\n' + 'extern "C" void quad(float *v, unsigned w, unsigned p) ' + '{ ps5_core_source_quad(v, w, p); }\n') + lib = Path(td) / 'quad.so' + subprocess.run(['c++', '-shared', '-fPIC', '-Wall', '-Wextra', '-Werror', + str(wrapper), '-o', str(lib)], check=True) + quad = ctypes.CDLL(str(lib)).quad + quad.argtypes = [ctypes.POINTER(ctypes.c_float), ctypes.c_uint, ctypes.c_uint] + # FCEUmm -> no content -> GBA -> GB -> no content -> FCEUmm. + for width, physical in [(256, 256), (4, 128), (240, 256), + (160, 192), (4, 128), (256, 256)]: + vertices = (ctypes.c_float * 48)() + quad(vertices, width, physical) + for base in (0, 24): + uv = [(vertices[i + 2], vertices[i + 3]) + for i in range(base, base + 24, 4)] + right = ctypes.c_float(width / physical).value + self.assertEqual(uv, [(0, 0), (0, 1), (right, 0), + (right, 0), (0, 1), (right, 1)]) + # At every output pixel centre, nearest sampling stays in + # initialized content, never the poisoned row padding. + samples = [int(((x + 0.5) / 1920) * right * physical) + for x in range(1920)] + self.assertEqual(min(samples), 0) + self.assertEqual(max(samples), width - 1) diff --git a/tests/test_core_imports.py b/tests/test_core_imports.py new file mode 100644 index 0000000..09e2995 --- /dev/null +++ b/tests/test_core_imports.py @@ -0,0 +1,40 @@ +"""Multiple cores share bindings without losing object imports or adapters.""" +import importlib.util +from pathlib import Path +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parent.parent +spec = importlib.util.spec_from_file_location('core_imports', ROOT / 'tools/core-imports.py') +imports = importlib.util.module_from_spec(spec) +spec.loader.exec_module(imports) + + +class CoreImports(unittest.TestCase): + def test_union_deduplicates_and_preserves_runtime_adapters(self): + first = '''1: 00000000 0 FUNC GLOBAL DEFAULT UND malloc +2: 00000000 0 OBJECT GLOBAL DEFAULT UND __isthreaded +3: 00000000 0 FUNC GLOBAL DEFAULT UND opendir''' + second = '''1: 00000000 0 FUNC GLOBAL DEFAULT UND malloc +2: 00000000 0 NOTYPE GLOBAL DEFAULT UND localtime_r +3: 00000000 0 FUNC GLOBAL DEFAULT UND rewinddir''' + with patch.object(imports.subprocess, 'check_output', side_effect=[first, second]): + collected = imports.collect_imports(['one.so', 'two.so']) + self.assertEqual(len(collected), 5) + generated = imports.generate(collected) + self.assertEqual(generated.count('asm("malloc")'), 1) + self.assertIn('[] asm("__isthreaded")', generated) + self.assertIn('asm("rtime_localtime")', generated) + self.assertIn('asm("ps5_rewinddir")', generated) + self.assertIn('asm("ps5_opendir")', generated) + + def test_reject_conflicting_types_and_tls(self): + with patch.object(imports.subprocess, 'check_output', side_effect=[ + '1: 0 0 FUNC GLOBAL DEFAULT UND symbol', + '1: 0 0 OBJECT GLOBAL DEFAULT UND symbol']): + with self.assertRaisesRegex(ValueError, 'Conflicting'): + imports.collect_imports(['one.so', 'two.so']) + with patch.object(imports.subprocess, 'check_output', return_value= + '1: 0 0 TLS GLOBAL DEFAULT UND variable'): + with self.assertRaisesRegex(ValueError, 'Unsupported'): + imports.collect_imports(['tls.so']) diff --git a/tests/test_core_loader.py b/tests/test_core_loader.py index 4f1c180..018d7e9 100644 --- a/tests/test_core_loader.py +++ b/tests/test_core_loader.py @@ -123,3 +123,56 @@ extern "C" void *ps5_core_import(const char *name) { data = bytearray(self.original) struct.pack_into(' +#include "tooling/mgba/ps5-video.h" +int main(void) { + assert(ps5_mgba_xrgb(0x000000ff) == 0x00ff0000); + assert(ps5_mgba_xrgb(0x0000ff00) == 0x0000ff00); + assert(ps5_mgba_xrgb(0x00ff0000) == 0x000000ff); + assert(ps5_mgba_xrgb(0xab123456) == 0xab563412); + return 0; +} +''') + binary = Path(directory) / 'colours' + subprocess.run(['cc', '-I', str(ROOT), str(source), '-o', str(binary)], check=True) + subprocess.run([str(binary)], check=True) diff --git a/tooling/mgba/CMakeLists.txt b/tooling/mgba/CMakeLists.txt new file mode 100644 index 0000000..7bcaf7a --- /dev/null +++ b/tooling/mgba/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.16) +project(PS5_mGBA C CXX) +if(NOT MGBA_SOURCE_DIR) + message(FATAL_ERROR "MGBA_SOURCE_DIR must name the pinned extracted source") +endif() +set(BUILD_LIBRETRO ON CACHE BOOL "" FORCE) +set(SKIP_LIBRARY ON CACHE BOOL "" FORCE) +set(DISABLE_DEPS ON CACHE BOOL "" FORCE) +set(BUILD_LTO OFF CACHE BOOL "" FORCE) +add_subdirectory("${MGBA_SOURCE_DIR}" mgba EXCLUDE_FROM_ALL) +# Use libretro XRGB8888 and the existing RGBA upload. Do not alter upstream files. +get_target_property(core_defines mgba_libretro COMPILE_DEFINITIONS) +list(REMOVE_ITEM core_defines COLOR_16_BIT COLOR_5_6_5 HAVE_XLOCALE) +# localtime_r is supplied by the frontend's locked rtime_localtime adapter. +list(APPEND core_defines HAVE_LOCALTIME_R MGBA_PS5) +set_property(TARGET mgba_libretro PROPERTY COMPILE_DEFINITIONS "${core_defines}") +# No payload CRT/static libc, host -lm, or other core-specific runtime library. +set_property(TARGET mgba_libretro PROPERTY LINK_LIBRARIES + kernel_web SceLibcInternal ScePosixForWebKit) +# Permit imports implemented by the frontend (localtime_r); the title's generated +# binding table still has to resolve every import at link time. +target_link_options(mgba_libretro PRIVATE -nostdlib -nodefaultlibs -Wl,-z,undefs + "-Wl,--build-id=sha1" "-Wl,-T,${CMAKE_CURRENT_LIST_DIR}/../native/ps5-core.ld") + +target_include_directories(mgba_libretro PRIVATE "${CMAKE_CURRENT_LIST_DIR}") diff --git a/tooling/mgba/ps5-toolchain.cmake b/tooling/mgba/ps5-toolchain.cmake new file mode 100644 index 0000000..6fb97c6 --- /dev/null +++ b/tooling/mgba/ps5-toolchain.cmake @@ -0,0 +1,16 @@ +# Cross-compile with this repository's SDK; never search host headers/libraries. +set(CMAKE_SYSTEM_NAME FreeBSD) +set(CMAKE_SYSTEM_PROCESSOR x86_64) +set(CMAKE_C_COMPILER "$ENV{PS5_PAYLOAD_SDK}/bin/prospero-clang") +set(CMAKE_CXX_COMPILER "$ENV{PS5_PAYLOAD_SDK}/bin/prospero-clang++") +set(CMAKE_AR "$ENV{PS5_PAYLOAD_SDK}/bin/prospero-ar") +set(CMAKE_RANLIB "$ENV{PS5_PAYLOAD_SDK}/bin/prospero-ranlib") +# Link-only probes must actually resolve functions; static probes give false positives. +# Cross CMake never executes these binaries, so no payload startup is required. +set(CMAKE_EXE_LINKER_FLAGS_INIT "-nostdlib -nostartfiles -nodefaultlibs -Wl,-e,0 -lkernel_web -lSceLibcInternal -lScePosixForWebKit") +set(CMAKE_FIND_ROOT_PATH "$ENV{PS5_PAYLOAD_SDK}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) diff --git a/tooling/mgba/ps5-video.h b/tooling/mgba/ps5-video.h new file mode 100644 index 0000000..96ab2f2 --- /dev/null +++ b/tooling/mgba/ps5-video.h @@ -0,0 +1,10 @@ +/* mGBA's native 32-bit software pixels are XBGR; libretro promises XRGB. */ +#ifndef PS5_MGBA_VIDEO_H +#define PS5_MGBA_VIDEO_H +#include +static inline uint32_t ps5_mgba_xrgb(uint32_t pixel) +{ + return (pixel & UINT32_C(0xff00ff00)) | ((pixel & UINT32_C(0xff)) << 16) | + ((pixel >> 16) & UINT32_C(0xff)); +} +#endif diff --git a/tools/apply-port-patches.py b/tools/apply-port-patches.py index f4e8b6d..4de8098 100755 --- a/tools/apply-port-patches.py +++ b/tools/apply-port-patches.py @@ -30,6 +30,14 @@ from pathlib import Path # (file, anchor, inserted-before-anchor, already-present-marker) EDITS = [ + ('libretro-common/file/archive_file_7z.c', '#include ', '/* patches/series, 0076: archive error diagnostics */\n#include \n#include \n#include ', 'patches/series, 0076: archive error diagnostics'), + ('libretro-common/file/archive_file_7z.c', ' return malloc(len);', '/* patches/series, 0076: checked main allocation */\n {\n void *result = malloc(len);\n if (!result)\n fprintf(stderr, "archive 7z: allocation failed bytes=%zu errno=%d\\n", len, errno);\n return result;\n }', 'patches/series, 0076: checked main allocation'), + ('libretro-common/file/archive_file_7z.c', 'static void *sevenzip_stream_alloc_tmp_impl(ISzAllocPtr p, size_t len)\n{\n if (len == 0)\n return 0;\n return malloc(len);\n}', '/* patches/series, 0076: checked temporary allocation */\nstatic void *sevenzip_stream_alloc_tmp_impl(ISzAllocPtr p, size_t len)\n{\n return sevenzip_stream_alloc_impl(p, len);\n}', 'patches/series, 0076: checked temporary allocation'), + ('libretro-common/file/archive_file_7z.c', ' if (InFile_Open(&archiveStream.file, path))\n return -1;', '/* patches/series, 0076: open failure */\n if (InFile_Open(&archiveStream.file, path))\n {\n fprintf(stderr, "archive 7z: open failed errno=%d\\n", errno);\n free(lookStream.buf);\n return -1;\n }', 'patches/series, 0076: open failure'), + ('libretro-common/file/archive_file_7z.c', ' if (SzArEx_Open(&db, &lookStream.vt, &allocImp, &allocTempImp) == SZ_OK)', '/* patches/series, 0076: header failure */\n SRes open_result = SzArEx_Open(&db, &lookStream.vt, &allocImp, &allocTempImp);\n if (open_result != SZ_OK)\n fprintf(stderr, "archive 7z: header failed result=%d errno=%d\\n", open_result, errno);\n if (open_result == SZ_OK)', 'patches/series, 0076: header failure'), + ('libretro-common/file/archive_file_7z.c', ' if (res != SZ_OK)\n break; /* This goes to the error section. */', '/* patches/series, 0076: extraction failure */\n if (res != SZ_OK)\n {\n fprintf(stderr, "archive 7z: extract failed result=%d bytes=%zu errno=%d\\n", res, output_size, errno);\n break;\n }', 'patches/series, 0076: extraction failure'), + ('libretro-common/file/archive_file_7z.c', ' *buf = malloc((size_t)(outsize + 1));\n ((char*)(*buf))[outsize]', '/* patches/series, 0076: guard content allocation */\n *buf = malloc((size_t)(outsize + 1));\n if (!*buf)\n {\n fprintf(stderr, "archive 7z: content allocation failed bytes=%lld errno=%d\\n", (long long)outsize + 1, errno);\n res = SZ_ERROR_MEM;\n break;\n }\n ((char*)(*buf))[outsize]', 'patches/series, 0076: guard content allocation'), + ('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'), @@ -2364,6 +2372,22 @@ EDITS = [ 'patches/series, 0070: SDK getdents adapter', ), + ('gfx/drivers/vulkan.c', ' vk->video = *video;', '/* patches/series, 0077: decode menu images for sampled RGBA */\n video_driver_set_rgba();\n RARCH_LOG("[PS5] Vulkan menu images use RGBA8.\\n");\n vk->video = *video;', 'patches/series, 0077: decode menu images for sampled RGBA'), + ('gfx/drivers/vulkan.c', 'static bool vulkan_get_current_sw_framebuffer(void *data,\n struct retro_framebuffer *framebuffer)\n{\n struct vk_per_frame *chain = NULL;\n vk_t *vk = (vk_t*)data;\n vk->chain =\n &vk->swapchain[vk->context->current_frame_index];\n chain = vk->chain;\n\n if (chain->texture.width != framebuffer->width ||\n chain->texture.height != framebuffer->height)\n {\n chain->texture = vulkan_create_texture(vk, &chain->texture,\n framebuffer->width, framebuffer->height, chain->texture.format,\n NULL, NULL, VULKAN_TEXTURE_STREAMED);\n {\n struct vk_texture *texture = &chain->texture;\n VK_MAP_PERSISTENT_TEXTURE(vk->context->device, texture);\n }\n\n if (chain->texture.type == VULKAN_TEXTURE_STAGING)\n {\n chain->texture_optimal = vulkan_create_texture(\n vk,\n &chain->texture_optimal,\n framebuffer->width,\n framebuffer->height,\n chain->texture.format, /* Ensure we use the non-remapped format. */\n NULL, NULL, VULKAN_TEXTURE_DYNAMIC);\n }\n }\n\n framebuffer->data = chain->texture.mapped;\n framebuffer->pitch = chain->texture.stride;\n framebuffer->format = vk->video.rgb32\n ? RETRO_PIXEL_FORMAT_XRGB8888 : RETRO_PIXEL_FORMAT_RGB565;\n framebuffer->memory_flags = 0;\n\n if (vk->context->memory_properties.memoryTypes[\n chain->texture.memory_type].propertyFlags &\n VK_MEMORY_PROPERTY_HOST_CACHED_BIT)\n framebuffer->memory_flags |= RETRO_MEMORY_TYPE_CACHED;\n\n return true;\n}\n', "/* patches/series, 0077: preserve original core pixels across cached frames */\nstatic bool vulkan_get_current_sw_framebuffer(void *data,\n struct retro_framebuffer *framebuffer)\n{\n /* Core XRGB and GPU RGBA cannot share writable storage. Cached frames must\n * retain the core's original pixels when Quick Menu pauses emulation. */\n (void)data;\n (void)framebuffer;\n return false;\n}\n", 'patches/series, 0077: preserve original core pixels across cached frames'), + ('gfx/drivers/vulkan.c', 'static uintptr_t vulkan_load_texture(void *video_data, void *data,\n bool threaded, enum texture_filter_type filter_type)\n{\n struct vk_texture *texture = NULL;\n vk_t *vk = (vk_t*)video_data;\n struct texture_image *image = (struct texture_image*)data;\n if (!image)\n return 0;\n\n if (!(texture = (struct vk_texture*)calloc(1, sizeof(*texture))))\n return 0;\n\n if (!image->pixels || !image->width || !image->height)\n {\n /* Create a dummy texture instead. */\n static const uint32_t checkerboard[] = {\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n };\n *texture = vulkan_create_texture(vk, NULL,\n 8, 8, VK_FORMAT_B8G8R8A8_UNORM,\n checkerboard, NULL, VULKAN_TEXTURE_STATIC);\n texture->flags &= ~(VK_TEX_FLAG_DEFAULT_SMOOTH\n | VK_TEX_FLAG_MIPMAP);\n }\n else\n {\n *texture = vulkan_create_texture(vk, NULL,\n image->width, image->height, VK_FORMAT_B8G8R8A8_UNORM,\n image->pixels, NULL, VULKAN_TEXTURE_STATIC);\n if (filter_type == TEXTURE_FILTER_MIPMAP_LINEAR || filter_type ==\n TEXTURE_FILTER_LINEAR)\n texture->flags |= VK_TEX_FLAG_DEFAULT_SMOOTH;\n if (filter_type == TEXTURE_FILTER_MIPMAP_LINEAR)\n texture->flags |= VK_TEX_FLAG_MIPMAP;\n }\n\n return (uintptr_t)texture;\n}\n', '/* patches/series, 0077: upload decoded menu images as RGBA */\nstatic uintptr_t vulkan_load_texture(void *video_data, void *data,\n bool threaded, enum texture_filter_type filter_type)\n{\n struct vk_texture *texture = NULL;\n vk_t *vk = (vk_t*)video_data;\n struct texture_image *image = (struct texture_image*)data;\n if (!image)\n return 0;\n\n if (!(texture = (struct vk_texture*)calloc(1, sizeof(*texture))))\n return 0;\n\n if (!image->pixels || !image->width || !image->height)\n {\n /* Create a dummy texture instead. */\n static const uint32_t checkerboard[] = {\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1,\n VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0, VK_T1, VK_T0,\n };\n *texture = vulkan_create_texture(vk, NULL,\n 8, 8, VK_FORMAT_R8G8B8A8_UNORM,\n checkerboard, NULL, VULKAN_TEXTURE_STATIC);\n texture->flags &= ~(VK_TEX_FLAG_DEFAULT_SMOOTH\n | VK_TEX_FLAG_MIPMAP);\n }\n else\n {\n *texture = vulkan_create_texture(vk, NULL,\n image->width, image->height, VK_FORMAT_R8G8B8A8_UNORM,\n image->pixels, NULL, VULKAN_TEXTURE_STATIC);\n if (filter_type == TEXTURE_FILTER_MIPMAP_LINEAR || filter_type ==\n TEXTURE_FILTER_LINEAR)\n texture->flags |= VK_TEX_FLAG_DEFAULT_SMOOTH;\n if (filter_type == TEXTURE_FILTER_MIPMAP_LINEAR)\n texture->flags |= VK_TEX_FLAG_MIPMAP;\n }\n\n return (uintptr_t)texture;\n}\n', 'patches/series, 0077: upload decoded menu images as RGBA'), + ('gfx/drivers_shader/shader_vulkan.h', ' VkFormat format;', ' VkFormat format;\n /* patches/series, 0078: zero means no padded source image. */\n unsigned physical_width;', 'patches/series, 0078: zero means'), + ('gfx/drivers/vulkan.c', ' struct vulkan_filter_chain_texture input;', ' /* patches/series, 0078: hardware images retain their own extent. */\n struct vulkan_filter_chain_texture input = {0};', 'patches/series, 0078: hardware images'), + ('gfx/drivers/vulkan.c', ' input.format = vk->default_texture.format;', ' input.format = vk->default_texture.format;\n /* patches/series, 0078: black fallback is also padded. */\n input.physical_width = ps5_vulkan_texture_width(input.width,\n vulkan_format_to_bpp(input.format));', 'patches/series, 0078: black fallback'), + ('gfx/drivers/vulkan.c', " input.format = VK_FORMAT_UNDEFINED; /* It's already configured. */", " input.format = VK_FORMAT_UNDEFINED; /* It's already configured. */\n /* patches/series, 0078: preserve logical size and expose sampled extent. */\n input.physical_width = ps5_vulkan_texture_width(tex->width,\n vulkan_format_to_bpp(tex->format));", 'patches/series, 0078: preserve logical size'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' Texture source;', ' /* patches/series, 0078: offscreen source starts without padding. */\n Texture source = {};', 'patches/series, 0078: offscreen source starts'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' unsigned i;\n Texture source;\n\n /* First frame, make sure our history and', ' unsigned i;\n /* patches/series, 0078: viewport source starts without padding. */\n Texture source = {};\n\n /* First frame, make sure our history and', 'patches/series, 0078: viewport source starts'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' source.texture.height = fb.get_size().height;', ' source.texture.height = fb.get_size().height;\n /* patches/series, 0078: pass outputs have an unpadded sampled extent. */\n source.texture.physical_width = 0;', 'patches/series, 0078: pass outputs have'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' bool final_pass;', ' bool final_pass;\n /* patches/series, 0078: per-sync UV buffers cannot race submitted draws. */\n std::vector> ps5_source_vbos;', 'patches/series, 0078: per-sync UV buffers'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' uint8_t *u = nullptr;\n\n curr_vp', ' uint8_t *u = nullptr;\n /* patches/series, 0078: crop software source UVs to initialized texels. */\n VkBuffer ps5_source_vbo = common->vbo->get_buffer();\n if (source.texture.physical_width > source.texture.width)\n {\n extern void ps5_core_source_quad(float *, unsigned, unsigned);\n if (ps5_source_vbos.empty())\n ps5_source_vbos.resize(num_sync_indices);\n auto &vbo = ps5_source_vbos[sync_index];\n if (!vbo)\n vbo.reset(new Buffer(device, memory_properties, 48 * sizeof(float),\n VK_BUFFER_USAGE_VERTEX_BUFFER_BIT));\n ps5_core_source_quad(static_cast(vbo->map()),\n source.texture.width, source.texture.physical_width);\n vbo->unmap();\n ps5_source_vbo = vbo->get_buffer();\n }\n\n curr_vp', 'patches/series, 0078: crop software source UVs'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' &common->vbo->get_buffer(),', ' /* patches/series, 0078: bind cropped first triangle. */\n &ps5_source_vbo,', 'patches/series, 0078: bind cropped first'), + ('gfx/drivers_shader/shader_vulkan.cpp', ' VkBuffer buffer = common->vbo->get_buffer();', ' /* patches/series, 0078: both triangles use the same cropped UVs. */\n VkBuffer buffer = ps5_source_vbo;', 'patches/series, 0078: both triangles'), + ('gfx/drivers_shader/shader_vulkan.cpp', 'void Pass::clear_vk()\n{', 'void Pass::clear_vk()\n{\n /* patches/series, 0078: rebuild UV slots with the swapchain. */\n ps5_source_vbos.clear();', 'patches/series, 0078: rebuild UV slots'), + ('gfx/drivers/vulkan.c', ' info.extent.width = ps5_vulkan_texture_width(width, vulkan_format_to_bpp(format));', ' /* patches/series, 0078: padded width follows the actual sampled format. */\n info.extent.width = ps5_vulkan_texture_width(width, vulkan_format_to_bpp(request));', 'patches/series, 0078: padded width follows'), ] diff --git a/tools/build-mgba.sh b/tools/build-mgba.sh new file mode 100755 index 0000000..c6a0d44 --- /dev/null +++ b/tools/build-mgba.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Cross-build pinned mGBA with this title's SDK. No host compiler fallback. +# Output: build/cores/stage/{cores,info}/; build-title.sh stages these in /app0. +set -euo pipefail +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +cd "$root" +[[ $# == 0 ]] || { echo "usage: ${0##*/}" >&2; exit 2; } +sdk="$root/.deps/native/ps5-payload-sdk" +[[ -x $sdk/bin/prospero-clang ]] || { echo "error: bootstrap this project's SDK first" >&2; exit 2; } +export PS5_PAYLOAD_SDK="$sdk" +export PS5_CLANG=/usr/bin/clang +# Both environment and make variables are explicit: upstream assigns CC/LD. +export CC="$sdk/bin/prospero-clang" CXX="$sdk/bin/prospero-clang++" +export AR="$sdk/bin/prospero-ar" LD="$CC" +revision=7a12d6d4b9acb14c0ae62c9166b6a2f3d08007f6 +source_sha=5cbf639e527fb586bf33e14d59037eab35f78f45154e0b79c784fff474c3bc37 +info_revision=5a74858ab2f7a50cebb5a6330895bc38899531c0 +info_sha=64444beb8268d3a57d53a45564c55049f39ff621315401fef86e82f3849042d6 +cache="$root/.deps/downloads" +mkdir -p "$cache" +fetch() { + local url=$1 target=$2 digest=$3 + if [[ ! -f $target ]]; then + curl --fail --location --retry 3 "$url" -o "$target.download" + printf '%s %s\n' "$digest" "$target.download" | sha256sum --check --status || { + rm -f -- "$target.download"; echo 'error: download digest mismatch' >&2; exit 1; + } + mv -- "$target.download" "$target" + fi + printf '%s %s\n' "$digest" "$target" | sha256sum --check --status || { + echo "error: cached input digest mismatch: $target" >&2; exit 1; + } +} +archive="$cache/mgba-$revision.tar.gz" +info="$cache/mgba_libretro.info" +fetch "https://codeload.github.com/libretro/mgba/tar.gz/$revision" "$archive" "$source_sha" +fetch "https://raw.githubusercontent.com/libretro/libretro-core-info/$info_revision/mgba_libretro.info" "$info" "$info_sha" +work="$root/build/cores/mgba" +stage="$root/build/cores/stage" +# Fresh extraction ensures stale objects or modified upstream sources never ship. +rm -rf -- "$work" +mkdir -p "$work" "$stage/cores" "$stage/info" +tar -xzf "$archive" --strip-components=1 -C "$work" +patch --batch --fuzz=0 -d "$work" -p1 < "$root/patches/mgba/native-locale-type.patch" +patch --batch --fuzz=0 -d "$work" -p1 < "$root/patches/mgba/native-config-path.patch" +patch --batch --fuzz=0 -d "$work" -p1 < "$root/patches/mgba/native-xrgb-output.patch" +# An extracted core must not inherit RetroArch's parent Git version/dirty state. +export GIT_CEILING_DIRECTORIES="$root/build/cores" +# The wrapper selects only upstream's libretro target and native runtime imports. +# CMake is required by this pinned upstream revision; target flags use XRGB8888. +cmake -S "$root/tooling/mgba" -B "$work/ps5-build" \ + -DCMAKE_TOOLCHAIN_FILE="$root/tooling/mgba/ps5-toolchain.cmake" \ + -DMGBA_SOURCE_DIR="$work" -DCMAKE_BUILD_TYPE=Release +cmake --build "$work/ps5-build" --target mgba_libretro --parallel "${JOBS:-8}" +cp -- "$work/ps5-build/mgba/mgba_libretro.so" "$work/mgba_libretro.so" +python3 tools/check-core.py "$work/mgba_libretro.so" --report "$work/abi.json" +cp -- "$work/mgba_libretro.so" "$stage/cores/mgba_libretro.so" +cp -- "$info" "$stage/info/mgba_libretro.info" +python3 - "$work" "$revision" "$source_sha" "$info_revision" "$info_sha" <<'PY' +import hashlib, json, pathlib, sys +work = pathlib.Path(sys.argv[1]) +report = json.loads((work / 'abi.json').read_text()) +report.update(source_revision=sys.argv[2], source_archive_sha256=sys.argv[3], + info_revision=sys.argv[4], info_sha256=sys.argv[5]) +report['sdk_compiler_wrapper_sha256'] = hashlib.sha256( + pathlib.Path('.deps/native/ps5-payload-sdk/bin/prospero-clang').read_bytes()).hexdigest() +report['port_inputs_sha256'] = {name: hashlib.sha256(pathlib.Path(name).read_bytes()).hexdigest() + for name in ('tooling/mgba/CMakeLists.txt', 'tooling/mgba/ps5-toolchain.cmake', + 'tooling/native/ps5-core.ld', 'patches/mgba/native-locale-type.patch', + 'patches/mgba/native-config-path.patch', 'patches/mgba/native-xrgb-output.patch', + 'tooling/mgba/ps5-video.h')} +(work / 'build.json').write_text(json.dumps(report, indent=2) + '\n') +PY +printf '==> [mgba] built and ABI-checked revision %s; console loading is a separate gate\n' "$revision" diff --git a/tools/build-title.sh b/tools/build-title.sh index 8a9f9f6..a2de8f8 100755 --- a/tools/build-title.sh +++ b/tools/build-title.sh @@ -51,8 +51,13 @@ 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" +core_names=(fceumm mgba) +core_files=() +for core_name in "${core_names[@]}"; do + bash "$root/tools/build-$core_name.sh" + core_files+=("$root/build/cores/stage/cores/${core_name}_libretro.so") +done +python3 "$root/tools/core-imports.py" "${core_files[@]}" # 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 @@ -155,6 +160,7 @@ 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", + "build/cores/stage/cores/mgba_libretro.so", "tools/build.sh", "tools/retroarch-flags.sh")] inputs += [pathlib.Path(name) for name in sys.argv[2:]] digest = hashlib.sha256() @@ -168,6 +174,7 @@ print("==> [title] build identity: " + identity) PY echo "==> [title] step 2/3: the title" +# Large frontend/core buffers use mapped memory; wrap all ownership operations. PS5_PAYLOAD_SDK="$sdk" \ PS5_CLANG=/usr/bin/clang \ PYTHONPATH="$root/tooling/pystub${PYTHONPATH:+:$PYTHONPATH}" \ @@ -176,7 +183,7 @@ APP_INCLUDE_PATHS="build/ra-conf build vendor/retroarch build/ra-conf/libretro-c APP_STATIC_ARCHIVES="build/ra/libretroarch.a" \ APP_VULKAN_ARCHIVES="${vulkan_archives[*]}" \ APP_EXTRA_OBJECTS="${vulkan_objects[*]}" \ -APP_LINK_FLAGS="$vulkan_flags" \ +APP_LINK_FLAGS="$vulkan_flags --wrap=malloc --wrap=calloc --wrap=realloc --wrap=free" \ make app title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' \ @@ -192,10 +199,12 @@ dist="$root/dist/$title_id" # initialise and the title exit 1 saying nothing. See config/retroarch.cfg. cp -a -- "$root/config/retroarch.cfg" "$dist/retroarch.cfg" mkdir -p "$dist/cores" "$dist/info" -cp -- "$root/build/cores/stage/cores/fceumm_libretro.so" "$dist/cores/" -cp -- "$root/build/cores/stage/info/fceumm_libretro.info" "$dist/info/" -# Older saved configs have an empty info path: upstream then searches cores/. -cp -- "$root/build/cores/stage/info/fceumm_libretro.info" "$dist/cores/" +for core_name in "${core_names[@]}"; do + cp -- "$root/build/cores/stage/cores/${core_name}_libretro.so" "$dist/cores/" + cp -- "$root/build/cores/stage/info/${core_name}_libretro.info" "$dist/info/" + # Older saved configs have an empty info path: upstream then searches cores/. + cp -- "$root/build/cores/stage/info/${core_name}_libretro.info" "$dist/cores/" +done # The Vulkan driver, beside the title, when it exists. # diff --git a/tools/core-imports.py b/tools/core-imports.py index 192e600..bab2c09 100644 --- a/tools/core-imports.py +++ b/tools/core-imports.py @@ -1,31 +1,53 @@ #!/usr/bin/env python3 -"""Generate direct native-runtime bindings for the built core's imports.""" +"""Generate the union of direct native-runtime bindings for the shipped cores.""" from pathlib import Path +import argparse 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 '] -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(&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') + +def collect_imports(cores): + imports = {} + for core in cores: + symbols = subprocess.check_output(['readelf', '--dyn-syms', '-W', str(core)], text=True) + 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', 'NOTYPE'): + raise ValueError(f'Unsupported native core import: {name} ({kind})') + if name in imports and imports[name] != kind: + raise ValueError(f'Conflicting native import types for {name}') + imports[name] = kind + return imports + + +def generate(imports): + lines = ['// Generated by tools/core-imports.py; never edit.', '#include '] + aliases = {'opendir': 'ps5_opendir', 'readdir': 'ps5_readdir', 'closedir': 'ps5_closedir', + 'localtime_r': 'rtime_localtime', 'rewinddir': 'ps5_rewinddir'} + 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(&core_import_{index});') + lines.extend([' return nullptr;', '}']) + return '\n'.join(lines) + '\n' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('cores', type=Path, nargs='+') + args = parser.parse_args() + imports = collect_imports(args.cores) + (ROOT / 'build/core_imports.inc').write_text(generate(imports)) + print(f'core imports: {len(imports)} explicit native bindings for {len(args.cores)} cores') + + +if __name__ == '__main__': + main() diff --git a/tools/run-title.sh b/tools/run-title.sh index 65c81c7..fc79354 100755 --- a/tools/run-title.sh +++ b/tools/run-title.sh @@ -40,19 +40,21 @@ deploy=1 watch=30 profile=0 audio_test=0 -core_test=0 +core_test=none 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 ;; + --core-test) core_test=fceumm ;; + --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] [--core-test]" >&2; exit 2 ;; + *) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba]]" >&2; exit 2 ;; esac shift done +case "$core_test" in none|fceumm|mgba) ;; *) echo "unknown core diagnostic: $core_test" >&2; exit 2 ;; esac [[ $watch =~ ^[0-9]+$ && $profile =~ ^[0-9]+$ ]] || { echo "durations must be integers" >&2; exit 2; } (( profile <= 60 )) || { echo "GPU profile duration must be 1..60 seconds" >&2; exit 2; } @@ -165,11 +167,11 @@ with connect(**dt.load_settings()) as ftp: pass control = f"/data/homebrew/{sys.argv[1]}/core-loader-test.txt" remove_if_present(ftp, control) - if int(sys.argv[4]): + if sys.argv[4] != "none": 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)") + ftp.storbinary(f"STOR {control}", io.BytesIO((sys.argv[4] + "\n").encode())) + print(f" armed native {sys.argv[4]} loader test (no game)") control = f"/data/homebrew/{sys.argv[1]}/gpu-profile.txt" remove_if_present(ftp, control) if int(sys.argv[2]): @@ -254,7 +256,7 @@ with connect(**dt.load_settings()) as ftp: 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]): + if sys.argv[6] != "none": names.extend(["core-loader-test.json", "core-recovery-test.json"]) if int(sys.argv[4]): names.append("audio-test.json") @@ -271,7 +273,7 @@ with connect(**dt.load_settings()) as ftp: 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"): + if report.get("build_identity") != f"build identity: {expected}" or report.get("core") != sys.argv[6] 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":