Add gui for adding custom drivers

This commit is contained in:
SSimco
2025-03-10 18:04:23 +02:00
parent e919c23ea5
commit fdb1f27e7d
29 changed files with 765 additions and 79 deletions
+3 -3
View File
@@ -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
+4
View File
@@ -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)
+3
View File
@@ -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>")
+109 -8
View File
@@ -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
+3
View File
@@ -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) {
+1 -1
View File
@@ -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,
@@ -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(),
)
}
@@ -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