From 476e04e2fb418757a23e3a347e82a462b019a4f3 Mon Sep 17 00:00:00 2001 From: DH Date: Thu, 10 Apr 2025 01:28:06 +0300 Subject: [PATCH] Add android native code --- .github/workflows/rpcs3.yml | 2 + .github/workflows/rpcsx.yml | 91 ++ android/CMakeLists.txt | 145 ++ android/src/rpcsx-android.cpp | 2644 +++++++++++++++++++++++++++++++++ 4 files changed, 2882 insertions(+) create mode 100644 android/CMakeLists.txt create mode 100644 android/src/rpcsx-android.cpp diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml index eec8801ec..f2d3a2888 100644 --- a/.github/workflows/rpcs3.yml +++ b/.github/workflows/rpcs3.yml @@ -121,6 +121,8 @@ jobs: CCACHE_INODECACHE: 'true' CCACHE_SLOPPINESS: 'time_macros' DEPS_CACHE_DIR: ./dependency_cache + RX_VERSION: 'Unknown' + RX_SHA: 'Unknown' steps: - name: Checkout repository diff --git a/.github/workflows/rpcsx.yml b/.github/workflows/rpcsx.yml index 8ce1674fe..15745b344 100644 --- a/.github/workflows/rpcsx.yml +++ b/.github/workflows/rpcsx.yml @@ -42,3 +42,94 @@ jobs: name: rpcsx-bin path: build/bin/* if-no-files-found: error + + build-android: + strategy: + fail-fast: false + matrix: + include: + - abi: arm64-v8a + arch: armv8-a + - abi: arm64-v8a + arch: armv8.1-a + - abi: arm64-v8a + arch: armv8.2-a + - abi: arm64-v8a + arch: armv8.4-a + - abi: arm64-v8a + arch: armv8.5-a + - abi: arm64-v8a + arch: armv9-a + - abi: arm64-v8a + arch: armv9.1-a + - abi: x86_64 + arch: x86-64 + + env: + NDK_VERSION: "29.0.13113456" + CMAKE_VERSION: "3.31.6" + RX_VERSION: 'Unknown' + RX_SHA: 'Unknown' + + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + lfs: true + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + + - name: Install Ninja + run: | + sudo apt update + sudo apt install ninja-build + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install packages + run: yes | sdkmanager "cmake;${{ env.CMAKE_VERSION }}" "ndk;${{ env.NDK_VERSION }}" + + - name: Configure + run: | + $ANDROID_SDK_ROOT/cmake/${{ env.CMAKE_VERSION }}/bin/cmake \ + -B build -S android -DANDROID_ABI=${{ matrix.abi }} \ + -DUSE_ARCH=${{ matrix.arch }} \ + -DANDROID_PLATFORM=android-31 \ + -DANDROID_NDK=$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }} \ + -DCMAKE_TOOLCHAIN_FILE=$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }}/build/cmake/android.toolchain.cmake \ + -DCMAKE_BUILD_TYPE=Release -G Ninja + + RX_VERSION=`cat android/.rx.version | awk -F'-' '{print $1}'` + RX_SHA=`cat android/.rx.version | awk -F'-' '{print $5}'` + echo "RX_VERSION=$RX_VERSION" >> "${{ github.env }}" + echo "RX_SHA=$RX_SHA" >> "${{ github.env }}" + + - name: Build + run: | + $ANDROID_SDK_ROOT/cmake/${{ env.CMAKE_VERSION }}/bin/cmake --build build + mv build/librpcsx-android.so librpcsx-android-${{ matrix.abi }}-${{ matrix.arch }}.so + + - name: Deploy build + uses: softprops/action-gh-release@v2 + if: | + github.event_name != 'pull_request' && + github.ref == 'refs/heads/master' && + github.repository == 'RPCSX/rpcsx' + with: + prerelease: false + make_latest: true + repository: RPCSX/rpcsx-build + token: ${{ secrets.BUILD_TOKEN }} + tag_name: v${{ env.RX_VERSION }}-${{ env.RX_SHA }} + files: librpcsx-android-${{ matrix.abi }}-${{ matrix.arch }}.so + body: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 000000000..0f6d9462f --- /dev/null +++ b/android/CMakeLists.txt @@ -0,0 +1,145 @@ +cmake_minimum_required(VERSION 3.16.9) +project("rpcsx-android") + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_POSITION_INDEPENDENT_CODE on) + +set(FFMPEG_VERSION 5.1) +set(LLVM_VERSION 19.1) + +option(USE_ARCH "Specify arch to build" "") + +if (NOT USE_ARCH STREQUAL "") + add_compile_options(-march=${USE_ARCH}) +endif() + +if (TEST_OVERRIDE_CPU) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") + set(ARCH_FLAGS "-mcpu=cortex-a53") + else() + set(ARCH_FLAGS "-mno-avx") + endif() + + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${ARCH_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${ARCH_FLAGS}") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${ARCH_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${ARCH_FLAGS}") +endif() + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") + set(RPCS3_DOWNLOAD_ARCH "arm64-v8a") +else() + set(RPCS3_DOWNLOAD_ARCH "x86-64") +endif() + +if(NOT EXISTS ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz) + message(STATUS "Downloading ffmpeg-${FFMPEG_VERSION}") + file(DOWNLOAD + https://github.com/RPCS3-Android/ffmpeg-android/releases/download/${FFMPEG_VERSION}/ffmpeg-${RPCS3_DOWNLOAD_ARCH}-Android.tar.gz + ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz + SHOW_PROGRESS + ) +endif() + +set(FFMPEG_PATH "${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}") + +make_directory(${FFMPEG_PATH}) +add_custom_command( + OUTPUT ${FFMPEG_PATH}/src + COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz + DEPENDS ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz + WORKING_DIRECTORY ${FFMPEG_PATH} + COMMENT "Unpacking ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz" +) + +add_custom_target(ffmpeg-unpack DEPENDS ${FFMPEG_PATH}/src) + +function(import_ffmpeg_library name) + add_custom_command( + OUTPUT "${FFMPEG_PATH}/lib${name}/lib${name}.a" + DEPENDS ffmpeg-unpack + WORKING_DIRECTORY ${FFMPEG_PATH} + ) + + add_custom_target(ffmpeg-unpack-${name} DEPENDS "${FFMPEG_PATH}/lib${name}/lib${name}.a") + add_library(ffmpeg::${name} STATIC IMPORTED GLOBAL) + set_property(TARGET ffmpeg::${name} PROPERTY IMPORTED_LOCATION "${FFMPEG_PATH}/lib${name}/lib${name}.a") + set_property(TARGET ffmpeg::${name} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_PATH}" "${FFMPEG_PATH}/src") + add_dependencies(ffmpeg::${name} ffmpeg-unpack-${name}) +endfunction() + +import_ffmpeg_library(avcodec) +import_ffmpeg_library(avformat) +import_ffmpeg_library(avfilter) +import_ffmpeg_library(avdevice) +import_ffmpeg_library(avutil) +import_ffmpeg_library(swscale) +import_ffmpeg_library(swresample) + +add_library(3rdparty_ffmpeg INTERFACE) +target_link_libraries(3rdparty_ffmpeg INTERFACE + ffmpeg::avformat + ffmpeg::avcodec + ffmpeg::avutil + ffmpeg::swscale + ffmpeg::swresample +) + +add_dependencies(3rdparty_ffmpeg ffmpeg-unpack) + + +if(NOT EXISTS ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.tar.gz) + message(STATUS "Downloading llvm-${LLVM_VERSION}") + file(DOWNLOAD + https://github.com/RPCS3-Android/llvm-android/releases/download/${LLVM_VERSION}/llvm-${RPCS3_DOWNLOAD_ARCH}-Android.tar.gz + ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.tar.gz + SHOW_PROGRESS + ) +endif() + +set(LLVM_DIR ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.7-Android/lib/cmake/llvm) + +if (NOT EXISTS ${LLVM_DIR}) + message(STATUS "Unpacking llvm-${LLVM_VERSION}") + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.tar.gz + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) +endif() + +set(WITH_RPCSX off) +set(WITH_RPCS3 on) +set(USE_SYSTEM_LIBUSB off) +set(USE_SYSTEM_CURL off) +set(USE_DISCORD_RPC off) +set(USE_SYSTEM_OPENCV off) +set(USE_SYSTEM_FFMPEG off) +set(USE_FAUDIO off) +set(USE_SDL2 off) +set(BUILD_LLVM off) +set(STATIC_LINK_LLVM on) +set(DISABLE_LTO on) +set(USE_LTO off) +set(USE_OPENSL off) +set(ASMJIT_NO_SHM_OPEN on) +set(USE_SYSTEM_ZLIB on) +set(USE_LIBEVDEV off) +set(COMPILE_FFMPEG off) + +add_subdirectory(${CMAKE_SOURCE_DIR}/.. rpcsx EXCLUDE_FROM_ALL) + +add_library(${CMAKE_PROJECT_NAME} SHARED + src/rpcsx-android.cpp +) + +target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC rpcs3/rpcs3) + +target_link_libraries(${CMAKE_PROJECT_NAME} + rpcs3 + rpcsx::fw::ps3 + rx + android + log + nativehelper +) diff --git a/android/src/rpcsx-android.cpp b/android/src/rpcsx-android.cpp new file mode 100644 index 000000000..43b42dc97 --- /dev/null +++ b/android/src/rpcsx-android.cpp @@ -0,0 +1,2644 @@ +#include "Crypto/unpkg.h" +#include "Crypto/unself.h" +#include "Emu/Audio/Cubeb/CubebBackend.h" +#include "Emu/Audio/Null/NullAudioBackend.h" +#include "Emu/Cell/PPUAnalyser.h" +#include "Emu/Cell/SPURecompiler.h" +#include "Emu/Cell/lv2/sys_sync.h" +#include "Emu/IdManager.h" +#include "Emu/Io/KeyboardHandler.h" +#include "Emu/Io/Null/NullKeyboardHandler.h" +#include "Emu/Io/Null/NullMouseHandler.h" +#include "Emu/Io/Null/NullPadHandler.h" +#include "Emu/Io/Null/null_camera_handler.h" +#include "Emu/Io/Null/null_music_handler.h" +#include "Emu/Io/pad_config_types.h" +#include "Emu/RSX/Null/NullGSRender.h" +#include "Emu/RSX/Overlays/overlay_manager.h" +#include "Emu/RSX/Overlays/overlay_save_dialog.h" +#include "Emu/RSX/Overlays/overlay_trophy_notification.h" +#include "Emu/RSX/RSXThread.h" +#include "Emu/RSX/VK/VKGSRender.h" +#include "Emu/localized_string_id.h" +#include "Emu/system_config.h" +#include "Emu/system_config_types.h" +#include "Emu/system_progress.hpp" +#include "Emu/system_utils.hpp" +#include "Emu/vfs_config.h" +#include "Input/ds3_pad_handler.h" +#include "Input/ds4_pad_handler.h" +#include "Input/dualsense_pad_handler.h" +#include "Input/hid_pad_handler.h" +#include "Input/pad_thread.h" +#include "Input/virtual_pad_handler.h" +#include "Loader/PSF.h" +#include "Loader/PUP.h" +#include "Loader/TAR.h" +#include "dev/block_dev.hpp" +#include "dev/iso.hpp" +#include "hidapi_libusb.h" +#include "libusb.h" +#include "rpcs3_version.h" +#include "rpcsx/fw/ps3/cellMsgDialog.h" +#include "rpcsx/fw/ps3/cellSysutil.h" +#include "util/File.h" +#include "util/JIT.h" +#include "util/StrFmt.h" +#include "util/StrUtil.h" +#include "util/Thread.h" +#include "util/asm.hpp" +#include "util/console.h" +#include "util/fixed_typemap.hpp" +#include "util/logs.hpp" +#include "util/serialization.hpp" +#include "util/sysinfo.hpp" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct AtExit { + std::function cb; + ~AtExit() { cb(); } +}; + +static bool g_initialized; +static std::atomic g_native_window; + +extern std::string g_android_executable_dir; +extern std::string g_android_config_dir; +extern std::string g_android_cache_dir; + +static std::mutex g_virtual_pad_mutex; +static std::shared_ptr g_virtual_pad; + +std::string g_input_config_override; +cfg_input_configurations g_cfg_input_configs; + +LOG_CHANNEL(rpcsx_android, "ANDROID"); + +struct LogListener : logs::listener { + LogListener() { logs::listener::add(this); } + + void log(u64 stamp, const logs::message &msg, const std::string &prefix, + const std::string &text) override { + int prio = 0; + switch (static_cast(msg)) { + case logs::level::always: + prio = ANDROID_LOG_INFO; + break; + case logs::level::fatal: + prio = ANDROID_LOG_FATAL; + break; + case logs::level::error: + prio = ANDROID_LOG_ERROR; + break; + case logs::level::todo: + prio = ANDROID_LOG_WARN; + break; + case logs::level::success: + prio = ANDROID_LOG_INFO; + break; + case logs::level::warning: + prio = ANDROID_LOG_WARN; + break; + case logs::level::notice: + prio = ANDROID_LOG_DEBUG; + break; + case logs::level::trace: + prio = ANDROID_LOG_VERBOSE; + break; + } + + __android_log_write(prio, "RPCS3", text.c_str()); + } +} static g_androidLogListener; + +struct GraphicsFrame : GSFrameBase { + mutable ANativeWindow *activeNativeWindow = nullptr; + mutable int width = 0; + mutable int height = 0; + + ~GraphicsFrame() { + if (activeNativeWindow != nullptr) { + ANativeWindow_release(activeNativeWindow); + } + } + + ANativeWindow *getNativeWindow() const { + ANativeWindow *result; + while ((result = g_native_window.load()) == nullptr) [[unlikely]] { + if (Emu.IsStopped()) { + return activeNativeWindow; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + if (result != activeNativeWindow) [[unlikely]] { + ANativeWindow_acquire(result); + + if (activeNativeWindow != nullptr) { + ANativeWindow_release(activeNativeWindow); + } + + activeNativeWindow = result; + + width = ANativeWindow_getWidth(result); + height = ANativeWindow_getHeight(result); + } + + return result; + } + + void close() override {} + void reset() override {} + bool shown() override { return true; } + void hide() override {} + void show() override {} + void toggle_fullscreen() override {} + + void delete_context(draw_context_t ctx) override {} + draw_context_t make_context() override { return nullptr; } + void set_current(draw_context_t ctx) override {} + void flip(draw_context_t ctx, bool skip_frame = false) override {} + int client_width() override { return width; } + int client_height() override { return height; } + f64 client_display_rate() override { return 30.f; } + bool has_alpha() override { + return ANativeWindow_getFormat(getNativeWindow()) == + WINDOW_FORMAT_RGBA_8888; + } + + display_handle_t handle() const override { return getNativeWindow(); } + + bool can_consume_frame() const override { return false; } + + void present_frame(std::vector &data, u32 pitch, u32 width, u32 height, + bool is_bgra) const override {} + void take_screenshot(std::vector &&sshot_data, u32 sshot_width, + u32 sshot_height, bool is_bgra) override {} +}; + +void jit_announce(uptr, usz, std::string_view); + +[[noreturn]] void report_fatal_error(std::string_view _text, + bool is_html = false, + bool include_help_text = true) { + std::string buf; + + buf = std::string(_text); + + // Check if thread id is in string + if (_text.find("\nThread id = "sv) == umax && !thread_ctrl::is_main()) { + // Append thread id if it isn't already, except on main thread + fmt::append(buf, "\n\nThread id = %u.", thread_ctrl::get_tid()); + } + + if (!g_tls_serialize_name.empty()) { + fmt::append(buf, "\nSerialized Object: %s", g_tls_serialize_name); + } + + const system_state state = Emu.GetStatus(false); + + if (state == system_state::stopped) { + fmt::append(buf, "\nEmulation is stopped"); + } else { + const std::string &name = Emu.GetTitleAndTitleID(); + fmt::append(buf, "\nTitle: \"%s\" (emulation is %s)", + name.empty() ? "N/A" : name.data(), + state == system_state::stopping ? "stopping" : "running"); + } + + fmt::append(buf, "\nBuild: \"%s\"", rpcs3::get_verbose_version()); + fmt::append(buf, "\nDate: \"%s\"", std::chrono::system_clock::now()); + + __android_log_write(ANDROID_LOG_FATAL, "RPCS3", buf.c_str()); + + jit_announce(0, 0, ""); + utils::trap(); + std::abort(); + std::terminate(); +} + +void qt_events_aware_op(int repeat_duration_ms, + std::function wrapped_op) { + /// ????? +} + +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); +} + +static std::string fix_dir_path(std::string string) { + if (!string.empty() && !string.ends_with('/')) { + string += '/'; + } + + return string; +} + +enum class FileType { + Unknown, + Pup, + Pkg, + Edat, + Rap, + Iso, +}; + +static FileType getFileType(const fs::file &file) { + file.seek(0); + if (PUPHeader pupHeader; file.read(pupHeader)) { + if (pupHeader.magic == "SCEUF\0\0\0"_u64) { + return FileType::Pup; + } + } + + file.seek(0); + if (PKGHeader pkgHeader; file.read(pkgHeader)) { + if (pkgHeader.pkg_magic == std::bit_cast>("\x7FPKG"_u32)) { + return FileType::Pkg; + } + } + + file.seek(0); + if (NPD_HEADER npdHeader; file.read(npdHeader)) { + if (npdHeader.magic == "NPD\0"_u32) { + return FileType::Edat; + } + } + + if (file.size() == 16) { + return FileType::Rap; + } + + if (iso_dev::open(std::make_unique(file))) { + return FileType::Iso; + } + + return FileType::Unknown; +} + +#define MAKE_STRING(id, x) [int(localized_string_id::id)] = {x, U##x} + +static std::pair g_strings[] = { + MAKE_STRING(RSX_OVERLAYS_COMPILING_SHADERS, "Compiling shaders"), + MAKE_STRING(RSX_OVERLAYS_COMPILING_PPU_MODULES, "Compiling PPU Modules"), + MAKE_STRING(RSX_OVERLAYS_MSG_DIALOG_YES, "Yes"), + MAKE_STRING(RSX_OVERLAYS_MSG_DIALOG_NO, "No"), + MAKE_STRING(RSX_OVERLAYS_MSG_DIALOG_CANCEL, "Back"), + MAKE_STRING(RSX_OVERLAYS_MSG_DIALOG_OK, "OK"), + MAKE_STRING(RSX_OVERLAYS_SAVE_DIALOG_TITLE, "Save Dialog"), + MAKE_STRING(RSX_OVERLAYS_SAVE_DIALOG_DELETE, "Delete Save"), + MAKE_STRING(RSX_OVERLAYS_SAVE_DIALOG_LOAD, "Load Save"), + MAKE_STRING(RSX_OVERLAYS_SAVE_DIALOG_SAVE, "Save"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_ACCEPT, "Enter"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_CANCEL, "Back"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_SPACE, "Space"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_BACKSPACE, "Backspace"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_SHIFT, "Shift"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_ENTER_TEXT, "[Enter Text]"), + MAKE_STRING(RSX_OVERLAYS_OSK_DIALOG_ENTER_PASSWORD, "[Enter Password]"), + MAKE_STRING(RSX_OVERLAYS_MEDIA_DIALOG_TITLE, "Select media"), + MAKE_STRING(RSX_OVERLAYS_MEDIA_DIALOG_TITLE_PHOTO_IMPORT, + "Select photo to import"), + MAKE_STRING(RSX_OVERLAYS_MEDIA_DIALOG_EMPTY, "No media found."), + MAKE_STRING(RSX_OVERLAYS_LIST_SELECT, "Enter"), + MAKE_STRING(RSX_OVERLAYS_LIST_CANCEL, "Back"), + MAKE_STRING(RSX_OVERLAYS_LIST_DENY, "Deny"), + MAKE_STRING(CELL_OSK_DIALOG_TITLE, "On Screen Keyboard"), + MAKE_STRING( + CELL_OSK_DIALOG_BUSY, + "The Home Menu can't be opened while the On Screen Keyboard is busy!"), + MAKE_STRING(CELL_SAVEDATA_CB_BROKEN, "Error - Save data corrupted"), + MAKE_STRING(CELL_SAVEDATA_CB_FAILURE, "Error - Failed to save or load"), + MAKE_STRING(CELL_SAVEDATA_CB_NO_DATA, "Error - Save data cannot be found"), + MAKE_STRING(CELL_SAVEDATA_NO_DATA, "There is no saved data."), + MAKE_STRING(CELL_SAVEDATA_NEW_SAVED_DATA_TITLE, "New Saved Data"), + MAKE_STRING(CELL_SAVEDATA_NEW_SAVED_DATA_SUB_TITLE, + "Select to create a new entry"), + MAKE_STRING(CELL_SAVEDATA_SAVE_CONFIRMATION, + "Do you want to save this data?"), + MAKE_STRING(CELL_SAVEDATA_AUTOSAVE, "Saving..."), + MAKE_STRING(CELL_SAVEDATA_AUTOLOAD, "Loading..."), + MAKE_STRING( + CELL_CROSS_CONTROLLER_FW_MSG, + "If your system software version on the PS Vita system is earlier than " + "1.80, you must update the system software to the latest version."), + MAKE_STRING(CELL_NP_RECVMESSAGE_DIALOG_TITLE, "Select Message"), + MAKE_STRING(CELL_NP_RECVMESSAGE_DIALOG_TITLE_INVITE, "Select Invite"), + MAKE_STRING(CELL_NP_RECVMESSAGE_DIALOG_TITLE_ADD_FRIEND, "Add Friend"), + MAKE_STRING(CELL_NP_RECVMESSAGE_DIALOG_FROM, "From:"), + MAKE_STRING(CELL_NP_RECVMESSAGE_DIALOG_SUBJECT, "Subject:"), + MAKE_STRING(CELL_NP_SENDMESSAGE_DIALOG_TITLE, "Select Message To Send"), + MAKE_STRING(CELL_NP_SENDMESSAGE_DIALOG_TITLE_INVITE, "Send Invite"), + MAKE_STRING(CELL_NP_SENDMESSAGE_DIALOG_TITLE_ADD_FRIEND, "Add Friend"), + MAKE_STRING(RECORDING_ABORTED, "Recording aborted!"), + MAKE_STRING(RPCN_NO_ERROR, "RPCN: No Error"), + MAKE_STRING(RPCN_ERROR_INVALID_INPUT, + "RPCN: Invalid Input (Wrong Host/Port)"), + MAKE_STRING(RPCN_ERROR_WOLFSSL, "RPCN Connection Error: WolfSSL Error"), + MAKE_STRING(RPCN_ERROR_RESOLVE, "RPCN Connection Error: Resolve Error"), + MAKE_STRING(RPCN_ERROR_CONNECT, "RPCN Connection Error"), + MAKE_STRING(RPCN_ERROR_LOGIN_ERROR, + "RPCN Login Error: Identification Error"), + MAKE_STRING(RPCN_ERROR_ALREADY_LOGGED, + "RPCN Login Error: User Already Logged In"), + MAKE_STRING(RPCN_ERROR_INVALID_LOGIN, "RPCN Login Error: Invalid Username"), + MAKE_STRING(RPCN_ERROR_INVALID_PASSWORD, + "RPCN Login Error: Invalid Password"), + MAKE_STRING(RPCN_ERROR_INVALID_TOKEN, "RPCN Login Error: Invalid Token"), + MAKE_STRING(RPCN_ERROR_INVALID_PROTOCOL_VERSION, + "RPCN Misc Error: Protocol Version Error (outdated RPCS3?)"), + MAKE_STRING(RPCN_ERROR_UNKNOWN, "RPCN: Unknown Error"), + MAKE_STRING(RPCN_SUCCESS_LOGGED_ON, "Successfully logged on RPCN!"), + MAKE_STRING(HOME_MENU_TITLE, "Home Menu"), + MAKE_STRING(HOME_MENU_EXIT_GAME, "Exit Game"), + MAKE_STRING(HOME_MENU_RESUME, "Resume Game"), + MAKE_STRING(HOME_MENU_FRIENDS, "Friends"), + MAKE_STRING(HOME_MENU_FRIENDS_REQUESTS, "Pending Friend Requests"), + MAKE_STRING(HOME_MENU_FRIENDS_BLOCKED, "Blocked Users"), + MAKE_STRING(HOME_MENU_FRIENDS_STATUS_ONLINE, "Online"), + MAKE_STRING(HOME_MENU_FRIENDS_STATUS_OFFLINE, "Offline"), + MAKE_STRING(HOME_MENU_FRIENDS_STATUS_BLOCKED, "Blocked"), + MAKE_STRING(HOME_MENU_FRIENDS_REQUEST_SENT, "You sent a friend request"), + MAKE_STRING(HOME_MENU_FRIENDS_REQUEST_RECEIVED, + "Sent you a friend request"), + MAKE_STRING(HOME_MENU_FRIENDS_REJECT_REQUEST, "Reject Request"), + MAKE_STRING(HOME_MENU_FRIENDS_NEXT_LIST, "Next list"), + MAKE_STRING(HOME_MENU_RESTART, "Restart Game"), + MAKE_STRING(HOME_MENU_SETTINGS, "Settings"), + MAKE_STRING(HOME_MENU_SETTINGS_SAVE, "Save custom configuration?"), + MAKE_STRING(HOME_MENU_SETTINGS_SAVE_BUTTON, "Save"), + MAKE_STRING(HOME_MENU_SETTINGS_DISCARD, + "Discard the current settings' changes?"), + MAKE_STRING(HOME_MENU_SETTINGS_DISCARD_BUTTON, "Discard"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO, "Audio"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO_MASTER_VOLUME, "Master Volume"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO_BACKEND, "Audio Backend"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO_BUFFERING, "Enable Buffering"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO_BUFFER_DURATION, + "Desired Audio Buffer Duration"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO_TIME_STRETCHING, + "Enable Time Stretching"), + MAKE_STRING(HOME_MENU_SETTINGS_AUDIO_TIME_STRETCHING_THRESHOLD, + "Time Stretching Threshold"), + MAKE_STRING(HOME_MENU_SETTINGS_VIDEO, "Video"), + MAKE_STRING(HOME_MENU_SETTINGS_VIDEO_FRAME_LIMIT, "Frame Limit"), + MAKE_STRING(HOME_MENU_SETTINGS_VIDEO_ANISOTROPIC_OVERRIDE, + "Anisotropic Filter Override"), + MAKE_STRING(HOME_MENU_SETTINGS_VIDEO_OUTPUT_SCALING, "Output Scaling"), + MAKE_STRING(HOME_MENU_SETTINGS_VIDEO_RCAS_SHARPENING, + "FidelityFX CAS Sharpening Intensity"), + MAKE_STRING(HOME_MENU_SETTINGS_VIDEO_STRETCH_TO_DISPLAY, + "Stretch To Display Area"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT, "Input"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_BACKGROUND_INPUT, + "Background Input Enabled"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_KEEP_PADS_CONNECTED, + "Keep Pads Connected"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_SHOW_PS_MOVE_CURSOR, + "Show PS Move Cursor"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_CAMERA_FLIP, "Camera Flip"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_PAD_MODE, "Pad Handler Mode"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_PAD_SLEEP, "Pad Handler Sleep"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_FAKE_MOVE_ROTATION_CONE_H, + "Fake PS Move Rotation Cone (Horizontal)"), + MAKE_STRING(HOME_MENU_SETTINGS_INPUT_FAKE_MOVE_ROTATION_CONE_V, + "Fake PS Move Rotation Cone (Vertical)"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED, "Advanced"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_PREFERRED_SPU_THREADS, + "Preferred SPU Threads"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_MAX_CPU_PREEMPTIONS, + "Max Power Saving CPU-Preemptions"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_ACCURATE_RSX_RESERVATION_ACCESS, + "Accurate RSX reservation access"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_SLEEP_TIMERS_ACCURACY, + "Sleep Timers Accuracy"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_MAX_SPURS_THREADS, + "Max SPURS Threads"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_DRIVER_WAKE_UP_DELAY, + "Driver Wake-Up Delay"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_VBLANK_FREQUENCY, + "VBlank Frequency"), + MAKE_STRING(HOME_MENU_SETTINGS_ADVANCED_VBLANK_NTSC, "VBlank NTSC Fixup"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS, "Overlays"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_TROPHY_POPUPS, + "Show Trophy Popups"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_RPCN_POPUPS, + "Show RPCN Popups"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_SHADER_COMPILATION_HINT, + "Show Shader Compilation Hint"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_PPU_COMPILATION_HINT, + "Show PPU Compilation Hint"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_AUTO_SAVE_LOAD_HINT, + "Show Autosave/Autoload Hint"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_PRESSURE_INTENSITY_TOGGLE_HINT, + "Show Pressure Intensity Toggle Hint"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_ANALOG_LIMITER_TOGGLE_HINT, + "Show Analog Limiter Toggle Hint"), + MAKE_STRING(HOME_MENU_SETTINGS_OVERLAYS_SHOW_MOUSE_AND_KB_TOGGLE_HINT, + "Show Mouse And Keyboard Toggle Hint"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY, "Performance Overlay"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_ENABLE, + "Enable Performance Overlay"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_ENABLE_FRAMERATE_GRAPH, + "Enable Framerate Graph"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_ENABLE_FRAMETIME_GRAPH, + "Enable Frametime Graph"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_DETAIL_LEVEL, + "Detail level"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_FRAMERATE_DETAIL_LEVEL, + "Framerate Graph Detail Level"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_FRAMETIME_DETAIL_LEVEL, + "Frametime Graph Detail Level"), + MAKE_STRING( + HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_FRAMERATE_DATAPOINT_COUNT, + "Framerate Datapoints"), + MAKE_STRING( + HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_FRAMETIME_DATAPOINT_COUNT, + "Frametime Datapoints"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_UPDATE_INTERVAL, + "Metrics Update Interval"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_POSITION, "Position"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_CENTER_X, + "Center Horizontally"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_CENTER_Y, + "Center Vertically"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_MARGIN_X, + "Horizontal Margin"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_MARGIN_Y, + "Vertical Margin"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_FONT_SIZE, "Font Size"), + MAKE_STRING(HOME_MENU_SETTINGS_PERFORMANCE_OVERLAY_OPACITY, "Opacity"), + MAKE_STRING(HOME_MENU_SETTINGS_DEBUG, "Debug"), + MAKE_STRING(HOME_MENU_SETTINGS_DEBUG_OVERLAY, "Debug Overlay"), + MAKE_STRING(HOME_MENU_SETTINGS_DEBUG_INPUT_OVERLAY, "Input Debug Overlay"), + MAKE_STRING(HOME_MENU_SETTINGS_DEBUG_DISABLE_VIDEO_OUTPUT, + "Disable Video Output"), + MAKE_STRING(HOME_MENU_SETTINGS_DEBUG_TEXTURE_LOD_BIAS, + "Texture LOD Bias Addend"), + MAKE_STRING(HOME_MENU_SCREENSHOT, "Take Screenshot"), + MAKE_STRING(HOME_MENU_SAVESTATE, "SaveState"), + MAKE_STRING(HOME_MENU_SAVESTATE_SAVE, "Save Emulation State"), + MAKE_STRING(HOME_MENU_SAVESTATE_AND_EXIT, "Save Emulation State And Exit"), + MAKE_STRING(HOME_MENU_RELOAD_SAVESTATE, "Reload Last Emulation State"), + MAKE_STRING(HOME_MENU_RECORDING, "Start/Stop Recording"), + MAKE_STRING(HOME_MENU_TROPHIES, "Trophies"), + MAKE_STRING(HOME_MENU_TROPHY_HIDDEN_TITLE, "Hidden trophy"), + MAKE_STRING(HOME_MENU_TROPHY_HIDDEN_DESCRIPTION, "This trophy is hidden"), + MAKE_STRING(HOME_MENU_TROPHY_PLATINUM_RELEVANT, "Platinum relevant"), + MAKE_STRING(HOME_MENU_TROPHY_GRADE_BRONZE, "Bronze"), + MAKE_STRING(HOME_MENU_TROPHY_GRADE_SILVER, "Silver"), + MAKE_STRING(HOME_MENU_TROPHY_GRADE_GOLD, "Gold"), + MAKE_STRING(HOME_MENU_TROPHY_GRADE_PLATINUM, "Platinum"), + MAKE_STRING(AUDIO_MUTED, "Audio muted"), + MAKE_STRING(AUDIO_UNMUTED, "Audio unmuted"), + MAKE_STRING(PROGRESS_DIALOG_PROGRESS, "Progress:"), + MAKE_STRING(PROGRESS_DIALOG_PROGRESS_ANALYZING, "Progress: analyzing..."), + MAKE_STRING(PROGRESS_DIALOG_REMAINING, "remaining"), + MAKE_STRING(PROGRESS_DIALOG_DONE, "done"), + MAKE_STRING(PROGRESS_DIALOG_FILE, "file"), + MAKE_STRING(PROGRESS_DIALOG_MODULE, "module"), + MAKE_STRING(PROGRESS_DIALOG_OF, "of"), + MAKE_STRING(PROGRESS_DIALOG_PLEASE_WAIT, "Please wait"), + MAKE_STRING(PROGRESS_DIALOG_STOPPING_PLEASE_WAIT, + "Stopping. Please wait..."), + MAKE_STRING(PROGRESS_DIALOG_SAVESTATE_PLEASE_WAIT, + "Creating savestate. Please wait..."), + MAKE_STRING(PROGRESS_DIALOG_SCANNING_PPU_EXECUTABLE, + "Scanning PPU Executable..."), + MAKE_STRING(PROGRESS_DIALOG_ANALYZING_PPU_EXECUTABLE, + "Analyzing PPU Executable..."), + MAKE_STRING(PROGRESS_DIALOG_SCANNING_PPU_MODULES, + "Scanning PPU Modules..."), + MAKE_STRING(PROGRESS_DIALOG_LOADING_PPU_MODULES, "Loading PPU Modules..."), + MAKE_STRING(PROGRESS_DIALOG_COMPILING_PPU_MODULES, + "Compiling PPU Modules..."), + MAKE_STRING(PROGRESS_DIALOG_LINKING_PPU_MODULES, "Linking PPU Modules..."), + MAKE_STRING(PROGRESS_DIALOG_APPLYING_PPU_CODE, "Applying PPU Code..."), + MAKE_STRING(PROGRESS_DIALOG_BUILDING_SPU_CACHE, "Building SPU Cache..."), + MAKE_STRING(EMULATION_PAUSED_RESUME_WITH_START, + "Press and hold the START button to resume"), + MAKE_STRING(EMULATION_RESUMING, "Resuming...!"), + MAKE_STRING(EMULATION_FROZEN, + "The PS3 application has likely crashed, you can close it."), + MAKE_STRING( + SAVESTATE_FAILED_DUE_TO_SAVEDATA, + "SaveState failed: Game saving is in progress, wait until finished."), + MAKE_STRING(SAVESTATE_FAILED_DUE_TO_VDEC, + "SaveState failed: VDEC-base video/cutscenes are in order, " + "wait for them to end or enable libvdec.sprx."), + MAKE_STRING(SAVESTATE_FAILED_DUE_TO_MISSING_SPU_SETTING, + "SaveState failed: Failed to lock SPU state, enabling " + "SPU-Compatible mode may fix it."), + MAKE_STRING(SAVESTATE_FAILED_DUE_TO_SPU, + "SaveState failed: Failed to lock SPU state, using SPU ASMJIT " + "will fix it."), + MAKE_STRING(INVALID, "Invalid"), +}; + +enum GameFlags { + kGameFlagLocked = 1 << 0, + kGameFlagTrial = 1 << 1, +}; + +struct GameInfo { + std::string path; + std::string name; + std::string iconPath; + int flags = 0; +}; + +class Progress { + JNIEnv *env; + jlong progressId; + jclass progressRepositoryClass; + jmethodID onProgressEventMethodId; + +public: + Progress(JNIEnv *env, jlong progressId) : env(env), progressId(progressId) { + progressRepositoryClass = + ensure(env->FindClass("net/rpcsx/ProgressRepository")); + onProgressEventMethodId = env->GetStaticMethodID( + progressRepositoryClass, "onProgressEvent", "(JJJLjava/lang/String;)Z"); + } + + bool report(jlong value, jlong max, const std::string &message = {}) { + return env->CallStaticBooleanMethod( + progressRepositoryClass, onProgressEventMethodId, progressId, value, + max, message.empty() ? nullptr : wrap(env, message)); + } + + void failure(const std::string &message = {}) { report(-1, 0, message); } + + void success(jlong value, const std::string &message = {}) { + value = std::max(value, 1); + report(value, value, message); + } + + jlong getProgressId() const { return progressId; } +}; + +static void sendFirmwareInstalled(JNIEnv *env, std::string version) { + auto fwRepositoryClass = + ensure(env->FindClass("net/rpcsx/FirmwareRepository")); + auto methodId = ensure(env->GetStaticMethodID( + fwRepositoryClass, "onFirmwareInstalled", "(Ljava/lang/String;)V")); + + env->CallStaticVoidMethod(fwRepositoryClass, methodId, wrap(env, version)); +} + +static void sendFirmwareCompiled(JNIEnv *env, std::string version) { + auto fwRepositoryClass = + ensure(env->FindClass("net/rpcsx/FirmwareRepository")); + auto methodId = ensure(env->GetStaticMethodID( + fwRepositoryClass, "onFirmwareCompiled", "(Ljava/lang/String;)V")); + + env->CallStaticVoidMethod(fwRepositoryClass, methodId, wrap(env, version)); +} + +static void sendGameInfo(JNIEnv *env, jlong progressId, + std::span infos) { + auto gameRepositoryClass = ensure(env->FindClass("net/rpcsx/GameRepository")); + auto addMethodId = ensure(env->GetStaticMethodID( + gameRepositoryClass, "add", "([Lnet/rpcsx/GameInfo;J)V")); + auto gameClass = ensure(env->FindClass("net/rpcsx/GameInfo")); + + jmethodID gameConstructor = ensure(env->GetMethodID( + gameClass, "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V")); + + std::vector objects; + objects.reserve(infos.size()); + + for (const auto &info : infos) { + auto path = Emu.GetCallbacks().resolve_path(info.path); + if (path.ends_with('/')) { + path.resize(path.size() - 1); + } + + objects.push_back(env->NewObject( + gameClass, gameConstructor, wrap(env, path), wrap(env, info.name), + wrap(env, Emu.GetCallbacks().resolve_path(info.iconPath)), + jint(info.flags))); + } + + auto result = env->NewObjectArray(objects.size(), gameClass, nullptr); + + for (std::size_t i = 0; i < objects.size(); ++i) { + env->SetObjectArrayElement(result, i, objects[i]); + } + + env->CallStaticVoidMethod(gameRepositoryClass, addMethodId, result, + progressId); +} + +static void sendVshBootable(JNIEnv *env, jlong progressId) { + auto dev_flash = g_cfg_vfs.get_dev_flash(); + + sendGameInfo( + env, progressId, + {{GameInfo{ + .path = dev_flash + "/vsh/module/vsh.self", + .name = "VSH", + .iconPath = dev_flash + "vsh/resource/explore/icon/icon_home.png", + }}}); +} + +static bool tryUnlockGame(const psf::registry &psf) { + auto contentId = psf::get_string(psf, "CONTENT_ID"); + + if (contentId.empty()) { + return true; + } + + const auto licenseDir = fmt::format( + "%shome/%s/exdata/", rpcs3::utils::get_hdd0_dir(), Emu.GetUsr()); + + const auto licenseFile = fmt::format("%s%s", licenseDir, contentId); + if (std::filesystem::is_regular_file(licenseFile + ".rap")) { + return true; + } + + if (std::filesystem::is_regular_file(licenseFile + ".edat")) { + return true; + } + + return false; +} + +static void collectGamePaths(std::vector &paths, + const std::string &rootDir) { + std::error_code ec; + std::vector workList; + workList.reserve(32); + if (!std::filesystem::is_directory(rootDir)) { + auto rootPath = std::filesystem::path(rootDir).parent_path(); + if (rootPath.filename() == "USRDIR") { + rootPath = rootPath.parent_path(); + } + if (rootPath.filename() == "PS3_GAME") { + rootPath = rootPath.parent_path(); + } + + workList.push_back(rootPath); + } else { + workList.push_back(rootDir); + } + + while (!workList.empty()) { + auto dir = std::move(workList.back()); + workList.pop_back(); + + for (auto entry : std::filesystem::directory_iterator(dir, ec)) { + if (entry.is_directory()) { + if (entry.path().filename() != "C00") { + workList.push_back(entry.path()); + } + + continue; + } + + if (entry.is_regular_file() && entry.path().filename() == "PARAM.SFO") { + paths.push_back(entry.path().parent_path().string()); + continue; + } + } + } +} + +static std::string locateEbootPath(const std::string &root) { + if (std::filesystem::is_regular_file(root)) { + return root; + } + + for (auto suffix : { + "/EBOOT.BIN", + "/USRDIR/EBOOT.BIN", + "/USRDIR/ISO.BIN.EDAT", + "/PS3_GAME/USRDIR/EBOOT.BIN", + }) { + std::string tryPath = root + suffix; + + if (std::filesystem::is_regular_file(tryPath)) { + return tryPath; + } + } + + return {}; +} + +static std::string locateParamSfoPath(const std::string &root) { + if (std::filesystem::is_regular_file(root)) { + return root; + } + + for (auto suffix : { + "/PARAM.SFO", + "/PS3_GAME/PARAM.SFO", + }) { + std::string tryPath = root + suffix; + + if (std::filesystem::is_regular_file(tryPath)) { + return tryPath; + } + } + + return {}; +} + +static std::optional +fetchGameInfo(const psf::registry &psf, + std::filesystem::path psfRootPath = {}) { + auto titleId = std::string(psf::get_string(psf, "TITLE_ID")); + auto name = std::string(psf::get_string(psf, "TITLE")); + auto bootable = psf::get_integer(psf, "BOOTABLE", 0); + auto category = psf::get_string(psf, "CATEGORY"); + + if (!bootable || titleId.empty()) { + return {}; + } + + bool isDiscGame = category == "DG"; + + std::string path; + + if (!isDiscGame) { + path = rpcs3::utils::get_hdd0_dir() + "game/" + titleId + "/"; + } else { + if (psfRootPath.empty()) { + path = fs::get_config_dir() + "games/" + titleId + "/"; + } else { + // Locate game root path + if (psfRootPath.filename() == "USRDIR") { + psfRootPath = psfRootPath.parent_path(); + } + + if (psfRootPath.filename() == "PS3_GAME") { + psfRootPath = psfRootPath.parent_path(); + } + + path = psfRootPath; + if (!path.ends_with('/')) { + path += '/'; + } + } + } + + auto dataPath = isDiscGame ? path + "PS3_GAME/" : path; + auto iconPath = dataPath + "ICON0.PNG"; + auto moviePath = dataPath + "ICON1.PAM"; + + int flags = 0; + + if (!isDiscGame) { + auto ebootPath = locateEbootPath(path); + + bool isLocked = false; + + if (!ebootPath.empty()) { + if (fs::file eboot{ebootPath}; + eboot && eboot.size() >= 4 && eboot.read() == "SCE\0"_u32) { + isLocked = !decrypt_self(eboot); + } + } + + if (isLocked) { + flags |= kGameFlagLocked; + rpcsx_android.warning("game %s is locked", path); + } + + auto c00Path = path + "/C00"; + + bool isTrial = std::filesystem::is_directory(c00Path); + + if (isTrial) { + if (!tryUnlockGame(psf)) { + flags |= kGameFlagTrial; + rpcsx_android.warning("game %s is trial", path); + } else { + auto c00IconPath = c00Path + "/ICON0.PNG"; + if (std::filesystem::is_regular_file(c00IconPath)) { + iconPath = c00IconPath; + } + + auto c00SfoPath = c00Path + "/PARAM.SFO"; + + if (std::filesystem::is_regular_file(c00IconPath)) { + auto c00Sfo = psf::load_object(c00SfoPath); + titleId = psf::get_string(c00Sfo, "TITLE_ID", titleId); + name = psf::get_string(c00Sfo, "TITLE", name); + } + } + } + } + + return GameInfo{ + .path = std::move(path), + .name = std::move(name), + .iconPath = std::move(iconPath), + .flags = flags, + }; +} + +static void collectGameInfo(JNIEnv *env, jlong progressId, + std::vector rootDirs) { + std::vector paths; + for (auto &&rootDir : rootDirs) { + collectGamePaths(paths, rootDir); + + rpcsx_android.notice("collectGameInfo: processed %s", rootDir); + } + + rpcsx_android.notice("collectGameInfo: found %d paths", paths.size()); + + Progress progress(env, progressId); + progress.report(0, paths.size()); + + std::vector gameInfos; + gameInfos.reserve(10); + std::size_t processed = 0; + + auto submit = [&] { + if (gameInfos.empty()) { + return; + } + + sendGameInfo(env, progressId, gameInfos); + progress.report(processed, paths.size()); + gameInfos.clear(); + }; + + for (auto &&path : paths) { + processed++; + + if (!std::filesystem::is_regular_file(path + "/PARAM.SFO")) { + continue; + } + + const auto psf = psf::load_object(path + "/PARAM.SFO"); + + rpcsx_android.notice("collectGameInfo: sfo at %s", path); + + if (auto gameInfo = fetchGameInfo(psf, path)) { + gameInfos.push_back(std::move(*gameInfo)); + + if (gameInfos.size() >= 10) { + submit(); + } + } + } + + submit(); + + progress.success(processed); +} + +class MainThreadProcessor { + std::mutex mutex; + std::condition_variable cv; + std::deque, atomic_t *>> queue; + +public: + void push(std::function cb, atomic_t *wakeUp = nullptr) { + std::lock_guard lock(mutex); + queue.push_back({std::move(cb), wakeUp}); + cv.notify_one(); + } + + void push(std::function cb, atomic_t *wakeUp = nullptr) { + push([cb = std::move(cb)](JNIEnv *) { cb(); }, wakeUp); + } + + void process(JNIEnv *env) { + while (true) { + std::function cb; + atomic_t *wakeUp = nullptr; + + { + std::unique_lock lock(mutex); + if (queue.empty()) { + cv.wait(lock); + continue; + } + + auto item = std::move(queue.front()); + queue.pop_front(); + + cb = std::move(item.first); + wakeUp = item.second; + } + + cb(env); + if (wakeUp) { + *wakeUp = true; + wakeUp->notify_all(); + } + } + } +} static g_mainThreadProcessor; + +static void invokeAsync(std::function cb) { + g_mainThreadProcessor.push(std::move(cb)); +} + +static void invokeSync(std::function cb) { + atomic_t wakeup{false}; + g_mainThreadProcessor.push(std::move(cb), &wakeup); + + while (wakeup.load() == false) { + wakeup.wait(false); + } +} + +struct ProgressMessageDialog : MsgDialogBase { + jlong progressId; + jlong value = 0; + jlong max = 0; + + ProgressMessageDialog(jlong progressId) : progressId(progressId) {} + + void Create(const std::string &msg, const std::string &title) override { + rpcsx_android.warning("ProgressMessageDialog::Create(%s, %s)", msg, title); + max = 100; + invokeSync([this, &msg](JNIEnv *env) { + Progress progress(env, progressId); + progress.report(0, 0, msg); + }); + } + + jlong getValue() const { + return value == max && max != 0 ? value - 1 : value; + } + + void Close(bool success) override { + rpcsx_android.warning("ProgressMessageDialog::Close(%s)", success); + invokeSync([this, success](JNIEnv *env) { + Progress progress(env, progressId); + progress.report(0, 0); + }); + + // Progress progress(env, progressId); + // if (success) { + // progress.success(0); + // } else { + // progress.failure(); + // } + // }); + } + + void SetMsg(const std::string &msg) override { + rpcsx_android.warning("ProgressMessageDialog::SetMsg(%s)", msg); + invokeSync([this, msg](JNIEnv *env) { + Progress(env, progressId).report(getValue(), max, msg); + }); + } + + void ProgressBarSetMsg(u32 progressBarIndex, + const std::string &msg) override { + rpcsx_android.warning("ProgressMessageDialog::ProgressBarSetMsg(%d, %s)", + progressBarIndex, msg); + if (progressBarIndex != 0) { + report_fatal_error("Unexpected progress index in progress dialog"); + } + + invokeSync([this, msg](JNIEnv *env) { + Progress(env, progressId).report(getValue(), max, msg); + }); + } + + void ProgressBarReset(u32 progressBarIndex) override { + rpcsx_android.warning("ProgressMessageDialog::ProgressBarReset(%d)", + progressBarIndex); + + if (progressBarIndex != 0) { + report_fatal_error("Unexpected progress index in progress dialog"); + } + + value = 0; + invokeSync( + [this](JNIEnv *env) { Progress(env, progressId).report(value, max); }); + } + + void ProgressBarInc(u32 progressBarIndex, u32 delta) override { + rpcsx_android.warning("ProgressMessageDialog::ProgressBarInc(%d, %d)", + progressBarIndex, delta); + + if (progressBarIndex != 0) { + report_fatal_error("Unexpected progress index in progress dialog"); + } + + value += delta; + + invokeSync([this](JNIEnv *env) { + Progress(env, progressId).report(getValue(), max); + }); + } + + void ProgressBarSetValue(u32 progressBarIndex, u32 value) override { + rpcsx_android.warning("ProgressMessageDialog::ProgressBarSetValue(%d, %d)", + progressBarIndex, value); + + if (progressBarIndex != 0) { + report_fatal_error("Unexpected progress index in progress dialog"); + } + + this->value = value; + + invokeSync([this](JNIEnv *env) { + Progress(env, progressId).report(getValue(), max); + }); + } + void ProgressBarSetLimit(u32 index, u32 limit) override { + rpcsx_android.warning("ProgressMessageDialog::ProgressBarSetLimit(%d, %d)", + index, limit); + + if (index != 0) { + report_fatal_error("Unexpected progress index in progress dialog"); + } + + max = limit; + + invokeSync([this](JNIEnv *env) { + Progress(env, progressId).report(getValue(), max); + }); + } +}; + +struct UiMessageDialog : MsgDialogBase { + // FIXME: implement + + void Create(const std::string &msg, const std::string &title) override {} + void Close(bool success) override {} + void SetMsg(const std::string &msg) override {} + void ProgressBarSetMsg(u32 progressBarIndex, + const std::string &msg) override {} + void ProgressBarReset(u32 progressBarIndex) override {} + void ProgressBarInc(u32 progressBarIndex, u32 delta) override {} + void ProgressBarSetValue(u32 progressBarIndex, u32 value) override {} + void ProgressBarSetLimit(u32 index, u32 limit) override {} +}; + +struct MessageDialog : MsgDialogBase { + std::unique_ptr impl = nullptr; + + void Create(const std::string &msg, const std::string &title) override { + auto progressId = s_pendingProgressId.load(); + + rpcsx_android.warning("MessageDialog::Create(%s, %s): source %s, id %d", + msg, title, source, progressId); + + if (progressId != -1) { + impl = std::make_unique(progressId); + } else { + impl = std::make_unique(); + } + + impl->type = type; + impl->source = source; + impl->Create(msg, title); + } + + void Close(bool success) override { impl->Close(success); } + + void SetMsg(const std::string &msg) override { impl->SetMsg(msg); } + + void ProgressBarSetMsg(u32 progressBarIndex, + const std::string &msg) override { + impl->ProgressBarSetMsg(progressBarIndex, msg); + } + + void ProgressBarReset(u32 progressBarIndex) override { + impl->ProgressBarReset(progressBarIndex); + } + + void ProgressBarInc(u32 progressBarIndex, u32 delta) override { + impl->ProgressBarInc(progressBarIndex, delta); + } + + void ProgressBarSetValue(u32 progressBarIndex, u32 value) override { + impl->ProgressBarSetValue(progressBarIndex, value); + } + + void ProgressBarSetLimit(u32 index, u32 limit) override { + impl->ProgressBarSetLimit(index, limit); + } + + static void pushPendingProgressId(jlong id) { + jlong value = -1; + + while (!s_pendingProgressId.compare_exchange_weak(value, id)) { + s_pendingProgressId.wait(value); + value = -1; + } + } + + static bool popPendingProgressId(jlong id) { + return s_pendingProgressId.compare_exchange_strong(id, -1); + } + +private: + static std::atomic s_pendingProgressId; +}; + +struct OverlaySaveDialog : SaveDialogBase { + s32 ShowSaveDataList(const std::string &base_dir, + std::vector &save_entries, s32 focused, + u32 op, vm::ptr listSet, + bool enable_overlay) override { + rpcsx_android.notice("ShowSaveDataList(save_entries=%d, focused=%d, " + "op=0x%x, listSet=*0x%x, enable_overlay=%d)", + save_entries.size(), focused, op, listSet, + enable_overlay); + + bool use_end = sysutil_send_system_cmd(CELL_SYSUTIL_DRAWING_BEGIN, 0) >= 0; + + auto atExit = AtExit([&] { + if (use_end) { + sysutil_send_system_cmd(CELL_SYSUTIL_DRAWING_END, 0); + } + }); + + if (!use_end) { + rpcsx_android.error( + "ShowSaveDataList(): Not able to notify DRAWING_BEGIN callback " + "because one has already been sent!"); + } + + if (auto manager = g_fxo->try_get()) { + rpcsx_android.notice("ShowSaveDataList: Showing native UI dialog"); + + s32 result = manager->create()->show( + base_dir, save_entries, focused, op, listSet, enable_overlay); + + if (result != rsx::overlays::user_interface::selection_code::error) { + rpcsx_android.notice( + "ShowSaveDataList: Native UI dialog returned with selection %d", + result); + + return result; + } + + rpcsx_android.error("ShowSaveDataList: Native UI dialog returned error"); + } + + return -2; + } +}; + +class OverlayTrophyNotification : public TrophyNotificationBase { +public: + s32 ShowTrophyNotification( + const SceNpTrophyDetails &trophy, + const std::vector &trophy_icon_buffer) override { + if (auto manager = g_fxo->try_get()) { + auto popup = std::make_shared(); + return manager->add(popup, false)->show(trophy, trophy_icon_buffer); + } + + return 0; + } +}; + +std::atomic MessageDialog::s_pendingProgressId = -1; + +struct CompilationWorkload { + jlong progressId; + std::string path; +}; + +extern bool ppu_load_exec(const ppu_exec_object &, bool virtual_load, + const std::string &, utils::serial * = nullptr); +extern void spu_load_exec(const spu_exec_object &); +extern void spu_load_rel_exec(const spu_rel_object &); +extern void ppu_precompile(std::vector &dir_queue, + std::vector *> *loaded_prx); +extern bool ppu_initialize(const ppu_module &, bool check_only = false, + u64 file_size = 0); +extern void ppu_finalize(const ppu_module &); +extern bool ppu_load_rel_exec(const ppu_rel_object &); + +class CompilationQueue { + std::atomic nextWorkTag{0}; + std::uint64_t lastProcessedTag = 0; + std::mutex queueMutex; + std::deque queue; + +public: + void push(CompilationWorkload workload) { + { + std::lock_guard lock(queueMutex); + queue.push_back(std::move(workload)); + } + + nextWorkTag.fetch_add(1); + } + + void push(Progress &progress, std::string path) { + progress.report(0, 0); + + push({ + .progressId = progress.getProgressId(), + .path = std::move(path), + }); + } + + void process(JNIEnv *env) { + while (true) { + auto nextWorkTagValue = nextWorkTag.load(); + + if (nextWorkTagValue == lastProcessedTag) { + nextWorkTag.wait(lastProcessedTag); + } + + if (nextWorkTagValue == lastProcessedTag || queue.empty()) { + continue; + } + + CompilationWorkload workload; + + { + std::lock_guard lock(queueMutex); + + if (queue.empty()) { + continue; + } + + workload = std::move(queue.front()); + queue.pop_front(); + } + + impl(env, std::move(workload)); + lastProcessedTag++; + } + } + +private: + void impl(JNIEnv *env, CompilationWorkload workload) { + if (workload.path.empty()) { + Progress(env, workload.progressId).success(0); + return; + } + + rpcsx_android.error("Creating cache initiated, state %d", + (int)Emu.GetStatus(false)); + + while (true) { + auto state = Emu.GetStatus(false); + + if (state == system_state::stopped || state == system_state::ready) { + break; + } + + rpcsx_android.error("Creating cache wait, state %d", (int)state); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + bool is_vsh = workload.path.ends_with("/vsh.self"); + + Emu.SetState(system_state::running); + + MessageDialog::pushPendingProgressId(workload.progressId); + + g_fxo->init>(); + g_fxo->init>(); + g_fxo->init(false, nullptr); + auto rootPath = std::filesystem::path(workload.path); + + if (is_vsh) { + rootPath = g_cfg_vfs.get_dev_flash() + "sys/external/"; + } else { + if (!std::filesystem::is_directory(rootPath)) { + rootPath = rootPath.parent_path(); + if (rootPath.filename() == "USRDIR") { + rootPath = rootPath.parent_path(); + } + } + } + + auto &_main = *ensure(g_fxo->try_get>()); + + if (fs::is_file(workload.path)) { + if (!is_vsh) { + auto sfoPath = locateParamSfoPath(rootPath); + + if (!sfoPath.empty()) { + const auto psf = psf::load_object(sfoPath); + rpcsx_android.warning("title id is %s", + psf::get_string(psf, "TITLE_ID")); + + Emu.SetTitleID(std::string(psf::get_string(psf, "TITLE_ID"))); + } else { + rpcsx_android.warning("param.sfo not found"); + } + } + + // Compile binary first + rpcsx_android.notice("Trying to load binary: %s", workload.path); + + fs::file src{workload.path}; + src = decrypt_self(src); + + const ppu_exec_object obj = src; + + if (obj == elf_error::ok && ppu_load_exec(obj, true, workload.path)) { + _main.path = workload.path; + } else { + rpcsx_android.error("Failed to load binary '%s' (%s)", workload.path, + obj.get_error()); + } + } + + std::vector dir_queue; + dir_queue.push_back(rootPath.string()); + + for (auto entry : std::filesystem::recursive_directory_iterator(rootPath)) { + if (entry.is_directory()) { + dir_queue.push_back(entry.path().string()); + } + } + + std::vector *> mod_list; + rpcsx_android.error("Going to analyze executable"); + + // FIXME: split states + if (!is_vsh) { + if (_main.analyse(0, _main.elf_entry, _main.seg0_code_end, + _main.applied_patches, std::vector{})) { + Emu.ConfigurePPUCache(); + Emu.SetTestMode(); + rpcsx_android.error("Going to precompile main PPU module"); + ppu_initialize(_main); + mod_list.emplace_back(&_main); + } + } + + ppu_precompile(dir_queue, mod_list.empty() ? nullptr : &mod_list); + + rpcsx_android.error("Finalization"); + g_fxo->reset(); + Emu.SetState(system_state::stopped); + + MessageDialog::popPendingProgressId(workload.progressId); + + Progress(env, workload.progressId).success(0); + } +} static g_compilationQueue; + +static void setupCallbacks() { + Emu.SetCallbacks({ + .call_from_main_thread = + [](std::function cb, atomic_t *wake_up) { + cb(); + if (wake_up) { + *wake_up = true; + } + }, + .on_run = [](auto...) {}, + .on_pause = [](auto...) {}, + .on_resume = [](auto...) {}, + .on_stop = [](auto...) {}, + .on_ready = [](auto...) {}, + .on_missing_fw = [](auto...) {}, + .on_emulation_stop_no_response = [](auto...) {}, + .on_save_state_progress = [](auto...) {}, + .enable_disc_eject = [](auto...) {}, + .enable_disc_insert = [](auto...) {}, + .handle_taskbar_progress = [](auto...) {}, + .init_kb_handler = + [](auto...) { + ensure(g_fxo->init( + Emu.DeserialManager())); + }, + .init_mouse_handler = + [](auto...) { + ensure(g_fxo->init( + Emu.DeserialManager())); + }, + .init_pad_handler = + [](auto...) { + ensure(g_fxo->init>(nullptr, nullptr, "")); + }, + .update_emu_settings = [](auto...) {}, + .save_emu_settings = + [](auto...) { + Emulator::SaveSettings(g_cfg.to_string(), Emu.GetTitleID()); + }, + .close_gs_frame = [](auto...) {}, + .get_gs_frame = [] { return std::make_unique(); }, + .get_camera_handler = + [](auto...) { return std::make_shared(); }, + .get_music_handler = + [](auto...) { return std::make_shared(); }, + .create_pad_handler = [](pad_handler type, void *thread, void *window) + -> std::shared_ptr { + switch (type) { + case pad_handler::keyboard: + case pad_handler::null: + break; + case pad_handler::ds3: + return std::make_shared(); + case pad_handler::ds4: + return std::make_shared(); + case pad_handler::dualsense: + return std::make_shared(); + case pad_handler::skateboard: + // return std::make_shared(); + break; + case pad_handler::move: + // return std::make_shared(); + break; +#ifdef _WIN32 + case pad_handler::xinput: + return std::make_shared(); + case pad_handler::mm: + return std::make_shared(); +#endif +#ifdef HAVE_SDL3 + case pad_handler::sdl: + return std::make_shared(); +#endif +#ifdef HAVE_LIBEVDEV + case pad_handler::evdev: + return std::make_shared(); +#endif + case pad_handler::virtual_pad: + return std::make_shared(); + } + + return std::make_shared(); + }, + .init_gs_render = + [](utils::serial *ar) { + switch (g_cfg.video.renderer.get()) { + case video_renderer::null: + g_fxo->init>(ar); + break; + case video_renderer::vulkan: + g_fxo->init>(ar); + break; + + default: + break; + } + }, + .get_audio = + [](auto...) { + std::shared_ptr result; + + switch (g_cfg.audio.renderer.get()) { + case audio_renderer::null: + result = std::make_shared(); + break; + + case audio_renderer::cubeb: + default: + result = std::make_shared(); + break; + } + + if (!result->Initialized()) { + rpcsx_android.error( + "Audio renderer %s could not be initialized, using a Null " + "renderer instead. Make sure that no other application is " + "running that might block audio access (e.g. Netflix).", + result->GetName()); + result = std::make_shared(); + } + return result; + }, + .get_audio_enumerator = [](auto...) { return nullptr; }, + .get_msg_dialog = [] { return std::make_shared(); }, + .get_osk_dialog = [](auto...) { return nullptr; }, + .get_save_dialog = + [](auto...) { return std::make_unique(); }, + .get_sendmessage_dialog = [](auto...) { return nullptr; }, + .get_recvmessage_dialog = [](auto...) { return nullptr; }, + .get_trophy_notification_dialog = + [](auto...) { return std::make_unique(); }, + .get_localized_string = [](localized_string_id id, + const char *) -> std::string { + if (int(id) < std::size(g_strings)) { + return g_strings[int(id)].first; + } + return ""; + }, + .get_localized_u32string = [](localized_string_id id, + const char *) -> std::u32string { + if (int(id) < std::size(g_strings)) { + return g_strings[int(id)].second; + } + return U""; + }, + .get_localized_setting = [](auto...) { return ""; }, + .play_sound = [](auto...) {}, + .get_image_info = [](auto...) { return false; }, + .get_scaled_image = [](auto...) { return false; }, + .resolve_path = + [](std::string_view arg) { + std::error_code ec; + auto result = + std::filesystem::weakly_canonical( + std::filesystem::path(fmt::replace_all(arg, "\\", "/")), ec) + .string(); + return ec ? std::string(arg) : result; + }, + .get_font_dirs = [](auto...) { return std::vector(); }, + .on_install_pkgs = + [](const std::vector &pkgs) { + for (const std::string &pkg : pkgs) { + if (!rpcs3::utils::install_pkg(pkg)) { + rpcsx_android.error("cd install pkgs: failed to install %s", + pkg); + return false; + } + } + return true; + }, + .add_breakpoint = [](auto...) {}, + .display_sleep_control_supported = [](auto...) { return false; }, + .enable_display_sleep = [](auto...) {}, + .check_microphone_permissions = [](auto...) {}, + }); +} + +static bool initVirtualPad(const std::shared_ptr &pad) { + u32 pclass_profile = 0; + pad->Init(CELL_PAD_STATUS_CONNECTED, + CELL_PAD_CAPABILITY_PS3_CONFORMITY | + CELL_PAD_CAPABILITY_PRESS_MODE | + CELL_PAD_CAPABILITY_HP_ANALOG_STICK | + CELL_PAD_CAPABILITY_ACTUATOR //| CELL_PAD_CAPABILITY_SENSOR_MODE + , + CELL_PAD_DEV_TYPE_STANDARD, CELL_PAD_PCLASS_TYPE_STANDARD, + pclass_profile, 0, 0, 50); + + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_UP); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_DOWN); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_LEFT); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_RIGHT); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_CROSS); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_SQUARE); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_CIRCLE); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_TRIANGLE); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_L1); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_L2); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_L3); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_R1); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL2, std::set{}, + CELL_PAD_CTRL_R2); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_R3); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_START); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_SELECT); + pad->m_buttons.emplace_back(CELL_PAD_BTN_OFFSET_DIGITAL1, std::set{}, + CELL_PAD_CTRL_PS); + + pad->m_sticks[0] = AnalogStick(CELL_PAD_BTN_OFFSET_ANALOG_LEFT_X, {}, {}); + pad->m_sticks[1] = AnalogStick(CELL_PAD_BTN_OFFSET_ANALOG_LEFT_Y, {}, {}); + pad->m_sticks[2] = AnalogStick(CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_X, {}, {}); + pad->m_sticks[3] = AnalogStick(CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_Y, {}, {}); + + pad->m_sensors[0] = + AnalogSensor(CELL_PAD_BTN_OFFSET_SENSOR_X, 0, 0, 0, DEFAULT_MOTION_X); + pad->m_sensors[1] = + AnalogSensor(CELL_PAD_BTN_OFFSET_SENSOR_Y, 0, 0, 0, DEFAULT_MOTION_Y); + pad->m_sensors[2] = + AnalogSensor(CELL_PAD_BTN_OFFSET_SENSOR_Z, 0, 0, 0, DEFAULT_MOTION_Z); + pad->m_sensors[3] = + AnalogSensor(CELL_PAD_BTN_OFFSET_SENSOR_G, 0, 0, 0, DEFAULT_MOTION_G); + + pad->m_vibrateMotors[0] = VibrateMotor(true, 0); + pad->m_vibrateMotors[1] = VibrateMotor(false, 0); + + if (pad->m_player_id == 0) { + std::lock_guard lock(g_virtual_pad_mutex); + g_virtual_pad = pad; + } + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadData( + JNIEnv *env, jobject, jint digital1, jint digital2, jint leftStickX, + jint leftStickY, jint rightStickX, jint rightStickY) { + + auto pad = [] { + std::shared_ptr result; + std::lock_guard lock(g_virtual_pad_mutex); + result = g_virtual_pad; + return result; + }(); + + if (pad == nullptr) { + return false; + } + + for (auto &btn : pad->m_buttons) { + if (btn.m_offset == CELL_PAD_BTN_OFFSET_DIGITAL1) { + btn.m_pressed = (digital1 & btn.m_outKeyCode) != 0; + + if (btn.m_outKeyCode == CELL_PAD_CTRL_PS && btn.m_pressed) { + if (auto padThread = pad::get_pad_thread(true)) { + padThread->open_home_menu(); + } + } + + } else if (btn.m_offset == CELL_PAD_BTN_OFFSET_DIGITAL2) { + btn.m_pressed = (digital2 & btn.m_outKeyCode) != 0; + } + + btn.m_value = btn.m_pressed ? 255 : 0; + } + + pad->m_sticks[0].m_value = leftStickX; + pad->m_sticks[1].m_value = leftStickY; + pad->m_sticks[2].m_value = rightStickX; + pad->m_sticks[3].m_value = rightStickY; + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_net_rpcsx_RPCSX_initialize(JNIEnv *env, jobject, jstring rootDir) { + auto rootDirStr = fix_dir_path(unwrap(env, rootDir)); + + if (g_android_executable_dir != rootDirStr) { + g_android_executable_dir = rootDirStr; + g_android_config_dir = rootDirStr + "config/"; + g_android_cache_dir = rootDirStr + "cache/"; + + std::filesystem::create_directories(g_android_config_dir); + std::error_code ec; + // std::filesystem::remove_all(g_android_cache_dir, ec); + std::filesystem::create_directories(g_android_cache_dir); + } + + if (g_initialized) { + return true; + } + + g_initialized = true; + + if (int r = libusb_set_option(nullptr, LIBUSB_OPTION_NO_DEVICE_DISCOVERY, + nullptr); + r != 0) { + rpcsx_android.warning( + "libusb_set_option(LIBUSB_OPTION_NO_DEVICE_DISCOVERY) -> %d", r); + } + + // Initialize thread pool finalizer // ??? + static_cast(named_thread("", [](int) {})); + + static std::unique_ptr log_file; + { + // Check free space + fs::device_stat stats{}; + if (!fs::statfs(fs::get_cache_dir(), stats) || + stats.avail_free < 128 * 1024 * 1024) { + std::fprintf(stderr, "Not enough free space for logs (%f KB)", + stats.avail_free / 1000000.); + } + + // preserve old log file + if (std::filesystem::exists(fs::get_log_dir() + "RPCSX.log")) { + std::error_code ec; + std::filesystem::remove(fs::get_log_dir() + "RPCSX.old.log", ec); + std::filesystem::rename(fs::get_log_dir() + "RPCSX.log", + fs::get_log_dir() + "RPCSX.old.log", ec); + } + + // Limit log size to ~25% of free space + log_file = logs::make_file_listener(fs::get_log_dir() + "RPCSX.log", + stats.avail_free / 4); + } + + logs::stored_message ver{rpcsx_android.always()}; + ver.text = fmt::format("RPCSX-ps3-android v%s", rx::getVersion().toString()); + + // Write System information + logs::stored_message sys{rpcsx_android.always()}; + sys.text = utils::get_system_info(); + + // Write OS version + logs::stored_message os{rpcsx_android.always()}; + os.text = utils::get_OS_version_string(); + + // Write current time + logs::stored_message time{rpcsx_android.always()}; + time.text = fmt::format("Current Time: %s", std::chrono::system_clock::now()); + + logs::set_init( + {std::move(ver), std::move(sys), std::move(os), std::move(time)}); + + auto set_rlim = [](int resource, std::uint64_t limit) { + rlimit64 rlim{}; + if (getrlimit64(resource, &rlim) != 0) { + rpcsx_android.error("failed to get rlimit for %d", resource); + return; + } + + rlim.rlim_cur = std::min(rlim.rlim_max, limit); + rpcsx_android.error("rlimit[%d] = %u (requested %u, max %u)", resource, + rlim.rlim_cur, limit, rlim.rlim_max); + + if (setrlimit64(resource, &rlim) != 0) { + rpcsx_android.error("failed to set rlimit for %d", resource); + return; + } + }; + + set_rlim(RLIMIT_MEMLOCK, RLIM_INFINITY); + set_rlim(RLIMIT_NOFILE, RLIM_INFINITY); + set_rlim(RLIMIT_STACK, 128 * 1024 * 1024); + set_rlim(RLIMIT_AS, RLIM_INFINITY); + + virtual_pad_handler::set_on_connect_cb(initVirtualPad); + setupCallbacks(); + Emu.SetHasGui(false); + Emu.Init(); + + g_cfg_input.player1.handler.set(pad_handler::virtual_pad); + g_cfg_input.player1.device.from_string("Virtual"); + g_cfg_input.save("", g_cfg_input_configs.default_config); + + g_cfg.core.llvm_cpu.from_string("cortex-a34"); + + Emulator::SaveSettings(g_cfg.to_string(), Emu.GetTitleID()); + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_net_rpcsx_RPCSX_processCompilationQueue(JNIEnv *env, jobject) { + g_compilationQueue.process(env); + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_net_rpcsx_RPCSX_startMainThreadProcessor(JNIEnv *env, jobject) { + g_mainThreadProcessor.process(env); + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_collectGameInfo( + JNIEnv *env, jobject, jstring jrootDir, jlong progressId) { + + if (std::filesystem::is_regular_file(g_cfg_vfs.get_dev_flash() + + "/vsh/module/vsh.self")) { + sendVshBootable(env, progressId); + } + + collectGameInfo(env, progressId, {unwrap(env, jrootDir)}); + return true; +} + +extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_shutdown(JNIEnv *env, + jobject) { + Emu.Kill(); +} + +extern "C" JNIEXPORT jint JNICALL Java_net_rpcsx_RPCSX_boot(JNIEnv *env, + jobject, + jstring jpath) { + Emu.SetForceBoot(true); + auto path = unwrap(env, jpath); + while (path.ends_with('/')) { + path.pop_back(); + } + + return static_cast(Emu.BootGame(path, "", false, cfg_mode::global)); +} + +extern "C" JNIEXPORT jint JNICALL Java_net_rpcsx_RPCSX_getState(JNIEnv *env, + jobject) { + return static_cast(Emu.GetStatus(false)); +} + +extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_kill(JNIEnv *env, + jobject) { + Emu.Kill(); +} + +extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_resume(JNIEnv *env, + jobject) { + Emu.Resume(); +} + +extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_openHomeMenu(JNIEnv *env, + jobject) { + if (auto padThread = pad::get_pad_thread(true)) { + padThread->open_home_menu(); + } +} + +extern "C" JNIEXPORT jstring JNICALL +Java_net_rpcsx_RPCSX_getTitleId(JNIEnv *env, jobject) { + return wrap(env, Emu.GetTitleID()); +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_surfaceEvent( + JNIEnv *env, jobject, jobject surface, jint event) { + rpcsx_android.warning("surface event %p, %d", surface, event); + + if (event == 2) { + auto prevWindow = g_native_window.exchange(nullptr); + if (prevWindow != nullptr) { + ANativeWindow_release(prevWindow); + } + + if (auto padThread = pad::get_pad_thread()) { + padThread->open_home_menu(); + } + + Emu.Pause(); + } else { + auto newWindow = ANativeWindow_fromSurface(env, surface); + + if (newWindow == nullptr) { + rpcsx_android.fatal("returned native window is null, surface %p", + surface); + return false; + } + + auto prevWindow = g_native_window.exchange(newWindow); + + if (newWindow != prevWindow) { + ANativeWindow_acquire(newWindow); + + if (prevWindow != nullptr) { + ANativeWindow_release(prevWindow); + } + } + + if (event == 0 && Emu.IsPaused()) { + Emu.Resume(); + } + } + + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_usbDeviceEvent( + JNIEnv *env, jobject, jint fd, jint vendorId, jint productId, jint event) { + rpcsx_android.warning( + "usb device event %d fd: %d, vendorId: %d, productId: %d", event, fd, + vendorId, productId); + + { + std::lock_guard lock(g_android_usb_devices_mutex); + + if (event == 0) { + g_android_usb_devices.push_back({ + .fd = int(fd), + .vendorId = u16(vendorId), + .productId = u16(productId), + }); + } else { + auto filter = [fd](auto device) { return device.fd == fd; }; + if (auto it = std::ranges::find_if(g_android_usb_devices, filter); + it != g_android_usb_devices.end()) { + g_android_usb_devices.erase(it); + } + } + } + + { + auto selectedHandler = g_cfg_input.player1.handler.get(); + std::string selectedDevice; + + std::map, + std::vector>> + handlerToDevices; + + auto collectDevices = [&](T handler) { + handler->Init(); + + std::vector devices; + for (const auto &device : handler->list_connected_devices()) { + devices.push_back(device.name); + } + + auto type = handler->m_type; + + handlerToDevices[type] = std::pair{ + std::move(handler), + std::move(devices), + }; + }; + + collectDevices(std::make_unique()); + collectDevices(std::make_unique()); + collectDevices(std::make_unique()); + + if (handlerToDevices[selectedHandler].second.empty()) { + selectedHandler = pad_handler::null; + } + + if (!handlerToDevices[pad_handler::dualsense].second.empty()) { + selectedHandler = pad_handler::dualsense; + } else if (!handlerToDevices[pad_handler::ds4].second.empty()) { + selectedHandler = pad_handler::ds4; + } else if (!handlerToDevices[pad_handler::ds3].second.empty()) { + selectedHandler = pad_handler::ds3; + } + + if (selectedHandler == pad_handler::null) { + selectedHandler = pad_handler::virtual_pad; + } + + if (selectedHandler != g_cfg_input.player1.handler.get()) { + rpcsx_android.warning("install %s pad handler", selectedHandler); + + g_cfg_input.player1.handler.set(selectedHandler); + + if (selectedHandler == pad_handler::null) { + g_cfg_input.player1.device.from_default(); + } else if (selectedHandler == pad_handler::virtual_pad) { + g_cfg_input.player1.handler.set(pad_handler::virtual_pad); + g_cfg_input.player1.device.from_string("Virtual"); + } else { + g_cfg_input.player1.device.from_string( + handlerToDevices[selectedHandler].second.front()); + handlerToDevices[selectedHandler].first->init_config( + &g_cfg_input.player1.config); + if (selectedHandler != pad_handler::virtual_pad) { + std::lock_guard lock(g_virtual_pad_mutex); + g_virtual_pad = nullptr; + } + } + + g_cfg_input.save("", g_cfg_input_configs.default_config); + + if (!Emu.IsStopped()) { + pad::reset(Emu.GetTitleID()); + } + } + } + + return true; +} + +static bool installPup(JNIEnv *env, fs::file &&pup_f, jlong progressId) { + Progress progress(env, progressId); + + pup_object pup(std::move(pup_f)); + AtExit atExit{[&] { pup.file().release_handle(); }}; + + if (static_cast(pup) == pup_error::hash_mismatch) { + rpcsx_android.fatal("installFw: invalid PUP"); + progress.failure("Selected file is not firmware update file"); + return false; + } + + if (static_cast(pup) != pup_error::ok) { + rpcsx_android.fatal("installFw: invalid PUP"); + progress.failure("Firmware update file is broken"); + return false; + } + + fs::file update_files_f = pup.get_file(0x300); + + const usz update_files_size = update_files_f ? update_files_f.size() : 0; + + if (!update_files_size) { + rpcsx_android.fatal("installFw: invalid PUP"); + progress.failure("Firmware update file is broken"); + return false; + } + + tar_object update_files(update_files_f); + + auto update_filenames = update_files.get_filenames(); + update_filenames.erase(std::remove_if(update_filenames.begin(), + update_filenames.end(), + [](const std::string &s) { + return !s.starts_with("dev_flash_"); + }), + update_filenames.end()); + + if (update_filenames.empty()) { + rpcsx_android.fatal("installFw: invalid PUP"); + progress.failure("Firmware update file is broken"); + return false; + } + + std::string version_string; + + if (fs::file version = pup.get_file(0x100)) { + version_string = version.to_string(); + } + + if (const usz version_pos = version_string.find('\n'); + version_pos != std::string::npos) { + version_string.erase(version_pos); + } + + if (version_string.empty()) { + rpcsx_android.fatal("installFw: invalid PUP"); + progress.failure("Firmware update file is broken"); + return false; + } + + sendVshBootable(env, progressId); + + jlong processed = 0; + for (const auto &update_filename : update_filenames) { + auto update_file_stream = update_files.get_file(update_filename); + + if (update_file_stream->m_file_handler) { + // Forcefully read all the data + update_file_stream->m_file_handler->handle_file_op( + *update_file_stream, 0, update_file_stream->get_size(umax), nullptr); + } + + fs::file update_file = fs::make_stream(std::move(update_file_stream->data)); + + SCEDecrypter self_dec(update_file); + self_dec.LoadHeaders(); + self_dec.LoadMetadata(SCEPKG_ERK, SCEPKG_RIV); + self_dec.DecryptData(); + + auto dev_flash_tar_f = self_dec.MakeFile(); + + if (dev_flash_tar_f.size() < 3) { + rpcsx_android.error( + "Firmware installation failed: Firmware could not be decompressed"); + + progress.failure("Firmware update file could not be decompressed"); + return false; + } + + tar_object dev_flash_tar(dev_flash_tar_f[2]); + + if (!dev_flash_tar.extract()) { + + rpcsx_android.error("Error while installing firmware: TAR contents are " + "invalid. (package=%s)", + update_filename); + + progress.failure(fmt::format("TAR contents are invalid (package=%s)", + update_filename)); + return false; + } + + if (!progress.report(processed++, update_filenames.size())) { + // Installation was cancelled + return false; + } + } + + sendFirmwareInstalled(env, utils::get_firmware_version()); + + g_compilationQueue.push(progress, + g_cfg_vfs.get_dev_flash() + "/vsh/module/vsh.self"); + return true; +} + +static bool installPkg(JNIEnv *env, fs::file &&file, jlong progressId) { + Progress progress(env, progressId); + + std::deque readers; + std::deque bootable_paths; + readers.emplace_back("dummy.pkg", std::move(file)); + + AtExit atExit{[&] { + for (auto &reader : readers) { + reader.file().release_handle(); + } + }}; + + package_install_result result = {}; + named_thread worker("PKG Installer", [&readers, &result, &bootable_paths] { + result = package_reader::extract_data(readers, bootable_paths); + return result.error == package_install_result::error_type::no_error; + }); + + for (auto &reader : readers) { + if (auto gameInfo = fetchGameInfo(reader.get_psf())) { + sendGameInfo(env, progressId, {{*gameInfo}}); + } + } + + const jlong maxProgress = 10000; + + while (true) { + std::uint64_t totalProgress = 0; + for (std::size_t index = 0; auto &reader : readers) { + if (result.error != package_install_result::error_type::no_error) { + progress.failure("Installation failed"); + for (package_reader &reader : readers) { + reader.abort_extract(); + } + return false; + } + + totalProgress += reader.get_progress(maxProgress); + } + + if (totalProgress == maxProgress * readers.size()) { + break; + } + + totalProgress /= readers.size(); + + if (!progress.report(totalProgress, maxProgress)) { + for (package_reader &reader : readers) { + reader.abort_extract(); + } + + return false; + } + + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + if (worker()) { + auto paths = std::vector(bootable_paths.begin(), bootable_paths.end()); + collectGameInfo(env, -1, paths); + + for (auto &path : paths) { + g_compilationQueue.push(progress, std::move(path)); + } + } + + return true; +} + +static bool installEdat(JNIEnv *env, fs::file &&file, jlong progressId, + std::string rootPath = {}) { + Progress progress(env, progressId); + + NPD_HEADER npdHeader; + if (!file.read(npdHeader)) { + progress.failure("Invalid EDAT file"); + return false; + } + + if (!rootPath.empty()) { + auto ebootPath = locateEbootPath(rootPath); + auto sfoPath = locateParamSfoPath(rootPath); + + if (sfoPath.empty()) { + progress.failure("Game is broken: PARAM.SFO not found"); + return false; + } + + auto psf = psf::load_object(sfoPath); + auto contentId = psf::get_string(psf, "CONTENT_ID"); + + if (contentId != npdHeader.content_id) { + progress.failure(fmt::format("File cannot be used for this game. EDAT " + "content ID missmatch %s vs %s", + contentId, npdHeader.content_id)); + return false; + } + } + + const auto licenseFile = + fmt::format("%shome/%s/exdata/%s.edat", rpcs3::utils::get_hdd0_dir(), + Emu.GetUsr(), npdHeader.content_id); + + file.seek(0); + + std::vector bytes(file.size()); + if (!file.read(bytes)) { + progress.failure("Failed to read key"); + return false; + } + + if (!fs::write_file(licenseFile, fs::open_mode::create + fs::open_mode::trunc, + bytes)) { + progress.failure(fmt::format("Failed to write EDAT to %s", licenseFile)); + return false; + } + + if (rootPath.empty()) { + rootPath = rpcs3::utils::get_hdd0_dir() + "game"; + } + + collectGameInfo(env, progressId, {rootPath}); + return true; +} + +static bool installRap(JNIEnv *env, fs::file &&file, jlong progressId, + const std::string &rootPath) { + Progress progress(env, progressId); + + auto ebootPath = locateEbootPath(rootPath); + + std::vector bytes; + if (!file.read(bytes, 16)) { + progress.failure("Failed to read key"); + return false; + } + + SelfAdditionalInfo info; + decrypt_self(fs::file(ebootPath), nullptr, &info); + + auto npd = [&]() -> NPD_HEADER * { + for (auto &supplemental : info.supplemental_hdr) { + if (supplemental.type == 3) { + return &supplemental.PS3_npdrm_header.npd; + } + } + + return nullptr; + }(); + + if (npd == nullptr) { + progress.failure("Failed to fetch NPDRM of SELF"); + return false; + } + + const auto licenseFile = + fmt::format("%shome/%s/exdata/%s.rap", rpcs3::utils::get_hdd0_dir(), + Emu.GetUsr(), npd->content_id); + + if (!fs::write_file(licenseFile, fs::open_mode::create + fs::open_mode::trunc, + bytes)) { + progress.failure(fmt::format("Failed to write key to %s", licenseFile)); + return false; + } + + if (!decrypt_self(fs::file(ebootPath))) { + progress.failure("Provided key is invalid for selected game"); + fs::remove_file(licenseFile); + return false; + } + + collectGameInfo(env, -1, {rootPath}); + g_compilationQueue.push(progress, std::move(ebootPath)); + return true; +} + +static bool installIso(JNIEnv *env, fs::file &&file, jlong progressId) { + auto optIso = iso_dev::open(std::make_unique(file)); + Progress progress(env, progressId); + + if (!optIso) { + progress.failure("Failed to read ISO"); + return false; + } + + auto iso = std::move(*optIso); + auto sfo_raw_file = iso.open("PS3_GAME/PARAM.SFO", fs::read); + + if (!sfo_raw_file) { + progress.failure("Failed to locate PARAM.SFO in ISO"); + return false; + } + + fs::file sfo_file; + sfo_file.reset(std::move(sfo_raw_file)); + + auto sfo = psf::load_object(sfo_file, "iso://PS3_GAME/PARAM.SFO"); + auto title_id = psf::get_string(sfo, "TITLE_ID"); + + if (title_id.empty()) { + progress.failure("Failed to fetch TITLE_ID from PARAM.SFO in ISO"); + return false; + } + + if (auto gameInfo = fetchGameInfo(sfo)) { + sendGameInfo(env, progressId, {{*gameInfo}}); + } + + std::filesystem::path destinationPath = + fs::get_config_dir() + "games/" + std::string(title_id); + std::size_t filesCount = 0; + + auto roots = [&] { + std::vector result; + std::vector workList; + workList.push_back({}); + result.push_back({}); + + while (!workList.empty()) { + auto path = std::move(workList.back()); + workList.pop_back(); + + fs::dir dir; + dir.reset(iso.open_dir(path)); + + for (auto entry : dir) { + if (entry.name == "." || entry.name == "..") { + continue; + } + if (entry.name == "PS3_UPDATE" && path.empty()) { + continue; + } + + if (entry.is_directory) { + result.push_back(path / entry.name); + workList.push_back(path / entry.name); + } else { + filesCount++; + } + } + } + + return result; + }(); + + progress.report(0, filesCount); + + std::size_t processedFiles = 0; + std::error_code ec; + + for (auto &root : roots) { + auto rootDestPath = root.empty() ? destinationPath : destinationPath / root; + + std::filesystem::create_directories(rootDestPath, ec); + if (ec) { + progress.failure(fmt::format("Failed to create dir %s: %s", + rootDestPath.string(), ec.message())); + return false; + } + + fs::dir dir; + dir.reset(iso.open_dir(root)); + + for (auto entry : dir) { + if (entry.name == "." || entry.name == "..") { + continue; + } + + auto entryDestPath = rootDestPath / entry.name; + + if (entry.is_directory) { + std::filesystem::create_directories(entryDestPath, ec); + if (ec) { + progress.failure(fmt::format("Failed to create dir %s: %s", + entryDestPath.string(), ec.message())); + return false; + } + + continue; + } + auto raw_file = iso.open(root / entry.name, fs::read); + + if (!raw_file) { + progress.failure(fmt::format("Failed to open file in ISO: %s", + (root / entry.name).string())); + return false; + } + + fs::file file; + file.reset(std::move(raw_file)); + + if (!fs::write_file(entryDestPath, + fs::open_mode::create + fs::open_mode::trunc, + file.to_vector())) { + progress.failure(fmt::format("Failed to write file: %s, dest %s", + entryDestPath.string(), + destinationPath.string())); + return false; + } + + progress.report(processedFiles++, filesCount); + } + } + + collectGameInfo(env, -1, {destinationPath}); + auto ebootPath = locateEbootPath(destinationPath); + g_compilationQueue.push(progress, std::move(ebootPath)); + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_installFw( + JNIEnv *env, jobject, jint fd, jlong progressId) { + return installPup(env, fs::file::from_native_handle(fd), progressId); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_net_rpcsx_RPCSX_isInstallableFile(JNIEnv *env, jobject, jint fd) { + auto file = fs::file::from_native_handle(fd); + AtExit atExit{[&] { file.release_handle(); }}; + + auto type = getFileType(file); + file.seek(0); + return type != FileType::Unknown && + type != FileType::Rap; // FIXME: implement rap preinstallation +} + +extern "C" JNIEXPORT jstring JNICALL +Java_net_rpcsx_RPCSX_getDirInstallPath(JNIEnv *env, jobject, jint fd) { + auto file = fs::file::from_native_handle(fd); + AtExit atExit{[&] { file.release_handle(); }}; + + auto psf = psf::load_object(file, ""); + if (auto gameInfo = fetchGameInfo(psf)) { + return wrap(env, gameInfo->path); + } + + return nullptr; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_net_rpcsx_RPCSX_install(JNIEnv *env, jobject, jint fd, jlong progressId) { + auto file = fs::file::from_native_handle(fd); + AtExit atExit{[&] { file.release_handle(); }}; + + auto type = getFileType(file); + file.seek(0); + + switch (type) { + case FileType::Unknown: + Progress(env, progressId).failure("Unsupported file type"); + return false; + + case FileType::Pup: + return installPup(env, std::move(file), progressId); + + case FileType::Pkg: + return installPkg(env, std::move(file), progressId); + + case FileType::Edat: + return installEdat(env, std::move(file), progressId); + + case FileType::Iso: + return installIso(env, std::move(file), progressId); + + case FileType::Rap: + Progress(env, progressId) + .failure("RAP file cannot be preinstalled. Use lock button on " + "installed game instead"); + return false; + } + + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_installKey( + JNIEnv *env, jobject, jint fd, jlong progressId, jstring gamePath) { + auto file = fs::file::from_native_handle(fd); + AtExit atExit{[&] { file.release_handle(); }}; + + auto type = getFileType(file); + file.seek(0); + + if (type == FileType::Rap) { + return installRap(env, std::move(file), progressId, unwrap(env, gamePath)); + } + + if (type == FileType::Edat) { + return installEdat(env, std::move(file), progressId, unwrap(env, gamePath)); + } + + Progress(env, progressId).failure("Unsupported key type"); + return false; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_net_rpcsx_RPCSX_systemInfo(JNIEnv *env, jobject) { + std::string result; + + fmt::append(result, "%s\n\nLLVM CPU: %s\n\n", utils::get_system_info(), + fallback_cpu_detection()); + + { + vk::instance device_enum_context; + if (device_enum_context.create("RPCS3")) { + device_enum_context.bind(); + const std::vector &gpus = + device_enum_context.enumerate_devices(); + + for (const auto &gpu : gpus) { + fmt::append(result, "GPU: %s\n\nDriver: %s (v%s)\n\nVulkan: %s", + gpu.get_name(), gpu.get_driver_name(), + gpu.get_driver_version(), gpu.get_driver_vk_version()); + } + } + } + + return wrap(env, result); +} + +static cfg::_base *find_cfg_node(cfg::_base *root, std::string_view path) { + auto pathList = fmt::split(path, {"@@"}); + std::ranges::reverse(pathList); + + while (!pathList.empty()) { + auto elem = pathList.back(); + pathList.pop_back(); + if (elem.empty()) { + continue; + } + + auto root_node = dynamic_cast(root); + if (root_node == nullptr) { + return nullptr; + } + + cfg::_base *child_node = nullptr; + + for (auto node : root_node->get_nodes()) { + if (node->get_name() == elem) { + child_node = node; + break; + } + } + + if (child_node == nullptr) { + return nullptr; + } + + root = child_node; + } + + return root; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_net_rpcsx_RPCSX_settingsGet(JNIEnv *env, jobject, jstring jpath) { + auto root = find_cfg_node(&g_cfg, unwrap(env, jpath)); + + if (root == nullptr) { + return nullptr; + } + + return wrap(env, root->to_json().dump(4)); +} + +extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_settingsSet( + JNIEnv *env, jobject, jstring jpath, jstring jvalue) { + nlohmann::json value; + try { + value = nlohmann::json::parse(unwrap(env, jvalue)); + } catch (...) { + rpcsx_android.error("settingsSet: node %s passed with invalid json '%s'", + unwrap(env, jpath), unwrap(env, jvalue)); + return false; + } + + auto root = find_cfg_node(&g_cfg, unwrap(env, jpath)); + + if (root == nullptr) { + rpcsx_android.error("settingsSet: node %s not found", unwrap(env, jpath)); + return false; + } + + if (!root->from_json(value, !Emu.IsStopped())) { + rpcsx_android.error("settingsSet: node %s not accepts value '%s'", + unwrap(env, jpath), value.dump()); + return false; + } + + Emulator::SaveSettings(g_cfg.to_string(), ""); + return true; +} + +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, rx::getVersion().toString()); +}