diff --git a/Makefile b/Makefile index 21e905b..f09283d 100644 --- a/Makefile +++ b/Makefile @@ -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' \ diff --git a/docs/ACTIVE.md b/docs/ACTIVE.md index 1ab7649..e2131fc 100644 --- a/docs/ACTIVE.md +++ b/docs/ACTIVE.md @@ -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. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index b84444f..ec8a30d 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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. diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index d6048ca..baa495d 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -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. diff --git a/docs/PHASE_LOG.md b/docs/PHASE_LOG.md index ec057ba..3018ceb 100644 --- a/docs/PHASE_LOG.md +++ b/docs/PHASE_LOG.md @@ -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. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 258b1e7..154a610 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -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. diff --git a/docs/TESTING.md b/docs/TESTING.md index 8265a9c..255da8e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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. diff --git a/evidence/snes9x-native/abi.json b/evidence/snes9x-native/abi.json new file mode 100644 index 0000000..276380f --- /dev/null +++ b/evidence/snes9x-native/abi.json @@ -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 +} diff --git a/evidence/snes9x-native/build.json b/evidence/snes9x-native/build.json new file mode 100644 index 0000000..275a86a --- /dev/null +++ b/evidence/snes9x-native/build.json @@ -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" + } +} diff --git a/evidence/snes9x-native/capture.json b/evidence/snes9x-native/capture.json new file mode 100644 index 0000000..ac1f9e3 --- /dev/null +++ b/evidence/snes9x-native/capture.json @@ -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." +} diff --git a/evidence/snes9x-native/driver-archives.json b/evidence/snes9x-native/driver-archives.json new file mode 100644 index 0000000..f027dce --- /dev/null +++ b/evidence/snes9x-native/driver-archives.json @@ -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" +} diff --git a/evidence/snes9x-native/expectation.json b/evidence/snes9x-native/expectation.json new file mode 100644 index 0000000..e75119c --- /dev/null +++ b/evidence/snes9x-native/expectation.json @@ -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." +} diff --git a/evidence/snes9x-native/iterations.json b/evidence/snes9x-native/iterations.json new file mode 100644 index 0000000..8e11d20 --- /dev/null +++ b/evidence/snes9x-native/iterations.json @@ -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." + } +] diff --git a/evidence/snes9x-native/loader.json b/evidence/snes9x-native/loader.json new file mode 100644 index 0000000..23dc3da --- /dev/null +++ b/evidence/snes9x-native/loader.json @@ -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 +} diff --git a/evidence/snes9x-native/recovery.json b/evidence/snes9x-native/recovery.json new file mode 100644 index 0000000..b6c4a50 --- /dev/null +++ b/evidence/snes9x-native/recovery.json @@ -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 +} diff --git a/patches/snes9x/native-xrgb-output.patch b/patches/snes9x/native-xrgb-output.patch new file mode 100644 index 0000000..ca0c981 --- /dev/null +++ b/patches/snes9x/native-xrgb-output.patch @@ -0,0 +1,78 @@ +--- a/libretro/libretro.cpp ++++ b/libretro/libretro.cpp +@@ -1,4 +1,5 @@ + #include "libretro.h" ++#include "ps5-video.h" + #include + #include + #include +@@ -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(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; diff --git a/src/core_loader_probe.cpp b/src/core_loader_probe.cpp index 549c762..005e6fc 100644 --- a/src/core_loader_probe.cpp +++ b/src/core_loader_probe.cpp @@ -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"); diff --git a/src/core_loader_ps5.cpp b/src/core_loader_ps5.cpp index b407e09..93d578d 100644 --- a/src/core_loader_ps5.cpp +++ b/src/core_loader_ps5.cpp @@ -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(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(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(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(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); diff --git a/tests/test_core_loader.py b/tests/test_core_loader.py index 018d7e9..473d72f 100644 --- a/tests/test_core_loader.py +++ b/tests/test_core_loader.py @@ -32,11 +32,22 @@ int run(void) { return call_import(++*pointer); } cls.original = bytes(data) wrapper = cls.folder / 'runtime.cpp' wrapper.write_text('''#include +#include +#include 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(&native_add) : nullptr; + if (allow && !std::strcmp(name, "native_add")) return reinterpret_cast(&native_add); +#define BIND(symbol) if (!std::strcmp(name, #symbol)) return reinterpret_cast(&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('> 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) diff --git a/tooling/native/core_cxx_runtime.cpp b/tooling/native/core_cxx_runtime.cpp new file mode 100644 index 0000000..62363ad --- /dev/null +++ b/tooling/native/core_cxx_runtime.cpp @@ -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 +#include + +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(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); +} diff --git a/tooling/snes9x/ps5-video.h b/tooling/snes9x/ps5-video.h new file mode 100644 index 0000000..b6734b4 --- /dev/null +++ b/tooling/snes9x/ps5-video.h @@ -0,0 +1,28 @@ +/* Preserve Snes9x's RGB565 renderer; libretro receives independent XRGB8888. */ +#ifndef PS5_SNES9X_VIDEO_H +#define PS5_SNES9X_VIDEO_H +#include +#include + +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 diff --git a/tools/build-snes9x.sh b/tools/build-snes9x.sh new file mode 100755 index 0000000..cd3f24c --- /dev/null +++ b/tools/build-snes9x.sh @@ -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" diff --git a/tools/build-title.sh b/tools/build-title.sh index a2de8f8..acd80bb 100755 --- a/tools/build-title.sh +++ b/tools/build-title.sh @@ -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() diff --git a/tools/run-title.sh b/tools/run-title.sh index fc79354..03699f5 100755 --- a/tools/run-title.sh +++ b/tools/run-title.sh @@ -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; }