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.
This commit is contained in:
Mihawk committed 2026-09-19 16:00:01 -04:00
1 parent 63a440414a
commit d2a91b395b
44 files changed
+1635 -136

No files matched your search

+5
View File
@@ -45,6 +45,10 @@ HOST_UNIT_TEST := build/tests/demo_renderer_tests
all: app
build: app
.PHONY: 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' \
+64 -74
View File
@@ -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.
+9
View File
@@ -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.
+36
View File
@@ -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.
+59
View File
@@ -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.
+104 -5
View File
@@ -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.
+44 -1
View File
@@ -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.
+124
View File
@@ -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
}
+138
View File
@@ -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"
}
}
+71
View File
@@ -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."
}
@@ -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"
]
}
@@ -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"
}
+19
View File
@@ -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."
}
+32
View File
@@ -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."
}
]
+1
View File
@@ -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}
+1
View File
@@ -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}
+31
View File
@@ -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
+78
View File
@@ -0,0 +1,78 @@
--- a/include/mgba-util/formatting.h
+++ b/include/mgba-util/formatting.h
@@ -14,14 +14,19 @@
#ifdef HAVE_XLOCALE
#include <xlocale.h>
-#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 <float.h>
-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
+32
View File
@@ -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));
}
+6
View File
@@ -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
+13
View File
@@ -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];
}
+33 -7
View File
@@ -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);
}
}
+41 -5
View File
@@ -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<const uintptr_t *>(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<uintptr_t>(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<void (*)()>(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;
+8 -4
View File
@@ -6,6 +6,8 @@ extern "C"
#include <gfx/video_driver.h>
#include <tasks/task_content.h>
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<unsigned (*)()>(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);
+16
View File
@@ -35,6 +35,7 @@
#include <cstring>
#include <exception>
#include <typeinfo>
#include <unistd.h>
#include <cxxabi.h>
@@ -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. */
}
+119
View File
@@ -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 <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <pthread.h>
#include <sys/mman.h>
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<void *>(*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<Mapping *>(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;
}
+14
View File
@@ -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<Directory *>(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<Directory *>(opaque);
+1
View File
@@ -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
}
+78
View File
@@ -0,0 +1,78 @@
#include <cassert>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <thread>
#include <vector>
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<unsigned char *>(__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<unsigned char *>(__wrap_realloc(p, large * 2));
assert(p && p[0] == 0xA5 && p[large - 1] == 0xA5);
p = static_cast<unsigned char *>(__wrap_realloc(p, 13));
assert(p && p[12] == 0xA5);
__wrap_free(p);
p = static_cast<unsigned char *>(__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<char *>(__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<std::thread> 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();
}
+22
View File
@@ -9,6 +9,7 @@ namespace
std::vector<std::vector<char>> batches;
size_t batch = 0;
int closed = 0;
bool seek_fails = false;
std::vector<char> 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");
+36
View File
@@ -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)
+40
View File
@@ -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'])
+53
View File
@@ -123,3 +123,56 @@ extern "C" void *ps5_core_import(const char *name) {
data = bytearray(self.original)
struct.pack_into('<I', data, offset + 8, 5) # R_X86_64_COPY
self.reject(data, 'unsupported x86-64 relocation')
def test_constructor_runs_once_per_mapping_and_rejects_bad_array(self):
source = self.folder / 'ctor.c'
source.write_text('''int ready;
__attribute__((constructor)) static void init(void) { ++ready; }
int state(void) { return ready; }
''')
path = self.folder / 'ctor.so'
subprocess.run(['cc', '-shared', '-nostdlib', '-fPIC', str(source),
'-Wl,-z,max-page-size=16384', '-Wl,-T,' + str(ROOT / 'tooling/native/ps5-core.ld'),
'-o', str(path)], check=True, capture_output=True)
data = bytearray(path.read_bytes())
data[7] = 9
path.write_bytes(data)
for _ in range(3):
handle = self.open(path)
self.assertTrue(handle, self.lib.ps5_core_dlerror())
other = self.open(path)
self.assertEqual(handle, other)
state = ctypes.CFUNCTYPE(ctypes.c_int)(self.lib.ps5_core_dlsym(handle, b'state'))
self.assertEqual(state(), 1)
self.lib.ps5_core_dlclose(other)
self.lib.ps5_core_dlclose(handle)
phoff = struct.unpack_from('<Q', data, 32)[0]
phcount = struct.unpack_from('<H', data, 56)[0]
dynamic = next(struct.unpack_from('<Q', data, phoff + i * 56 + 8)[0]
for i in range(phcount)
if struct.unpack_from('<I', data, phoff + i * 56)[0] == 2)
at = dynamic
while struct.unpack_from('<q', data, at)[0] != 25: # DT_INIT_ARRAY
self.assertNotEqual(struct.unpack_from('<q', data, at)[0], 0)
at += 16
original = bytearray(data)
init_address = struct.unpack_from('<Q', data, at + 8)[0]
struct.pack_into('<Q', data, at + 8, 2**64 - 8)
self.reject(data, 'initializer array')
data = original
writable = next(struct.unpack_from('<Q', data, phoff + i * 56 + 16)[0]
for i in range(phcount)
if struct.unpack_from('<II', data, phoff + i * 56) == (1, 6))
shoff = struct.unpack_from('<Q', data, 40)[0]
shcount = struct.unpack_from('<H', data, 60)[0]
changed = False
for i in range(shcount):
if struct.unpack_from('<I', data, shoff + i * 64 + 4)[0] != 4:
continue
offset, size = struct.unpack_from('<QQ', data, shoff + i * 64 + 24)
for reloc in range(offset, offset + size, 24):
if struct.unpack_from('<Q', data, reloc)[0] == init_address:
struct.pack_into('<q', data, reloc + 16, writable)
changed = True
self.assertTrue(changed)
self.reject(data, 'initializer callback outside executable segment')
+1 -1
View File
@@ -300,7 +300,7 @@ class PortPatches(unittest.TestCase):
change deliberately, in a commit that says why.
"""
self.assertEqual(
len(self.blocks()), 116,
len(self.blocks()), 139,
"the patch count changed: if a block was added or removed on purpose, "
"update this number in the same commit and say why in its message")
+17
View File
@@ -0,0 +1,17 @@
"""Exercise mapped allocations together with genuinely libc-owned buffers."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
class NativeMemory(unittest.TestCase):
def test_large_buffers_and_mixed_ownership(self):
with tempfile.TemporaryDirectory() as directory:
binary = Path(directory) / 'memory-test'
subprocess.run(['c++', '-std=c++17', '-pthread', '-I.',
'tests/memory_ps5_test.cpp', 'src/memory_ps5.cpp',
'-o', str(binary)], cwd=ROOT, check=True)
subprocess.run([str(binary)], check=True)
+26
View File
@@ -0,0 +1,26 @@
"""mGBA XBGR must become libretro XRGB without changing the native buffer."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
class MgbaVideo(unittest.TestCase):
def test_native_primary_colours_to_libretro(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / 'colours.c'
source.write_text('''#include <assert.h>
#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)
+25
View File
@@ -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}")
+16
View File
@@ -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)
+10
View File
@@ -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 <stdint.h>
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
+24
View File
@@ -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 <stdlib.h>', '/* patches/series, 0076: archive error diagnostics */\n#include <stdlib.h>\n#include <stdio.h>\n#include <errno.h>', '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<std::unique_ptr<Buffer>> 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<float *>(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'),
]
+74
View File
@@ -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"
+16 -7
View File
@@ -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.
#
+46 -24
View File
@@ -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 <cstring>']
aliases = {'opendir': 'ps5_opendir', 'readdir': 'ps5_readdir', 'closedir': 'ps5_closedir'}
for index, (name, kind) in enumerate(sorted(imports.items())):
target = aliases.get(name, name)
declaration = f'void core_import_{index}()' if kind == 'FUNC' else f'char core_import_{index}[]'
lines.append(f'extern "C" {declaration} asm("{target}");')
lines.extend(['extern "C" void *ps5_core_import(const char *name)', '{'])
for index, name in enumerate(sorted(imports)):
lines.append(f' if (!std::strcmp(name, "{name}")) return reinterpret_cast<void *>(&core_import_{index});')
lines.extend([' return nullptr;', '}'])
(ROOT / 'build/core_imports.inc').write_text('\n'.join(lines) + '\n')
print(f'core imports: {len(imports)} explicit native bindings generated')
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 <cstring>']
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<void *>(&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()
+10 -8
View File
@@ -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":