Add native Snes9x with verified colours and core-local C++ cleanup

Build pinned libretro Snes9x 1.63 with this project's SDK and official metadata.
Convert final RGB565 frames to independent XRGB8888 storage for the existing
Vulkan upload path. Integrate staging, native imports and loader diagnostics.

Keep C++ destructor registration inside the unloadable core. Validate bounded
fini arrays before constructors and execute cleanup at last-reference close.
Test all 65536 colours through GPU upload, pitches/bounds/cached frames and
C++ destruction/reload/malformed finalizers. Existing core bytes are unchanged.

Verification: bash tools/verify.sh passed format/unit/build/integration/evidence
with 60 tests. bash tools/run-title.sh --no-build --core-test=snes9x --watch 180
passed loader/recovery and remained running until runner close. Owner: Works
flawlessly, answering colours/audio/input and menu/content-transition checks.
Zero Vulkan refusals, GPU API failures or kernel fatal-signal reports.

Evidence: evidence/snes9x-native/{capture,expectation,iterations,abi,build}.json,
loader/recovery reports and exact linked-driver hashes. Replay with
bash tools/verify.sh evidence. Frontend identity:
64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53.
PS5_Vulkan was not modified; special-mode/save-state/performance coverage is
not implied by this ordinary-game acceptance.
This commit is contained in:
Mihawk committed 2026-09-19 16:19:59 -04:00
1 parent d2a91b395b
commit 9155dd5c23
25 files changed
+1172 -71

No files matched your search

+5
View File
@@ -45,6 +45,10 @@ HOST_UNIT_TEST := build/tests/demo_renderer_tests
all: app
build: app
.PHONY: snes9x
snes9x:
@bash tools/build-snes9x.sh
.PHONY: mgba
mgba:
@bash tools/build-mgba.sh
@@ -169,6 +173,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 snes9x Build and ABI-check the pinned PS5 Snes9x core' \
'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' \
+60 -57
View File
@@ -4,75 +4,78 @@ _Updated: 2026-09-19_
## Now
**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.
**Snes9x 1.63 is ported and owner-confirmed working.** The owner answered
“Works flawlessly” to gameplay colours/audio/input and menu/content-transition
checks. Build and console evidence is in `evidence/snes9x-native/`.
No next core is selected by this file.
## Verified build and evidence
- 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.
- Frontend identity: `64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53`.
- Snes9x source: `fae2fea08f74180759ef540ee94259213f503480`.
- Snes9x SHA-256: `e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac`.
Core size 3,392,576 bytes; 25 callbacks; 4 initializers and 1 finalizer.
Source, metadata, port-input and SDK hashes are in the evidence build report.
- All five host gates pass, 60 tests: `/tmp/snes9x-verify.log`.
Preserved ELF: `klog/snes9x-64bef2e7.elf`.
- `tools/run-title.sh --no-build --core-test=snes9x --watch 180` verifies upload,
eight load/export/API/identity/unload cycles, and failed-load recovery with the
live frontend. Both diagnostic reports pass; missing-core errors are deliberate.
- Owner-loaded SNES archive negotiated XRGB8888, 256x224, 60.10 Hz and 32040 Hz
audio; these are declared rates, not measured throughput. Live log also records
return to the 320x240 dummy menu. Owner: “Works flawlessly.”
- Final trace: zero Vulkan refusals and GPU API failure records; kernel has zero
fatal signal reports. Title remained open for 180s; runner closed it. This is
a menu/game/menu window, not 180 seconds of uninterrupted gameplay.
- Raw: `klog/snes9x-first-run.log`, `klog/run-PPSA99169-161400.log`,
`klog/snes9x-first-live-retroarch.log`, `klog/snes9x-first-live-trace.txt`.
- FCEUmm and mGBA core bytes are unchanged from their accepted builds. Their
prior evidence is in `evidence/native-core-loading/` and `evidence/mgba-native/`.
- PS5_Vulkan was read only. Its four linked archive hashes stayed unchanged
during this build and are recorded in `evidence/snes9x-native/driver-archives.json`.
The owner develops that repository concurrently; no source revision is inferred.
## What changed
- `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/`.
- `make snes9x`: pinned upstream libretro Makefile, explicit native SDK tools,
official `.info` in info/ and cores/, integrated title/import-table build.
- Snes9x keeps RGB565 internally. Its video callback converts into a separate
bounded XRGB8888 buffer for the existing frontend RGBA GPU upload path.
All 65,536 RGB565 colours are host-tested end to end; padded pitch, cached
source immutability, bounds and 256/512/602/1024 widths are covered too.
- Core-local C++ destructor registration prevents stale process-exit callbacks.
Loader validates bounded fini arrays before any initializer, runs cleanup in
reverse order at last close, then unmaps. Reload and malformed-file tests pass.
- Existing texture cropping, immutable cached uploads and native Quit remain.
Vulkan is still the GPU presentation route; no frontend video patch changed
for Snes9x and no change to PS5_Vulkan was needed.
## Limits
- 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.
- Ordinary game/return-to-menu acceptance does not establish special-chip,
subsystem BIOS, NTSC filter, HD Mode 7, hires/interlace or every-game coverage.
The owner confirmed the requested test, but the captured geometry sequence
does not independently establish a full cross-core transition matrix.
- Save RAM/state round trips, RTC, long-run A/V sync, measured performance,
arbitrary Slang presets/history and linear-filter edges remain separate work.
- Loader still rejects TLS, legacy DT_INIT/DT_FINI, nonempty preinit arrays,
unsupported relocations and additional dependencies; no unwind registration.
Lifecycle callbacks must not reenter the loader. Faulty cores share the process.
- mGBA's 16-bit-only colour correction/interframe blending remain disabled in
its XRGB build. Native-owned realloc retains native allocation ownership.
- Upstream Snes9x metadata still says display_version 1.61; runtime accurately
reports 1.63 plus source revision. Official metadata was not rewritten.
## Preserved platform and files
## Preserved platform and 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.
XMB stays default and RGUI selectable. Vulkan remains active; video_ps5 stays
registered/selectable as CPU fallback. Native audio/input/binding, config saving,
FTP permissions (0777) and directory browsing remain. Useful logs are retained.
Deployments preserve live settings/games. No ROM or BIOS is shipped.
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.
See `docs/DEPLOYMENT.md` for core build and upload procedures.
## Operating notes
@@ -80,6 +83,6 @@ Uploads/runs are authorized without further permission. Never interrupt an
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.
Preserve live frontend logs before relaunch replaces them. Owner can supply
images; a matching camera cadence can hide flicker. Do not infer duration or
Preserve live frontend logs before relaunch replaces them. Owner supplies images;
a matching camera cadence can hide flicker. Do not infer gameplay duration or
manual closure solely from the runner's final process count.
+10 -1
View File
@@ -207,7 +207,16 @@ 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.
covers all shipped 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.
`make snes9x` builds and ABI-checks Snes9x without uploading. The title build
stages snes9x_libretro.so and official metadata alongside the existing cores;
rebuild the frontend too, since its import table includes C++ runtime bindings.
Use `tools/run-title.sh --no-build --core-test=snes9x --watch 180` for loader and
recovery diagnostics followed by manual SNES archive loading. Preserve existing
config/content; verify gameplay colours, audio/input, Quick Menu, Close Content
and a subsequent game before accepting the run.
+28
View File
@@ -2744,3 +2744,31 @@ 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.
## 2026-09-19 — Snes9x pixel contract and C++ core unload
Snes9x 1.63's libretro renderer outputs RGB565 and negotiates that format at
normal and subsystem content load. The port leaves renderer arithmetic and
filter output intact, then converts at the three final video callback sites to
independent XRGB8888 storage. Both negotiation sites declare XRGB8888, matching
the existing frontend's RGBA conversion and matching-format staging/image copy.
All 65,536 RGB565 inputs pass through both actual helpers in the host test with
correct expanded R/G/B channels and alpha 255. Additional tests cover non-tight
pitch, repeated frames without source mutation, bounds, hires and NTSC widths.
Owner's console result: “Works flawlessly” for gameplay and menu transitions;
trace confirms XRGB8888 game output and return to the dummy menu.
This core has four C++ initializers. A plain build imports __cxa_atexit, which
would register callbacks into a mapping that the native loader later frees.
The deployed core instead hides a module-local destructor registry behind the
upstream export map. Its single fini-array callback drains that list before
unmapping. Loader validates the entire bounded finalizer table, and all targets,
before constructors run; last-reference close calls it in reverse array order.
Host tests verify destruction order, reference counting, repeated reload and
rejection without side effects. Eight actual Snes9x load/unload cycles and the
resident-menu recovery test pass on the PS5. This is not TLS or general C++
exception/unwind support.
Source and linked archive provenance are in `evidence/snes9x-native/`.
PS5_Vulkan was not edited; FCEUmm/mGBA core binaries retain their accepted hashes.
+60
View File
@@ -2128,3 +2128,63 @@ 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.
## 2026-09-19 — Snes9x with explicit colour and C++ lifetime contracts
Implemented the requested Snes9x port using pinned official libretro revision
fae2fea08f74180759ef540ee94259213f503480 (1.63), source archive SHA-256
0d4b0c4181d66668ec0040eb17f90a559f7b7fa6cbd9198593c3bdbe79acd029.
`make snes9x` invokes the upstream libretro Makefile with this project's SDK;
no alternate SDK or payload CRT is introduced. Upstream LTO-off, no-strict-
aliasing and no-exceptions/no-RTTI choices remain. Explicit native/C++ bindings
require -z undefs for core link but are resolved by the title link. Metadata
revision/digests and all port inputs are captured in the evidence build report.
Upstream info display_version remains 1.61; the actual core reports 1.63 + pin.
Colour handling leaves RGB565 renderer/filter arithmetic intact and converts
only final frames into a separate bounded XRGB8888 buffer. Both libretro pixel
format negotiation sites and every video callback path are patched. Host tests
exhaust all 65,536 RGB565 colours through the actual frontend RGBA upload, with
additional pitch, capacity, cached-source, hires/NTSC/4x dimension checks.
Initial static build 81f07c1a… passed ABI but was not deployed: its imported
__cxa_atexit would retain callbacks into unloaded code. The final core includes
an export-hidden C++ destructor registry; a validated fini-array callback runs
before last-close unmap. Loader tests exercise reverse destruction order,
reference counting/reload and invalid finalizer rejection before constructors.
The host C++ fixture initially hit GNU ld's overlapping EH FDE header error with
the PS5 linker layout; --no-eh-frame-hdr fixes that host-only fixture. The target
build linked normally. TLS/legacy init/fini/unwind registration remain unsupported.
Verification and console acceptance:
- `bash tools/verify.sh`: PASS format, unit, build, integration, evidence;
60 tests. Log `/tmp/snes9x-verify.log`. Final core SHA-256:
e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac,
3,392,576 bytes, 25 exports, 4 initializers, 1 finalizer.
- Frontend identity:
64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53.
Preserved ELF `klog/snes9x-64bef2e7.elf`.
- `bash tools/run-title.sh --no-build --core-test=snes9x --watch 180`:
verified deployment, eight native loader cycles and resident-menu recovery
passed. Deliberate missing-core errors are part of that successful test.
Raw `klog/snes9x-first-run.log`, kernel `klog/run-PPSA99169-161400.log`.
One current-build launch, zero Vulkan refusals/API failure records and zero
kernel fatal signal reports. Title remained open through 180s and runner
closed it. No claim of 180s uninterrupted gameplay.
- Owner-loaded archive negotiated XRGB8888, 256x224, declared 60.10 Hz and
32040 Hz audio, then returned to the dummy menu. Owner answered the combined
colours/sound/controls/Quick Menu/Close Content/next-content question with
“Works flawlessly.” The geometry log independently records menu -> Snes9x ->
menu; no exhaustive cross-core matrix or special-mode coverage is claimed.
- FCEUmm/mGBA binaries retain their accepted hashes. PS5_Vulkan was read only;
its exact linked archive hashes stayed unchanged during verification and are
saved in the evidence. No driver source revision is inferred.
Evidence: `evidence/snes9x-native/`; replay `bash tools/verify.sh evidence`.
Raw logs/private filenames remain ignored. BIOS subsystems, special chips,
interlace/hires/NTSC/HD Mode 7 console tests, saves/states and long-run timing are
separate acceptance work. Build/ABI reports are static checks; their false
console_loading_verified field is superseded only by the separate console
capture, not rewritten into an unsupported claim.
+37 -3
View File
@@ -415,7 +415,7 @@ release allocations and report the failing operation; successful handles are
reference counted and unmapped on the final close.
`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
explicitly shipped FCEUmm, mGBA and Snes9x ELFs; bindings and all 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
@@ -424,7 +424,7 @@ 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, legacy DT_INIT/DT_FINI, finalizers, C++ unwind
linker: no TLS, ELF interpreter, legacy DT_INIT/DT_FINI, 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.
@@ -479,7 +479,7 @@ the archive reports its own 0.11.0 version, with the exact source revision in
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,
`build/cores/stage/info/mgba_libretro.info`. The title build stages all shipped 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.
@@ -540,3 +540,37 @@ The host regression covers FCEUmm, the blank menu frame, GBA and GB widths acros
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.
## Snes9x core build and colour contract
`make snes9x` runs `tools/build-snes9x.sh` against pinned official libretro source
`fae2fea08f74180759ef540ee94259213f503480` (Snes9x 1.63). Its own libretro Makefile
selects the sources; CC/CXX/AR/LD are explicitly the project's SDK wrappers.
LTO remains off as upstream defaults, and upstream's no-strict-aliasing,
no-exceptions/no-RTTI flags remain. No alternate SDK, CRT, SDL or host library
is linked. The existing native linker script separates RX/R/RW pages. Native
imports and C++ SDK runtime references are bound by the frontend's explicit
union table; `-z undefs` permits these at core link, while the title link must
resolve every entry. Metadata uses the same official core-info revision as the
other cores; its upstream display_version says 1.61, while the core reports 1.63
plus its source revision. The metadata otherwise stays upstream unchanged.
The renderer retains RGB565 including its NTSC and hires paths. The callback
patch converts to a separate bounded XRGB8888 buffer, reports XRGB8888 at both
normal and subsystem load sites, and supplies the converted 32-bit pitch.
The existing frontend XRGB-to-RGBA conversion then feeds matching staging and
sampled textures. Cached native pixels remain unchanged. Tests cover every
RGB565 value end to end, non-tight pitch and 256/512/602/1024-pixel widths through
478 rows. These tests do not by themselves establish console acceptance of
NTSC filtering, HD Mode 7 or interlaced games.
Snes9x has C++ globals. `tooling/native/core_cxx_runtime.cpp` is linked locally
inside this core, hidden by upstream's version script. Its own __cxa_atexit list
runs in reverse registration order via a fini-array callback, so unloaded code
is never retained in the process-wide exit registry. The loader accepts bounded
DT_FINI_ARRAY/DT_FINI_ARRAYSZ, validates all targets before any initializer, and
runs finalizers in reverse array order on the last close before unmapping.
Reload gets a new registry; a rejected load executes no callbacks. This does not
add TLS, legacy init/fini functions or exception-unwind registration support.
As with initializers, lifecycle callbacks must not reenter this bounded loader.
+23
View File
@@ -255,3 +255,26 @@ 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.
### Snes9x
`tests/test_snes9x_video.py` exhaustively checks 65,536 RGB565 values through the
actual core conversion and frontend GPU upload helpers. It also covers padded
input rows, source immutability, repeated cached frames, common/hires/NTSC/4x
sizes, and rejection without writing on bad dimensions/pitch/capacity.
`test_core_loader.py` loads a C++ ELF with this core-local destructor registry:
construct once, defer destruction while another handle exists, destroy in reverse
order on the last close, reload repeatedly, and reject malformed finalizer tables
or non-executable callbacks before any constructor. The host fixture disables
GNU ld's EH frame header because this PS5 layout is not a host unwind layout;
exceptions/unwind registration are outside the loader contract.
Console acceptance uses `tools/run-title.sh --no-build --core-test=snes9x
--watch 180`: eight loader cycles and live-menu recovery, then owner-loaded SNES
archive with correct colours, sound and controls. Open Quick Menu, Close Content
and load another game/core to check persistent colour/flicker regressions. Owner
visual confirmation and matching build identity are required. Core-declared FPS
and audio rate are metadata, not performance measurements. NTSC filters, special
chips, subsystem BIOSes, interlace/hires modes, SRAM/state round trips and long-run
A/V timing require separate coverage beyond an ordinary game acceptance run.
+179
View File
@@ -0,0 +1,179 @@
{
"sha256": "e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac",
"bytes": 3392576,
"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": [
"_ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4findEcm",
"_ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5rfindEcm",
"_ZNKSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEE3strEv",
"_ZNKSt3__16locale9use_facetERNS0_2idE",
"_ZNKSt3__18ios_base6getlocEv",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5eraseEmm",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendEPKc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendEPKcm",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6assignEPKc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6assignEPKcm",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6resizeEmc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9push_backEc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED2Ev",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEaSERKS5_",
"_ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE6sentryC1ERS3_b",
"_ZNSt3__113basic_istreamIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_",
"_ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev",
"_ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEj",
"_ZNSt3__114basic_iostreamIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__115basic_streambufIcNS_11char_traitsIcEEEC2Ev",
"_ZNSt3__115basic_streambufIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__122__libcpp_verbose_abortEPKcz",
"_ZNSt3__15ctypeIcE2idE",
"_ZNSt3__16localeD1Ev",
"_ZNSt3__18ios_base4initEPv",
"_ZNSt3__18ios_base5clearEj",
"_ZNSt3__19basic_iosIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__1plIcNS_11char_traitsIcEENS_9allocatorIcEEEENS_12basic_stringIT_T0_T1_EEPKS6_RKS9_",
"_ZTTNSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTTNSt3__119basic_istringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTVNSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTVNSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTVNSt3__119basic_istringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZdaPv",
"_ZdlPvm",
"_Znam",
"_Znwm",
"__cxa_guard_acquire",
"__cxa_guard_release",
"__error",
"__isthreaded",
"__stderrp",
"atoi",
"calloc",
"close",
"closedir",
"cos",
"exit",
"exp",
"fclose",
"ferror",
"fflush",
"fileno",
"fopen",
"fprintf",
"fread",
"free",
"fseeko",
"ftello",
"ftruncate",
"fwrite",
"isalnum",
"isalpha",
"isspace",
"isxdigit",
"localtime",
"lseek",
"malloc",
"mbstowcs",
"memchr",
"memcmp",
"memcpy",
"memmove",
"memset",
"mkdir",
"open",
"opendir",
"perror",
"pow",
"printf",
"pthread_mutex_destroy",
"pthread_mutex_lock",
"pthread_mutex_unlock",
"putchar",
"puts",
"rand",
"read",
"readdir",
"realloc",
"remove",
"rename",
"setlocale",
"setvbuf",
"sincos",
"snprintf",
"sprintf",
"srand",
"sscanf",
"stat",
"strcasecmp",
"strcat",
"strchr",
"strcmp",
"strcpy",
"strdup",
"strftime",
"strlen",
"strncasecmp",
"strncmp",
"strncpy",
"strrchr",
"strstr",
"strtod",
"strtok",
"strtol",
"strtoul",
"time",
"tolower",
"toupper",
"vsnprintf",
"wcslen",
"wcstombs",
"write"
],
"load_segments": [
{
"flags": 5,
"alignment": 16384
},
{
"flags": 4,
"alignment": 16384
},
{
"flags": 6,
"alignment": 16384
}
],
"abi_passed": true,
"console_loading_verified": false
}
+191
View File
@@ -0,0 +1,191 @@
{
"sha256": "e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac",
"bytes": 3392576,
"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": [
"_ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4findEcm",
"_ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5rfindEcm",
"_ZNKSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEE3strEv",
"_ZNKSt3__16locale9use_facetERNS0_2idE",
"_ZNKSt3__18ios_base6getlocEv",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5eraseEmm",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendEPKc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendEPKcm",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6assignEPKc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6assignEPKcm",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6resizeEmc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9push_backEc",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED2Ev",
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEaSERKS5_",
"_ZNSt3__113basic_istreamIcNS_11char_traitsIcEEE6sentryC1ERS3_b",
"_ZNSt3__113basic_istreamIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryC1ERS3_",
"_ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEE6sentryD1Ev",
"_ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEj",
"_ZNSt3__114basic_iostreamIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__115basic_streambufIcNS_11char_traitsIcEEEC2Ev",
"_ZNSt3__115basic_streambufIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__122__libcpp_verbose_abortEPKcz",
"_ZNSt3__15ctypeIcE2idE",
"_ZNSt3__16localeD1Ev",
"_ZNSt3__18ios_base4initEPv",
"_ZNSt3__18ios_base5clearEj",
"_ZNSt3__19basic_iosIcNS_11char_traitsIcEEED2Ev",
"_ZNSt3__1plIcNS_11char_traitsIcEENS_9allocatorIcEEEENS_12basic_stringIT_T0_T1_EEPKS6_RKS9_",
"_ZTTNSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTTNSt3__119basic_istringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTVNSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTVNSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZTVNSt3__119basic_istringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE",
"_ZdaPv",
"_ZdlPvm",
"_Znam",
"_Znwm",
"__cxa_guard_acquire",
"__cxa_guard_release",
"__error",
"__isthreaded",
"__stderrp",
"atoi",
"calloc",
"close",
"closedir",
"cos",
"exit",
"exp",
"fclose",
"ferror",
"fflush",
"fileno",
"fopen",
"fprintf",
"fread",
"free",
"fseeko",
"ftello",
"ftruncate",
"fwrite",
"isalnum",
"isalpha",
"isspace",
"isxdigit",
"localtime",
"lseek",
"malloc",
"mbstowcs",
"memchr",
"memcmp",
"memcpy",
"memmove",
"memset",
"mkdir",
"open",
"opendir",
"perror",
"pow",
"printf",
"pthread_mutex_destroy",
"pthread_mutex_lock",
"pthread_mutex_unlock",
"putchar",
"puts",
"rand",
"read",
"readdir",
"realloc",
"remove",
"rename",
"setlocale",
"setvbuf",
"sincos",
"snprintf",
"sprintf",
"srand",
"sscanf",
"stat",
"strcasecmp",
"strcat",
"strchr",
"strcmp",
"strcpy",
"strdup",
"strftime",
"strlen",
"strncasecmp",
"strncmp",
"strncpy",
"strrchr",
"strstr",
"strtod",
"strtok",
"strtol",
"strtoul",
"time",
"tolower",
"toupper",
"vsnprintf",
"wcslen",
"wcstombs",
"write"
],
"load_segments": [
{
"flags": 5,
"alignment": 16384
},
{
"flags": 4,
"alignment": 16384
},
{
"flags": 6,
"alignment": 16384
}
],
"abi_passed": true,
"console_loading_verified": false,
"source_revision": "fae2fea08f74180759ef540ee94259213f503480",
"source_archive_sha256": "0d4b0c4181d66668ec0040eb17f90a559f7b7fa6cbd9198593c3bdbe79acd029",
"info_revision": "5a74858ab2f7a50cebb5a6330895bc38899531c0",
"info_sha256": "fa62b78d58bc4c30f4e0a4a581f0cb3252447b7f198b55bd6ebe2e4b9bde5cc2",
"sdk_compiler_wrapper_sha256": "3dcf3139662db7b0f796c2073bc8f673a5557e48bc24d2e413896d33847a69db",
"port_inputs_sha256": {
"tools/build-snes9x.sh": "5c4c3293a0e885bf4e16382e122d3b00d620d5b3dda8cd3d0c8df084adf5a57a",
"tooling/native/ps5-core.ld": "5ffe0f0e99e23de5245afb46fa6dfda6dd860b53d942a5fe2f06eaf9e520df42",
"tooling/native/core_cxx_runtime.cpp": "f621c896579d20a27077d135284a26ffc11ed5c27064a55aba8af63ddd58d0bc",
"patches/snes9x/native-xrgb-output.patch": "6f610d172afd419c7b1b08ad7910576551f132e12fe21e9294ff7ff91bd816a1",
"tooling/snes9x/ps5-video.h": "96a2c9e770212d07227a7bede0911cfb086bb2d8113699c17f894d8a1a593ad6"
}
}
+73
View File
@@ -0,0 +1,73 @@
{
"step": "snes9x-native",
"recorded": "2026-09-19",
"revision": "8b588e6 + native Snes9x port",
"command": "bash tools/verify.sh; bash tools/run-title.sh --no-build --core-test=snes9x --watch 180 (owner selects SNES archive and menu transitions manually)",
"raw_capture": "klog/snes9x-first-run.log",
"raw_lines": 10341,
"lines": [
"frontend build identity: 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"Snes9x SHA-256: e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac",
"core loader test: passed=1 cycles=8 exports=25 api=1 name=Snes9x",
"core recovery test: passed=1 selection_rejected=1 content_rejected=1 menu_context_preserved=1 menu_core_load=1",
"core loader: ran 4 initializers",
"core loader: ran 1 finalizers",
"[INFO] [Environ] SET_PIXEL_FORMAT: RGB565.",
"[INFO] [Core] Geometry: 320x240, Aspect: 1.333, FPS: 60.00, Sample rate: 48000.00 Hz.",
"[INFO] [Environ] SET_PIXEL_FORMAT: XRGB8888.",
"[INFO] [Core] Geometry: 256x224, Aspect: 1.333, FPS: 60.10, Sample rate: 32040.00 Hz.",
"[INFO] [Environ] SET_PIXEL_FORMAT: RGB565.",
"[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",
"Title still running after 180s; runner closed it. This is a menu/game/menu test, not 180 seconds of continuous gameplay.",
"owner: Works flawlessly",
"Owner confirmation answered the colours, sound, controls and Quick Menu/Close Content/next-content question. Captured geometry independently shows menu -> Snes9x -> menu; no exhaustive cross-core matrix is claimed.",
"Host verification: PASS format unit build integration evidence; 60 tests",
"RGB565 conversion: all 65536 values match final RGBA bytes; pitch/bounds/source immutability/common/hires/NTSC/4x dimensions pass host tests.",
"FCEUmm and mGBA core bytes unchanged; PS5_Vulkan not modified; exact driver archive hashes recorded."
],
"loader": {
"build_identity": "build identity: 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"core": "snes9x",
"passed": true,
"cycles": 8,
"exports": 25,
"api": 1,
"missing_rejected": true,
"unknown_symbol_rejected": true
},
"recovery": {
"build_identity": "build identity: 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"core": "snes9x",
"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,
"initializer_records": 16,
"finalizer_records": 16,
"watch_seconds": 180,
"continuous_gameplay_seconds": null,
"owner_confirmed": true,
"build_report_note": "console_loading_verified=false in static ABI/build reports describes that check alone; this console capture provides separate acceptance."
}
@@ -0,0 +1,6 @@
{
"build/driver/ps5/libps5vk.ps5.a": "e691159519272da11856b024ffe56675ff3f289f69d3c8c44c09e933f83d8886",
".deps/native/vulkan-runtime/lib/libvk_runtime.ps5.a": "4106a2c56b269bc52a3976b6bc0134c00b5006ff5358a4b45355afce4a907aa7",
"build/driver/ps5/libpsbc_driver.ps5.a": "a7605370c28895a3b5a13d0718ebfea2abf1b9cf842633de07533775745b21b8",
".deps/native/psbc/lib/libpsbc_support.ps5.a": "e36d3e2a3fa93b0a74db15b9efce7aad4ed7ac4e024de98d5f23dc5fee90acf8"
}
+19
View File
@@ -0,0 +1,19 @@
{
"step": "snes9x-native",
"note": "Native ordinary SNES archive loading, correct colour conversion and owner-confirmed menu transitions, with per-core C++ cleanup.",
"must_contain": [
"frontend build identity: 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"Snes9x SHA-256: e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac",
"core loader test: passed=1 cycles=8 exports=25 api=1 name=Snes9x",
"core recovery test: passed=1 selection_rejected=1 content_rejected=1 menu_context_preserved=1 menu_core_load=1",
"core loader: ran 4 initializers",
"core loader: ran 1 finalizers",
"[INFO] [Environ] SET_PIXEL_FORMAT: XRGB8888.",
"[INFO] [Core] Geometry: 256x224, Aspect: 1.333, FPS: 60.10, Sample rate: 32040.00 Hz.",
"current-build launches: 1; Vulkan refusals: 0; GPU API failure records: 0; kernel fatal signal reports: 0",
"owner: Works flawlessly",
"Host verification: PASS format unit build integration evidence; 60 tests"
],
"must_not_contain": [],
"human_check": "Owner replied \u201cWorks flawlessly\u201d to gameplay colours/audio/input and menu/content transition test. Special chips/modes and comprehensive cross-core coverage are not implied."
}
+20
View File
@@ -0,0 +1,20 @@
[
{
"stage": "initial cross build",
"log": "/tmp/snes9x-first-build.log",
"core_sha256": "81f07c1ac7b148c9400d2ebf58861420ba0942a2c184db44483a4a7a4eabd461",
"outcome": "Built and passed static ABI. Not deployed: imported process-wide __cxa_atexit would retain callbacks into unloadable code."
},
{
"stage": "core-local C++ cleanup",
"log": "/tmp/snes9x-runtime-build.log",
"core_sha256": "e8b66c5f6656afe927181e3fb4fb5d13152ae525fc64705b59af5d9a847a7cac",
"outcome": "Private destructor registry, 4 initializers and 1 finalizer; no imported __cxa_atexit/__cxa_finalize. Host C++ fixture initially hit GNU ld overlapping EH FDE header error with PS5 layout; --no-eh-frame-hdr fixes host-only fixture. Actual PS5 build linked normally. Last-close cleanup/reload and malformed-finalizer rejection tests pass."
},
{
"stage": "full verification",
"log": "/tmp/snes9x-verify.log",
"frontend_identity": "64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"outcome": "All five gates pass; 60 tests, including every RGB565 input through XRGB callback and RGBA upload, pitches/cached source/bounds and high-resolution sizes."
}
]
+10
View File
@@ -0,0 +1,10 @@
{
"build_identity": "build identity: 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"core": "snes9x",
"passed": true,
"cycles": 8,
"exports": 25,
"api": 1,
"missing_rejected": true,
"unknown_symbol_rejected": true
}
+9
View File
@@ -0,0 +1,9 @@
{
"build_identity": "build identity: 64bef2e7826e5a63a6644a933e24ef5403d28c754230c0038eb8a76b4baa1c53",
"core": "snes9x",
"passed": true,
"selection_rejected": true,
"content_rejected": true,
"menu_context_preserved": true,
"menu_core_load": true
}
+78
View File
@@ -0,0 +1,78 @@
--- a/libretro/libretro.cpp
+++ b/libretro/libretro.cpp
@@ -1,4 +1,5 @@
#include "libretro.h"
+#include "ps5-video.h"
#include <streams/file_stream.h>
#include <vfs/vfs_hybrid.h>
#include <file/file_path.h>
@@ -74,6 +75,24 @@
retro_log_printf_t log_cb = NULL;
static retro_video_refresh_t video_cb = NULL;
+/* PS5: use the frontend's verified XRGB upload route, including NTSC output. */
+static void ps5_video_frame(const void *pixels, unsigned width, unsigned height, size_t pitch)
+{
+ static uint32_t output[MAX_SNES_WIDTH_4X * MAX_SNES_HEIGHT];
+ if (!pixels)
+ {
+ video_cb(NULL, width, height, (size_t)width * sizeof(uint32_t));
+ return;
+ }
+ if (!ps5_snes9x_frame(output, sizeof(output) / sizeof(output[0]),
+ static_cast<const uint16_t *>(pixels), pitch, width, height))
+ {
+ if (log_cb)
+ log_cb(RETRO_LOG_ERROR, "PS5 frame exceeds conversion capacity or has invalid pitch.\n");
+ return;
+ }
+ video_cb(output, width, height, (size_t)width * sizeof(uint32_t));
+}
static retro_audio_sample_t audio_cb = NULL;
static retro_audio_sample_batch_t audio_batch_cb = NULL;
static retro_input_poll_t poll_cb = NULL;
@@ -1522,10 +1541,10 @@
if (rom_loaded)
{
- /* If we're in RGB565 format, switch frontend to that */
+ /* Native pixels remain RGB565; callback converts to XRGB8888. */
if (RED_SHIFT_BITS == 11)
{
- enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_RGB565;
+ enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888;
if (!environ_cb || !environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
{
return false;
@@ -1673,7 +1692,7 @@
{
if(RED_SHIFT_BITS == 11)
{
- enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_RGB565;
+ enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888;
if (!environ_cb || !environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
return false;
}
@@ -2548,7 +2567,7 @@
else
snes_ntsc_blit(snes_ntsc, GFX.Screen, GFX.Pitch / 2, burst_phase, width, height, snes_ntsc_buffer, MAX_SNES_WIDTH_NTSC * 2);
- video_cb(snes_ntsc_buffer + ((int)(MAX_SNES_WIDTH_NTSC) * overscan_offset), SNES_NTSC_OUT_WIDTH(256), height, MAX_SNES_WIDTH_NTSC * 2);
+ ps5_video_frame(snes_ntsc_buffer + ((int)(MAX_SNES_WIDTH_NTSC) * overscan_offset), SNES_NTSC_OUT_WIDTH(256), height, MAX_SNES_WIDTH_NTSC * 2);
}
else if (width == MAX_SNES_WIDTH && hires_blend)
{
@@ -2594,11 +2613,11 @@
width >>= 1;
}
- video_cb(GFX.Screen + ((int)(GFX.Pitch >> 1) * overscan_offset), width, height, GFX.Pitch);
+ ps5_video_frame(GFX.Screen + ((int)(GFX.Pitch >> 1) * overscan_offset), width, height, GFX.Pitch);
}
else
{
- video_cb(GFX.Screen + ((int)(GFX.Pitch >> 1) * overscan_offset), width, height, GFX.Pitch);
+ ps5_video_frame(GFX.Screen + ((int)(GFX.Pitch >> 1) * overscan_offset), width, height, GFX.Pitch);
}
return TRUE;
+6
View File
@@ -50,6 +50,12 @@ extern "C" void ps5_core_loader_test_if_requested()
test_path = "/app0/cores/mgba_libretro.so";
test_name = "mGBA";
}
else if (fields == 1 && !std::strcmp(selection, "snes9x"))
{
test_core = "snes9x";
test_path = "/app0/cores/snes9x_libretro.so";
test_name = "Snes9x";
}
else if (fields != 1 || std::strcmp(selection, "fceumm"))
{
std::fprintf(stderr, "core loader test: unsupported selection\n");
+43 -6
View File
@@ -42,6 +42,8 @@ struct Module
size_t symbol_count;
const char *strings;
size_t string_size;
const uintptr_t *finalizers;
size_t finalizer_count;
};
Module *modules = nullptr;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
@@ -244,7 +246,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;
uint64_t init_array = 0, init_bytes = 0, fini_array = 0, fini_bytes = 0;
bool have_fini_array = false, have_fini_size = false;
bool have_init_array = false, have_init_size = false;
for (size_t i = 0; i < dynamic->p_filesz / sizeof(Elf64_Dyn); ++i)
{
@@ -277,11 +280,24 @@ bool load(Module *m)
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))
if (tag == DT_FINI_ARRAY)
{
if (have_fini_array)
return fail("duplicate finalizer array");
have_fini_array = true;
fini_array = value;
}
if (tag == DT_FINI_ARRAYSZ)
{
if (have_fini_size)
return fail("duplicate finalizer array size");
have_fini_size = true;
fini_bytes = value;
}
// Legacy init/fini functions, TLS and C++ unwinding remain unsupported.
if (tag == DT_TEXTREL || ((tag == DT_INIT || tag == DT_FINI || tag == DT_PREINIT_ARRAYSZ ||
tag == DT_RELSZ || tag == 35 /* DT_RELRSZ */) &&
value))
return fail("unsupported core initialization/relocation feature");
}
if (!terminated)
@@ -292,6 +308,12 @@ bool load(Module *m)
return fail("invalid initializer array range/size");
if (have_init_array && !have_init_size)
return fail("initializer array has no size");
if (fini_bytes &&
(!have_fini_array || fini_array % sizeof(uint64_t) || fini_bytes % sizeof(uint64_t) ||
fini_bytes > 1024 * sizeof(uint64_t) || !mapped(m, fini_array, fini_bytes, PF_R)))
return fail("invalid finalizer array range/size");
if (have_fini_array && !have_fini_size)
return fail("finalizer 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);
@@ -370,6 +392,15 @@ bool load(Module *m)
if (initializers[i] < base || !mapped(m, initializers[i] - base, 1, PF_X))
return fail("initializer callback outside executable segment");
}
const auto *finalizers =
fini_bytes ? reinterpret_cast<const uintptr_t *>(m->base + fini_array) : nullptr;
const size_t finalizer_count = fini_bytes / sizeof(uintptr_t);
for (size_t i = 0; i < finalizer_count; ++i)
{
const uintptr_t base = reinterpret_cast<uintptr_t>(m->base);
if (finalizers[i] < base || !mapped(m, finalizers[i] - base, 1, PF_X))
return fail("finalizer 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)
@@ -386,6 +417,8 @@ bool load(Module *m)
}
for (size_t i = 0; i < initializer_count; ++i)
reinterpret_cast<void (*)()>(initializers[i])();
m->finalizers = finalizers;
m->finalizer_count = finalizer_count;
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",
@@ -474,6 +507,10 @@ extern "C" int ps5_core_dlclose(void *handle)
if (--m->references == 0)
{
*at = m->next;
for (size_t i = m->finalizer_count; i; --i)
reinterpret_cast<void (*)()>(m->finalizers[i - 1])();
if (m->finalizer_count)
std::fprintf(stderr, "core loader: ran %zu finalizers\n", m->finalizer_count);
release(m);
}
pthread_mutex_unlock(&mutex);
+78 -1
View File
@@ -32,11 +32,22 @@ int run(void) { return call_import(++*pointer); }
cls.original = bytes(data)
wrapper = cls.folder / 'runtime.cpp'
wrapper.write_text('''#include <cstring>
#include <cstdlib>
#include <pthread.h>
static bool allow = true;
static int events = 0;
static void native_record(int value) { events = events * 10 + value; }
extern "C" void clear_events() { events = 0; }
extern "C" int get_events() { return events; }
extern "C" void allow_import(bool value) { allow = value; }
static int native_add(int value) { return value + 40; }
extern "C" void *ps5_core_import(const char *name) {
return allow && !std::strcmp(name, "native_add") ? reinterpret_cast<void *>(&native_add) : nullptr;
if (allow && !std::strcmp(name, "native_add")) return reinterpret_cast<void *>(&native_add);
#define BIND(symbol) if (!std::strcmp(name, #symbol)) return reinterpret_cast<void *>(&symbol)
BIND(native_record); BIND(malloc); BIND(free);
BIND(pthread_mutex_lock); BIND(pthread_mutex_unlock); BIND(pthread_mutex_destroy);
#undef BIND
return nullptr;
}
''')
harness = cls.folder / 'loader.so'
@@ -176,3 +187,69 @@ int state(void) { return ready; }
changed = True
self.assertTrue(changed)
self.reject(data, 'initializer callback outside executable segment')
def test_cpp_destructors_run_on_last_close_and_bad_finalizer_rejected(self):
source = self.folder / 'dtor.cpp'
source.write_text('''extern "C" void native_record(int);
struct Object {
int marker;
Object(int value) : marker(value) { native_record(value); }
~Object() { native_record(marker + 1); }
};
static Object a(1), b(3);
''')
path = self.folder / 'dtor.so'
subprocess.run(['c++', '-shared', '-nostdlib', '-fPIC', '-fno-exceptions', '-fno-rtti',
str(source), str(ROOT / 'tooling/native/core_cxx_runtime.cpp'),
'-Wl,--no-eh-frame-hdr',
'-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(4):
self.lib.clear_events()
handle = self.open(path)
self.assertTrue(handle, self.lib.ps5_core_dlerror())
other = self.open(path)
self.assertEqual(handle, other)
self.assertEqual(self.lib.get_events(), 13)
self.lib.ps5_core_dlclose(other)
self.assertEqual(self.lib.get_events(), 13)
self.lib.ps5_core_dlclose(handle)
self.assertEqual(self.lib.get_events(), 1342)
# Invalid finalizer is rejected BEFORE any constructor can run.
self.lib.clear_events()
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] != 26: # DT_FINI_ARRAY
self.assertNotEqual(struct.unpack_from('<q', data, at)[0], 0)
at += 16
fini_address = struct.unpack_from('<Q', data, at + 8)[0]
original = bytes(data)
struct.pack_into('<Q', data, at + 8, 2**64 - 8)
self.reject(data, 'finalizer array')
self.assertEqual(self.lib.get_events(), 0)
data = bytearray(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] == fini_address:
struct.pack_into('<q', data, reloc + 16, writable)
changed = True
self.assertTrue(changed)
self.reject(data, 'finalizer callback outside executable segment')
self.assertEqual(self.lib.get_events(), 0)
+66
View File
@@ -0,0 +1,66 @@
"""Check every RGB565 colour through the exact core and frontend upload helpers."""
import ctypes
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
class Snes9xVideo(unittest.TestCase):
def test_colours_pitch_bounds_and_cached_source(self):
with tempfile.TemporaryDirectory() as td:
source = Path(td) / 'video.cpp'
source.write_text('#include "' + str(ROOT / 'tooling/snes9x/ps5-video.h') + '"\n'
'#include "' + str(ROOT / 'src/core_frame_ps5.cpp') + '"\n'
'extern "C" bool convert(uint32_t *d, size_t n, '
'const uint16_t *s, size_t p, unsigned w, unsigned h) '
'{ return ps5_snes9x_frame(d, n, s, p, w, h); }\n')
lib = Path(td) / 'video.so'
subprocess.run(['c++', '-shared', '-fPIC', '-Wall', '-Wextra', '-Werror',
str(source), '-o', str(lib)], check=True)
api = ctypes.CDLL(str(lib))
convert = api.convert
convert.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p,
ctypes.c_size_t, ctypes.c_uint, ctypes.c_uint]
convert.restype = ctypes.c_bool
upload = api.ps5_core_frame_rgba
upload.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p,
ctypes.c_size_t, ctypes.c_uint, ctypes.c_uint]
# All 65536 inputs, not only primaries, catch bit-order/green-width errors.
src = (ctypes.c_uint16 * 65536)(*range(65536))
core = (ctypes.c_uint32 * 65536)()
gpu = (ctypes.c_uint8 * (65536 * 4))()
self.assertTrue(convert(core, 65536, src, 512, 256, 256))
upload(gpu, 1024, core, 1024, 256, 256)
for pixel in range(65536):
r, g, b = pixel >> 11, (pixel >> 5) & 63, pixel & 31
self.assertEqual(list(gpu[pixel * 4:pixel * 4 + 4]),
[r * 8 + r // 4, g * 4 + g // 16, b * 8 + b // 4, 255])
self.assertEqual(list(src), list(range(65536)))
# Actual common, hires, NTSC and 4x widths; non-tight input row pitch.
for width, height in [(256, 224), (512, 478), (602, 239), (1024, 478)]:
pitch = (width + 8) * 2
data = (ctypes.c_uint16 * ((width + 8) * height))()
out = (ctypes.c_uint32 * (width * height + 1))()
out[-1] = 0xdeadbeef
for y in range(height):
data[y * (width + 8)] = 0xf800
data[y * (width + 8) + width - 1] = 0x001f
data[y * (width + 8) + width] = 0xffff
original = bytes(data)
for _ in range(2):
self.assertTrue(convert(out, width * height, data, pitch, width, height))
self.assertEqual(bytes(data), original)
for y in range(height):
self.assertEqual(out[y * width], 0xff0000)
self.assertEqual(out[y * width + width - 1], 0x0000ff)
self.assertEqual(out[-1], 0xdeadbeef)
before = bytes(out)
for capacity, stride, w, h in [(width * height - 1, pitch, width, height),
(width * height, width * 2 - 2, width, height),
(width * height, pitch + 1, width, height),
(width * height, pitch, 0, height)]:
self.assertFalse(convert(out, capacity, data, stride, w, h))
self.assertEqual(bytes(out), before)
+64
View File
@@ -0,0 +1,64 @@
/* Core-local C++ destructor ownership. Never register unloadable code with the
* process-wide atexit list. The native loader executes .fini_array before unmap. */
#include <cstdlib>
#include <pthread.h>
namespace
{
struct Destructor
{
void (*callback)(void *);
void *argument;
void *dso;
Destructor *next;
};
Destructor *pending = nullptr;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
} // namespace
extern "C"
{
void *__dso_handle = &__dso_handle;
int __cxa_atexit(void (*callback)(void *), void *argument, void *dso)
{
auto *entry = static_cast<Destructor *>(std::malloc(sizeof(Destructor)));
if (!entry)
return -1;
entry->callback = callback;
entry->argument = argument;
entry->dso = dso;
pthread_mutex_lock(&lock);
entry->next = pending;
pending = entry;
pthread_mutex_unlock(&lock);
return 0;
}
void __cxa_finalize(void *dso)
{
for (;;)
{
pthread_mutex_lock(&lock);
Destructor **slot = &pending;
while (*slot && dso && (*slot)->dso != dso)
slot = &(*slot)->next;
Destructor *entry = *slot;
if (entry)
*slot = entry->next;
pthread_mutex_unlock(&lock);
if (!entry)
break;
auto callback = entry->callback;
void *argument = entry->argument;
std::free(entry);
callback(argument);
}
}
}
__attribute__((destructor)) static void finish_core()
{
__cxa_finalize(nullptr);
pthread_mutex_destroy(&lock);
}
+28
View File
@@ -0,0 +1,28 @@
/* Preserve Snes9x's RGB565 renderer; libretro receives independent XRGB8888. */
#ifndef PS5_SNES9X_VIDEO_H
#define PS5_SNES9X_VIDEO_H
#include <stddef.h>
#include <stdint.h>
static inline uint32_t ps5_snes9x_xrgb(uint16_t pixel)
{
const uint32_t r = pixel >> 11;
const uint32_t g = (pixel >> 5) & 63u;
const uint32_t b = pixel & 31u;
return (((r << 3) | (r >> 2)) << 16) | (((g << 2) | (g >> 4)) << 8) |
(b << 3) | (b >> 2);
}
static inline bool ps5_snes9x_frame(uint32_t *output, size_t capacity,
const uint16_t *input, size_t pitch,
unsigned width, unsigned height)
{
if (!width || !height || width > capacity / height || pitch % sizeof(uint16_t) ||
pitch / sizeof(uint16_t) < width)
return false;
for (unsigned y = 0; y < height; ++y)
for (unsigned x = 0; x < width; ++x)
output[(size_t)y * width + x] = ps5_snes9x_xrgb(input[(size_t)y * (pitch / 2) + x]);
return true;
}
#endif
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Cross-build pinned Snes9x 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="$CXX"
revision=fae2fea08f74180759ef540ee94259213f503480
source_sha=0d4b0c4181d66668ec0040eb17f90a559f7b7fa6cbd9198593c3bdbe79acd029
info_revision=5a74858ab2f7a50cebb5a6330895bc38899531c0
info_sha=fa62b78d58bc4c30f4e0a4a581f0cb3252447b7f198b55bd6ebe2e4b9bde5cc2
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/snes9x-$revision.tar.gz"
info="$cache/snes9x_libretro.info"
fetch "https://codeload.github.com/libretro/snes9x/tar.gz/$revision" "$archive" "$source_sha"
fetch "https://raw.githubusercontent.com/libretro/libretro-core-info/$info_revision/snes9x_libretro.info" "$info" "$info_sha"
work="$root/build/cores/snes9x"
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/snes9x/native-xrgb-output.patch"
# Compile this core's destructor registry as a local object; upstream's version
# script hides it. Its fini-array callback runs before native-loader unmap.
"$CXX" -std=c++11 -fPIC -fno-exceptions -fno-rtti -c \
"$root/tooling/native/core_cxx_runtime.cpp" -o "$work/core_cxx_runtime.o"
# Keep upstream renderer/feature selection; convert only at the video callback.
# C++ runtime imports resolve against the title's SDK runtime at native load time.
# No separate payload CRT/libc or host libraries are linked into this core.
make -C "$work/libretro" -j"${JOBS:-8}" platform=unix \
CC="$CC" CXX="$CXX" AR="$AR" LD="$LD" \
GIT_VERSION="\" ${revision:0:7}\"" LTO= \
CPPFLAGS="-I$root/tooling/snes9x" \
LIBS="$work/core_cxx_runtime.o -lkernel_web -lSceLibcInternal -lScePosixForWebKit" \
LDFLAGS="-nostdlib -nodefaultlibs -Wl,-z,undefs -Wl,--build-id=sha1 -Wl,-T,$root/tooling/native/ps5-core.ld"
cp -- "$work/libretro/snes9x_libretro.so" "$work/snes9x_libretro.so"
python3 tools/check-core.py "$work/snes9x_libretro.so" --report "$work/abi.json"
cp -- "$work/snes9x_libretro.so" "$stage/cores/snes9x_libretro.so"
cp -- "$info" "$stage/info/snes9x_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 ('tools/build-snes9x.sh', 'tooling/native/ps5-core.ld', 'tooling/native/core_cxx_runtime.cpp',
'patches/snes9x/native-xrgb-output.patch', 'tooling/snes9x/ps5-video.h')}
(work / 'build.json').write_text(json.dumps(report, indent=2) + '\n')
PY
printf '==> [snes9x] built and ABI-checked revision %s; console loading is a separate gate\n' "$revision"
+2 -1
View File
@@ -51,7 +51,7 @@ sdk="$root/.deps/native/ps5-payload-sdk"
echo "==> [title] step 1/3: the frontend"
"$root/tools/build-retroarch.sh"
core_names=(fceumm mgba)
core_names=(fceumm mgba snes9x)
core_files=()
for core_name in "${core_names[@]}"; do
bash "$root/tools/build-$core_name.sh"
@@ -161,6 +161,7 @@ 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",
"build/cores/stage/cores/snes9x_libretro.so",
"tools/build.sh", "tools/retroarch-flags.sh")]
inputs += [pathlib.Path(name) for name in sys.argv[2:]]
digest = hashlib.sha256()
+2 -2
View File
@@ -50,11 +50,11 @@ while (( $# )); do
--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[=fceumm|mgba]]" >&2; exit 2 ;;
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x]]" >&2; exit 2 ;;
esac
shift
done
case "$core_test" in none|fceumm|mgba) ;; *) echo "unknown core diagnostic: $core_test" >&2; exit 2 ;; esac
case "$core_test" in none|fceumm|mgba|snes9x) ;; *) 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; }