From fdb1f27e7d83a4d0de4577e493ad8b8f499b410d Mon Sep 17 00:00:00 2001 From: SSimco <37044560+SSimco@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:07:01 +0200 Subject: [PATCH] Add gui for adding custom drivers --- .gitmodules | 6 +- CMakeLists.txt | 4 + dependencies/libadrenotools | 1 + src/Cafe/CMakeLists.txt | 3 + .../HW/Latte/Renderer/Vulkan/VulkanAPI.cpp | 117 +++++++++- src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h | 5 + src/android/app/build.gradle.kts | 3 + src/android/app/src/main/cpp/JNIUtils.h | 2 +- .../app/src/main/cpp/NativeActiveSettings.cpp | 21 ++ .../app/src/main/cpp/NativeEmulation.cpp | 17 +- .../app/src/main/cpp/NativeSettings.cpp | 18 +- .../java/info/cemu/cemu/CemuApplication.kt | 7 +- .../cemu/graphicpacks/GraphicPacksActivity.kt | 10 +- .../info/cemu/cemu/guicore/SingleSelection.kt | 71 +++--- .../nativeinterface/NativeActiveSettings.kt | 9 + .../cemu/nativeinterface/NativeEmulation.kt | 6 +- .../cemu/nativeinterface/NativeSettings.kt | 12 + .../cemu/cemu/settings/SettingsActivity.kt | 30 ++- .../customdrivers/CustomDriversScreen.kt | 206 ++++++++++++++++++ .../customdrivers/CustomDriversViewModel.kt | 201 +++++++++++++++++ .../graphics/GraphicsSettingsScreen.kt | 21 +- .../cemu/titlemanager/TitleListViewModel.kt | 4 +- .../main/java/info/cemu/cemu/utils/Json.kt | 17 ++ .../src/main/java/info/cemu/cemu/utils/Zip.kt | 7 +- .../app/src/main/res/values/strings.xml | 1 + src/config/ActiveSettings.cpp | 11 + src/config/ActiveSettings.h | 22 +- src/config/CemuConfig.cpp | 8 + src/config/CemuConfig.h | 4 + 29 files changed, 765 insertions(+), 79 deletions(-) create mode 160000 dependencies/libadrenotools create mode 100644 src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt create mode 100644 src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversViewModel.kt create mode 100644 src/android/app/src/main/java/info/cemu/cemu/utils/Json.kt diff --git a/.gitmodules b/.gitmodules index a2cded61..16b04b9a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,9 +21,9 @@ [submodule "dependencies/xbyak_aarch64"] path = dependencies/xbyak_aarch64 url = https://github.com/fujitsu/xbyak_aarch64 +[submodule "dependencies/cpuid"] + path = dependencies/cpuid + url = https://github.com/SSimco/cpuid [submodule "dependencies/libadrenotools"] path = dependencies/libadrenotools url = https://github.com/bylaws/libadrenotools -[submodule "dependencies/libucontext/libucontext"] - path = dependencies/libucontext/libucontext - url = https://github.com/kaniini/libucontext diff --git a/CMakeLists.txt b/CMakeLists.txt index 56212697..80f41639 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -255,4 +255,8 @@ if (NOT ZArchive_FOUND) add_subdirectory("dependencies/ZArchive" EXCLUDE_FROM_ALL) endif() +if(ANDROID) + add_subdirectory("dependencies/libadrenotools" EXCLUDE_FROM_ALL) +endif() + add_subdirectory(src) diff --git a/dependencies/libadrenotools b/dependencies/libadrenotools new file mode 160000 index 00000000..8fae8ce2 --- /dev/null +++ b/dependencies/libadrenotools @@ -0,0 +1 @@ +Subproject commit 8fae8ce254dfc1344527e05301e43f37dea2df80 diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 2b7e3bf3..ca142e09 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -549,6 +549,9 @@ if(ANDROID) Filesystem/fscDeviceAndroidSAF.cpp Filesystem/fscDeviceAndroidSAF.h ) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "(aarch64)|(AARCH64)") + target_link_libraries(CemuCafe PRIVATE adrenotools) + endif() endif() set_property(TARGET CemuCafe PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.cpp index ad32b541..2df980cc 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.cpp @@ -136,10 +136,93 @@ bool InitializeDeviceVulkan(VkDevice device) #else +void* g_vulkan_so = nullptr; + +#if __ANDROID__ +bool SupportsLoadingCustomDriver() +{ +#ifdef __aarch64__ + std::error_code ec; + return fs::exists("/dev/kgsl-3d0", ec); +#else + return false; +#endif +} + +#ifdef __aarch64__ + +constexpr auto CUSTOM_DRIVER_LIB_NAME = "custom_vulkan.so"; + +#include +#include +#include +#include "config/ActiveSettings.h" + +std::string get_custom_driver_lib_name(const fs::path& driver_path) +{ + static constexpr auto LIB_NAME_MEMBER = "libraryName"; + std::ifstream in(driver_path / "meta.json"); + if (!in.is_open()) + return {}; + rapidjson::IStreamWrapper str(in); + rapidjson::Document doc; + doc.ParseStream(str); + + if (!doc.HasMember(LIB_NAME_MEMBER) || !doc[LIB_NAME_MEMBER].IsString()) + return {}; + + std::string lib_name = doc[LIB_NAME_MEMBER].GetString(); + + std::error_code ec; + if (!fs::exists(driver_path / lib_name, ec)) + return {}; + + return lib_name; +} + +void* load_custom_driver() +{ + std::string driver_path = g_config.data().custom_driver_path; + if (driver_path.empty()) + return nullptr; + std::string driver_name = get_custom_driver_lib_name(driver_path); + if (driver_name.empty()) + return nullptr; + + std::error_code ec; + fs::copy(fs::path(driver_path) / driver_name, ActiveSettings::GetInternalPath(CUSTOM_DRIVER_LIB_NAME), fs::copy_options::overwrite_existing, ec); + + void* vulkan_so = adrenotools_open_libvulkan( + RTLD_NOW | RTLD_LOCAL, + ADRENOTOOLS_DRIVER_CUSTOM, + nullptr, + (ActiveSettings::GetNativeLibPath().string() + "/").c_str(), + (ActiveSettings::GetInternalPath().string() + "/").c_str(), + CUSTOM_DRIVER_LIB_NAME, + nullptr, + nullptr); + if (!vulkan_so) + { + cemuLog_log(LogType::Force, "Failed to load custom driver"); + return nullptr; + } + cemuLog_log(LogType::Force, "Loaded custom driver"); + return vulkan_so; +} +#endif // __aarch64__ + +#endif // __ANDROID__ + void* dlopen_vulkan_loader() { #if BOOST_OS_LINUX - void* vulkan_so = dlopen("libvulkan.so", RTLD_NOW); + static void* vulkan_so = nullptr; +#if __ANDROID__ && defined(__aarch64__) + vulkan_so = load_custom_driver(); + if (vulkan_so) + return vulkan_so; +#endif + vulkan_so = dlopen("libvulkan.so", RTLD_NOW); if(!vulkan_so) vulkan_so = dlopen("libvulkan.so.1", RTLD_NOW); #elif BOOST_OS_MACOS @@ -150,17 +233,19 @@ void* dlopen_vulkan_loader() bool InitializeGlobalVulkan() { - void* vulkan_so = dlopen_vulkan_loader(); + g_vulkan_so = dlopen_vulkan_loader(); - if(g_vulkan_available) + if (g_vulkan_available) return true; - if (!vulkan_so) + if (!g_vulkan_so) { cemuLog_log(LogType::Force, "Vulkan loader not available."); return false; } + void* vulkan_so = g_vulkan_so; + #define VKFUNC_INIT #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" @@ -169,26 +254,42 @@ bool InitializeGlobalVulkan() cemuLog_log(LogType::Force, "vkEnumerateInstanceVersion not available. Outdated graphics driver or Vulkan runtime?"); return false; } - + g_vulkan_available = true; return true; } +void CleanupGlobalVulkan() +{ + if (g_vulkan_so) + { + dlclose(g_vulkan_so); + g_vulkan_so = nullptr; + } + + g_vulkan_available = false; + +#if __ANDROID__ && defined(__aarch64__) + std::error_code ec; + fs::remove(ActiveSettings::GetInternalPath(CUSTOM_DRIVER_LIB_NAME), ec); +#endif +} + bool InitializeInstanceVulkan(VkInstance instance) { - void* vulkan_so = dlopen_vulkan_loader(); + void* vulkan_so = g_vulkan_so; if (!vulkan_so) return false; #define VKFUNC_INSTANCE_INIT #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" - + return true; } bool InitializeDeviceVulkan(VkDevice device) { - void* vulkan_so = dlopen_vulkan_loader(); + void* vulkan_so = g_vulkan_so; if (!vulkan_so) return false; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h index b66438db..76cad56c 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h @@ -10,6 +10,11 @@ bool InitializeGlobalVulkan(); bool InitializeInstanceVulkan(VkInstance instance); bool InitializeDeviceVulkan(VkDevice device); + +#if __ANDROID__ +bool SupportsLoadingCustomDriver(); +#endif + extern bool g_vulkan_available; #endif diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 6c69ddcb..8a6720ea 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -47,6 +47,9 @@ android { versionName = getVersionName() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + packaging { + jniLibs.useLegacyPackaging = true + } val keystoreFilePath: String? = System.getenv("ANDROID_KEYSTORE_FILE") signingConfigs { if (keystoreFilePath != null) { diff --git a/src/android/app/src/main/cpp/JNIUtils.h b/src/android/app/src/main/cpp/JNIUtils.h index d91fb0d8..730fdbe3 100644 --- a/src/android/app/src/main/cpp/JNIUtils.h +++ b/src/android/app/src/main/cpp/JNIUtils.h @@ -182,7 +182,7 @@ namespace JNIUtils jobject createJavaLongArrayList(JNIEnv* env, const std::vector& values); template - jobject newObject(JNIEnv* env, const std::string& className, const std::string& ctrSig = "()V", TArgs... args) + jobject newObject(JNIEnv* env, const std::string& className, const std::string& ctrSig = "()V", TArgs&&... args) { jclass javaClass = env->FindClass(className.c_str()); jmethodID ctrId = env->GetMethodID(javaClass, "", ctrSig.c_str()); diff --git a/src/android/app/src/main/cpp/NativeActiveSettings.cpp b/src/android/app/src/main/cpp/NativeActiveSettings.cpp index 4c7c8479..71993588 100644 --- a/src/android/app/src/main/cpp/NativeActiveSettings.cpp +++ b/src/android/app/src/main/cpp/NativeActiveSettings.cpp @@ -12,3 +12,24 @@ Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_getUserDataPath(JNIEnv* { return JNIUtils::toJString(env, ActiveSettings::GetUserDataPath()); } + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_initializeActiveSettings(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring data_path, jstring cache_path) +{ + std::string dataPath = JNIUtils::toString(env, data_path); + std::string cachePath = JNIUtils::toString(env, cache_path); + std::set failedWriteAccess; + ActiveSettings::SetPaths(false, {}, dataPath, dataPath, cachePath, dataPath, failedWriteAccess); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_setNativeLibDir(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring native_lib_dir) +{ + ActiveSettings::SetNativeLibPath(JNIUtils::toString(env, native_lib_dir)); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_setInternalDir(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring internal_dir) +{ + ActiveSettings::SetInternalDir(JNIUtils::toString(env, internal_dir)); +} \ No newline at end of file diff --git a/src/android/app/src/main/cpp/NativeEmulation.cpp b/src/android/app/src/main/cpp/NativeEmulation.cpp index 88bad82a..05bf1a9d 100644 --- a/src/android/app/src/main/cpp/NativeEmulation.cpp +++ b/src/android/app/src/main/cpp/NativeEmulation.cpp @@ -158,15 +158,6 @@ Java_info_cemu_cemu_nativeinterface_NativeEmulation_setReplaceTVWithPadView([[ma GuiSystem::getWindowInfo().set_keystate(GuiSystem::PlatformKeyCodes::TAB, swapped); } -extern "C" [[maybe_unused]] JNIEXPORT void JNICALL -Java_info_cemu_cemu_nativeinterface_NativeEmulation_initializeActiveSettings(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring data_path, jstring cache_path) -{ - std::string dataPath = JNIUtils::toString(env, data_path); - std::string cachePath = JNIUtils::toString(env, cache_path); - std::set failedWriteAccess; - ActiveSettings::SetPaths(false, {}, dataPath, dataPath, cachePath, dataPath, failedWriteAccess); -} - extern "C" [[maybe_unused]] JNIEXPORT void JNICALL Java_info_cemu_cemu_nativeinterface_NativeEmulation_initializeEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { @@ -177,12 +168,12 @@ Java_info_cemu_cemu_nativeinterface_NativeEmulation_initializeEmulation([[maybe_ ActiveSettings::Init(); LatteOverlay_init(); CemuCommonInit(); - InitializeGlobalVulkan(); } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL Java_info_cemu_cemu_nativeinterface_NativeEmulation_initializeRenderer(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject j_testSurface) { + InitializeGlobalVulkan(); using ANativewindow_Ptr = std::unique_ptr; JNIUtils::handleNativeException(env, [&]() { cemu_assert_debug(j_testSurface != nullptr); @@ -216,6 +207,12 @@ Java_info_cemu_cemu_nativeinterface_NativeEmulation_recreateRenderSurface([[mayb // TODO } +extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL +Java_info_cemu_cemu_nativeinterface_NativeEmulation_supportsLoadingCustomDriver([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + return SupportsLoadingCustomDriver(); +} + extern "C" [[maybe_unused]] JNIEXPORT void JNICALL Java_info_cemu_cemu_nativeinterface_NativeEmulation_setSurface(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject surface, jboolean is_main_canvas) { diff --git a/src/android/app/src/main/cpp/NativeSettings.cpp b/src/android/app/src/main/cpp/NativeSettings.cpp index c6c4f867..d491ea9a 100644 --- a/src/android/app/src/main/cpp/NativeSettings.cpp +++ b/src/android/app/src/main/cpp/NativeSettings.cpp @@ -361,4 +361,20 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setConsoleLanguage([[maybe_un { g_config.data().console_language = static_cast(console_language); g_config.Save(); -} \ No newline at end of file +} + +extern "C" [[maybe_unused]] JNIEXPORT jstring JNICALL +Java_info_cemu_cemu_nativeinterface_NativeSettings_getCustomDriverPath(JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + std::string customDriverPath = g_config.data().custom_driver_path; + if (customDriverPath.empty()) + return nullptr; + return JNIUtils::toJString(env, customDriverPath); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeSettings_setCustomDriverPath(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring custom_driver_path) +{ + g_config.data().custom_driver_path = JNIUtils::toString(env, custom_driver_path); + g_config.Save(); +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt index 703a1f53..5594f7ef 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt @@ -1,7 +1,10 @@ package info.cemu.cemu import android.app.Application -import info.cemu.cemu.nativeinterface.NativeEmulation.initializeActiveSettings +import android.util.Log +import info.cemu.cemu.nativeinterface.NativeActiveSettings.initializeActiveSettings +import info.cemu.cemu.nativeinterface.NativeActiveSettings.setInternalDir +import info.cemu.cemu.nativeinterface.NativeActiveSettings.setNativeLibDir import info.cemu.cemu.nativeinterface.NativeEmulation.initializeEmulation import info.cemu.cemu.nativeinterface.NativeEmulation.setDPI import info.cemu.cemu.nativeinterface.NativeGraphicPacks.refreshGraphicPacks @@ -44,6 +47,8 @@ class CemuApplication : Application() { val displayMetrics = resources.displayMetrics setDPI(displayMetrics.density) initializeActiveSettings(internalFolder.toString(), internalFolder.toString()) + setNativeLibDir(applicationInfo.nativeLibraryDir) + setInternalDir(dataDir.absolutePath) initializeEmulation() initializeSwkbd() refreshGraphicPacks() diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt index 0c2f0396..508c0df4 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksActivity.kt @@ -4,6 +4,8 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.MutableCreationExtras import androidx.lifecycle.viewmodel.compose.viewModel @@ -75,7 +77,13 @@ fun GraphicPacksNav( NavHost( navController = navController, - startDestination = GraphicPackRoutes.GraphicPacksRootSectionRoute + startDestination = GraphicPackRoutes.GraphicPacksRootSectionRoute, + enterTransition = { + EnterTransition.None + }, + exitTransition = { + ExitTransition.None + } ) { composable { backStackEntry -> val graphicPackViewModel: GraphicPackViewModel = viewModel(backStackEntry) diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt index 799e366b..f8a330f5 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/SingleSelection.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -141,46 +142,46 @@ fun SelectDialog( .fillMaxWidth(), shape = RoundedCornerShape(16.dp), ) { - Box( + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 8.dp + ), + text = label, + fontSize = 24.sp, + ) + Column( modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp) + .weight(weight = 1.0f, fill = false) + .verticalScroll(rememberScrollState()), ) { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.padding(8.dp), - text = label, - fontSize = 24.sp, + choices.forEach { choice -> + Choice( + label = choiceToString(choice), + selected = currentChoice == choice, + isEnabled = isChoiceEnabled(choice), + onClick = { + onChoiceChanged(choice) + onDismissRequest() + }, ) - Column( - modifier = Modifier - .padding(vertical = 8.dp) - .weight(weight = 1.0f, fill = false) - .verticalScroll(rememberScrollState()), - ) { - choices.forEach { choice -> - val isEnabled = isChoiceEnabled(choice) - Choice( - label = choiceToString(choice), - selected = currentChoice == choice, - isEnabled = isEnabled, - onClick = { - onChoiceChanged(choice) - onDismissRequest() - }, - ) - } - } - TextButton( - onClick = onDismissRequest, - modifier = Modifier - .padding(8.dp) - .align(Alignment.End), - ) { - Text(stringResource(R.string.cancel)) - } } } + + HorizontalDivider() + + TextButton( + onClick = onDismissRequest, + modifier = Modifier + .padding(16.dp) + .align(Alignment.End), + ) { + Text(stringResource(R.string.cancel)) + } } } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt index ecde9c49..7205494a 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt @@ -1,6 +1,15 @@ package info.cemu.cemu.nativeinterface object NativeActiveSettings { + @JvmStatic + external fun initializeActiveSettings(dataPath: String, cachePath: String) + + @JvmStatic + external fun setNativeLibDir(nativeLibDir: String) + + @JvmStatic + external fun setInternalDir(internalDir: String) + @JvmStatic external fun getMLCPath(): String diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeEmulation.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeEmulation.kt index feec86d8..867290a9 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeEmulation.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeEmulation.kt @@ -3,9 +3,6 @@ package info.cemu.cemu.nativeinterface import android.view.Surface object NativeEmulation { - @JvmStatic - external fun initializeActiveSettings(dataPath: String?, cachePath: String?) - @JvmStatic external fun initializeEmulation() @@ -38,4 +35,7 @@ object NativeEmulation { @JvmStatic external fun recreateRenderSurface(isMainCanvas: Boolean) + + @JvmStatic + external fun supportsLoadingCustomDriver(): Boolean } diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt index f46f9cdf..358e5cf2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeSettings.kt @@ -192,4 +192,16 @@ object NativeSettings { @JvmStatic external fun setConsoleLanguage(value: Int) + + /** + * @return the selected driver directory path. If it's null, then the default system driver will be used. + */ + @JvmStatic + external fun getCustomDriverPath(): String? + + /** + * Sets the selected driver directory [path]. To use the default system driver, pass a null [path]. + */ + @JvmStatic + external fun setCustomDriverPath(path: String?) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt index 2f7f868d..5b52bc6c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsActivity.kt @@ -3,6 +3,8 @@ package info.cemu.cemu.settings import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -11,6 +13,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import info.cemu.cemu.guicore.ActivityContent import info.cemu.cemu.settings.audio.AudioSettingsScreen +import info.cemu.cemu.settings.customdrivers.CustomDriversScreen import info.cemu.cemu.settings.gamespath.GamePathsScreen import info.cemu.cemu.settings.general.GeneralSettingsScreen import info.cemu.cemu.settings.graphics.GraphicsSettingsScreen @@ -32,9 +35,9 @@ class SettingsActivity : ComponentActivity() { } } -sealed class SettingsRoutes { +private sealed class SettingsRoutes { @Serializable - object GeneralSettingsRoute + object GeneralSettings @Serializable object GeneralSettingsScreenRoute @@ -51,6 +54,9 @@ sealed class SettingsRoutes { @Serializable object GraphicsSettingsScreenRoute + @Serializable + object CustomDriversScreenRoute + @Serializable object GamePathsScreenRoute @@ -82,13 +88,19 @@ fun SettingsNav( NavHost( navController = navController, - startDestination = SettingsRoutes.SettingsHomeScreenRoute + startDestination = SettingsRoutes.SettingsHomeScreenRoute, + enterTransition = { + EnterTransition.None + }, + exitTransition = { + ExitTransition.None + } ) { composable { SettingsHomeScreen( navigateBack = ::navigateBack, actions = SettingsHomeScreenActions( - goToGeneralSettings = { navController.navigate(SettingsRoutes.GeneralSettingsRoute) }, + goToGeneralSettings = { navController.navigate(SettingsRoutes.GeneralSettings) }, goToInputSettings = { navController.navigate(SettingsRoutes.InputSettingsRoute) }, goToGraphicsSettings = { navController.navigate(SettingsRoutes.GraphicsSettingsScreenRoute) }, goToAudioSettings = { navController.navigate(SettingsRoutes.AudioSettingsScreenRoute) }, @@ -104,6 +116,14 @@ fun SettingsNav( composable { GraphicsSettingsScreen( navigateBack = ::navigateBack, + goToCustomDriversSettings = { + navController.navigate(SettingsRoutes.CustomDriversScreenRoute) + } + ) + } + composable { + CustomDriversScreen( + navigateBack = ::navigateBack, ) } composable { @@ -143,7 +163,7 @@ fun SettingsNav( ) } } - navigation(startDestination = SettingsRoutes.GeneralSettingsScreenRoute) { + navigation(startDestination = SettingsRoutes.GeneralSettingsScreenRoute) { composable { GeneralSettingsScreen( navigateBack = ::navigateBack, diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt new file mode 100644 index 00000000..eba29cd8 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt @@ -0,0 +1,206 @@ +package info.cemu.cemu.settings.customdrivers + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.RadioButton +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import info.cemu.cemu.R +import info.cemu.cemu.guicore.ScreenContentLazy +import kotlinx.coroutines.launch + +@Composable +fun CustomDriversScreen( + navigateBack: () -> Unit, + customDriversViewModel: CustomDriversViewModel = viewModel(), +) { + val coroutineScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val installedDrivers by customDriversViewModel.installedDrivers.collectAsState() + val isSystemDriverSelected by customDriversViewModel.isSystemDriverSelected.collectAsState() + val context = LocalContext.current + + val customDriversInstallLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + val installStatus = context.contentResolver.openInputStream(uri)?.use { + customDriversViewModel.installDriver(it) + } + val errorMessage = when (installStatus) { + DriverInstallStatus.AlreadyInstalled -> "Driver already installed" + DriverInstallStatus.ErrorInstalling -> "Failed to install driver" + else -> return@rememberLauncherForActivityResult + } + coroutineScope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar(errorMessage) + } + } + + ScreenContentLazy( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + appBarText = "Custom drivers", + navigateBack = navigateBack, + actions = { + IconButton(onClick = { customDriversInstallLauncher.launch(arrayOf("application/zip")) }) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Add custom driver", + ) + } + }, + ) { + item { + SystemDriverListItem( + selected = isSystemDriverSelected, + onSelect = customDriversViewModel::setSystemDriverSelected + ) + } + items(installedDrivers) { + CustomDriverListItem( + driver = it, + onDelete = { customDriversViewModel.deleteDriver(it) }, + onSelect = { customDriversViewModel.setDriverSelected(it) } + ) + } + } +} + +@Composable +private fun SystemDriverListItem(selected: Boolean, onSelect: () -> Unit) { + DriverListItem( + driverLabel = "System driver", + selected = selected, + onSelect = onSelect + ) +} + +@Composable +private fun CustomDriverListItem(driver: Driver, onDelete: () -> Unit, onSelect: () -> Unit) { + var showDriverInfo by remember { mutableStateOf(false) } + + DriverListItem( + driverLabel = driver.metadata.name, + selected = driver.selected, + onSelect = onSelect, + labelExtraContent = { + IconButton(onClick = { showDriverInfo = !showDriverInfo }) { + Icon( + modifier = Modifier.rotate(if (showDriverInfo) 180f else 0f), + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = "Show driver metadata" + ) + } + IconButton(onClick = onDelete) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.remove_game_path), + ) + } + } + ) { + if (showDriverInfo) { + DriverMetadataInfo(driver.metadata) + } + } +} + +@Composable +private fun DriverListItem( + driverLabel: String, + selected: Boolean, + onSelect: () -> Unit, + labelExtraContent: @Composable RowScope.() -> Unit = {}, + content: @Composable ColumnScope.() -> Unit = {}, +) { + Card( + modifier = Modifier + .animateContentSize() + .fillMaxWidth() + .padding(8.dp), + onClick = onSelect + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton(selected = selected, onClick = onSelect) + Text( + modifier = Modifier + .padding(horizontal = 4.dp) + .basicMarquee(iterations = Int.MAX_VALUE) + .weight(1.0f), + text = driverLabel, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ) + labelExtraContent() + } + content() + } +} + +@Composable +private fun DriverMetadataInfo(metadata: DriverMetadata) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + DriverMetadataInfo("Description", metadata.description) + DriverMetadataInfo("Author", metadata.author) + DriverMetadataInfo("Package version", metadata.packageVersion) + DriverMetadataInfo("Vendor", metadata.vendor) + DriverMetadataInfo("Driver version", metadata.driverVersion) + DriverMetadataInfo("Min api", metadata.minApi) + } +} + +@Composable +private fun DriverMetadataInfo(label: String, info: T) { + Text( + modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 2.dp), + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + text = label + ) + Text( + modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 2.dp), + fontSize = 14.sp, + text = info.toString(), + ) +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversViewModel.kt new file mode 100644 index 00000000..cba4e30d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversViewModel.kt @@ -0,0 +1,201 @@ +@file:OptIn(ExperimentalPathApi::class, ExperimentalUuidApi::class) + +package info.cemu.cemu.settings.customdrivers + +import android.os.Build +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import info.cemu.cemu.nativeinterface.NativeActiveSettings +import info.cemu.cemu.nativeinterface.NativeSettings +import info.cemu.cemu.utils.decodeJsonFromFile +import info.cemu.cemu.utils.unzip +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.serialization.Serializable +import java.io.File +import java.io.InputStream +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteRecursively +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.io.path.moveTo +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@Serializable +data class DriverMetadata( + val schemaVersion: Int, + val name: String, + val description: String, + val author: String, + val packageVersion: String, + val vendor: String, + val driverVersion: String, + val minApi: Int, + val libraryName: String, +) + +data class Driver( + val path: String, + val metadata: DriverMetadata, + val selected: Boolean = false, +) + +enum class DriverInstallStatus { + Installed, + AlreadyInstalled, + ErrorInstalling, +} + +class CustomDriversViewModel : ViewModel() { + private val selectedDriverPath = MutableStateFlow(NativeSettings.getCustomDriverPath()) + val isSystemDriverSelected = selectedDriverPath.map { it == null }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + false + ) + + private val _installedDrivers = MutableStateFlow>(emptyList()) + val installedDrivers = _installedDrivers.asStateFlow() + + init { + _installedDrivers.value = parseInstalledDrivers() + } + + private fun parseInstalledDrivers(): List { + val customDriversDir = getCustomDriversDir() + + if (!customDriversDir.isDirectory()) + return emptyList() + + val driverDirs: Array = customDriversDir.toFile().listFiles() ?: return emptyList() + + val drivers = mutableListOf() + val selectedDriver = selectedDriverPath.value + + for (driverDir in driverDirs) { + if (!driverDir.isDirectory) + continue + val metadata = + decodeJsonFromFile(driverDir.resolve(META_FILE_NAME)) ?: continue + val driver = Driver( + path = driverDir.path, + metadata = metadata, + selected = selectedDriver == driverDir.path, + ) + drivers.add(driver) + } + + drivers.sortBy { it.metadata.name } + + return drivers + } + + fun installDriver(driverZipFileInputStream: InputStream): DriverInstallStatus { + val tempDir = + Path(NativeActiveSettings.getUserDataPath()).resolve(Uuid.random().toString()) + + try { + tempDir.createDirectories() + unzip(driverZipFileInputStream, tempDir) + + val metadata = + decodeJsonFromFile(tempDir.resolve(META_FILE_NAME).toFile()) + if (metadata == null + || metadata.minApi > Build.VERSION.SDK_INT + || metadata.schemaVersion != SUPPORTED_SCHEMA_VERSION + || !tempDir.resolve(metadata.libraryName).exists() + ) { + tempDir.deleteRecursively() + return DriverInstallStatus.ErrorInstalling + } + + if (_installedDrivers.value.any { it.metadata == metadata }) { + tempDir.deleteRecursively() + return DriverInstallStatus.AlreadyInstalled + } + + val customDriversDir = getCustomDriversDir() + customDriversDir.createDirectories() + val driverPath = tempDir.moveTo(customDriversDir.resolve(tempDir.fileName)) + + _installedDrivers.value = _installedDrivers.value.toMutableList().apply { + val driver = Driver( + metadata = metadata, + path = driverPath.toString(), + ) + add(driver) + sortBy { it.metadata.name } + } + + return DriverInstallStatus.Installed + } catch (exception: Exception) { + tempDir.deleteRecursively() + return DriverInstallStatus.ErrorInstalling + } + } + + fun deleteDriver(driver: Driver) { + if (!_installedDrivers.value.any { it == driver }) + return + + _installedDrivers.value -= driver + if (selectedDriverPath.value == driver.path) { + selectedDriverPath.value = null + NativeSettings.setCustomDriverPath(null) + } + + Path(driver.path).toFile().deleteRecursively() + } + + fun setSystemDriverSelected() { + if (selectedDriverPath.value == null) + return + + val installedDrivers = _installedDrivers.value.toMutableList() + val oldSelectedDriverIndex = installedDrivers.indexOfFirst { it.selected } + if (oldSelectedDriverIndex != -1) { + installedDrivers[oldSelectedDriverIndex] = + installedDrivers[oldSelectedDriverIndex].copy(selected = false) + _installedDrivers.value = installedDrivers + } + + selectedDriverPath.value = null + NativeSettings.setCustomDriverPath(null) + } + + fun setDriverSelected(driver: Driver) { + if (selectedDriverPath.value == driver.path) + return + + val installedDrivers = _installedDrivers.value.toMutableList() + + val oldSelectedDriverIndex = installedDrivers.indexOfFirst { it.selected } + if (oldSelectedDriverIndex != -1) + installedDrivers[oldSelectedDriverIndex] = + installedDrivers[oldSelectedDriverIndex].copy(selected = false) + + val newSelectedDriverIndex = installedDrivers.indexOf(driver) + if (newSelectedDriverIndex == -1) + return + installedDrivers[newSelectedDriverIndex] = driver.copy(selected = true) + + _installedDrivers.value = installedDrivers + + NativeSettings.setCustomDriverPath(driver.path) + selectedDriverPath.value = driver.path + } + + companion object { + private const val SUPPORTED_SCHEMA_VERSION = 1 + private const val META_FILE_NAME = "meta.json" + private const val CUSTOM_DRIVERS_DIR_NAME = "customDrivers" + private fun getCustomDriversDir() = + Path(NativeActiveSettings.getUserDataPath()).resolve(CUSTOM_DRIVERS_DIR_NAME) + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt index 65d4b226..002e64e2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt @@ -1,18 +1,20 @@ package info.cemu.cemu.settings.graphics import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.res.stringResource import info.cemu.cemu.R +import info.cemu.cemu.guicore.Button import info.cemu.cemu.guicore.ScreenContent import info.cemu.cemu.guicore.SingleSelection import info.cemu.cemu.guicore.Toggle import info.cemu.cemu.guicore.enumtostringmapper.native.fullscreenScalingModeToStringId import info.cemu.cemu.guicore.enumtostringmapper.native.scalingFilterToStringId import info.cemu.cemu.guicore.enumtostringmapper.native.vsyncModeToStringId +import info.cemu.cemu.nativeinterface.NativeEmulation import info.cemu.cemu.nativeinterface.NativeSettings - -val ScalingFilterChoices = listOf( +private val SCALING_FILTER_CHOICES = listOf( NativeSettings.SCALING_FILTER_BILINEAR_FILTER, NativeSettings.SCALING_FILTER_BICUBIC_FILTER, NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER, @@ -20,11 +22,20 @@ val ScalingFilterChoices = listOf( ) @Composable -fun GraphicsSettingsScreen(navigateBack: () -> Unit) { +fun GraphicsSettingsScreen(navigateBack: () -> Unit, goToCustomDriversSettings: () -> Unit) { + val supportsLoadingCustomDrivers = + rememberSaveable { NativeEmulation.supportsLoadingCustomDriver() } + ScreenContent( appBarText = stringResource(R.string.general_settings), navigateBack = navigateBack, ) { + if (supportsLoadingCustomDrivers) { + Button( + label = stringResource(R.string.custom_drivers), + onClick = goToCustomDriversSettings + ) + } Toggle( label = stringResource(R.string.async_shader_compile), description = stringResource(R.string.async_shader_compile_description), @@ -63,14 +74,14 @@ fun GraphicsSettingsScreen(navigateBack: () -> Unit) { initialChoice = NativeSettings::getUpscalingFilter, onChoiceChanged = NativeSettings::setUpscalingFilter, choiceToString = { stringResource(scalingFilterToStringId(it)) }, - choices = ScalingFilterChoices, + choices = SCALING_FILTER_CHOICES, ) SingleSelection( label = stringResource(R.string.downscale_filter), initialChoice = NativeSettings::getDownscalingFilter, onChoiceChanged = NativeSettings::setDownscalingFilter, choiceToString = { stringResource(scalingFilterToStringId(it)) }, - choices = ScalingFilterChoices, + choices = SCALING_FILTER_CHOICES, ) } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt index 19bba2f0..27649c37 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleListViewModel.kt @@ -320,7 +320,6 @@ class TitleListViewModel : ViewModel() { withContext(Dispatchers.IO) { val contentResolver = context.contentResolver val buffer = ByteArray(8192) - var bytesWritten = 0L val (totalSize, entries) = listFilesInSourceDirs( contentResolver = contentResolver, @@ -337,7 +336,10 @@ class TitleListViewModel : ViewModel() { backupFile.deleteRecursively() if (installFile.exists()) installFile.renameTo(backupFile) + installStarted = true + _titleInstallProgress.value = Pair(0, totalSize) + var bytesWritten = 0L for (file in entries) { yield() diff --git a/src/android/app/src/main/java/info/cemu/cemu/utils/Json.kt b/src/android/app/src/main/java/info/cemu/cemu/utils/Json.kt new file mode 100644 index 00000000..3b964153 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/utils/Json.kt @@ -0,0 +1,17 @@ +package info.cemu.cemu.utils + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import java.io.File + +@OptIn(ExperimentalSerializationApi::class) +inline fun decodeJsonFromFile(file: File): T? { + return try { + file.inputStream().use { + Json.decodeFromStream(it) + } + } catch (exception: Exception) { + null + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/utils/Zip.kt b/src/android/app/src/main/java/info/cemu/cemu/utils/Zip.kt index 93a42353..0d9cdf95 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/utils/Zip.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/utils/Zip.kt @@ -1,14 +1,13 @@ package info.cemu.cemu.utils import java.io.FileOutputStream -import java.io.IOException import java.io.InputStream +import java.nio.file.Path import java.nio.file.Paths import java.util.zip.ZipEntry import java.util.zip.ZipInputStream -@Throws(IOException::class) -fun unzip(stream: InputStream?, targetDir: String) { +fun unzip(stream: InputStream, targetDir: String) { ZipInputStream(stream).use { zipInputStream -> var zipEntry: ZipEntry val buffer = ByteArray(8192) @@ -19,6 +18,8 @@ fun unzip(stream: InputStream?, targetDir: String) { } } +fun unzip(stream: InputStream, targetDir: Path) = unzip(stream, targetDir.toString()) + private fun extractZipEntry( zipInputStream: ZipInputStream, zipEntry: ZipEntry, diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index d36f3e7e..bf732e27 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -291,6 +291,7 @@ It seems that the selected title is already installed, do you want to reinstall it? It seems that a newer version is already installed, do you still want to install the older version? OK + Custom drivers Enabled Convert to WUA Current progress: %1$s diff --git a/src/config/ActiveSettings.cpp b/src/config/ActiveSettings.cpp index 560f2986..8a8797a1 100644 --- a/src/config/ActiveSettings.cpp +++ b/src/config/ActiveSettings.cpp @@ -261,3 +261,14 @@ fs::path ActiveSettings::GetDefaultMLCPath() return GetUserDataPath("mlc01"); } +#if __ANDROID__ +void ActiveSettings::SetNativeLibPath(const fs::path& nativeLibPath) +{ + s_native_lib_path = nativeLibPath; +} + +void ActiveSettings::SetInternalDir(const fs::path& internalDirPath) +{ + s_internal_dir_path = internalDirPath; +} +#endif diff --git a/src/config/ActiveSettings.h b/src/config/ActiveSettings.h index e672fbee..c0a63bca 100644 --- a/src/config/ActiveSettings.h +++ b/src/config/ActiveSettings.h @@ -22,7 +22,7 @@ private: cemu_assert_debug(format.empty() || (format[0] != L'/' && format[0] != L'\\')); return path / fmt::format(fmt::runtime(format), std::forward(args)...); } - static fs::path GetPath(const fs::path& path, std::string_view p) + static fs::path GetPath(const fs::path& path, std::string_view p) { std::basic_string_view s((const char8_t*)p.data(), p.size()); return path / fs::path(s); @@ -58,6 +58,15 @@ public: [[nodiscard]] static fs::path GetMlcPath(); +#if __ANDROID__ + template + [[nodiscard]] static fs::path GetNativeLibPath(TArgs&&... args){ return GetPath(s_native_lib_path, std::forward(args)...); }; + + template + [[nodiscard]] static fs::path GetInternalPath(TArgs&&... args){ return GetPath(s_internal_dir_path, std::forward(args)...); }; +#endif + + template [[nodiscard]] static fs::path GetMlcPath(TArgs&&... args){ return GetPath(GetMlcPath(), std::forward(args)...); }; static bool IsCustomMlcPath(); @@ -75,8 +84,17 @@ private: inline static fs::path s_data_path; inline static fs::path s_executable_filename; // cemu.exe inline static fs::path s_mlc_path; +#if __ANDROID__ + inline static fs::path s_native_lib_path; + inline static fs::path s_internal_dir_path; +#endif public: + +#if __ANDROID__ + static void SetNativeLibPath(const fs::path& nativeLibPath); + static void SetInternalDir(const fs::path& internalDirPath); +#endif // can be called before Init [[nodiscard]] static bool IsPortableMode(); @@ -90,7 +108,7 @@ public: [[nodiscard]] static uint8 GetTimerShiftFactor(); static void SetTimerShiftFactor(uint8 shiftFactor); - + // gpu [[nodiscard]] static PrecompiledShaderOption GetPrecompiledShadersOption(); [[nodiscard]] static bool RenderUpsideDownEnabled(); diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 1287a6ae..8af966b4 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -344,6 +344,10 @@ void CemuConfig::Load(XMLConfigParser& parser) auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); emulated_usb_devices.emulate_infinity_base = usbdevices.get("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base); + +#if __ANDROID__ + custom_driver_path = parser.get("custom_driver_path", ""); +#endif } void CemuConfig::Save(XMLConfigParser& parser) @@ -543,6 +547,10 @@ void CemuConfig::Save(XMLConfigParser& parser) auto usbdevices = config.set("EmulatedUsbDevices"); usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); usbdevices.set("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base.GetValue()); + +#if __ANDROID__ + config.set("custom_driver_path", custom_driver_path.GetValue()); +#endif } GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 47a05e27..f720df10 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -378,6 +378,10 @@ struct CemuConfig ConfigValue disable_screensaver{DISABLE_SCREENSAVER_DEFAULT}; #undef DISABLE_SCREENSAVER_DEFAULT +#if __ANDROID__ + ConfigValue custom_driver_path{}; +#endif + std::vector game_paths; std::mutex game_cache_entries_mutex; std::vector game_cache_entries;