mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
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:
1 parent
63a440414a
commit
d2a91b395b
44 files changed
+1635
-136
No files matched your search
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
@@ -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'])
|
||||
@@ -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')
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Executable
+74
@@ -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
@@ -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
@@ -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
@@ -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":
|
||||
|
||||
Reference in new issue
Block a user