Merge pull request #11867 from K0bin/adrenotools

Implement loading custom drivers on Android
This commit is contained in:
JMC47
2023-06-11 14:17:39 -04:00
committed by GitHub
33 changed files with 904 additions and 36 deletions

View File

@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version "1.7.20"
}
task copyProfile (type: Copy) {
@ -110,6 +111,7 @@ android {
externalNativeBuild {
cmake {
path "../../../CMakeLists.txt"
version "3.22.1+"
}
}
namespace 'org.dolphinemu.dolphinemu'
@ -122,7 +124,7 @@ android {
abiFilters "arm64-v8a", "x86_64" //, "armeabi-v7a", "x86"
// Remove the line below if you want to build the C++ unit tests
targets "main"
//targets "main", "hook_impl", "main_hook", "gsl_alloc_hook", "file_redirect_hook"
}
}
}
@ -160,6 +162,9 @@ dependencies {
// For loading game covers from disk and GameTDB
implementation 'io.coil-kt:coil:2.2.2'
// For loading custom GPU drivers
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3"
implementation 'com.nononsenseapps:filepicker:4.2.1'
}

View File

@ -41,7 +41,8 @@
android:supportsRtl="true"
android:isGame="true"
android:banner="@drawable/banner_tv"
android:hasFragileUserData="true">
android:hasFragileUserData="true"
android:extractNativeLibs="true">
<meta-data
android:name="android.max_aspect"
android:value="2.1"/>

View File

@ -62,6 +62,12 @@ enum class StringSetting(
Settings.SECTION_GFX_ENHANCEMENTS,
"PostProcessingShader",
""
),
GFX_DRIVER_LIB_NAME(
Settings.FILE_GFX,
Settings.SECTION_GFX_SETTINGS,
"DriverLibName",
""
);
override val isOverridden: Boolean

View File

@ -49,7 +49,8 @@ enum class MenuTag {
WIIMOTE_MOTION_INPUT_1("wiimote_motion_input", 0),
WIIMOTE_MOTION_INPUT_2("wiimote_motion_input", 1),
WIIMOTE_MOTION_INPUT_3("wiimote_motion_input", 2),
WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3);
WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3),
GPU_DRIVERS("gpu_drivers");
var tag: String
private set

View File

@ -1,5 +1,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// GPU driver implementation partially based on:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.settings.ui
import android.content.Context
@ -20,6 +24,7 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivitySettingsBinding
@ -27,6 +32,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsFragment.Companion.newInstance
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.InsetsHelper
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint
@ -165,8 +171,21 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
super.onActivityResult(requestCode, resultCode, result)
// If the user picked a file, as opposed to just backing out.
if (resultCode == RESULT_OK) {
if (requestCode != MainPresenter.REQUEST_DIRECTORY) {
if (resultCode != RESULT_OK) {
return
}
when (requestCode) {
MainPresenter.REQUEST_DIRECTORY -> {
val path = FileBrowserHelper.getSelectedPath(result)
fragment!!.adapter!!.onFilePickerConfirmation(path!!)
}
MainPresenter.REQUEST_GAME_FILE
or MainPresenter.REQUEST_SD_FILE
or MainPresenter.REQUEST_WAD_FILE
or MainPresenter.REQUEST_WII_SAVE_FILE
or MainPresenter.REQUEST_NAND_BIN_FILE -> {
val uri = canonicalizeIfPossible(result!!.data!!)
val validExtensions: Set<String> =
if (requestCode == MainPresenter.REQUEST_GAME_FILE) FileBrowserHelper.GAME_EXTENSIONS else FileBrowserHelper.RAW_EXTENSION
@ -178,9 +197,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
contentResolver.takePersistableUriPermission(uri, takeFlags)
fragment!!.adapter!!.onFilePickerConfirmation(uri.toString())
}
} else {
val path = FileBrowserHelper.getSelectedPath(result)
fragment!!.adapter!!.onFilePickerConfirmation(path!!)
}
}
}

View File

@ -3,10 +3,15 @@
package org.dolphinemu.dolphinemu.features.settings.ui
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@ -14,10 +19,15 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.ui.main.MainActivity
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
import java.util.*
import kotlin.collections.ArrayList
@ -111,6 +121,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
override fun loadSubMenu(menuKey: MenuTag) {
if (menuKey == MenuTag.GPU_DRIVERS) {
showGpuDriverDialog()
return
}
activityView!!.showSettingsFragment(
menuKey,
null,
@ -170,6 +185,74 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
}
override fun showGpuDriverDialog() {
if (presenter.gpuDriver == null) {
return
}
val msg = "${presenter!!.gpuDriver!!.name} ${presenter!!.gpuDriver!!.driverVersion}"
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.gpu_driver_dialog_title))
.setMessage(msg)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int ->
presenter.useSystemDriver()
}
.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
askForDriverFile()
}
.show()
}
private fun askForDriverFile() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = "application/zip"
}
startActivityForResult(intent, MainPresenter.REQUEST_GPU_DRIVER)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// If the user picked a file, as opposed to just backing out.
if (resultCode != AppCompatActivity.RESULT_OK) {
return
}
if (requestCode != MainPresenter.REQUEST_GPU_DRIVER) {
return
}
val uri = data?.data ?: return
presenter.installDriver(uri)
}
override fun onDriverInstallDone(result: GpuDriverInstallResult) {
val view = binding?.root ?: return
Snackbar
.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG)
.show()
}
override fun onDriverUninstallDone() {
Toast.makeText(
requireContext(),
R.string.gpu_driver_dialog_uninstall_done,
Toast.LENGTH_SHORT
).show()
}
private fun resolveInstallResultString(result: GpuDriverInstallResult) = when (result) {
GpuDriverInstallResult.Success -> getString(R.string.gpu_driver_install_success)
GpuDriverInstallResult.InvalidArchive -> getString(R.string.gpu_driver_install_invalid_archive)
GpuDriverInstallResult.MissingMetadata -> getString(R.string.gpu_driver_install_missing_metadata)
GpuDriverInstallResult.InvalidMetadata -> getString(R.string.gpu_driver_install_invalid_metadata)
GpuDriverInstallResult.UnsupportedAndroidVersion -> getString(R.string.gpu_driver_install_unsupported_android_version)
GpuDriverInstallResult.AlreadyInstalled -> getString(R.string.gpu_driver_install_already_installed)
GpuDriverInstallResult.FileNotFound -> getString(R.string.gpu_driver_install_file_not_found)
}
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"

View File

@ -4,11 +4,16 @@ package org.dolphinemu.dolphinemu.features.settings.ui
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.ArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.activities.UserDataActivity
@ -25,6 +30,7 @@ import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog
import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter
import org.dolphinemu.dolphinemu.features.settings.model.*
import org.dolphinemu.dolphinemu.features.settings.model.view.*
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.*
import java.util.*
@ -45,6 +51,9 @@ class SettingsFragmentPresenter(
private var controllerNumber = 0
private var controllerType = 0
var gpuDriver: GpuDriverMetadata? = null
private val libNameSetting: StringSetting = StringSetting.GFX_DRIVER_LIB_NAME
fun onCreate(menuTag: MenuTag, gameId: String?, extras: Bundle) {
this.gameId = gameId
this.menuTag = menuTag
@ -56,6 +65,11 @@ class SettingsFragmentPresenter(
controllerNumber = menuTag.subType
} else if (menuTag.isSerialPort1Menu) {
serialPort1Type = extras.getInt(ARG_SERIALPORT1_TYPE)
} else if (menuTag == MenuTag.GRAPHICS) {
this.gpuDriver =
GpuDriverHelper.getInstalledDriverMetadata() ?: GpuDriverHelper.getSystemDriverMetadata(
context.applicationContext
)
}
}
@ -1250,6 +1264,15 @@ class SettingsFragmentPresenter(
MenuTag.ADVANCED_GRAPHICS
)
)
if (GpuDriverHelper.supportsCustomDriverLoading() && this.gpuDriver != null) {
sl.add(
SubmenuSetting(
context,
R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS
)
)
}
}
private fun addEnhanceSettings(sl: ArrayList<SettingsItem>) {
@ -2113,7 +2136,7 @@ class SettingsFragmentPresenter(
profileString: String,
controllerNumber: Int
) {
val profiles = ProfileDialogPresenter(menuTag).getProfileNames(false)
val profiles = ProfileDialogPresenter(menuTag!!).getProfileNames(false)
val profileKey = profileString + "Profile" + (controllerNumber + 1)
sl.add(
StringSingleChoiceSetting(
@ -2324,6 +2347,45 @@ class SettingsFragmentPresenter(
)
}
fun installDriver(uri: Uri) {
val context = this.context.applicationContext
CoroutineScope(Dispatchers.IO).launch {
val stream = context.contentResolver.openInputStream(uri)
if (stream == null) {
GpuDriverHelper.uninstallDriver()
withContext(Dispatchers.Main) {
fragmentView.onDriverInstallDone(GpuDriverInstallResult.FileNotFound)
}
return@launch
}
val result = GpuDriverHelper.installDriver(stream)
withContext(Dispatchers.Main) {
with(this@SettingsFragmentPresenter) {
this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata()
?: GpuDriverHelper.getSystemDriverMetadata(context) ?: return@withContext
this.libNameSetting.setString(this.settings!!, this.gpuDriver!!.libraryName)
}
fragmentView.onDriverInstallDone(result)
}
}
}
fun useSystemDriver() {
CoroutineScope(Dispatchers.IO).launch {
GpuDriverHelper.uninstallDriver()
withContext(Dispatchers.Main) {
with(this@SettingsFragmentPresenter) {
this.gpuDriver =
GpuDriverHelper.getInstalledDriverMetadata()
?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext)
this.libNameSetting.setString(this.settings!!, "")
}
fragmentView.onDriverUninstallDone()
}
}
}
companion object {
private val LOG_TYPE_NAMES = NativeLibrary.GetLogTypeNames()
const val ARG_CONTROLLER_TYPE = "controller_type"

View File

@ -6,6 +6,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
/**
* Abstraction for a screen showing a list of settings. Instances of
@ -111,4 +112,21 @@ interface SettingsFragmentView {
* @param visible Whether the warning should be visible.
*/
fun setOldControllerSettingsWarningVisibility(visible: Boolean)
/**
* Called when the driver installation is finished
*
* @param result The result of the driver installation
*/
fun onDriverInstallDone(result: GpuDriverInstallResult)
/**
* Called when the driver uninstall process is finished
*/
fun onDriverUninstallDone()
/**
* Shows a dialog asking the user to install or uninstall a GPU driver
*/
fun showGpuDriverDialog()
}

View File

@ -0,0 +1,61 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package org.dolphinemu.dolphinemu.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.*
import java.io.File
data class GpuDriverMetadata(
val name : String,
val author : String,
val packageVersion : String,
val vendor : String,
val driverVersion : String,
val minApi : Int,
val description : String,
val libraryName : String,
) {
private constructor(metadataV1 : GpuDriverMetadataV1) : this(
name = metadataV1.name,
author = metadataV1.author,
packageVersion = metadataV1.packageVersion,
vendor = metadataV1.vendor,
driverVersion = metadataV1.driverVersion,
minApi = metadataV1.minApi,
description = metadataV1.description,
libraryName = metadataV1.libraryName,
)
val label get() = "${name}-v${packageVersion}"
companion object {
private const val SCHEMA_VERSION_V1 = 1
fun deserialize(metadataFile : File) : GpuDriverMetadata {
val metadataJson = Json.parseToJsonElement(metadataFile.readText())
return when (metadataJson.jsonObject["schemaVersion"]?.jsonPrimitive?.intOrNull) {
SCHEMA_VERSION_V1 -> GpuDriverMetadata(Json.decodeFromJsonElement<GpuDriverMetadataV1>(metadataJson))
else -> throw SerializationException("Unsupported metadata version")
}
}
}
}
@Serializable
private data class GpuDriverMetadataV1(
val schemaVersion : Int,
val name : String,
val author : String,
val packageVersion : String,
val vendor : String,
val driverVersion : String,
val minApi : Int,
val description : String,
val libraryName : String,
)

View File

@ -286,6 +286,7 @@ class MainPresenter(private val mainView: MainView, private val activity: Fragme
const val REQUEST_WAD_FILE = 4
const val REQUEST_WII_SAVE_FILE = 5
const val REQUEST_NAND_BIN_FILE = 6
const val REQUEST_GPU_DRIVER = 7
private var shouldRescanLibrary = true

View File

@ -18,18 +18,17 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
/**
* A class that spawns its own thread in order perform initialization.
*
@ -46,6 +45,7 @@ public final class DirectoryInitialization
private static volatile boolean areDirectoriesAvailable = false;
private static String userPath;
private static String sysPath;
private static String driverPath;
private static boolean isUsingLegacyUserDirectory = false;
public enum DirectoryInitializationState
@ -88,8 +88,7 @@ public final class DirectoryInitialization
directoryState.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED);
}
@Nullable
private static File getLegacyUserDirectoryPath()
@Nullable private static File getLegacyUserDirectoryPath()
{
File externalPath = Environment.getExternalStorageDirectory();
if (externalPath == null)
@ -98,8 +97,7 @@ public final class DirectoryInitialization
return new File(externalPath, "dolphin-emu");
}
@Nullable
public static File getUserDirectoryPath(Context context)
@Nullable public static File getUserDirectoryPath(Context context)
{
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
return null;
@ -107,8 +105,8 @@ public final class DirectoryInitialization
isUsingLegacyUserDirectory =
preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context);
return isUsingLegacyUserDirectory ?
getLegacyUserDirectoryPath() : context.getExternalFilesDir(null);
return isUsingLegacyUserDirectory ? getLegacyUserDirectoryPath() :
context.getExternalFilesDir(null);
}
private static boolean setDolphinUserDirectory(Context context)
@ -153,6 +151,19 @@ public final class DirectoryInitialization
// Let the native code know where the Sys directory is.
sysPath = sysDirectory.getPath();
SetSysDirectory(sysPath);
File driverDirectory = new File(context.getFilesDir(), "GPUDrivers");
driverDirectory.mkdirs();
File driverExtractedDir = new File(driverDirectory, "Extracted");
driverExtractedDir.mkdirs();
File driverTmpDir = new File(driverDirectory, "Tmp");
driverTmpDir.mkdirs();
File driverFileRedirectDir = new File(driverDirectory, "FileRedirect");
driverFileRedirectDir.mkdirs();
SetGpuDriverDirectories(driverDirectory.getPath(),
context.getApplicationInfo().nativeLibraryDir);
DirectoryInitialization.driverPath = driverExtractedDir.getAbsolutePath();
}
private static void deleteDirectoryRecursively(@NonNull final File file)
@ -213,6 +224,16 @@ public final class DirectoryInitialization
return sysPath;
}
public static String getExtractedDriverDirectory()
{
if (!areDirectoriesAvailable)
{
throw new IllegalStateException(
"DirectoryInitialization must run before accessing the driver directory!");
}
return driverPath;
}
public static File getGameListCache(Context context)
{
return new File(context.getExternalCacheDir(), "gamelist.cache");
@ -235,16 +256,14 @@ public final class DirectoryInitialization
}
catch (IOException e)
{
Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
e.getMessage());
Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + e.getMessage());
}
return false;
}
private static void copyAssetFolder(String assetFolder, File outputFolder, Context context)
{
Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
outputFolder);
Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + outputFolder);
try
{
@ -267,8 +286,7 @@ public final class DirectoryInitialization
}
createdFolder = true;
}
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
context);
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), context);
copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), context);
}
}
@ -340,8 +358,8 @@ public final class DirectoryInitialization
private static boolean preferLegacyUserDirectory(Context context)
{
return PermissionsHandler.isExternalStorageLegacy() &&
!PermissionsHandler.isWritePermissionDenied() &&
isExternalFilesDirEmpty(context) && legacyUserDirectoryExists();
!PermissionsHandler.isWritePermissionDenied() && isExternalFilesDirEmpty(context) &&
legacyUserDirectoryExists();
}
public static boolean isUsingLegacyUserDirectory()
@ -389,4 +407,6 @@ public final class DirectoryInitialization
}
private static native void SetSysDirectory(String path);
private static native void SetGpuDriverDirectories(String path, String libPath);
}

View File

@ -0,0 +1,148 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Partially based on:
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
// Partially based on:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.utils
import android.content.Context
import android.os.Build
import kotlinx.serialization.SerializationException
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
import java.io.File
import java.io.InputStream
private const val GPU_DRIVER_META_FILE = "meta.json"
interface GpuDriverHelper {
companion object {
/**
* Returns information about the system GPU driver.
* @return An array containing the driver vendor and the driver version, in this order, or `null` if an error occurred
*/
private external fun getSystemDriverInfo(): Array<String>?
/**
* Queries the driver for custom driver loading support.
* @return `true` if the device supports loading custom drivers, `false` otherwise
*/
external fun supportsCustomDriverLoading(): Boolean
/**
* Queries the driver for manual max clock forcing support
*/
external fun supportsForceMaxGpuClocks(): Boolean
/**
* Calls into the driver to force the GPU to run at the maximum possible clock speed
* @param force Whether to enable or disable the forced clocks
*/
external fun forceMaxGpuClocks(enable: Boolean)
/**
* Uninstalls the currently installed custom driver
*/
fun uninstallDriver() {
File(DirectoryInitialization.getExtractedDriverDirectory())
.deleteRecursively()
File(DirectoryInitialization.getExtractedDriverDirectory()).mkdir()
}
fun getInstalledDriverMetadata(): GpuDriverMetadata? {
val metadataFile = File(
DirectoryInitialization.getExtractedDriverDirectory(),
GPU_DRIVER_META_FILE
)
if (!metadataFile.exists()) {
return null;
}
return try {
GpuDriverMetadata.deserialize(metadataFile)
} catch (e: SerializationException) {
null
}
}
/**
* Fetches metadata about the system driver.
* @return A [GpuDriverMetadata] object containing data about the system driver
*/
fun getSystemDriverMetadata(context: Context): GpuDriverMetadata? {
val systemDriverInfo = getSystemDriverInfo()
if (systemDriverInfo.isNullOrEmpty()) {
return null;
}
return GpuDriverMetadata(
name = context.getString(R.string.system_driver),
author = "",
packageVersion = "",
vendor = systemDriverInfo[0],
driverVersion = systemDriverInfo[1],
minApi = 0,
description = context.getString(R.string.system_driver_desc),
libraryName = ""
)
}
/**
* Installs the given driver to the emulator's drivers directory.
* @param stream InputStream of a driver package
* @return The exit status of the installation process
*/
fun installDriver(stream: InputStream): GpuDriverInstallResult {
uninstallDriver()
val driverDir = File(DirectoryInitialization.getExtractedDriverDirectory())
try {
ZipUtils.unzip(stream, driverDir)
} catch (e: Exception) {
e.printStackTrace()
uninstallDriver()
return GpuDriverInstallResult.InvalidArchive
}
// Check that the metadata file exists
val metadataFile = File(driverDir, GPU_DRIVER_META_FILE)
if (!metadataFile.isFile) {
uninstallDriver()
return GpuDriverInstallResult.MissingMetadata
}
// Check that the driver metadata is valid
val driverMetadata = try {
GpuDriverMetadata.deserialize(metadataFile)
} catch (e: SerializationException) {
uninstallDriver()
return GpuDriverInstallResult.InvalidMetadata
}
// Check that the device satisfies the driver's minimum Android version requirements
if (Build.VERSION.SDK_INT < driverMetadata.minApi) {
uninstallDriver()
return GpuDriverInstallResult.UnsupportedAndroidVersion
}
return GpuDriverInstallResult.Success
}
}
}
enum class GpuDriverInstallResult {
Success,
InvalidArchive,
MissingMetadata,
InvalidMetadata,
UnsupportedAndroidVersion,
AlreadyInstalled,
FileNotFound
}

View File

@ -0,0 +1,110 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package org.dolphinemu.dolphinemu.utils
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
interface ZipUtils {
companion object {
/**
* Extracts a zip file to the given target directory.
* @exception IOException if unzipping fails for any reason
*/
@Throws(IOException::class)
fun unzip(file : File, targetDirectory : File) {
ZipFile(file).use { zipFile ->
for (zipEntry in zipFile.entries()) {
val destFile = createNewFile(targetDirectory, zipEntry)
// If the zip entry is a file, we need to create its parent directories
val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
// Create the destination directory
if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
throw FileNotFoundException("Failed to create destination directory: $destDirectory")
// If the entry is a directory we don't need to copy anything
if (zipEntry.isDirectory)
continue
// Copy bytes to destination
try {
zipFile.getInputStream(zipEntry).use { inputStream ->
destFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
} catch (e : IOException) {
if (destFile.exists())
destFile.delete()
throw e
}
}
}
}
/**
* Extracts a zip file from the given stream to the given target directory.
*
* This method is ~5x slower than [unzip], as [ZipInputStream] uses a fixed `512` bytes buffer for inflation,
* instead of `8192` bytes or more used by input streams returned by [ZipFile].
* This results in ~8x the amount of JNI calls, producing an increased number of array bounds checking, which kills performance.
* Usage of this method is discouraged when possible, [unzip] should be used instead.
* Nevertheless, it's the only option when extracting zips obtained from content URIs, as a [File] object cannot be obtained from them.
* @exception IOException if unzipping fails for any reason
*/
@Throws(IOException::class)
fun unzip(stream : InputStream, targetDirectory : File) {
ZipInputStream(BufferedInputStream(stream)).use { zis ->
do {
// Get the next entry, break if we've reached the end
val zipEntry = zis.nextEntry ?: break
val destFile = createNewFile(targetDirectory, zipEntry)
// If the zip entry is a file, we need to create its parent directories
val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
// Create the destination directory
if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
throw FileNotFoundException("Failed to create destination directory: $destDirectory")
// If the entry is a directory we don't need to copy anything
if (zipEntry.isDirectory)
continue
// Copy bytes to destination
try {
BufferedOutputStream(destFile.outputStream()).use { zis.copyTo(it) }
} catch (e : IOException) {
if (destFile.exists())
destFile.delete()
throw e
}
} while (true)
}
}
/**
* Safely creates a new destination file where the given zip entry will be extracted to.
*
* @exception IOException if the file was being created outside of the target directory
* **see:** [Zip Slip](https://github.com/snyk/zip-slip-vulnerability)
*/
@Throws(IOException::class)
private fun createNewFile(destinationDir : File, zipEntry : ZipEntry) : File {
val destFile = File(destinationDir, zipEntry.name)
val destDirPath = destinationDir.canonicalPath
val destFilePath = destFile.canonicalPath
if (!destFilePath.startsWith(destDirPath + File.separator))
throw IOException("Entry is outside of the target dir: " + zipEntry.name)
return destFile
}
}
}

View File

@ -841,6 +841,23 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="about_github"><a href="https://github.com/dolphin-emu/dolphin">GitHub</a></string>
<string name="about_support"><a href="https://forums.dolphin-emu.org/">Support</a></string>
<string name="about_copyright_warning">\u00A9 20032015+ Dolphin Team. \u201cGameCube\u201d and \u201cWii\u201d are trademarks of Nintendo. Dolphin is not affiliated with Nintendo in any way.</string>
<string name="system_driver">System driver</string>
<string name="system_driver_desc">The GPU driver that is part of the OS.</string>
<!-- Custom GPU drivers -->
<string name="gpu_driver_dialog_title">Select the GPU driver for Dolphin</string>
<string name="gpu_driver_dialog_system">Default</string>
<string name="gpu_driver_dialog_install">Install driver</string>
<string name="gpu_driver_dialog_uninstall_done">Successfully switched to the system driver</string>
<string name="gpu_driver_submenu">GPU Driver</string>
<string name="gpu_driver_install_inprogress">Installing the GPU driver…</string>
<string name="gpu_driver_install_success">GPU driver installed successfully</string>
<string name="gpu_driver_install_invalid_archive">Failed to unzip the provided driver package</string>
<string name="gpu_driver_install_missing_metadata">The supplied driver package is invalid due to missing metadata.</string>
<string name="gpu_driver_install_invalid_metadata">The supplied driver package contains invalid metadata, it may be corrupted.</string>
<string name="gpu_driver_install_unsupported_android_version">Your device doesn\'t meet the minimum Android version requirements for the supplied driver.</string>
<string name="gpu_driver_install_already_installed">The supplied driver package is already installed.</string>
<string name="gpu_driver_install_file_not_found">The selected file could not be found or accessed.</string>
<!-- Emulated USB Devices -->
<string name="emulated_usb_devices">Emulated USB Devices</string>