mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
ARMSX3: Android port of RPCS3, proof of concept
Adds an Android build of the RPCS3 core plus a Compose UI, and fixes several things that stopped it working on ARM64. Renderer: - Emit concrete bounds for runtime sized arrays in uniform blocks when VK_EXT_shader_uniform_buffer_unsized_array is missing. Adreno does not have the extension, so every game pipeline failed with VK_ERROR_UNKNOWN and only overlays drew. - Probe and request that extension properly instead of chaining its feature struct unconditionally. - Hand VMA the Vulkan function pointers it needs under VK_NO_PROTOTYPES. - Rebuild the surface and swapchain when the window is lost instead of killing the RSX thread. - Only create a GLES context when the GL renderer is actually selected. SPU: - Sum instead of taking an absolute difference in the ARM64 block verification checksum. The difference collides on the near identical job binaries an SPU job manager streams through one local store address, so a cached block could run against another job's code. Threading: - Implement thread affinity on Android using sched_setaffinity. - Add an ARM big.LITTLE core arrangement so SPU and RSX threads land on the fast cores. Misc: - Detect the host CPU for the LLVM JIT instead of pinning cortex-a34. - Fall back to the default audio device when cubeb cannot enumerate.
This commit is contained in:
+35
@@ -126,3 +126,38 @@ CMakeUserPresets.json
|
||||
|
||||
.cache/
|
||||
.lldbinit
|
||||
|
||||
# --- ARMSX3 (Android port) ---
|
||||
# Gradle / Android Studio build output
|
||||
android/**/build/
|
||||
android/**/.gradle/
|
||||
android/**/.cxx/
|
||||
android/**/local.properties
|
||||
android/**/*.iml
|
||||
.idea/
|
||||
|
||||
# Built native libraries. libarmsx3-core.so alone is ~98 MB stripped; these are
|
||||
# build products and must not enter git history. See BUILDING.md.
|
||||
android/**/jniLibs/**/*.so
|
||||
android/**/cpp/**/*.so
|
||||
|
||||
# Discord Social SDK is proprietary and has no published license, so it is not
|
||||
# redistributed here. Fetch it from Discord's developer portal into
|
||||
# app/libs/ and app/src/main/cpp/discord_sdk/ before building that flavour.
|
||||
android/**/libs/discord_partner_sdk.aar
|
||||
android/**/discord_sdk/
|
||||
|
||||
# Signing material
|
||||
*.keystore
|
||||
*.jks
|
||||
android/**/keystore.properties
|
||||
|
||||
# Secrets kept out of source (see RA user-agent handling)
|
||||
**/ra_ua_secret.h
|
||||
|
||||
# Third party checkouts. Not submodules, clone these yourself before building.
|
||||
# librashader https://github.com/SnowflakePowered/librashader
|
||||
# libadrenotools https://github.com/bylaws/libadrenotools
|
||||
3rdparty/librashader/
|
||||
android/app-upstream/
|
||||
android/**/cpp/libadrenotools/
|
||||
|
||||
Vendored
+14
@@ -112,6 +112,14 @@ if (NOT ANDROID AND NOT APPLE)
|
||||
else()
|
||||
target_link_libraries(3rdparty_opengl INTERFACE OpenGL::GL OpenGL::GLU OpenGL::GLX)
|
||||
endif()
|
||||
elseif (ANDROID)
|
||||
# ARMSX3: Android has OpenGL, it just spells it OpenGL ES 3.2. The headers come
|
||||
# from the NDK sysroot, so nothing needs to be found or added to the include
|
||||
# path. Nothing is LINKED either, on purpose: rpcs3/Emu/RSX/GL/OpenGL_ES.cpp
|
||||
# dlopen()s libEGL/libGLESv2 and resolves every entry point by hand, because a
|
||||
# NEEDED dependency on libGLESv2.so would bind to the system driver forever and
|
||||
# make ANGLE impossible. libdl comes with the NDK by default.
|
||||
add_library(3rdparty_opengl INTERFACE)
|
||||
else()
|
||||
add_library(3rdparty_opengl INTERFACE)
|
||||
target_compile_definitions(3rdparty_opengl INTERFACE WITHOUT_OPENGL=1)
|
||||
@@ -237,6 +245,12 @@ if (NOT ANDROID)
|
||||
else()
|
||||
add_library(3rdparty_openal INTERFACE)
|
||||
target_compile_definitions(3rdparty_openal INTERFACE WITHOUT_OPENAL=1)
|
||||
# ARMSX3: WITHOUT_OPENAL compiles out every OpenAL *call* (cellMic.cpp guards
|
||||
# all 7 sites) but cellMic.h still includes "alc.h" unconditionally for the
|
||||
# ALC types in its declarations. Expose the headers so the include resolves;
|
||||
# nothing links against OpenAL on Android.
|
||||
target_include_directories(3rdparty_openal INTERFACE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/OpenAL/openal-soft/include/AL")
|
||||
endif()
|
||||
|
||||
# FAudio
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# ARMSX3 — bring-up status
|
||||
|
||||
Session of 2026-08-05. Read this first.
|
||||
|
||||
## TL;DR
|
||||
|
||||
The native core builds against **upstream RPCS3**, not RPCSX. Our delta to upstream is
|
||||
small and entirely guarded, which is the whole point: RPCSX is 1,960 commits and ~16
|
||||
months behind upstream and is missing every one of whatcookie's 2026 ARM64 commits.
|
||||
We get those for free and stay rebasable.
|
||||
|
||||
The Android app shell is forked from `rpcsx-ui-android` into `android/armsx3-app`,
|
||||
rebranded, re-themed purple, and the ARMSX2 audio layer is ported in.
|
||||
|
||||
---
|
||||
|
||||
## The single most useful fact
|
||||
|
||||
**The app `dlopen()`s the core and resolves `_rpcsx_*` symbols via `dlsym`.**
|
||||
`app/src/main/cpp/native-lib.cpp` loads a core `.so` at runtime — that is why
|
||||
rpcsx-build publishes 7 `-march` variants (armv8-a … armv9.1-a) and the app picks one.
|
||||
|
||||
Our core exports **26/26** of the symbols the shell dlsyms. Verified compatible.
|
||||
So `librpcsx-android.so` drops into the existing shell unchanged — you do not need
|
||||
any UI work to get a build that boots games.
|
||||
|
||||
---
|
||||
|
||||
## Upstream files we modified (keep this list near zero)
|
||||
|
||||
| File | Lines | Why |
|
||||
|---|---|---|
|
||||
| `CMakeLists.txt` | 6 | `if(ANDROID)` include ffmpeg.cmake before 3rdparty; add `android/` subdir |
|
||||
| `rpcs3/Emu/Io/pad_config_types.h` | 3 | `#ifdef __ANDROID__ virtual_pad` enum value |
|
||||
| `rpcs3/Emu/Io/pad_config_types.cpp` | 3 | matching `fmt_class_string` case — **without this the value serialises as `unknown` and the handler silently never gets selected** |
|
||||
| `3rdparty/CMakeLists.txt` | 6 | expose OpenAL headers on Android (see below) |
|
||||
| `rpcs3/util/media_utils.cpp` | 3 | `#include "util/ffmpeg_compat.h"` |
|
||||
|
||||
Everything else is additive: `android/`, `rpcs3/dev/`, `rpcs3/Input/virtual_pad_handler.*`,
|
||||
`rpcs3/util/ffmpeg_compat.h`.
|
||||
|
||||
## Traps found the hard way (do not re-derive these)
|
||||
|
||||
1. **NDK 29 / clang 21 is required. NDK 28.2 is clang 19.0.1 and will not build this.**
|
||||
`fmt::throw_exception` is a CTAD struct whose ctor *and* dtor are `[[noreturn]]`, not a
|
||||
function. clang 19.0.1 fails to propagate that, so every `default:` ending in it trips
|
||||
`-Werror,-Wreturn-type`. It killed 4 TUs at 2510/3123 — *after* LLVM had fully built.
|
||||
Do **not** silence it with `-Wno-error=return-type`; the code is correct.
|
||||
Cheap way to test a toolchain hypothesis without a 30-min rebuild: pull the failing
|
||||
compile line out of the ninja log, `sed` the compiler+sysroot to the other NDK, strip
|
||||
`-o/-MD/-MT/-MF`, add `-fsyntax-only`. 30 seconds instead of 30 minutes.
|
||||
|
||||
2. **Never make `android/` the top-level CMake project** (RPCSX does). Upstream assumes
|
||||
`CMAKE_SOURCE_DIR == repo root` in ≥4 places (`FindWolfSSL.cmake`, `FindZLIB.cmake`,
|
||||
`3rdparty/protobuf`, `3rdparty/llvm`). Configuring with `-S android` makes them all
|
||||
resolve to `android/3rdparty/...`. Configure from root; use `android/configure.sh`.
|
||||
|
||||
3. **Upstream pins LLVM 22.x.** RPCSX's prebuilt `llvm-android-arm64-v8a` is 20.1.3 — two
|
||||
majors behind, unusable (ORC/JIT API breaks). Must build from source.
|
||||
|
||||
4. **Upstream's Android path is half-finished in two places.** Its ffmpeg block is
|
||||
`if(NOT ANDROID)` but it aliases `3rdparty::ffmpeg` **unguarded**. Its OpenAL branch
|
||||
defines `WITHOUT_OPENAL=1` but exposes no include dir, while `cellMic.h` includes
|
||||
`alc.h` unconditionally (the *calls* are guarded — 7 sites in cellMic.cpp — only the
|
||||
include is not).
|
||||
|
||||
5. **ffmpeg**: upstream targets 7.1+ APIs (`avcodec_get_supported_config`/`AVCodecConfig`,
|
||||
const `AVChannelLayout*`). The only Android prebuilt in existence is 5.1. Stopgap is
|
||||
`rpcs3/util/ffmpeg_compat.h`, version-gated so it self-disables. **Real fix: cross-compile
|
||||
ffmpeg 7.1 for Android, then delete that header and its one `#include`.**
|
||||
|
||||
---
|
||||
|
||||
## Done
|
||||
|
||||
- `agents/ARMSX3` on upstream RPCS3 `652cf60bf`, branch `armsx3-bringup`,
|
||||
remotes `upstream`=RPCS3, `rpcsx`=RPCSX.
|
||||
- Android layer ported: 43/55 of RPCSX's includes resolved untouched, 9 mechanical
|
||||
remaps, 3 `rx::` calls → `utils::trap()` / `rpcs3::get_version()`.
|
||||
- `android/configure.sh` — reproducible configure with every cross-compile override
|
||||
documented inline (`USE_NATIVE_INSTRUCTIONS=OFF` matters: it means `-march=native`,
|
||||
which probes the *host* Mac).
|
||||
- App shell forked to `android/armsx3-app`, `applicationId = com.armsx3`, name ARMSX3.
|
||||
- **Purple Material3 theme** seeded from the logo, both light and dark.
|
||||
- **Audio ported from ARMSX2 mono**: `MenuSfx.kt`, `LibraryMusic.kt`, `PauseMusic.kt`
|
||||
+ 14 SFX wavs + pause music. `MainActivityRuntime.prefs` → `GeneralSettings.raw`
|
||||
(new accessor on the same `app_prefs` store); `EmuState` → RPCSX's `EmulatorState`.
|
||||
- Your track re-encoded 320kbps/7.2MB → 128k/3.6MB as `res/raw/library_music.m4a`.
|
||||
- `CoverRepository.kt` written for aldostools covers.
|
||||
|
||||
## Covers — measured, not assumed
|
||||
|
||||
`https://raw.githubusercontent.com/aldostools/Resources/main/COV/<TITLE_ID>.JPG`
|
||||
Flat layout, `TITLE_ID` is exactly what PARAM.SFO gives (`BLUS30443`). Verified 200s.
|
||||
|
||||
- Covers are **260×300, aspect 0.866** — multiMAN style, **not** the ~0.72 PS3 retail
|
||||
sleeve ratio. Draw at `CoverRepository.COVER_ASPECT` or everything letterboxes.
|
||||
- **There are no 3D covers in that repo**, so that feature is out, as you said.
|
||||
- Repo is 3.1 GB — fetch per-title, never clone. `CoverRepository` caches to
|
||||
`filesDir/covers`, writes a `.miss` marker on 404 so coverless titles aren't
|
||||
re-requested forever, and downloads via `.part`+rename so an interrupted fetch
|
||||
can't leave a truncated jpg that looks valid.
|
||||
|
||||
## Not done — and why
|
||||
|
||||
- **Setup/onboarding screen.** rpcsx-ui-android has *none* (no Setup/Onboard/Welcome
|
||||
files; `startDestination = "games"`, missing firmware is just a nag dialog in
|
||||
GamesScreen). ARMSX2's `ui/onboarding/` is 1,039 lines and depends on `ArmsBackdrop`,
|
||||
`ArmsLogo`, `StatusChip`, `padFocusRing`, i18n.
|
||||
**This is not a rename job**: PS2 BIOS is a ROM file you point at; PS3 firmware is a
|
||||
PUP you *install* and decrypt into `dev_flash`. `BiosInfo` (ROMVER region byte, packed
|
||||
version) has no PS3 analogue. The RPCS3 side is `installFw(fd, progressId)` +
|
||||
`FirmwareRepository{None,Installed,Compiled}` + `utils::get_firmware_version()`.
|
||||
- Shaders, controller skins, cover UI wiring, perf tab, save-state UI, touch controls
|
||||
reconcile — all mapped below, none ported yet.
|
||||
- RetroAchievements — **dropped**, RA has no PS3 support.
|
||||
|
||||
## Port map (ARMSX2 mono → ARMSX3)
|
||||
|
||||
Source root: `agents/armsx2-push-staging/platforms/android/app/src/main/java/com/armsx2`
|
||||
|
||||
| Feature | Files |
|
||||
|---|---|
|
||||
| Shaders | `ShaderRepo.kt`, `ShaderParams.kt`, `ui/common/ShaderChainSection.kt`, `ShaderManagerSection.kt`, `ShaderParamsEditor.kt` |
|
||||
| Controller skins | `ControllerSkinStore.kt`, `SkinRepo.kt`, `ui/settings/SkinsTab.kt` |
|
||||
| Covers UI | `CoverRegionIndex.kt`, `ui/common/GameCoverArt.kt` |
|
||||
| Custom drivers | `CustomDriver.kt`, `ui/common/DriverManagerSection.kt` — **reconcile, don't port**: RPCSX already has adrenotools + `GpuDriversScreen` |
|
||||
| Save states | `ui/saves/SaveManagerScreen.kt`, `SaveStatePicker.kt` (core already has savestates) |
|
||||
| Perf | `ui/settings/PerformanceTab.kt` |
|
||||
| Touch | `ui/touch/TouchControls.kt`, `GestureLayer.kt`, `LightgunLayer.kt` — reconcile with RPCSX's `PadOverlay*` (7 files) + `OverlayEditActivity` |
|
||||
| Theme | `ui/theme/Color.kt`, `Theme.kt`, `Type.kt` |
|
||||
| Settings search | `ui/settingshub/SettingsSearchOverlay.kt` |
|
||||
|
||||
## Already free — do not rebuild these
|
||||
|
||||
**Core (upstream RPCS3):** Vulkan (29 files), OpenGL (18), `video_renderer{null,opengl,vulkan}`,
|
||||
save states, and perf knobs incl. **Frame limit** (fps cap), **Sleep Timers Accuracy**,
|
||||
**Thread Scheduler Mode**, framerate/frametime graphs.
|
||||
|
||||
**RPCSX UI:** touch controls (7 files), **layout editor** (5), adrenotools custom-driver
|
||||
loading + driver UI (6), PS3 firmware install (5), SAF provider, USB, PPU precompile
|
||||
service, surface handling.
|
||||
|
||||
## Open items needing you
|
||||
|
||||
1. **Logo** — I can't write an image from chat to disk. Drop the 512×512 at
|
||||
`~/Downloads/armsx3-logo.png` and it gets wired to launcher icon + onboarding.
|
||||
2. **`kr.co.iefriends.pcsx2.NativeApp`** is imported by ARMSX2's `config/Settings.kt` —
|
||||
a *different* rights holder from you. Must be replaced before that file crosses over.
|
||||
(You confirmed you hold the rest of the ARMSX2 UI rights.)
|
||||
3. **`Bin2Pbp.7z`** — that's a PS1→PSP EBOOT tool, unrelated to PS3. Left untouched;
|
||||
assumed an accidental attach.
|
||||
4. **Licensing**: RPCS3 is **GPL-2.0-only** (stated in its README). ARMSX2 is GPL-3.0.
|
||||
Incompatible. Since you hold the ARMSX2 UI rights, ARMSX3's UI needs to go out under
|
||||
GPL-2.0-only to combine with the core.
|
||||
@@ -102,6 +102,13 @@ if(MSVC)
|
||||
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:/wd4530;/utf-8>") # C++ exception handler used, but unwind semantics are not enabled
|
||||
endif()
|
||||
|
||||
# ARMSX3: upstream skips its FFMPEG block on Android (3rdparty/CMakeLists.txt
|
||||
# `if(NOT ANDROID)`) but still aliases 3rdparty::ffmpeg -> 3rdparty_ffmpeg
|
||||
# unguarded, so the target has to exist before 3rdparty/ is processed.
|
||||
if(ANDROID)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/android/ffmpeg.cmake)
|
||||
endif()
|
||||
|
||||
add_subdirectory(3rdparty)
|
||||
|
||||
if (DISABLE_LTO)
|
||||
@@ -136,4 +143,10 @@ if(BUILD_RPCS3_TESTS)
|
||||
endif()
|
||||
add_subdirectory(rpcs3)
|
||||
|
||||
# ARMSX3: the Android JNI shared library. Kept behind ANDROID so desktop builds
|
||||
# are byte-for-byte unaffected.
|
||||
if(ANDROID)
|
||||
add_subdirectory(android)
|
||||
endif()
|
||||
|
||||
set_directory_properties(PROPERTIES VS_STARTUP_PROJECT rpcs3)
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
RPCS3
|
||||
=====
|
||||
ARMSX3
|
||||
======
|
||||
|
||||
[](https://github.com/RPCS3/rpcs3/actions/workflows/rpcs3.yml)
|
||||
[](https://discord.gg/rpcs3)
|
||||
Proof of concept Android port of RPCS3.
|
||||
|
||||
The world's first free and open-source PlayStation 3 emulator/debugger, written in C++ for Windows, Linux, macOS and FreeBSD.
|
||||
This is early work. A game boots and plays, but it is slow and most of it is
|
||||
untested. It is not a usable emulator yet.
|
||||
|
||||
You can find some basic information on our [**website**](https://rpcs3.net/). Game info is being populated on the [**Wiki**](https://wiki.rpcs3.net/).
|
||||
For discussion about this emulator, PS3 emulation, and game compatibility reports, please visit our [**forums**](https://forums.rpcs3.net) and our [**Discord server**](https://discord.gg/RPCS3).
|
||||
Status
|
||||
------
|
||||
|
||||
[**Support the Lead Developers on Patreon**](https://rpcs3.net/patreon)
|
||||
Skate 3 boots, loads and reaches gameplay at roughly 20 to 30 fps on a
|
||||
Snapdragon 8 Gen 2. Rendering, audio, touch controls and physical controllers
|
||||
work. Almost nothing else has been tested.
|
||||
|
||||
## Contributing
|
||||
Differences from upstream RPCS3
|
||||
-------------------------------
|
||||
|
||||
If you want to help the project but do not code, the best way to help out is to test games and make bug reports. See:
|
||||
* [Quickstart](https://rpcs3.net/quickstart)
|
||||
Some of the fixes here are not in upstream and affect any ARM64 build, not only
|
||||
Android:
|
||||
|
||||
If you want to contribute as a developer, please take a look at the following pages:
|
||||
* Shaders declared runtime sized arrays inside uniform blocks, which requires
|
||||
VK_EXT_shader_uniform_buffer_unsized_array. Adreno does not support that
|
||||
extension, so every game pipeline failed to compile and nothing rendered.
|
||||
Concrete array bounds are emitted when the extension is missing.
|
||||
|
||||
* [Coding Style](https://github.com/RPCS3/rpcs3/wiki/Coding-Style)
|
||||
* [Developer Information](https://github.com/RPCS3/rpcs3/wiki/Developer-Information)
|
||||
* The ARM64 SPU block verification checksum folded two thirds of every block
|
||||
through an absolute difference. That collides on the near identical job
|
||||
binaries an SPU job manager streams through the same local store address, so
|
||||
a cached block could end up running against another job's code. It sums now.
|
||||
|
||||
You should also contact any of the developers in the forums or in the Discord server to learn more about the current state of the emulator.
|
||||
* Thread affinity was compiled out on Android, and the core had no ARM
|
||||
big.LITTLE topology, so SPU and RSX threads were never placed on the fast
|
||||
cores.
|
||||
|
||||
### AI Use
|
||||
* The LLVM JIT target was pinned to cortex-a34, an in order core from 2016. It
|
||||
detects the host now.
|
||||
|
||||
Use of AI tools for research and reverse engineering purposes is permitted. However, contributors are expected to fully own and understand all code they submit. Any communication with the team — including code, code comments, and GitHub comments — must come from the human contributor, not an AI agent acting autonomously.
|
||||
Building
|
||||
--------
|
||||
|
||||
We have unfortunately seen a rise in untested and unverified AI-generated slop being submitted to this project. This wastes maintainer time and, in worse cases, such changes get merged and break functionality for all users. Repeated violations will result in a ban from the repository. Please be respectful of everyone's time.
|
||||
See BUILDING.md.
|
||||
|
||||
**Pull requests opened by AI agents or automated tools must include a disclosure in the PR description** stating the scope of AI involvement — which parts were AI-generated and what human testing or review was performed prior to submission. PRs that omit this disclosure may be closed without review.
|
||||
The Discord Social SDK is proprietary and is not redistributed here. Get it from
|
||||
Discord's developer portal if you want that feature.
|
||||
|
||||
If you are unsure about your work, open a discussion issue to talk it through with the team, or reach out to a maintainer on [Discord](https://discord.gg/RPCS3).
|
||||
License
|
||||
-------
|
||||
|
||||
## Building
|
||||
GPL-2.0-only, the same as RPCS3. See LICENSE. Some files may be licensed
|
||||
differently, check the file headers.
|
||||
|
||||
See [BUILDING.md](BUILDING.md) for more information about how to setup an environment to build RPCS3.
|
||||
|
||||
## Running
|
||||
|
||||
Check our friendly [quickstart](https://rpcs3.net/quickstart) guide to make sure your computer meets the minimum system requirements to run RPCS3.
|
||||
|
||||
Don't forget to have your graphics driver up to date and to install the [Visual C++ Redistributable Packages for Visual Studio 2022](https://aka.ms/vs/17/release/VC_redist.x64.exe) if you are a Windows user.
|
||||
|
||||
## License
|
||||
|
||||
Most files are licensed under the terms of GNU GPL-2.0-only License; see LICENSE file for details. Some files may be licensed differently; check appropriate file headers for details.
|
||||
Based on RPCS3, https://github.com/RPCS3/rpcs3
|
||||
|
||||
@@ -109,6 +109,21 @@ namespace cfg
|
||||
return {};
|
||||
}
|
||||
|
||||
// ARMSX3: expose the numeric range through the base interface.
|
||||
// _int/_float/uint carry Min/Max as static constexpr TEMPLATE parameters,
|
||||
// which are unreachable from a _base* -- so a generic settings UI cannot
|
||||
// tell a slider from a free-text field without these. Mirrors the
|
||||
// to_string/def_to_string pattern; empty means "not a bounded numeric".
|
||||
virtual std::string min_to_string() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
virtual std::string max_to_string() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// Try to convert from string (optional)
|
||||
virtual bool from_string(std::string_view value, bool dynamic = false);
|
||||
|
||||
@@ -382,6 +397,16 @@ namespace cfg
|
||||
return std::to_string(def);
|
||||
}
|
||||
|
||||
std::string min_to_string() const override
|
||||
{
|
||||
return std::to_string(min);
|
||||
}
|
||||
|
||||
std::string max_to_string() const override
|
||||
{
|
||||
return std::to_string(max);
|
||||
}
|
||||
|
||||
bool from_string(std::string_view value, bool /*dynamic*/ = false) override
|
||||
{
|
||||
s64 result;
|
||||
@@ -473,6 +498,16 @@ namespace cfg
|
||||
return "0.0";
|
||||
}
|
||||
|
||||
std::string min_to_string() const override
|
||||
{
|
||||
return std::to_string(min);
|
||||
}
|
||||
|
||||
std::string max_to_string() const override
|
||||
{
|
||||
return std::to_string(max);
|
||||
}
|
||||
|
||||
bool from_string(std::string_view value, bool /*dynamic*/ = false) override
|
||||
{
|
||||
f64 result;
|
||||
@@ -560,6 +595,16 @@ namespace cfg
|
||||
return std::to_string(def);
|
||||
}
|
||||
|
||||
std::string min_to_string() const override
|
||||
{
|
||||
return std::to_string(min);
|
||||
}
|
||||
|
||||
std::string max_to_string() const override
|
||||
{
|
||||
return std::to_string(max);
|
||||
}
|
||||
|
||||
bool from_string(std::string_view value, bool /*dynamic*/ = false) override
|
||||
{
|
||||
u64 result;
|
||||
|
||||
+135
-1
@@ -9,6 +9,7 @@
|
||||
#include "Thread.h"
|
||||
#include "Utilities/JIT.h"
|
||||
#include <cfenv>
|
||||
#include <charconv>
|
||||
|
||||
#ifdef ARCH_ARM64
|
||||
#include "Emu/CPU/Backends/AArch64/AArch64Signal.h"
|
||||
@@ -3550,11 +3551,79 @@ void thread_ctrl::silent_exit() noexcept
|
||||
std::abort();
|
||||
}
|
||||
|
||||
|
||||
#if defined(ARCH_ARM64) && defined(__linux__)
|
||||
// Per-core capacity from sysfs, read ONCE.
|
||||
//
|
||||
// Deliberately plain POSIX I/O rather than fs::file: sysfs nodes report
|
||||
// st_size == 0, so a size-based read returns nothing, and fs::file raised
|
||||
// "Unexpected fs::error OK" from whichever thread asked -- which killed the RSX
|
||||
// thread outright, since get_affinity_mask() runs on it.
|
||||
static const std::array<u32, 64>& get_arm_core_capacities()
|
||||
{
|
||||
static const std::array<u32, 64> s_caps = []
|
||||
{
|
||||
std::array<u32, 64> caps{};
|
||||
|
||||
for (u32 core = 0; core < 64u; core++)
|
||||
{
|
||||
char path[128];
|
||||
std::snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/cpu_capacity", core);
|
||||
|
||||
const int fd = ::open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char buf[32]{};
|
||||
const auto got = ::read(fd, buf, sizeof(buf) - 1);
|
||||
::close(fd);
|
||||
|
||||
if (got > 0)
|
||||
{
|
||||
caps[core] = static_cast<u32>(std::atoi(buf));
|
||||
}
|
||||
}
|
||||
|
||||
return caps;
|
||||
}();
|
||||
|
||||
return s_caps;
|
||||
}
|
||||
#endif
|
||||
|
||||
void thread_ctrl::detect_cpu_layout()
|
||||
{
|
||||
if (!g_native_core_layout.compare_and_swap_test(native_core_arrangement::undefined, native_core_arrangement::generic))
|
||||
return;
|
||||
|
||||
#if defined(ARCH_ARM64) && defined(__linux__)
|
||||
// Heterogeneous if the kernel reports differing per-core capacity. Every
|
||||
// big.LITTLE/DynamIQ SoC exposes cpu_capacity; a uniform machine reports the
|
||||
// same value everywhere (or nothing at all), and falls through to generic.
|
||||
{
|
||||
const auto& caps = get_arm_core_capacities();
|
||||
u32 lowest = umax, highest = 0;
|
||||
|
||||
for (u32 core = 0; core < 64u; core++)
|
||||
{
|
||||
if (caps[core])
|
||||
{
|
||||
lowest = std::min(lowest, caps[core]);
|
||||
highest = std::max(highest, caps[core]);
|
||||
}
|
||||
}
|
||||
|
||||
if (highest && lowest != umax && highest > lowest)
|
||||
{
|
||||
sig_log.notice("Detected ARM heterogeneous CPU (capacity %u..%u)", lowest, highest);
|
||||
g_native_core_layout.store(native_core_arrangement::arm_big_little);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const auto system_id = utils::get_cpu_brand();
|
||||
if (system_id.find("Ryzen") != umax)
|
||||
{
|
||||
@@ -3624,6 +3693,58 @@ u64 thread_ctrl::get_affinity_mask(thread_class group)
|
||||
{
|
||||
return all_cores_mask;
|
||||
}
|
||||
case native_core_arrangement::arm_big_little:
|
||||
{
|
||||
// Put the threads that gate the frame -- SPU and RSX -- on the cores
|
||||
// that can actually keep up, and leave the little cluster for PPU and
|
||||
// helpers. Without this the OS spreads six SPU threads across a mix of
|
||||
// big and LITTLE cores, and the slow ones set the pace.
|
||||
const auto& caps = get_arm_core_capacities();
|
||||
u64 fast_mask = 0;
|
||||
u64 slow_mask = 0;
|
||||
u32 threshold = 0;
|
||||
|
||||
for (u32 core = 0; core < 64u; core++)
|
||||
{
|
||||
threshold = std::max(threshold, caps[core]);
|
||||
}
|
||||
|
||||
// Anything within 25% of the fastest core counts as "fast", so a
|
||||
// mid cluster (A715/A710) joins the prime core rather than being
|
||||
// lumped in with the A510s.
|
||||
threshold = threshold * 3 / 4;
|
||||
|
||||
for (u32 core = 0; core < 64u; core++)
|
||||
{
|
||||
if (~process_affinity_mask & (u64{1} << core))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const u32 capacity = caps[core];
|
||||
|
||||
((capacity && capacity >= threshold) ? fast_mask : slow_mask) |= (u64{1} << core);
|
||||
}
|
||||
|
||||
if (!fast_mask || !slow_mask)
|
||||
{
|
||||
// Degenerate reading -- do not fence anything off.
|
||||
return all_cores_mask;
|
||||
}
|
||||
|
||||
switch (group)
|
||||
{
|
||||
case thread_class::spu:
|
||||
case thread_class::rsx:
|
||||
return fast_mask;
|
||||
case thread_class::ppu:
|
||||
// PPU still needs a fast core for the main thread, but letting it
|
||||
// spill to the little cluster keeps it out of the SPUs' way.
|
||||
return all_cores_mask;
|
||||
default:
|
||||
return slow_mask;
|
||||
}
|
||||
}
|
||||
case native_core_arrangement::amd_ccx:
|
||||
{
|
||||
if (thread_count <= 8)
|
||||
@@ -3890,7 +4011,11 @@ void thread_ctrl::set_thread_affinity_mask(u64 mask)
|
||||
thread_affinity_policy_data_t policy = { static_cast<integer_t>(std::countr_zero(mask)) };
|
||||
thread_port_t mach_thread = pthread_mach_thread_np(pthread_self());
|
||||
thread_policy_set(mach_thread, THREAD_AFFINITY_POLICY, reinterpret_cast<thread_policy_t>(&policy), !mask ? 0 : 1);
|
||||
#elif !defined(ANDROID) && (defined(__linux__) || defined(__DragonFly__) || defined(__FreeBSD__))
|
||||
// NOTE: Android was excluded here upstream, which made every affinity request a
|
||||
// silent no-op -- Thread Scheduler Mode looked settable but did nothing. bionic
|
||||
// provides pthread_setaffinity_np, and RPCSX runs this path on Android without
|
||||
// the carve-out. Failures are already logged rather than fatal.
|
||||
#elif (defined(__linux__) || defined(__DragonFly__) || defined(__FreeBSD__))
|
||||
if (!mask)
|
||||
{
|
||||
// Reset affinity mask
|
||||
@@ -3918,11 +4043,20 @@ void thread_ctrl::set_thread_affinity_mask(u64 mask)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ANDROID
|
||||
// bionic has no pthread_setaffinity_np. sched_setaffinity with pid 0 targets
|
||||
// the calling THREAD on Linux, which is what we want.
|
||||
if (sched_setaffinity(0, sizeof(cpu_set_t), &cs) != 0)
|
||||
{
|
||||
sig_log.error("Failed to set thread affinity 0x%x: errno %d.", mask, errno);
|
||||
}
|
||||
#else
|
||||
if (int err = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cs))
|
||||
{
|
||||
sig_log.error("Failed to set thread affinity 0x%x: error %d.", mask, err);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
u64 thread_ctrl::get_thread_affinity_mask()
|
||||
|
||||
+5
-1
@@ -13,7 +13,11 @@ enum class native_core_arrangement : u32
|
||||
undefined,
|
||||
generic,
|
||||
intel_ht,
|
||||
amd_ccx
|
||||
amd_ccx,
|
||||
// ARM heterogeneous (big.LITTLE / DynamIQ). Cores differ in throughput by
|
||||
// ~3x, so "any core" is not a sane default for a thread that must keep up
|
||||
// with an emulated SPE.
|
||||
arm_big_little
|
||||
};
|
||||
|
||||
enum class thread_class : u32
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# ARMSX3: the Android shared library, built on UPSTREAM RPCS3.
|
||||
#
|
||||
# NOTE ON LAYOUT (this is the important part):
|
||||
# RPCSX makes android/ the top-level CMake project and does
|
||||
# `add_subdirectory(..)` to pull the emulator in. That does not work against
|
||||
# upstream, because upstream assumes CMAKE_SOURCE_DIR == repo root in at least
|
||||
# four places -- buildfiles/cmake/FindWolfSSL.cmake, buildfiles/cmake/FindZLIB.cmake,
|
||||
# 3rdparty/protobuf/CMakeLists.txt and 3rdparty/llvm/CMakeLists.txt all build
|
||||
# paths as ${CMAKE_SOURCE_DIR}/3rdparty/... . Configuring from android/ makes
|
||||
# every one of those resolve to android/3rdparty/... and fail.
|
||||
#
|
||||
# So here the repo ROOT stays the top-level project and this is an ordinary
|
||||
# subdirectory, added from the root CMakeLists behind `if(ANDROID)`. Build
|
||||
# option overrides are passed on the cmake command line (-D...) rather than
|
||||
# set() here, since they must be visible before add_subdirectory(3rdparty).
|
||||
#
|
||||
# Upstream's core target is `rpcs3_emu`. Upstream's rpcs3/CMakeLists.txt already
|
||||
# guards Qt / rpcs3qt / rpcs3_lib / the rpcs3 executable behind `if (NOT ANDROID)`
|
||||
# while calling `add_subdirectory(Emu)` unconditionally, so a core-only Android
|
||||
# configuration is something upstream half-supports already.
|
||||
|
||||
# rpcs3/Input/ is not a library upstream -- its sources are compiled straight
|
||||
# into the `rpcs3` executable, which lives inside `if (NOT ANDROID)`. So on
|
||||
# Android nothing builds them, yet rpcs3_emu references pad_thread and
|
||||
# ps_move_tracker (cellPad's LDD pad API, cellGem's tracker), and the link fails
|
||||
# with undefined symbols. Build the platform-safe subset here.
|
||||
#
|
||||
# Deliberately EXCLUDED, and why:
|
||||
# evdev_/xinput_/mm_/sdl_* - Linux/Windows/SDL backends, all disabled
|
||||
# basic_keyboard_/basic_mouse_/
|
||||
# keyboard_pad_/raw_mouse_/
|
||||
# gui_pad_thread/mouse_gyro_ - Qt-dependent (rpcs3qt is not built here)
|
||||
# camera_video_sink - Qt multimedia
|
||||
set(ARMSX3_INPUT_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/pad_thread.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/product_info.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/hid_pad_handler.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/ds3_pad_handler.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/ds4_pad_handler.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/dualsense_pad_handler.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/skateboard_pad_handler.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/ps_move_handler.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/ps_move_config.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/ps_move_calibration.cpp
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/ps_move_tracker.cpp
|
||||
# pad_thread references mouse_gyro_handler directly, so it is not optional
|
||||
# despite the "mouse" name.
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/mouse_gyro_handler.cpp
|
||||
# Ours: on-screen touch controls.
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/Input/virtual_pad_handler.cpp
|
||||
)
|
||||
|
||||
add_library(rpcsx-android SHARED
|
||||
src/rpcsx-android.cpp
|
||||
# Carried from RPCSX; no upstream equivalent.
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/dev/iso.cpp
|
||||
${ARMSX3_INPUT_SOURCES}
|
||||
# rpcs3::get_version() and friends. Like Input/, this is compiled into the
|
||||
# `rpcs3` executable upstream, not into rpcs3_emu, so Android never gets it.
|
||||
${CMAKE_SOURCE_DIR}/rpcs3/rpcs3_version.cpp
|
||||
)
|
||||
|
||||
# Ship as libarmsx3-core.so, which is the name the app calls
|
||||
# System.loadLibrary("armsx3-core") for. Setting OUTPUT_NAME (rather than
|
||||
# renaming the file after the fact) also fixes the SONAME, so the file name and
|
||||
# the name the dynamic linker registers it under agree -- a mismatch there works
|
||||
# by luck for a plain dlopen and stops working the moment anything resolves it
|
||||
# by soname.
|
||||
set_target_properties(rpcsx-android PROPERTIES OUTPUT_NAME armsx3-core)
|
||||
|
||||
target_compile_features(rpcsx-android PRIVATE cxx_std_23)
|
||||
|
||||
# The librashader upscaler lives in rpcs3/Emu/RSX/VK/, i.e. it compiles into
|
||||
# rpcs3_emu -- not into this target -- so the header search path has to be added
|
||||
# there. This directory is processed after add_subdirectory(rpcs3), so the target
|
||||
# already exists. librashader_ld.h is a header-only permissive loader that
|
||||
# dlopen()s librashader.so at runtime; nothing MPL-licensed is linked in. See the
|
||||
# licensing note at the top of upscalers/librashader_pass.h.
|
||||
target_include_directories(rpcs3_emu PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/librashader/include
|
||||
)
|
||||
|
||||
target_include_directories(rpcsx-android PRIVATE
|
||||
${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/rpcs3
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/librashader/include
|
||||
# rpcsx-android.cpp pulls in Input/hid_pad_handler.h (DS3/DS4/DualSense
|
||||
# over USB) and hidapi_libusb.h/libusb.h directly. rpcs3_emu links these
|
||||
# PRIVATE, so their include dirs do not propagate to us.
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/hidapi/hidapi/hidapi
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/hidapi/hidapi/libusb
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/libusb/libusb/libusb
|
||||
# Utilities/bin_patch.h (patch_engine) includes util/yaml.hpp, which includes
|
||||
# yaml-cpp. rpcs3_emu links yaml-cpp PRIVATE, so it does not propagate here.
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/yaml-cpp/yaml-cpp/include
|
||||
)
|
||||
|
||||
target_link_libraries(rpcsx-android
|
||||
rpcs3_emu
|
||||
# ps_move_tracker/ps_move_calibration use the Fusion AHRS solver. rpcs3_lib
|
||||
# links this upstream; we build those Input sources ourselves, so we need it.
|
||||
3rdparty::fusion
|
||||
# The HID pad handlers (DS3/DS4/DualSense/skateboard/PS Move) are ours to
|
||||
# build here, so hidapi + libusb are ours to link too.
|
||||
3rdparty::hidapi
|
||||
3rdparty::libusb
|
||||
android
|
||||
log
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,150 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.rpcsx"
|
||||
compileSdk = 36
|
||||
ndkVersion = "29.0.13113456"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.armsx3"
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "${System.getenv("RX_VERSION") ?: "local"}${if (System.getenv("RX_SHA") != null) "-" + System.getenv("RX_SHA") else ""}"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a")
|
||||
}
|
||||
|
||||
buildConfigField("String", "Version", "\"v${versionName}\"")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
val keystoreAlias = System.getenv("KEYSTORE_ALIAS") ?: ""
|
||||
val keystorePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
|
||||
val keystorePath = System.getenv("KEYSTORE_PATH") ?: ""
|
||||
|
||||
if (keystorePath.isNotEmpty() && file(keystorePath).exists() && file(keystorePath).length() > 0) {
|
||||
create("custom-key") {
|
||||
keyAlias = keystoreAlias
|
||||
keyPassword = keystorePassword
|
||||
storeFile = file(keystorePath)
|
||||
storePassword = keystorePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.findByName("custom-key") ?: signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.30.5"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.15"
|
||||
}
|
||||
|
||||
packaging {
|
||||
// This is necessary for libadrenotools custom driver loading
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
// ARMSX3: fail the build if the bundled ANGLE libraries are not there.
|
||||
//
|
||||
// This check exists because of a specific, expensive bug in ARMSX2: the repo's
|
||||
// blanket `*.so` gitignore rule swallowed the ANGLE prebuilts, they never made it
|
||||
// into release staging, the APK shipped without them, and the core fell back to
|
||||
// the system GLES driver in complete silence. Users reported "ANGLE is broken"
|
||||
// and there was nothing in any log to contradict them.
|
||||
//
|
||||
// jniLibs/.gitignore now un-ignores the two files by name. This task is the
|
||||
// second lock: packaging an APK that claims to support ANGLE without shipping
|
||||
// ANGLE is a build error, not a runtime surprise. The core-side counterpart is
|
||||
// the loud error in gl::es::egl_initialize() when the override library is
|
||||
// selected but cannot be dlopen'd.
|
||||
val verifyAngleLibs by tasks.registering {
|
||||
val angleLibs = listOf("libEGL_angle.so", "libGLESv2_angle.so")
|
||||
val jniLibDir = file("src/main/jniLibs/arm64-v8a")
|
||||
|
||||
doLast {
|
||||
val missing = angleLibs.filter { !File(jniLibDir, it).isFile }
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"ANGLE libraries missing from ${'$'}jniLibDir: ${'$'}{missing.joinToString(", ")}.\n" +
|
||||
"The OpenGL renderer's ANGLE option cannot work without them and would " +
|
||||
"silently fall back to the system GLES driver.\n" +
|
||||
"They are tracked in git - check them out, or remove the ANGLE option."
|
||||
)
|
||||
}
|
||||
|
||||
angleLibs.forEach {
|
||||
logger.lifecycle("ANGLE: packaging ${'$'}it (${'$'}{File(jniLibDir, it).length()} bytes)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }
|
||||
.configureEach { dependsOn(verifyAngleLibs) }
|
||||
|
||||
base.archivesName = "rpcsx"
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.ui.tooling.preview.android)
|
||||
val composeBom = platform("androidx.compose:compose-bom:2026.02.01")
|
||||
implementation(composeBom)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.activity)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.squareup.okhttp3)
|
||||
implementation(libs.androidx.documentfile)
|
||||
implementation(libs.materialswitch)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# ARMSX3 R8 / ProGuard rules.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# JNI callback surface -- DO NOT REMOVE
|
||||
# ---------------------------------------------------------------------------
|
||||
# The core (librpcsx-android.so) reaches back into Java by NAME STRING:
|
||||
#
|
||||
# rpcsx-android.cpp:603 FindClass("net/rpcsx/ProgressRepository")
|
||||
# rpcsx-android.cpp:626 FindClass("net/rpcsx/FirmwareRepository")
|
||||
# rpcsx-android.cpp:644 FindClass("net/rpcsx/GameRepository")
|
||||
# rpcsx-android.cpp:647 FindClass("net/rpcsx/GameInfo") + GetMethodID ctor
|
||||
# + NewObject
|
||||
#
|
||||
# These classes carry @Keep on some members, but AndroidX's default rule for it
|
||||
# is -keepclassmembers, which preserves the MEMBERS and still allows R8 to
|
||||
# RENAME THE CLASS. A renamed class makes FindClass return null, so the app
|
||||
# works in debug and dies the moment you ship a minified release -- the worst
|
||||
# possible failure shape. Keep the classes themselves, by name.
|
||||
-keep class net.rpcsx.ProgressRepository { *; }
|
||||
-keep class net.rpcsx.FirmwareRepository { *; }
|
||||
-keep class net.rpcsx.GameRepository { *; }
|
||||
-keep class net.rpcsx.GameInfo { *; }
|
||||
|
||||
# GameInfo is constructed from native via GetMethodID("<init>", ...) with an
|
||||
# exact JVM signature, and is also kotlinx-serialized. Keep its shape intact.
|
||||
-keepclassmembers class net.rpcsx.GameInfo {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
|
||||
# RPCSX declares the dlsym'd entry points as `external fun`. The default
|
||||
# android-optimize config keeps native method names, but the DECLARING class
|
||||
# must survive too or JNI has nothing to bind to.
|
||||
-keep class net.rpcsx.RPCSX { *; }
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serialization
|
||||
# ---------------------------------------------------------------------------
|
||||
# kotlinx.serialization generates synthetic $$serializer members reached only
|
||||
# reflectively. Losing them turns saved library/firmware state into silent load
|
||||
# failures on the first launch after an update.
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.**
|
||||
-keepclassmembers @kotlinx.serialization.Serializable class ** {
|
||||
*** Companion;
|
||||
*** serializer(...);
|
||||
}
|
||||
-keepclasseswithmembers class ** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OkHttp / Okio (skins, shaders, drivers and covers all ride HttpClient)
|
||||
# ---------------------------------------------------------------------------
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Diagnostics
|
||||
# ---------------------------------------------------------------------------
|
||||
# Keep line numbers so a user-reported release crash is traceable, but hide
|
||||
# original file names.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.rpcsx
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("net.rpcsx", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.USB_PERMISSION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" />
|
||||
<uses-feature android:name="android.hardware.usb.accessory" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_armsx3"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_armsx3_round"
|
||||
android:supportsRtl="true"
|
||||
android:appCategory="game"
|
||||
android:theme="@style/Theme.RPCSX"
|
||||
tools:targetApi="31">
|
||||
|
||||
<service
|
||||
android:name=".PrecompilerService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.game_mode_config"
|
||||
android:resource="@xml/game_config" />
|
||||
|
||||
<activity
|
||||
android:name=".RPCSXActivity"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="rpcsx.intent.action.Emulator" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".overlay.OverlayEditActivity"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false" >
|
||||
</activity>
|
||||
|
||||
<!-- ARMSX3: the boot logo is the launcher entry; it hands off to
|
||||
MainActivity (forwarding the original intent's action/data/extras
|
||||
and URI grants, so share/open-with still work). Falls through to
|
||||
MainActivity on tap, Back, playback error, or a 6s hard timeout,
|
||||
so a bad codec can never strand the user on black. -->
|
||||
<activity
|
||||
android:name=".BootSplashActivity"
|
||||
android:theme="@style/Theme.ARMSX3.Boot"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:authorities="net.rpcsx.documents"
|
||||
android:name="net.rpcsx.provider.AppDataDocumentProvider"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".utils.PackageInstallStatusReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project("rpcsx-android")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE on)
|
||||
|
||||
if (CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
|
||||
add_subdirectory(libadrenotools)
|
||||
else()
|
||||
add_library(adrenotools INTERFACE)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp)
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
android
|
||||
log
|
||||
adrenotools
|
||||
)
|
||||
@@ -0,0 +1,325 @@
|
||||
#include <algorithm>
|
||||
#include <android/dlext.h>
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <jni.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
#if defined(__aarch64__)
|
||||
#include <adrenotools/driver.h>
|
||||
#include <adrenotools/priv.h>
|
||||
#endif
|
||||
|
||||
struct RPCSXApi {
|
||||
bool (*overlayPadData)(int digital1, int digital2, int leftStickX,
|
||||
int leftStickY, int rightStickX, int rightStickY);
|
||||
bool (*initialize)(std::string_view rootDir, std::string_view user);
|
||||
bool (*processCompilationQueue)(JNIEnv *env);
|
||||
bool (*startMainThreadProcessor)(JNIEnv *env);
|
||||
bool (*collectGameInfo)(JNIEnv *env, std::string_view rootDir,
|
||||
long progressId);
|
||||
void (*shutdown)();
|
||||
int (*boot)(std::string_view path_);
|
||||
int (*getState)();
|
||||
void (*kill)();
|
||||
void (*resume)();
|
||||
void (*openHomeMenu)();
|
||||
std::string (*getTitleId)();
|
||||
bool (*surfaceEvent)(JNIEnv *env, jobject surface, jint event);
|
||||
bool (*usbDeviceEvent)(int fd, int vendorId, int productId, int event);
|
||||
bool (*installFw)(JNIEnv *env, int fd, long progressId);
|
||||
bool (*isInstallableFile)(jint fd);
|
||||
jstring (*getDirInstallPath)(JNIEnv *env, jint fd);
|
||||
bool (*install)(JNIEnv *env, int fd, long progressId);
|
||||
bool (*installKey)(JNIEnv *env, int fd, long progressId,
|
||||
std::string_view gamePath);
|
||||
std::string (*systemInfo)();
|
||||
void (*loginUser)(std::string_view userId);
|
||||
std::string (*getUser)();
|
||||
std::string (*settingsGet)(std::string_view path);
|
||||
bool (*settingsSet)(std::string_view path, std::string_view valueString);
|
||||
std::string (*getVersion)();
|
||||
void *(*setCustomDriver)(void *driverHandle);
|
||||
};
|
||||
|
||||
struct RPCSXLibrary : RPCSXApi {
|
||||
void *handle = nullptr;
|
||||
|
||||
RPCSXLibrary() = default;
|
||||
RPCSXLibrary(const RPCSXLibrary &) = delete;
|
||||
RPCSXLibrary(RPCSXLibrary &&other) { swap(other); }
|
||||
RPCSXLibrary &operator=(RPCSXLibrary &&other) {
|
||||
swap(other);
|
||||
return *this;
|
||||
}
|
||||
~RPCSXLibrary() {
|
||||
if (handle) {
|
||||
::dlclose(handle);
|
||||
}
|
||||
}
|
||||
|
||||
void swap(RPCSXLibrary &other) noexcept {
|
||||
std::swap(handle, other.handle);
|
||||
std::swap(static_cast<RPCSXApi &>(*this), static_cast<RPCSXApi &>(other));
|
||||
}
|
||||
|
||||
static std::optional<RPCSXLibrary> Open(const char *path) {
|
||||
void *handle = ::dlopen(path, RTLD_LOCAL | RTLD_NOW);
|
||||
if (handle == nullptr) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "RPCSX-UI",
|
||||
"Failed to open RPCSX library at %s, error %s", path,
|
||||
::dlerror());
|
||||
return {};
|
||||
}
|
||||
|
||||
RPCSXLibrary result;
|
||||
result.handle = handle;
|
||||
|
||||
// clang-format off
|
||||
result.overlayPadData = reinterpret_cast<decltype(overlayPadData)>(dlsym(handle, "_rpcsx_overlayPadData"));
|
||||
result.initialize = reinterpret_cast<decltype(initialize)>(dlsym(handle, "_rpcsx_initialize"));
|
||||
result.processCompilationQueue = reinterpret_cast<decltype(processCompilationQueue)>(dlsym(handle, "_rpcsx_processCompilationQueue"));
|
||||
result.startMainThreadProcessor = reinterpret_cast<decltype(startMainThreadProcessor)>(dlsym(handle, "_rpcsx_startMainThreadProcessor"));
|
||||
result.collectGameInfo = reinterpret_cast<decltype(collectGameInfo)>(dlsym(handle, "_rpcsx_collectGameInfo"));
|
||||
result.shutdown = reinterpret_cast<decltype(shutdown)>(dlsym(handle, "_rpcsx_shutdown"));
|
||||
result.boot = reinterpret_cast<decltype(boot)>(dlsym(handle, "_rpcsx_boot"));
|
||||
result.getState = reinterpret_cast<decltype(getState)>(dlsym(handle, "_rpcsx_getState"));
|
||||
result.kill = reinterpret_cast<decltype(kill)>(dlsym(handle, "_rpcsx_kill"));
|
||||
result.resume = reinterpret_cast<decltype(resume)>(dlsym(handle, "_rpcsx_resume"));
|
||||
result.openHomeMenu = reinterpret_cast<decltype(openHomeMenu)>(dlsym(handle, "_rpcsx_openHomeMenu"));
|
||||
result.getTitleId = reinterpret_cast<decltype(getTitleId)>(dlsym(handle, "_rpcsx_getTitleId"));
|
||||
result.surfaceEvent = reinterpret_cast<decltype(surfaceEvent)>(dlsym(handle, "_rpcsx_surfaceEvent"));
|
||||
result.usbDeviceEvent = reinterpret_cast<decltype(usbDeviceEvent)>(dlsym(handle, "_rpcsx_usbDeviceEvent"));
|
||||
result.installFw = reinterpret_cast<decltype(installFw)>(dlsym(handle, "_rpcsx_installFw"));
|
||||
result.isInstallableFile = reinterpret_cast<decltype(isInstallableFile)>(dlsym(handle, "_rpcsx_isInstallableFile"));
|
||||
result.getDirInstallPath = reinterpret_cast<decltype(getDirInstallPath)>(dlsym(handle, "_rpcsx_getDirInstallPath"));
|
||||
result.install = reinterpret_cast<decltype(install)>(dlsym(handle, "_rpcsx_install"));
|
||||
result.installKey = reinterpret_cast<decltype(installKey)>(dlsym(handle, "_rpcsx_installKey"));
|
||||
result.systemInfo = reinterpret_cast<decltype(systemInfo)>(dlsym(handle, "_rpcsx_systemInfo"));
|
||||
result.loginUser = reinterpret_cast<decltype(loginUser)>(dlsym(handle, "_rpcsx_loginUser"));
|
||||
result.getUser = reinterpret_cast<decltype(getUser)>(dlsym(handle, "_rpcsx_getUser"));
|
||||
result.settingsGet = reinterpret_cast<decltype(settingsGet)>(dlsym(handle, "_rpcsx_settingsGet"));
|
||||
result.settingsSet = reinterpret_cast<decltype(settingsSet)>(dlsym(handle, "_rpcsx_settingsSet"));
|
||||
result.getVersion = reinterpret_cast<decltype(getVersion)>(dlsym(handle, "_rpcsx_getVersion"));
|
||||
result.setCustomDriver = reinterpret_cast<decltype(setCustomDriver)>(dlsym(handle, "_rpcsx_setCustomDriver"));
|
||||
// clang-format on
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
static RPCSXLibrary rpcsxLib;
|
||||
|
||||
static std::string unwrap(JNIEnv *env, jstring string) {
|
||||
auto resultBuffer = env->GetStringUTFChars(string, nullptr);
|
||||
std::string result(resultBuffer);
|
||||
env->ReleaseStringUTFChars(string, resultBuffer);
|
||||
return result;
|
||||
}
|
||||
static jstring wrap(JNIEnv *env, const std::string &string) {
|
||||
return env->NewStringUTF(string.c_str());
|
||||
}
|
||||
static jstring wrap(JNIEnv *env, const char *string) {
|
||||
return env->NewStringUTF(string);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_openLibrary(JNIEnv *env, jobject, jstring path) {
|
||||
if (auto library = RPCSXLibrary::Open(unwrap(env, path).c_str())) {
|
||||
rpcsxLib = std::move(*library);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_getLibraryVersion(JNIEnv *env, jobject, jstring path) {
|
||||
if (auto library = RPCSXLibrary::Open(unwrap(env, path).c_str())) {
|
||||
if (auto getVersion = library->getVersion) {
|
||||
return wrap(env, getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadData(
|
||||
JNIEnv *, jobject, jint digital1, jint digital2, jint leftStickX,
|
||||
jint leftStickY, jint rightStickX, jint rightStickY) {
|
||||
return rpcsxLib.overlayPadData(digital1, digital2, leftStickX, leftStickY,
|
||||
rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_initialize(
|
||||
JNIEnv *env, jobject, jstring rootDir, jstring user) {
|
||||
return rpcsxLib.initialize(unwrap(env, rootDir), unwrap(env, user));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_processCompilationQueue(JNIEnv *env, jobject) {
|
||||
return rpcsxLib.processCompilationQueue(env);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_startMainThreadProcessor(JNIEnv *env, jobject) {
|
||||
return rpcsxLib.startMainThreadProcessor(env);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_collectGameInfo(
|
||||
JNIEnv *env, jobject, jstring jrootDir, jlong progressId) {
|
||||
return rpcsxLib.collectGameInfo(env, unwrap(env, jrootDir), progressId);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_shutdown(JNIEnv *env,
|
||||
jobject) {
|
||||
return rpcsxLib.shutdown();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL Java_net_rpcsx_RPCSX_boot(JNIEnv *env,
|
||||
jobject,
|
||||
jstring jpath) {
|
||||
return rpcsxLib.boot(unwrap(env, jpath));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL Java_net_rpcsx_RPCSX_getState(JNIEnv *env,
|
||||
jobject) {
|
||||
return rpcsxLib.getState();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_kill(JNIEnv *env,
|
||||
jobject) {
|
||||
return rpcsxLib.kill();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_resume(JNIEnv *env,
|
||||
jobject) {
|
||||
return rpcsxLib.resume();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_openHomeMenu(JNIEnv *env,
|
||||
jobject) {
|
||||
return rpcsxLib.openHomeMenu();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_getTitleId(JNIEnv *env, jobject) {
|
||||
return wrap(env, rpcsxLib.getTitleId());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_surfaceEvent(
|
||||
JNIEnv *env, jobject, jobject surface, jint event) {
|
||||
return rpcsxLib.surfaceEvent(env, surface, event);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_usbDeviceEvent(
|
||||
JNIEnv *env, jobject, jint fd, jint vendorId, jint productId, jint event) {
|
||||
return rpcsxLib.usbDeviceEvent(fd, vendorId, productId, event);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_installFw(
|
||||
JNIEnv *env, jobject, jint fd, jlong progressId) {
|
||||
return rpcsxLib.installFw(env, fd, progressId);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_isInstallableFile(JNIEnv *env, jobject, jint fd) {
|
||||
return rpcsxLib.isInstallableFile(fd);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_getDirInstallPath(JNIEnv *env, jobject, jint fd) {
|
||||
return rpcsxLib.getDirInstallPath(env, fd);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_install(JNIEnv *env, jobject, jint fd, jlong progressId) {
|
||||
return rpcsxLib.install(env, fd, progressId);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_installKey(
|
||||
JNIEnv *env, jobject, jint fd, jlong progressId, jstring gamePath) {
|
||||
return rpcsxLib.installKey(env, fd, progressId, unwrap(env, gamePath));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_systemInfo(JNIEnv *env, jobject) {
|
||||
return wrap(env, rpcsxLib.systemInfo());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_net_rpcsx_RPCSX_loginUser(JNIEnv *env, jobject, jstring user_id) {
|
||||
return rpcsxLib.loginUser(unwrap(env, user_id));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL Java_net_rpcsx_RPCSX_getUser(JNIEnv *env,
|
||||
jobject) {
|
||||
return wrap(env, rpcsxLib.getUser());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_settingsGet(JNIEnv *env, jobject, jstring jpath) {
|
||||
return wrap(env, rpcsxLib.settingsGet(unwrap(env, jpath)));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_settingsSet(
|
||||
JNIEnv *env, jobject, jstring jpath, jstring jvalue) {
|
||||
return rpcsxLib.settingsSet(unwrap(env, jpath), unwrap(env, jvalue));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_supportsCustomDriverLoading(JNIEnv *env,
|
||||
jobject instance) {
|
||||
return access("/dev/kgsl-3d0", F_OK) == 0;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_getVersion(JNIEnv *env, jobject) {
|
||||
return wrap(env, rpcsxLib.getVersion());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_setCustomDriver(JNIEnv *env, jobject, jstring jpath,
|
||||
jstring jlibraryName, jstring jhookDir) {
|
||||
#ifdef __aarch64__
|
||||
if (rpcsxLib.setCustomDriver == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto path = unwrap(env, jpath);
|
||||
void *loader = nullptr;
|
||||
|
||||
if (!path.empty()) {
|
||||
auto hookDir = unwrap(env, jhookDir);
|
||||
auto libraryName = unwrap(env, jlibraryName);
|
||||
__android_log_print(ANDROID_LOG_INFO, "RPCSX-UI", "Loading custom driver %s",
|
||||
path.c_str());
|
||||
|
||||
::dlerror();
|
||||
loader = adrenotools_open_libvulkan(
|
||||
RTLD_NOW, ADRENOTOOLS_DRIVER_CUSTOM, nullptr, (hookDir + "/").c_str(),
|
||||
(path + "/").c_str(), libraryName.c_str(), nullptr, nullptr);
|
||||
|
||||
if (loader == nullptr) {
|
||||
__android_log_print(ANDROID_LOG_INFO, "RPCSX-UI",
|
||||
"Failed to load custom driver at '%s': %s",
|
||||
path.c_str(), ::dlerror());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto prevLoader = rpcsxLib.setCustomDriver(loader);
|
||||
if (prevLoader != nullptr) {
|
||||
::dlclose(prevLoader);
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif // __aarch64__
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,136 @@
|
||||
package net.rpcsx
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PerformanceHintManager
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import net.rpcsx.utils.GeneralSettings
|
||||
|
||||
/**
|
||||
* Android-side power/perf controls.
|
||||
*
|
||||
* Deliberately scoped to what the CORE does not already provide. RPCS3's own
|
||||
* config already exposes -- and the generated settings screen already renders --
|
||||
* Frame limit (the fps cap), Second Frame Limit, Thread Scheduler Mode, Sleep
|
||||
* Timers Accuracy, PPU/SPURS thread counts, Vblank Rate and Driver Wake-Up
|
||||
* Delay. Duplicating those here would create two controls for one value, which
|
||||
* is the classic "I changed the setting and nothing happened" bug.
|
||||
*
|
||||
* What is left is genuinely platform-only:
|
||||
* - Sustained performance mode: asks the platform for a LOWER but flat clock
|
||||
* ceiling. Counter-intuitive but usually a win for a long session: it trades
|
||||
* a hot burst for a frame rate that does not collapse once the device
|
||||
* thermally throttles.
|
||||
* - ADPF CPU clock hint: tells the platform how long our frame work actually
|
||||
* took, so the governor can pick a clock instead of guessing from load. On
|
||||
* an emulator the guess is usually wrong -- a busy-waiting SPU thread reads
|
||||
* as 100% load whether or not it needs the clock.
|
||||
*/
|
||||
object AndroidPerformance {
|
||||
private const val SustainedKey = "perf.sustained"
|
||||
private const val ClockHintKey = "perf.clockHint"
|
||||
private const val TargetFpsKey = "perf.clockHint.targetFps"
|
||||
|
||||
/** Flat-clock mode. Off by default -- it lowers peak clocks. */
|
||||
val sustainedMode = mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* ADPF. Off by default and EXPERIMENTAL: reporting bad numbers is worse
|
||||
* than reporting none, because the governor then actively fights you.
|
||||
*/
|
||||
val clockHint = mutableStateOf(false)
|
||||
|
||||
/** Target frame rate the hint session is built around. */
|
||||
val clockHintTargetFps = mutableIntStateOf(60)
|
||||
|
||||
private var hintSession: PerformanceHintManager.Session? = null
|
||||
|
||||
val isSustainedSupported: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
|
||||
val isClockHintSupported: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
fun load() {
|
||||
if (!GeneralSettings.isInitialized()) return
|
||||
sustainedMode.value = GeneralSettings.raw.getBoolean(SustainedKey, false)
|
||||
clockHint.value = GeneralSettings.raw.getBoolean(ClockHintKey, false)
|
||||
clockHintTargetFps.intValue = GeneralSettings.raw.getInt(TargetFpsKey, 60)
|
||||
}
|
||||
|
||||
fun setSustained(activity: Activity, enabled: Boolean) {
|
||||
sustainedMode.value = enabled
|
||||
GeneralSettings.raw.edit().putBoolean(SustainedKey, enabled).apply()
|
||||
applySustained(activity)
|
||||
}
|
||||
|
||||
/** Call from the emulator Activity's onCreate/onResume. */
|
||||
fun applySustained(activity: Activity) {
|
||||
if (!isSustainedSupported) return
|
||||
runCatching {
|
||||
activity.window.setSustainedPerformanceMode(sustainedMode.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun setClockHint(context: Context, enabled: Boolean) {
|
||||
clockHint.value = enabled
|
||||
GeneralSettings.raw.edit().putBoolean(ClockHintKey, enabled).apply()
|
||||
if (enabled) startHintSession(context) else stopHintSession()
|
||||
}
|
||||
|
||||
fun setClockHintTargetFps(fps: Int) {
|
||||
val clamped = fps.coerceIn(15, 240)
|
||||
clockHintTargetFps.intValue = clamped
|
||||
GeneralSettings.raw.edit().putInt(TargetFpsKey, clamped).apply()
|
||||
}
|
||||
|
||||
private fun targetDurationNanos(): Long =
|
||||
1_000_000_000L / clockHintTargetFps.intValue.coerceAtLeast(1)
|
||||
|
||||
/**
|
||||
* Open an ADPF session for the CURRENT thread.
|
||||
*
|
||||
* Must be called from the thread whose work we are reporting -- the session
|
||||
* is bound to specific tids. Calling it from a UI thread and then reporting
|
||||
* emulator frame times would describe the wrong thread entirely.
|
||||
*/
|
||||
fun startHintSession(context: Context) {
|
||||
if (!isClockHintSupported || !clockHint.value) return
|
||||
if (hintSession != null) return
|
||||
|
||||
runCatching {
|
||||
val manager = context.getSystemService(PerformanceHintManager::class.java)
|
||||
?: return
|
||||
hintSession = manager.createHintSession(
|
||||
intArrayOf(android.os.Process.myTid()),
|
||||
targetDurationNanos()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopHintSession() {
|
||||
runCatching { hintSession?.close() }
|
||||
hintSession = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the wall time one frame's work actually took.
|
||||
*
|
||||
* Only meaningful if it reflects real work. Reporting a fabricated or
|
||||
* frame-pacing-derived number makes the governor's model worse than having
|
||||
* no hint at all.
|
||||
*/
|
||||
fun reportActualFrameTime(nanos: Long) {
|
||||
val session = hintSession ?: return
|
||||
if (nanos <= 0) return
|
||||
runCatching { session.reportActualWorkDuration(nanos) }
|
||||
}
|
||||
|
||||
/** Retarget an existing session after the user changes the fps goal. */
|
||||
fun updateTargetWorkDuration() {
|
||||
val session = hintSession ?: return
|
||||
runCatching { session.updateTargetWorkDuration(targetDurationNanos()) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package net.rpcsx
|
||||
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* ARMSX3: selects ANGLE (GLES-on-Vulkan) for the OpenGL renderer.
|
||||
*
|
||||
* The whole hook is two environment variables. `gl::es::egl_initialize()` in the
|
||||
* core reads `ARMSX3_ANGLE_EGL_LIBRARY` with getenv, in this same process, and
|
||||
* dlopen()s that instead of the system libEGL. It has to be a private soname -
|
||||
* Android resolves `libEGL.so` and `libGLESv2.so` through the public-library
|
||||
* namespace, so an APK physically cannot shadow them; that is why the bundled
|
||||
* files are named libEGL_angle.so / libGLESv2_angle.so.
|
||||
*
|
||||
* Must run before the GS thread creates its context, i.e. before a game boots.
|
||||
*
|
||||
* On the diagnostics below: ARMSX2 shipped an APK whose ANGLE libraries had been
|
||||
* eaten by a blanket `*.so` gitignore rule. The core fell back to the system GLES
|
||||
* driver silently, users reported "ANGLE is broken", and no log said otherwise.
|
||||
* Three separate things now make that impossible to repeat: jniLibs/.gitignore
|
||||
* un-ignores the two files by name, the `verifyAngleLibs` Gradle task fails the
|
||||
* build if they are not in the APK, and both this class and the core log loudly
|
||||
* when ANGLE was asked for and could not be had.
|
||||
*/
|
||||
object AngleConfig {
|
||||
private const val TAG = "ARMSX3"
|
||||
private const val EGL_LIB = "libEGL_angle.so"
|
||||
private const val GLES_LIB = "libGLESv2_angle.so"
|
||||
|
||||
const val ENV_EGL = "ARMSX3_ANGLE_EGL_LIBRARY"
|
||||
const val ENV_GLES = "ARMSX3_ANGLE_GLES_LIBRARY"
|
||||
|
||||
/** True when both ANGLE libraries are actually present in the APK. */
|
||||
fun isAvailable(nativeLibraryDir: String): Boolean =
|
||||
File(nativeLibraryDir, EGL_LIB).isFile && File(nativeLibraryDir, GLES_LIB).isFile
|
||||
|
||||
/**
|
||||
* @param enabled the user's "use ANGLE for OpenGL" setting
|
||||
* @param renderer the selected renderer; ANGLE only affects the OpenGL one
|
||||
*/
|
||||
fun apply(nativeLibraryDir: String, enabled: Boolean, renderer: String?) {
|
||||
val wanted = enabled && renderer.equals("opengl", ignoreCase = true)
|
||||
|
||||
if (!wanted) {
|
||||
runCatching { Os.unsetenv(ENV_EGL) }
|
||||
runCatching { Os.unsetenv(ENV_GLES) }
|
||||
Log.i(TAG, "@@ANGLE@@ off (renderer=$renderer enabled=$enabled)")
|
||||
return
|
||||
}
|
||||
|
||||
val egl = File(nativeLibraryDir, EGL_LIB)
|
||||
val gles = File(nativeLibraryDir, GLES_LIB)
|
||||
|
||||
if (!egl.isFile || !gles.isFile) {
|
||||
// Distinguish "the user never picked ANGLE" from "the user picked ANGLE
|
||||
// and the APK does not contain it". Only the second is a bug, and it is
|
||||
// invisible without this line.
|
||||
runCatching { Os.unsetenv(ENV_EGL) }
|
||||
runCatching { Os.unsetenv(ENV_GLES) }
|
||||
Log.e(
|
||||
TAG,
|
||||
"@@ANGLE@@ MISSING_LIBS dir=$nativeLibraryDir egl=${egl.isFile} gles=${gles.isFile} " +
|
||||
"-> falling back to the system GLES driver"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
runCatching {
|
||||
Os.setenv(ENV_EGL, egl.absolutePath, true)
|
||||
Os.setenv(ENV_GLES, gles.absolutePath, true)
|
||||
Log.i(TAG, "@@ANGLE@@ enabled egl=${egl.absolutePath}")
|
||||
}.onFailure {
|
||||
Log.e(TAG, "@@ANGLE@@ error ${it.javaClass.simpleName}: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.rpcsx
|
||||
|
||||
/**
|
||||
* Canonical ARMSX3 endpoints.
|
||||
*
|
||||
* Kept in one place because these are referenced from the drawer, the About /
|
||||
* What's New screen and the updater. The repo does not exist publicly yet --
|
||||
* when it does, nothing else needs touching.
|
||||
*/
|
||||
object ArmsxLinks {
|
||||
/** GitHub org/repo. What's New reads its Releases feed. */
|
||||
const val REPO = "https://github.com/ARMSX2/ARMSX3"
|
||||
|
||||
/** Shared with ARMSX2 -- same community. */
|
||||
const val DISCORD = "https://discord.gg/2Tynvwhc4A"
|
||||
|
||||
const val WEBSITE = "https://armsx2.net/"
|
||||
|
||||
/** Where "report an issue" should land. */
|
||||
const val ISSUES = "$REPO/issues"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user