mirror of
https://github.com/izzy2lost/WeeU.git
synced 2026-07-06 00:19:59 -07:00
Add gui for adding custom drivers
This commit is contained in:
+3
-3
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
Submodule dependencies/libadrenotools added at 8fae8ce254
@@ -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$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
@@ -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 <adrenotools/driver.h>
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/istreamwrapper.h>
|
||||
#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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace JNIUtils
|
||||
jobject createJavaLongArrayList(JNIEnv* env, const std::vector<uint64_t>& values);
|
||||
|
||||
template<typename... TArgs>
|
||||
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, "<init>", ctrSig.c_str());
|
||||
|
||||
@@ -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<fs::path> 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));
|
||||
}
|
||||
@@ -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<fs::path> 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<ANativeWindow, decltype(&ANativeWindow_release)>;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -361,4 +361,20 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setConsoleLanguage([[maybe_un
|
||||
{
|
||||
g_config.data().console_language = static_cast<CafeConsoleLanguage>(console_language);
|
||||
g_config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<GraphicPackRoutes.GraphicPacksRootSectionRoute> { backStackEntry ->
|
||||
val graphicPackViewModel: GraphicPackViewModel = viewModel(backStackEntry)
|
||||
|
||||
@@ -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 <T> 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
|
||||
@@ -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<SettingsRoutes.SettingsHomeScreenRoute> {
|
||||
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<SettingsRoutes.GraphicsSettingsScreenRoute> {
|
||||
GraphicsSettingsScreen(
|
||||
navigateBack = ::navigateBack,
|
||||
goToCustomDriversSettings = {
|
||||
navController.navigate(SettingsRoutes.CustomDriversScreenRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
composable<SettingsRoutes.CustomDriversScreenRoute> {
|
||||
CustomDriversScreen(
|
||||
navigateBack = ::navigateBack,
|
||||
)
|
||||
}
|
||||
composable<SettingsRoutes.OverlaySettingsScreenRoute> {
|
||||
@@ -143,7 +163,7 @@ fun SettingsNav(
|
||||
)
|
||||
}
|
||||
}
|
||||
navigation<SettingsRoutes.GeneralSettingsRoute>(startDestination = SettingsRoutes.GeneralSettingsScreenRoute) {
|
||||
navigation<SettingsRoutes.GeneralSettings>(startDestination = SettingsRoutes.GeneralSettingsScreenRoute) {
|
||||
composable<SettingsRoutes.GeneralSettingsScreenRoute> {
|
||||
GeneralSettingsScreen(
|
||||
navigateBack = ::navigateBack,
|
||||
|
||||
+206
@@ -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 <T> 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(),
|
||||
)
|
||||
}
|
||||
+201
@@ -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<List<Driver>>(emptyList())
|
||||
val installedDrivers = _installedDrivers.asStateFlow()
|
||||
|
||||
init {
|
||||
_installedDrivers.value = parseInstalledDrivers()
|
||||
}
|
||||
|
||||
private fun parseInstalledDrivers(): List<Driver> {
|
||||
val customDriversDir = getCustomDriversDir()
|
||||
|
||||
if (!customDriversDir.isDirectory())
|
||||
return emptyList()
|
||||
|
||||
val driverDirs: Array<File> = customDriversDir.toFile().listFiles() ?: return emptyList()
|
||||
|
||||
val drivers = mutableListOf<Driver>()
|
||||
val selectedDriver = selectedDriverPath.value
|
||||
|
||||
for (driverDir in driverDirs) {
|
||||
if (!driverDir.isDirectory)
|
||||
continue
|
||||
val metadata =
|
||||
decodeJsonFromFile<DriverMetadata>(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<DriverMetadata>(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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user