From 1382f73d4e99b7eafdc8657b32fc0f300afa16c7 Mon Sep 17 00:00:00 2001 From: pineappleEA Date: Wed, 29 Nov 2023 20:41:00 +0100 Subject: [PATCH] early-access version 3998 --- CMakeLists.txt | 5 + README.md | 2 +- externals/CMakeLists.txt | 8 + externals/gamemode/CMakeLists.txt | 11 + externals/gamemode/include/gamemode_client.h | 379 ++++++++++++++ .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 5 + .../yuzu/yuzu_emu/adapters/FolderAdapter.kt | 76 +++ .../features/settings/model/IntSetting.kt | 1 + .../features/settings/model/Settings.kt | 24 - .../settings/model/view/SettingsItem.kt | 9 + .../features/settings/ui/SettingsActivity.kt | 30 +- .../features/settings/ui/SettingsAdapter.kt | 2 - .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../fragments/AddGameFolderDialogFragment.kt | 53 ++ .../yuzu_emu/fragments/EmulationFragment.kt | 4 +- .../GameFolderPropertiesDialogFragment.kt | 72 +++ .../yuzu_emu/fragments/GameFoldersFragment.kt | 128 +++++ .../fragments/HomeSettingsFragment.kt | 13 +- .../fragments/SettingsDialogFragment.kt | 8 - .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 8 +- .../java/org/yuzu/yuzu_emu/model/GameDir.kt | 13 + .../org/yuzu/yuzu_emu/model/GamesViewModel.kt | 61 ++- .../org/yuzu/yuzu_emu/model/HomeViewModel.kt | 19 - .../yuzu/yuzu_emu/model/SettingsViewModel.kt | 3 - .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 33 +- .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt | 21 + .../org/yuzu/yuzu_emu/utils/GameHelper.kt | 39 +- .../org/yuzu/yuzu_emu/utils/NativeConfig.kt | 20 + .../app/src/main/jni/android_config.cpp | 50 ++ src/android/app/src/main/jni/android_config.h | 8 +- .../app/src/main/jni/android_settings.h | 8 + src/android/app/src/main/jni/id_cache.cpp | 16 + src/android/app/src/main/jni/id_cache.h | 2 + src/android/app/src/main/jni/native.cpp | 8 + .../app/src/main/jni/native_config.cpp | 52 ++ .../app/src/main/res/layout/card_folder.xml | 70 +++ .../src/main/res/layout/dialog_add_folder.xml | 45 ++ .../res/layout/dialog_folder_properties.xml | 30 ++ .../src/main/res/layout/fragment_folders.xml | 48 ++ .../main/res/navigation/home_navigation.xml | 7 + .../app/src/main/res/values/arrays.xml | 18 + .../app/src/main/res/values/dimens.xml | 2 +- .../app/src/main/res/values/strings.xml | 12 + src/common/CMakeLists.txt | 19 +- src/common/free_region_manager.h | 55 ++ src/common/host_memory.cpp | 193 +++++-- src/common/host_memory.h | 15 +- src/common/linux/gamemode.cpp | 39 ++ src/common/linux/gamemode.h | 24 + src/common/settings.cpp | 19 + src/common/settings.h | 14 + src/common/settings_common.h | 1 + src/common/settings_enums.h | 2 + src/common/signal_chain.cpp | 42 ++ src/common/signal_chain.h | 19 + src/common/wall_clock.cpp | 4 +- src/core/CMakeLists.txt | 16 + src/core/arm/arm_interface.cpp | 2 + src/core/arm/arm_interface.h | 3 + src/core/arm/nce/arm_nce.cpp | 400 +++++++++++++++ src/core/arm/nce/arm_nce.h | 108 ++++ src/core/arm/nce/arm_nce.s | 222 ++++++++ src/core/arm/nce/arm_nce_asm_definitions.h | 29 ++ src/core/arm/nce/guest_context.h | 50 ++ src/core/arm/nce/instructions.h | 147 ++++++ src/core/arm/nce/patch.cpp | 474 ++++++++++++++++++ src/core/arm/nce/patch.h | 98 ++++ src/core/cpu_manager.cpp | 2 + src/core/device_memory.cpp | 3 +- src/core/hle/kernel/code_set.h | 14 + src/core/hle/kernel/k_address_space_info.cpp | 4 +- src/core/hle/kernel/k_page_table_base.cpp | 33 +- src/core/hle/kernel/k_page_table_base.h | 3 +- src/core/hle/kernel/k_process.cpp | 23 +- src/core/hle/kernel/k_process.h | 14 +- src/core/hle/kernel/k_process_page_table.h | 9 +- src/core/hle/kernel/k_thread.h | 16 + src/core/hle/kernel/physical_core.cpp | 14 +- .../service/hid/controllers/touchscreen.cpp | 12 +- .../hle/service/hid/controllers/touchscreen.h | 4 + src/core/hle/service/hid/hid_server.cpp | 16 + src/core/hle/service/hid/hid_server.h | 1 + src/core/hle/service/set/set_sys.cpp | 4 +- .../loader/deconstructed_rom_directory.cpp | 63 ++- src/core/loader/kip.cpp | 3 +- src/core/loader/nro.cpp | 63 ++- src/core/loader/nro.h | 2 +- src/core/loader/nso.cpp | 67 ++- src/core/loader/nso.h | 7 +- src/core/memory.cpp | 71 ++- src/core/memory.h | 18 +- src/frontend_common/config.cpp | 2 + src/tests/common/host_memory.cpp | 71 +-- src/video_core/buffer_cache/usage_tracker.h | 4 +- src/yuzu/CMakeLists.txt | 2 +- src/yuzu/configuration/configure_cpu.cpp | 12 + src/yuzu/configuration/configure_cpu.h | 1 + src/yuzu/configuration/configure_cpu.ui | 30 ++ src/yuzu/configuration/configure_general.cpp | 43 +- src/yuzu/configuration/configure_general.ui | 27 + src/yuzu/configuration/configure_system.ui | 2 +- src/yuzu/configuration/shared_translation.cpp | 9 + src/yuzu/main.cpp | 30 ++ src/yuzu/main.h | 1 + src/yuzu_cmd/yuzu.cpp | 12 + 105 files changed, 3851 insertions(+), 280 deletions(-) create mode 100755 externals/gamemode/CMakeLists.txt create mode 100755 externals/gamemode/include/gamemode_client.h create mode 100755 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt create mode 100755 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt create mode 100755 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt create mode 100755 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt create mode 100755 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt create mode 100755 src/android/app/src/main/res/layout/card_folder.xml create mode 100755 src/android/app/src/main/res/layout/dialog_add_folder.xml create mode 100755 src/android/app/src/main/res/layout/dialog_folder_properties.xml create mode 100755 src/android/app/src/main/res/layout/fragment_folders.xml create mode 100755 src/common/free_region_manager.h create mode 100755 src/common/linux/gamemode.cpp create mode 100755 src/common/linux/gamemode.h create mode 100755 src/common/signal_chain.cpp create mode 100755 src/common/signal_chain.h create mode 100755 src/core/arm/nce/arm_nce.cpp create mode 100755 src/core/arm/nce/arm_nce.h create mode 100755 src/core/arm/nce/arm_nce.s create mode 100755 src/core/arm/nce/arm_nce_asm_definitions.h create mode 100755 src/core/arm/nce/guest_context.h create mode 100755 src/core/arm/nce/instructions.h create mode 100755 src/core/arm/nce/patch.cpp create mode 100755 src/core/arm/nce/patch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bae4db706..cce2016db 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,6 +260,11 @@ if (UNIX) add_definitions(-DYUZU_UNIX=1) endif() +if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux")) + set(HAS_NCE 1) + add_definitions(-DHAS_NCE=1) +endif() + # Configure C++ standard # =========================== diff --git a/README.md b/README.md index 6867b887c..8577ab040 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ yuzu emulator early access ============= -This is the source code for early-access 3997. +This is the source code for early-access 3998. ## Legal Notice diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index a650eac84..fc6cbce84 100755 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -20,6 +20,10 @@ if ((ARCHITECTURE_x86 OR ARCHITECTURE_x86_64) AND NOT TARGET xbyak::xbyak) endif() # Dynarmic +if (ARCHITECTURE_arm64 AND NOT TARGET merry::oaknut) + add_subdirectory(oaknut) +endif() + if ((ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64) AND NOT TARGET dynarmic::dynarmic) set(DYNARMIC_IGNORE_ASSERTS ON) add_subdirectory(dynarmic) @@ -189,6 +193,10 @@ if (ANDROID) endif() endif() +if (UNIX) + add_subdirectory(gamemode) +endif() + # Breakpad # https://github.com/microsoft/vcpkg/blob/master/ports/breakpad/CMakeLists.txt if (YUZU_CRASH_DUMPS AND NOT TARGET libbreakpad_client) diff --git a/externals/gamemode/CMakeLists.txt b/externals/gamemode/CMakeLists.txt new file mode 100755 index 000000000..87095642e --- /dev/null +++ b/externals/gamemode/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + +project(gamemode LANGUAGES CXX C) + +add_library(gamemode include/gamemode_client.h) + +target_link_libraries(gamemode PRIVATE common) + +target_include_directories(gamemode PUBLIC include) +set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C) diff --git a/externals/gamemode/include/gamemode_client.h b/externals/gamemode/include/gamemode_client.h new file mode 100755 index 000000000..184812334 --- /dev/null +++ b/externals/gamemode/include/gamemode_client.h @@ -0,0 +1,379 @@ +// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive +// SPDX-License-Identifier: BSD-3-Clause + +/* + +Copyright (c) 2017-2019, Feral Interactive +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Feral Interactive nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + */ +#ifndef CLIENT_GAMEMODE_H +#define CLIENT_GAMEMODE_H +/* + * GameMode supports the following client functions + * Requests are refcounted in the daemon + * + * int gamemode_request_start() - Request gamemode starts + * 0 if the request was sent successfully + * -1 if the request failed + * + * int gamemode_request_end() - Request gamemode ends + * 0 if the request was sent successfully + * -1 if the request failed + * + * GAMEMODE_AUTO can be defined to make the above two functions apply during static init and + * destruction, as appropriate. In this configuration, errors will be printed to stderr + * + * int gamemode_query_status() - Query the current status of gamemode + * 0 if gamemode is inactive + * 1 if gamemode is active + * 2 if gamemode is active and this client is registered + * -1 if the query failed + * + * int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process + * 0 if the request was sent successfully + * -1 if the request failed + * -2 if the request was rejected + * + * int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process + * 0 if the request was sent successfully + * -1 if the request failed + * -2 if the request was rejected + * + * int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process + * 0 if gamemode is inactive + * 1 if gamemode is active + * 2 if gamemode is active and this client is registered + * -1 if the query failed + * + * const char* gamemode_error_string() - Get an error string + * returns a string describing any of the above errors + * + * Note: All the above requests can be blocking - dbus requests can and will block while the daemon + * handles the request. It is not recommended to make these calls in performance critical code + */ + +#include +#include + +#include +#include + +#include + +#include + +static char internal_gamemode_client_error_string[512] = { 0 }; + +/** + * Load libgamemode dynamically to dislodge us from most dependencies. + * This allows clients to link and/or use this regardless of runtime. + * See SDL2 for an example of the reasoning behind this in terms of + * dynamic versioning as well. + */ +static volatile int internal_libgamemode_loaded = 1; + +/* Typedefs for the functions to load */ +typedef int (*api_call_return_int)(void); +typedef const char *(*api_call_return_cstring)(void); +typedef int (*api_call_pid_return_int)(pid_t); + +/* Storage for functors */ +static api_call_return_int REAL_internal_gamemode_request_start = NULL; +static api_call_return_int REAL_internal_gamemode_request_end = NULL; +static api_call_return_int REAL_internal_gamemode_query_status = NULL; +static api_call_return_cstring REAL_internal_gamemode_error_string = NULL; +static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL; +static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL; +static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL; + +/** + * Internal helper to perform the symbol binding safely. + * + * Returns 0 on success and -1 on failure + */ +__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol( + void *handle, const char *name, void **out_func, size_t func_size, bool required) +{ + void *symbol_lookup = NULL; + char *dl_error = NULL; + + /* Safely look up the symbol */ + symbol_lookup = dlsym(handle, name); + dl_error = dlerror(); + if (required && (dl_error || !symbol_lookup)) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "dlsym failed - %s", + dl_error); + return -1; + } + + /* Have the symbol correctly, copy it to make it usable */ + memcpy(out_func, &symbol_lookup, func_size); + return 0; +} + +/** + * Loads libgamemode and needed functions + * + * Returns 0 on success and -1 on failure + */ +__attribute__((always_inline)) static inline int internal_load_libgamemode(void) +{ + /* We start at 1, 0 is a success and -1 is a fail */ + if (internal_libgamemode_loaded != 1) { + return internal_libgamemode_loaded; + } + + /* Anonymous struct type to define our bindings */ + struct binding { + const char *name; + void **functor; + size_t func_size; + bool required; + } bindings[] = { + { "real_gamemode_request_start", + (void **)&REAL_internal_gamemode_request_start, + sizeof(REAL_internal_gamemode_request_start), + true }, + { "real_gamemode_request_end", + (void **)&REAL_internal_gamemode_request_end, + sizeof(REAL_internal_gamemode_request_end), + true }, + { "real_gamemode_query_status", + (void **)&REAL_internal_gamemode_query_status, + sizeof(REAL_internal_gamemode_query_status), + false }, + { "real_gamemode_error_string", + (void **)&REAL_internal_gamemode_error_string, + sizeof(REAL_internal_gamemode_error_string), + true }, + { "real_gamemode_request_start_for", + (void **)&REAL_internal_gamemode_request_start_for, + sizeof(REAL_internal_gamemode_request_start_for), + false }, + { "real_gamemode_request_end_for", + (void **)&REAL_internal_gamemode_request_end_for, + sizeof(REAL_internal_gamemode_request_end_for), + false }, + { "real_gamemode_query_status_for", + (void **)&REAL_internal_gamemode_query_status_for, + sizeof(REAL_internal_gamemode_query_status_for), + false }, + }; + + void *libgamemode = NULL; + + /* Try and load libgamemode */ + libgamemode = dlopen("libgamemode.so.0", RTLD_NOW); + if (!libgamemode) { + /* Attempt to load unversioned library for compatibility with older + * versions (as of writing, there are no ABI changes between the two - + * this may need to change if ever ABI-breaking changes are made) */ + libgamemode = dlopen("libgamemode.so", RTLD_NOW); + if (!libgamemode) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "dlopen failed - %s", + dlerror()); + internal_libgamemode_loaded = -1; + return -1; + } + } + + /* Attempt to bind all symbols */ + for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) { + struct binding *binder = &bindings[i]; + + if (internal_bind_libgamemode_symbol(libgamemode, + binder->name, + binder->functor, + binder->func_size, + binder->required)) { + internal_libgamemode_loaded = -1; + return -1; + }; + } + + /* Success */ + internal_libgamemode_loaded = 0; + return 0; +} + +/** + * Redirect to the real libgamemode + */ +__attribute__((always_inline)) static inline const char *gamemode_error_string(void) +{ + /* If we fail to load the system gamemode, or we have an error string already, return our error + * string instead of diverting to the system version */ + if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') { + return internal_gamemode_client_error_string; + } + + /* Assert for static analyser that the function is not NULL */ + assert(REAL_internal_gamemode_error_string != NULL); + + return REAL_internal_gamemode_error_string(); +} + +/** + * Redirect to the real libgamemode + * Allow automatically requesting game mode + * Also prints errors as they happen. + */ +#ifdef GAMEMODE_AUTO +__attribute__((constructor)) +#else +__attribute__((always_inline)) static inline +#endif +int gamemode_request_start(void) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + /* Assert for static analyser that the function is not NULL */ + assert(REAL_internal_gamemode_request_start != NULL); + + if (REAL_internal_gamemode_request_start() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + return 0; +} + +/* Redirect to the real libgamemode */ +#ifdef GAMEMODE_AUTO +__attribute__((destructor)) +#else +__attribute__((always_inline)) static inline +#endif +int gamemode_request_end(void) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + /* Assert for static analyser that the function is not NULL */ + assert(REAL_internal_gamemode_request_end != NULL); + + if (REAL_internal_gamemode_request_end() < 0) { +#ifdef GAMEMODE_AUTO + fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); +#endif + return -1; + } + + return 0; +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_query_status(void) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_query_status == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_query_status missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_query_status(); +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_request_start_for == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_request_start_for missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_request_start_for(pid); +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_request_end_for == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_request_end_for missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_request_end_for(pid); +} + +/* Redirect to the real libgamemode */ +__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid) +{ + /* Need to load gamemode */ + if (internal_load_libgamemode() < 0) { + return -1; + } + + if (REAL_internal_gamemode_query_status_for == NULL) { + snprintf(internal_gamemode_client_error_string, + sizeof(internal_gamemode_client_error_string), + "gamemode_query_status_for missing (older host?)"); + return -1; + } + + return REAL_internal_gamemode_query_status_for(pid); +} + +#endif // CLIENT_GAMEMODE_H diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index f2ba2504c..e0f01127c 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -299,6 +299,11 @@ object NativeLibrary { */ external fun getPerfStats(): DoubleArray + /** + * Returns the current CPU backend. + */ + external fun getCpuBackend(): String + /** * Notifies the core emulation that the orientation has changed. */ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt new file mode 100755 index 000000000..ab657a7b9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.net.Uri +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.CardFolderBinding +import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment +import org.yuzu.yuzu_emu.model.GameDir +import org.yuzu.yuzu_emu.model.GamesViewModel + +class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : + ListAdapter( + AsyncDifferConfig.Builder(DiffCallback()).build() + ) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): FolderAdapter.FolderViewHolder { + CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return FolderViewHolder(it) } + } + + override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) = + holder.bind(currentList[position]) + + inner class FolderViewHolder(val binding: CardFolderBinding) : + RecyclerView.ViewHolder(binding.root) { + private lateinit var gameDir: GameDir + + fun bind(gameDir: GameDir) { + this.gameDir = gameDir + + binding.apply { + path.text = Uri.parse(gameDir.uriString).path + path.postDelayed( + { + path.isSelected = true + path.ellipsize = TextUtils.TruncateAt.MARQUEE + }, + 3000 + ) + + buttonEdit.setOnClickListener { + GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir) + .show( + activity.supportFragmentManager, + GameFolderPropertiesDialogFragment.TAG + ) + } + + buttonDelete.setOnClickListener { + gamesViewModel.removeFolder(this@FolderViewHolder.gameDir) + } + } + } + } + + private class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { + return oldItem == newItem + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index 151362124..ef10b209f 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -10,6 +10,7 @@ enum class IntSetting( override val category: Settings.Category, override val androidDefault: Int? = null ) : AbstractIntSetting { + CPU_BACKEND("cpu_backend", Settings.Category.Cpu), CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu), REGION_INDEX("region_index", Settings.Category.System), LANGUAGE_INDEX("language_index", Settings.Category.System), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index d005c656e..e3cd66185 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -3,33 +3,9 @@ package org.yuzu.yuzu_emu.features.settings.model -import android.text.TextUtils -import android.widget.Toast import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.utils.NativeConfig object Settings { - private val context get() = YuzuApplication.appContext - - fun saveSettings(gameId: String = "") { - if (TextUtils.isEmpty(gameId)) { - Toast.makeText( - context, - context.getString(R.string.ini_saved), - Toast.LENGTH_SHORT - ).show() - NativeConfig.saveSettings() - } else { - // TODO: Save custom game settings - Toast.makeText( - context, - context.getString(R.string.gameid_saved, gameId), - Toast.LENGTH_SHORT - ).show() - } - } - enum class Category { Android, Audio, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index 6aba69dbe..e198b18a0 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -77,6 +77,15 @@ abstract class SettingsItem( "%" ) ) + put( + SingleChoiceSetting( + IntSetting.CPU_BACKEND, + R.string.cpu_backend, + 0, + R.array.cpuBackendArm64Names, + R.array.cpuBackendArm64Values + ) + ) put( SingleChoiceSetting( IntSetting.CPU_ACCURACY, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 48bdbdd75..64bfc6dd0 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -19,12 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.NavHostFragment import androidx.navigation.navArgs import com.google.android.material.color.MaterialColors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.io.IOException import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding -import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment import org.yuzu.yuzu_emu.model.SettingsViewModel @@ -53,10 +54,6 @@ class SettingsActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) - if (savedInstanceState != null) { - settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE) - } - if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION ) { @@ -127,12 +124,6 @@ class SettingsActivity : AppCompatActivity() { } } - override fun onSaveInstanceState(outState: Bundle) { - // Critical: If super method is not called, rotations will be busted. - super.onSaveInstanceState(outState) - outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave) - } - override fun onStart() { super.onStart() // TODO: Load custom settings contextually @@ -141,16 +132,10 @@ class SettingsActivity : AppCompatActivity() { } } - /** - * If this is called, the user has left the settings screen (potentially through the - * home button) and will expect their changes to be persisted. So we kick off an - * IntentService which will do so on a background thread. - */ override fun onStop() { super.onStop() - if (isFinishing && settingsViewModel.shouldSave) { - Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...") - Settings.saveSettings() + CoroutineScope(Dispatchers.IO).launch { + NativeConfig.saveSettings() } } @@ -160,9 +145,6 @@ class SettingsActivity : AppCompatActivity() { } fun onSettingsReset() { - // Prevents saving to a non-existent settings file - settingsViewModel.shouldSave = false - // Delete settings file because the user may have changed values that do not exist in the UI NativeConfig.unloadConfig() val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) @@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() { windowInsets } } - - companion object { - private const val KEY_SHOULD_SAVE = "should_save" - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index a7a029fc1..af2c1e582 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -105,7 +105,6 @@ class SettingsAdapter( fun onBooleanClick(item: SwitchSetting, checked: Boolean) { item.checked = checked settingsViewModel.setShouldReloadSettingsList(true) - settingsViewModel.shouldSave = true } fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { @@ -161,7 +160,6 @@ class SettingsAdapter( epochTime += timePicker.hour.toLong() * 60 * 60 epochTime += timePicker.minute.toLong() * 60 if (item.value != epochTime) { - settingsViewModel.shouldSave = true notifyItemChanged(position) item.value = epochTime } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 8b71e32f3..7425728c6 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -269,6 +269,7 @@ class SettingsFragmentPresenter( add(BooleanSetting.RENDERER_DEBUG.key) add(HeaderSetting(R.string.cpu)) + add(IntSetting.CPU_BACKEND.key) add(IntSetting.CPU_ACCURACY.key) add(BooleanSetting.CPU_DEBUG_MODE.key) add(SettingsItem.FASTMEM_COMBINED) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt new file mode 100755 index 000000000..dec2b7cf1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.DialogInterface +import android.net.Uri +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding +import org.yuzu.yuzu_emu.model.GameDir +import org.yuzu.yuzu_emu.model.GamesViewModel + +class AddGameFolderDialogFragment : DialogFragment() { + private val gamesViewModel: GamesViewModel by activityViewModels() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val binding = DialogAddFolderBinding.inflate(layoutInflater) + val folderUriString = requireArguments().getString(FOLDER_URI_STRING) + if (folderUriString == null) { + dismiss() + } + binding.path.text = Uri.parse(folderUriString).path + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.add_game_folder) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked) + gamesViewModel.addFolder(newGameDir) + } + .setNegativeButton(android.R.string.cancel, null) + .setView(binding.root) + .show() + } + + companion object { + const val TAG = "AddGameFolderDialogFragment" + + private const val FOLDER_URI_STRING = "FolderUriString" + + fun newInstance(folderUriString: String): AddGameFolderDialogFragment { + val args = Bundle() + args.putString(FOLDER_URI_STRING, folderUriString) + val fragment = AddGameFolderDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index c32fa0d7e..734c1d5ca 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -414,8 +414,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { perfStatsUpdater = { if (emulationViewModel.emulationStarted.value) { val perfStats = NativeLibrary.getPerfStats() + val cpuBackend = NativeLibrary.getCpuBackend() if (_binding != null) { - binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS]) + binding.showFpsText.text = + String.format("FPS: %.1f\n%s", perfStats[FPS], cpuBackend) } perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt new file mode 100755 index 000000000..b6c2e4635 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding +import org.yuzu.yuzu_emu.model.GameDir +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable + +class GameFolderPropertiesDialogFragment : DialogFragment() { + private val gamesViewModel: GamesViewModel by activityViewModels() + + private var deepScan = false + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val binding = DialogFolderPropertiesBinding.inflate(layoutInflater) + val gameDir = requireArguments().parcelable(GAME_DIR)!! + + // Restore checkbox state + binding.deepScanSwitch.isChecked = + savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan + + // Ensure that we can get the checkbox state even if the view is destroyed + deepScan = binding.deepScanSwitch.isChecked + binding.deepScanSwitch.setOnClickListener { + deepScan = binding.deepScanSwitch.isChecked + } + + return MaterialAlertDialogBuilder(requireContext()) + .setView(binding.root) + .setTitle(R.string.game_folder_properties) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + val folderIndex = gamesViewModel.folders.value.indexOf(gameDir) + if (folderIndex != -1) { + gamesViewModel.folders.value[folderIndex].deepScan = + binding.deepScanSwitch.isChecked + gamesViewModel.updateGameDirs() + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(DEEP_SCAN, deepScan) + } + + companion object { + const val TAG = "GameFolderPropertiesDialogFragment" + + private const val GAME_DIR = "GameDir" + + private const val DEEP_SCAN = "DeepScan" + + fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment { + val args = Bundle() + args.putParcelable(GAME_DIR, gameDir) + val fragment = GameFolderPropertiesDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt new file mode 100755 index 000000000..341a37fdb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.FolderAdapter +import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.ui.main.MainActivity + +class GameFoldersFragment : Fragment() { + private var _binding: FragmentFoldersBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + private val gamesViewModel: GamesViewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + + gamesViewModel.onOpenGameFoldersFragment() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentFoldersBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = false, animated = true) + homeViewModel.setStatusBarShadeVisibility(visible = false) + + binding.toolbarFolders.setNavigationOnClickListener { + binding.root.findNavController().popBackStack() + } + + binding.listFolders.apply { + layoutManager = GridLayoutManager( + requireContext(), + resources.getInteger(R.integer.grid_columns) + ) + adapter = FolderAdapter(requireActivity(), gamesViewModel) + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.folders.collect { + (binding.listFolders.adapter as FolderAdapter).submitList(it) + } + } + } + + val mainActivity = requireActivity() as MainActivity + binding.buttonAdd.setOnClickListener { + mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) + } + + setInsets() + } + + override fun onStop() { + super.onStop() + gamesViewModel.onCloseGameFoldersFragment() + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams + mlpToolbar.leftMargin = leftInsets + mlpToolbar.rightMargin = rightInsets + binding.toolbarFolders.layoutParams = mlpToolbar + + val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) + val mlpFab = + binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams + mlpFab.leftMargin = leftInsets + fabSpacing + mlpFab.rightMargin = rightInsets + fabSpacing + mlpFab.bottomMargin = barInsets.bottom + fabSpacing + binding.buttonAdd.layoutParams = mlpFab + + val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams + mlpListFolders.leftMargin = leftInsets + mlpListFolders.rightMargin = rightInsets + binding.listFolders.layoutParams = mlpListFolders + + binding.listFolders.updatePadding( + bottom = barInsets.bottom + + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + ) + + windowInsets + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 4720daec4..3addc2e63 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() { ) add( HomeSetting( - R.string.select_games_folder, + R.string.manage_game_folders, R.string.select_games_folder_description, R.drawable.ic_add, { - mainActivity.getGamesDirectory.launch( - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data - ) - }, - { true }, - 0, - 0, - homeViewModel.gamesDir + binding.root.findNavController() + .navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment) + } ) ) add( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt index d18ec6974..b88d2c038 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt @@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> settingsViewModel.clickedItem!!.setting.reset() settingsViewModel.setAdapterItemChanged(position) - settingsViewModel.shouldSave = true } .setNegativeButton(android.R.string.cancel, null) .create() @@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener is SingleChoiceSetting -> { val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting val value = getValueForSingleChoiceSelection(scSetting, which) - if (scSetting.selectedValue != value) { - settingsViewModel.shouldSave = true - } scSetting.selectedValue = value } is StringSingleChoiceSetting -> { val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting val value = scSetting.getValueAt(which) - if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true scSetting.selectedValue = value } is SliderSetting -> { val sliderSetting = settingsViewModel.clickedItem as SliderSetting - if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) { - settingsViewModel.shouldSave = true - } sliderSetting.selectedValue = settingsViewModel.sliderProgress.value } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index c66bb635a..c4277735d 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization -import org.yuzu.yuzu_emu.utils.GameHelper +import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.ViewUtils class SetupFragment : Fragment() { @@ -184,11 +184,7 @@ class SetupFragment : Fragment() { R.string.add_games_warning_description, R.string.add_games_warning_help, { - val preferences = - PreferenceManager.getDefaultSharedPreferences( - YuzuApplication.appContext - ) - if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { + if (NativeConfig.getGameDirs().isNotEmpty()) { StepState.COMPLETE } else { StepState.INCOMPLETE diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt new file mode 100755 index 000000000..274bc1c7b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class GameDir( + val uriString: String, + var deepScan: Boolean +) : Parcelable diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index 8512ed17c..752d98c10 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -12,6 +12,7 @@ import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString @@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.utils.GameHelper import org.yuzu.yuzu_emu.utils.GameMetadata +import org.yuzu.yuzu_emu.utils.NativeConfig class GamesViewModel : ViewModel() { val games: StateFlow> get() = _games @@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() { val searchFocused: StateFlow get() = _searchFocused private val _searchFocused = MutableStateFlow(false) + private val _folders = MutableStateFlow(mutableListOf()) + val folders = _folders.asStateFlow() + init { // Ensure keys are loaded so that ROM metadata can be decrypted. NativeLibrary.reloadKeys() @@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() { viewModelScope.launch { withContext(Dispatchers.IO) { + getGameDirs() if (storedGames!!.isNotEmpty()) { val deserializedGames = mutableSetOf() storedGames.forEach { @@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() { _searchFocused.value = searchFocused } - fun reloadGames(directoryChanged: Boolean) { + fun reloadGames(directoriesChanged: Boolean) { if (isReloading.value) { return } @@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() { setGames(GameHelper.getGames()) _isReloading.value = false - if (directoryChanged) { + if (directoriesChanged) { setShouldSwapData(true) } } } } + + fun addFolder(gameDir: GameDir) = + viewModelScope.launch { + withContext(Dispatchers.IO) { + NativeConfig.addGameDir(gameDir) + getGameDirs() + } + } + + fun removeFolder(gameDir: GameDir) = + viewModelScope.launch { + withContext(Dispatchers.IO) { + val gameDirs = _folders.value.toMutableList() + val removedDirIndex = gameDirs.indexOf(gameDir) + if (removedDirIndex != -1) { + gameDirs.removeAt(removedDirIndex) + NativeConfig.setGameDirs(gameDirs.toTypedArray()) + getGameDirs() + } + } + } + + fun updateGameDirs() = + viewModelScope.launch { + withContext(Dispatchers.IO) { + NativeConfig.setGameDirs(_folders.value.toTypedArray()) + getGameDirs() + } + } + + fun onOpenGameFoldersFragment() = + viewModelScope.launch { + withContext(Dispatchers.IO) { + getGameDirs() + } + } + + fun onCloseGameFoldersFragment() = + viewModelScope.launch { + withContext(Dispatchers.IO) { + getGameDirs(true) + } + } + + private fun getGameDirs(reloadList: Boolean = false) { + val gameDirs = NativeConfig.getGameDirs() + _folders.value = gameDirs.toMutableList() + if (reloadList) { + reloadGames(true) + } + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index 756f76721..251b5a667 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -3,15 +3,9 @@ package org.yuzu.yuzu_emu.model -import android.net.Uri -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.preference.PreferenceManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.utils.GameHelper class HomeViewModel : ViewModel() { val navigationVisible: StateFlow> get() = _navigationVisible @@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() { val shouldPageForward: StateFlow get() = _shouldPageForward private val _shouldPageForward = MutableStateFlow(false) - val gamesDir: StateFlow get() = _gamesDir - private val _gamesDir = MutableStateFlow( - Uri.parse( - PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - .getString(GameHelper.KEY_GAME_PATH, "") - ).path ?: "" - ) - var navigatedToSetup = false fun setNavigationVisibility(visible: Boolean, animated: Boolean) { @@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() { fun setShouldPageForward(pageForward: Boolean) { _shouldPageForward.value = pageForward } - - fun setGamesDir(activity: FragmentActivity, dir: String) { - ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) - _gamesDir.value = dir - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index 6f947674e..ccc981e95 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -13,8 +13,6 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem class SettingsViewModel : ViewModel() { var game: Game? = null - var shouldSave = false - var clickedItem: SettingsItem? = null val shouldRecreate: StateFlow get() = _shouldRecreate @@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() { fun clear() { game = null - shouldSave = false } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index bd2f4cd25..16323a316 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.getPublicFilesDir @@ -252,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { super.onResume() } + override fun onStop() { + super.onStop() + CoroutineScope(Dispatchers.IO).launch { + NativeConfig.saveSettings() + } + } + override fun onDestroy() { EmulationActivity.stopForegroundService(this) super.onDestroy() @@ -293,20 +301,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider { Intent.FLAG_GRANT_READ_URI_PERMISSION ) - // When a new directory is picked, we currently will reset the existing games - // database. This effectively means that only one game directory is supported. - PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() - .putString(GameHelper.KEY_GAME_PATH, result.toString()) - .apply() + val uriString = result.toString() + val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString } + if (folder != null) { + Toast.makeText( + applicationContext, + R.string.folder_already_added, + Toast.LENGTH_SHORT + ).show() + return + } - Toast.makeText( - applicationContext, - R.string.games_dir_selected, - Toast.LENGTH_LONG - ).show() - - gamesViewModel.reloadGames(true) - homeViewModel.setGamesDir(this, result.path!!) + AddGameFolderDialogFragment.newInstance(uriString) + .show(supportFragmentManager, AddGameFolderDialogFragment.TAG) } val getProdKey = diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 8c3268e9c..bbe7bfa92 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -364,6 +364,27 @@ object FileUtil { .lowercase() } + fun isTreeUriValid(uri: Uri): Boolean { + val resolver = context.contentResolver + val columns = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE + ) + return try { + val docId: String = if (isRootTreeUri(uri)) { + DocumentsContract.getTreeDocumentId(uri) + } else { + DocumentsContract.getDocumentId(uri) + } + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId) + resolver.query(childrenUri, columns, null, null, null) + true + } catch (_: Exception) { + false + } + } + @Throws(IOException::class) fun getStringFromFile(file: File): String = String(file.readBytes(), StandardCharsets.UTF_8) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index e6aca6b44..55010dc59 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.MinimalDocumentFile object GameHelper { - const val KEY_GAME_PATH = "game_path" + private const val KEY_OLD_GAME_PATH = "game_path" const val KEY_GAMES = "Games" private lateinit var preferences: SharedPreferences @@ -22,15 +23,43 @@ object GameHelper { fun getGames(): List { val games = mutableListOf() val context = YuzuApplication.appContext - val gamesDir = - PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "") - val gamesUri = Uri.parse(gamesDir) preferences = PreferenceManager.getDefaultSharedPreferences(context) + val gameDirs = mutableListOf() + val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: "" + if (oldGamesDir.isNotEmpty()) { + gameDirs.add(GameDir(oldGamesDir, true)) + preferences.edit().remove(KEY_OLD_GAME_PATH).apply() + } + gameDirs.addAll(NativeConfig.getGameDirs()) + // Ensure keys are loaded so that ROM metadata can be decrypted. NativeLibrary.reloadKeys() - addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3) + val badDirs = mutableListOf() + gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> + val gameDirUri = Uri.parse(gameDir.uriString) + val isValid = FileUtil.isTreeUriValid(gameDirUri) + if (isValid) { + addGamesRecursive( + games, + FileUtil.listFiles(gameDirUri), + if (gameDir.deepScan) 3 else 1 + ) + } else { + badDirs.add(index) + } + } + + // Remove all game dirs with insufficient permissions from config + if (badDirs.isNotEmpty()) { + var offset = 0 + badDirs.forEach { + gameDirs.removeAt(it - offset) + offset++ + } + } + NativeConfig.setGameDirs(gameDirs.toTypedArray()) // Cache list of games found on disk val serializedGames = mutableSetOf() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt index 87e579fa7..f4e1bb13f 100755 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt @@ -3,6 +3,8 @@ package org.yuzu.yuzu_emu.utils +import org.yuzu.yuzu_emu.model.GameDir + object NativeConfig { /** * Creates a Config object and opens the emulation config. @@ -54,4 +56,22 @@ object NativeConfig { external fun getConfigHeader(category: Int): String external fun getPairedSettingKey(key: String): String + + /** + * Gets every [GameDir] in AndroidSettings::values.game_dirs + */ + @Synchronized + external fun getGameDirs(): Array + + /** + * Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array + */ + @Synchronized + external fun setGameDirs(dirs: Array) + + /** + * Adds a single [GameDir] to the AndroidSettings::values.game_dirs array + */ + @Synchronized + external fun addGameDir(dir: GameDir) } diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index 3041c25c9..767d8ea83 100755 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -34,6 +34,7 @@ void AndroidConfig::SaveAllValues() { void AndroidConfig::ReadAndroidValues() { if (global) { ReadAndroidUIValues(); + ReadUIValues(); } } @@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() { EndGroup(); } +void AndroidConfig::ReadUIValues() { + BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); + + ReadPathValues(); + + EndGroup(); +} + +void AndroidConfig::ReadPathValues() { + BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); + + const int gamedirs_size = BeginArray(std::string("gamedirs")); + for (int i = 0; i < gamedirs_size; ++i) { + SetArrayIndex(i); + AndroidSettings::GameDir game_dir; + game_dir.path = ReadStringSetting(std::string("path")); + game_dir.deep_scan = + ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false)); + AndroidSettings::values.game_dirs.push_back(game_dir); + } + EndArray(); + + EndGroup(); +} + void AndroidConfig::SaveAndroidValues() { if (global) { SaveAndroidUIValues(); + SaveUIValues(); } WriteToIni(); @@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() { EndGroup(); } +void AndroidConfig::SaveUIValues() { + BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); + + SavePathValues(); + + EndGroup(); +} + +void AndroidConfig::SavePathValues() { + BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); + + BeginArray(std::string("gamedirs")); + for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) { + SetArrayIndex(i); + const auto& game_dir = AndroidSettings::values.game_dirs[i]; + WriteSetting(std::string("path"), game_dir.path); + WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false)); + } + EndArray(); + + EndGroup(); +} + std::vector& AndroidConfig::FindRelevantList(Settings::Category category) { auto& map = Settings::values.linkage.by_category; if (map.contains(category)) { diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h index e679392fd..f490be016 100755 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h @@ -19,9 +19,9 @@ protected: void ReadAndroidUIValues(); void ReadHidbusValues() override {} void ReadDebugControlValues() override {} - void ReadPathValues() override {} + void ReadPathValues() override; void ReadShortcutValues() override {} - void ReadUIValues() override {} + void ReadUIValues() override; void ReadUIGamelistValues() override {} void ReadUILayoutValues() override {} void ReadMultiplayerValues() override {} @@ -30,9 +30,9 @@ protected: void SaveAndroidUIValues(); void SaveHidbusValues() override {} void SaveDebugControlValues() override {} - void SavePathValues() override {} + void SavePathValues() override; void SaveShortcutValues() override {} - void SaveUIValues() override {} + void SaveUIValues() override; void SaveUIGamelistValues() override {} void SaveUILayoutValues() override {} void SaveMultiplayerValues() override {} diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index 37bc33918..fc0523206 100755 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -9,9 +9,17 @@ namespace AndroidSettings { +struct GameDir { + std::string path; + bool deep_scan = false; +}; + struct Values { Settings::Linkage linkage; + // Path settings + std::vector game_dirs; + // Android Settings::Setting picture_in_picture{linkage, false, "picture_in_picture", Settings::Category::Android}; diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 960abf95a..a56ed5662 100755 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -13,6 +13,8 @@ static JavaVM* s_java_vm; static jclass s_native_library_class; static jclass s_disk_cache_progress_class; static jclass s_load_callback_stage_class; +static jclass s_game_dir_class; +static jmethodID s_game_dir_constructor; static jmethodID s_exit_emulation_activity; static jmethodID s_disk_cache_load_progress; static jmethodID s_on_emulation_started; @@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() { return s_load_callback_stage_class; } +jclass GetGameDirClass() { + return s_game_dir_class; +} + +jmethodID GetGameDirConstructor() { + return s_game_dir_constructor; +} + jmethodID GetExitEmulationActivity() { return s_exit_emulation_activity; } @@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_load_callback_stage_class = reinterpret_cast(env->NewGlobalRef(env->FindClass( "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage"))); + const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir"); + s_game_dir_class = reinterpret_cast(env->NewGlobalRef(game_dir_class)); + s_game_dir_constructor = env->GetMethodID(game_dir_class, "", "(Ljava/lang/String;Z)V"); + env->DeleteLocalRef(game_dir_class); + // Initialize methods s_exit_emulation_activity = env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); @@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_disk_cache_progress_class); env->DeleteGlobalRef(s_load_callback_stage_class); + env->DeleteGlobalRef(s_game_dir_class); // UnInitialize applets SoftwareKeyboard::CleanupJNI(env); diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index b76158928..855649efa 100755 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread(); jclass GetNativeLibraryClass(); jclass GetDiskCacheProgressClass(); jclass GetDiskCacheLoadCallbackStageClass(); +jclass GetGameDirClass(); +jmethodID GetGameDirConstructor(); jmethodID GetExitEmulationActivity(); jmethodID GetDiskCacheLoadProgress(); jmethodID GetOnEmulationStarted(); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 617288ae4..ed5ce6f8a 100755 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -694,6 +694,14 @@ jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jcl return j_stats; } +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) { + if (Settings::IsNceEnabled()) { + return ToJString(env, "NCE"); + } + + return ToJString(env, "JIT"); +} + void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env, jclass clazz, jstring j_path) {} diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 8e81816e5..763b2164c 100755 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -11,6 +11,7 @@ #include "common/settings.h" #include "frontend_common/config.h" #include "jni/android_common/android_common.h" +#include "jni/id_cache.h" std::unique_ptr config; @@ -253,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e return ToJString(env, setting->PairedSetting()->GetLabel()); } +jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { + jclass gameDirClass = IDCache::GetGameDirClass(); + jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); + jobjectArray jgameDirArray = + env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr); + for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) { + jobject jgameDir = + env->NewObject(gameDirClass, gameDirConstructor, + ToJString(env, AndroidSettings::values.game_dirs[i].path), + static_cast(AndroidSettings::values.game_dirs[i].deep_scan)); + env->SetObjectArrayElement(jgameDirArray, i, jgameDir); + } + return jgameDirArray; +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj, + jobjectArray gameDirs) { + AndroidSettings::values.game_dirs.clear(); + int size = env->GetArrayLength(gameDirs); + + if (size == 0) { + return; + } + + jobject dir = env->GetObjectArrayElement(gameDirs, 0); + jclass gameDirClass = IDCache::GetGameDirClass(); + jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;"); + jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z"); + for (int i = 0; i < size; ++i) { + dir = env->GetObjectArrayElement(gameDirs, i); + jstring juriString = static_cast(env->GetObjectField(dir, uriStringField)); + jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField); + std::string uriString = GetJString(env, juriString); + AndroidSettings::values.game_dirs.push_back( + AndroidSettings::GameDir{uriString, static_cast(jdeepScanBoolean)}); + } +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj, + jobject gameDir) { + jclass gameDirClass = IDCache::GetGameDirClass(); + jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;"); + jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z"); + + jstring juriString = static_cast(env->GetObjectField(gameDir, uriStringField)); + jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField); + std::string uriString = GetJString(env, juriString); + AndroidSettings::values.game_dirs.push_back( + AndroidSettings::GameDir{uriString, static_cast(jdeepScanBoolean)}); +} + } // extern "C" diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml new file mode 100755 index 000000000..4e0c04b6b --- /dev/null +++ b/src/android/app/src/main/res/layout/card_folder.xml @@ -0,0 +1,70 @@ + + + + + + + + + +