Explorar o código

Merge pull request #11908 from t895/log-spam

android: Fix URI parsing in native code
liamwhite %!s(int64=2) %!d(string=hai) anos
pai
achega
07276cf62a

+ 25 - 37
src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt

@@ -5,6 +5,7 @@ package org.yuzu.yuzu_emu
 
 import android.app.Dialog
 import android.content.DialogInterface
+import android.net.Uri
 import android.os.Bundle
 import android.text.Html
 import android.text.method.LinkMovementMethod
@@ -16,7 +17,7 @@ import androidx.fragment.app.DialogFragment
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import java.lang.ref.WeakReference
 import org.yuzu.yuzu_emu.activities.EmulationActivity
-import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath
+import org.yuzu.yuzu_emu.utils.DocumentsTree
 import org.yuzu.yuzu_emu.utils.FileUtil
 import org.yuzu.yuzu_emu.utils.Log
 import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
@@ -68,7 +69,7 @@ object NativeLibrary {
     @Keep
     @JvmStatic
     fun openContentUri(path: String?, openmode: String?): Int {
-        return if (isNativePath(path!!)) {
+        return if (DocumentsTree.isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.openContentUri(path, openmode)
         } else {
             FileUtil.openContentUri(path, openmode)
@@ -78,7 +79,7 @@ object NativeLibrary {
     @Keep
     @JvmStatic
     fun getSize(path: String?): Long {
-        return if (isNativePath(path!!)) {
+        return if (DocumentsTree.isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.getFileSize(path)
         } else {
             FileUtil.getFileSize(path)
@@ -88,23 +89,41 @@ object NativeLibrary {
     @Keep
     @JvmStatic
     fun exists(path: String?): Boolean {
-        return if (isNativePath(path!!)) {
+        return if (DocumentsTree.isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.exists(path)
         } else {
-            FileUtil.exists(path)
+            FileUtil.exists(path, suppressLog = true)
         }
     }
 
     @Keep
     @JvmStatic
     fun isDirectory(path: String?): Boolean {
-        return if (isNativePath(path!!)) {
+        return if (DocumentsTree.isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.isDirectory(path)
         } else {
             FileUtil.isDirectory(path)
         }
     }
 
+    @Keep
+    @JvmStatic
+    fun getParentDirectory(path: String): String =
+        if (DocumentsTree.isNativePath(path)) {
+            YuzuApplication.documentsTree!!.getParentDirectory(path)
+        } else {
+            path
+        }
+
+    @Keep
+    @JvmStatic
+    fun getFilename(path: String): String =
+        if (DocumentsTree.isNativePath(path)) {
+            YuzuApplication.documentsTree!!.getFilename(path)
+        } else {
+            FileUtil.getFilename(Uri.parse(path))
+        }
+
     /**
      * Returns true if pro controller isn't available and handheld is
      */
@@ -215,32 +234,6 @@ object NativeLibrary {
 
     external fun initGameIni(gameID: String?)
 
-    /**
-     * Gets the embedded icon within the given ROM.
-     *
-     * @param filename the file path to the ROM.
-     * @return a byte array containing the JPEG data for the icon.
-     */
-    external fun getIcon(filename: String): ByteArray
-
-    /**
-     * Gets the embedded title of the given ISO/ROM.
-     *
-     * @param filename The file path to the ISO/ROM.
-     * @return the embedded title of the ISO/ROM.
-     */
-    external fun getTitle(filename: String): String
-
-    external fun getDescription(filename: String): String
-
-    external fun getGameId(filename: String): String
-
-    external fun getRegions(filename: String): String
-
-    external fun getCompany(filename: String): String
-
-    external fun isHomebrew(filename: String): Boolean
-
     external fun setAppDirectory(directory: String)
 
     /**
@@ -293,11 +286,6 @@ object NativeLibrary {
      */
     external fun stopEmulation()
 
-    /**
-     * Resets the in-memory ROM metadata cache.
-     */
-    external fun resetRomMetadata()
-
     /**
      * Returns true if emulation is running (or is paused).
      */

+ 1 - 1
src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt

@@ -147,7 +147,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
 
     private class DiffCallback : DiffUtil.ItemCallback<Game>() {
         override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
-            return oldItem.gameId == newItem.gameId
+            return oldItem.programId == newItem.programId
         }
 
         override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {

+ 7 - 10
src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt

@@ -12,15 +12,14 @@ import kotlinx.serialization.Serializable
 @Serializable
 class Game(
     val title: String,
-    val description: String,
-    val regions: String,
     val path: String,
-    val gameId: String,
-    val company: String,
+    val programId: String,
+    val developer: String,
+    val version: String,
     val isHomebrew: Boolean
 ) : Parcelable {
-    val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
-    val keyLastPlayedTime get() = "${gameId}_LastPlayed"
+    val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime"
+    val keyLastPlayedTime get() = "${programId}_LastPlayed"
 
     override fun equals(other: Any?): Boolean {
         if (other !is Game) {
@@ -32,11 +31,9 @@ class Game(
 
     override fun hashCode(): Int {
         var result = title.hashCode()
-        result = 31 * result + description.hashCode()
-        result = 31 * result + regions.hashCode()
         result = 31 * result + path.hashCode()
-        result = 31 * result + gameId.hashCode()
-        result = 31 * result + company.hashCode()
+        result = 31 * result + programId.hashCode()
+        result = 31 * result + developer.hashCode()
         result = 31 * result + isHomebrew.hashCode()
         return result
     }

+ 4 - 5
src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt

@@ -14,15 +14,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.MissingFieldException
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.utils.GameHelper
+import org.yuzu.yuzu_emu.utils.GameMetadata
 
-@OptIn(ExperimentalSerializationApi::class)
 class GamesViewModel : ViewModel() {
     val games: StateFlow<List<Game>> get() = _games
     private val _games = MutableStateFlow(emptyList<Game>())
@@ -58,7 +56,8 @@ class GamesViewModel : ViewModel() {
                         val game: Game
                         try {
                             game = Json.decodeFromString(it)
-                        } catch (e: MissingFieldException) {
+                        } catch (e: Exception) {
+                            // We don't care about any errors related to parsing the game cache
                             return@forEach
                         }
 
@@ -113,7 +112,7 @@ class GamesViewModel : ViewModel() {
 
         viewModelScope.launch {
             withContext(Dispatchers.IO) {
-                NativeLibrary.resetRomMetadata()
+                GameMetadata.resetMetadata()
                 setGames(GameHelper.getGames())
                 _isReloading.value = false
 

+ 17 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt

@@ -42,6 +42,23 @@ class DocumentsTree {
         return node != null && node.isDirectory
     }
 
+    fun getParentDirectory(filepath: String): String {
+        val node = resolvePath(filepath)!!
+        val parentNode = node.parent
+        if (parentNode != null && parentNode.isDirectory) {
+            return parentNode.uri!!.toString()
+        }
+        return node.uri!!.toString()
+    }
+
+    fun getFilename(filepath: String): String {
+        val node = resolvePath(filepath)
+        if (node != null) {
+            return node.name!!
+        }
+        return filepath
+    }
+
     private fun resolvePath(filepath: String): DocumentsNode? {
         val tokens = StringTokenizer(filepath, File.separator, false)
         var iterator = root

+ 4 - 2
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt

@@ -144,7 +144,7 @@ object FileUtil {
      * @param path Native content uri path
      * @return bool
      */
-    fun exists(path: String?): Boolean {
+    fun exists(path: String?, suppressLog: Boolean = false): Boolean {
         var c: Cursor? = null
         try {
             val mUri = Uri.parse(path)
@@ -152,7 +152,9 @@ object FileUtil {
             c = context.contentResolver.query(mUri, columns, null, null, null)
             return c!!.count > 0
         } catch (e: Exception) {
-            Log.info("[FileUtil] Cannot find file from given path, error: " + e.message)
+            if (!suppressLog) {
+                Log.info("[FileUtil] Cannot find file from given path, error: " + e.message)
+            }
         } finally {
             closeQuietly(c)
         }

+ 8 - 9
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt

@@ -71,27 +71,26 @@ object GameHelper {
 
     fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
         val filePath = uri.toString()
-        var name = NativeLibrary.getTitle(filePath)
+        var name = GameMetadata.getTitle(filePath)
 
         // If the game's title field is empty, use the filename.
         if (name.isEmpty()) {
             name = FileUtil.getFilename(uri)
         }
-        var gameId = NativeLibrary.getGameId(filePath)
+        var programId = GameMetadata.getProgramId(filePath)
 
         // If the game's ID field is empty, use the filename without extension.
-        if (gameId.isEmpty()) {
-            gameId = name.substring(0, name.lastIndexOf("."))
+        if (programId.isEmpty()) {
+            programId = name.substring(0, name.lastIndexOf("."))
         }
 
         val newGame = Game(
             name,
-            NativeLibrary.getDescription(filePath).replace("\n", " "),
-            NativeLibrary.getRegions(filePath),
             filePath,
-            gameId,
-            NativeLibrary.getCompany(filePath),
-            NativeLibrary.isHomebrew(filePath)
+            programId,
+            GameMetadata.getDeveloper(filePath),
+            GameMetadata.getVersion(filePath),
+            GameMetadata.getIsHomebrew(filePath)
         )
 
         if (addedToLibrary) {

+ 1 - 2
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt

@@ -18,7 +18,6 @@ import coil.key.Keyer
 import coil.memory.MemoryCache
 import coil.request.ImageRequest
 import coil.request.Options
-import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.model.Game
@@ -36,7 +35,7 @@ class GameIconFetcher(
     }
 
     private fun decodeGameIcon(uri: String): Bitmap? {
-        val data = NativeLibrary.getIcon(uri)
+        val data = GameMetadata.getIcon(uri)
         return BitmapFactory.decodeByteArray(
             data,
             0,

+ 20 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt

@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+object GameMetadata {
+    external fun getTitle(path: String): String
+
+    external fun getProgramId(path: String): String
+
+    external fun getDeveloper(path: String): String
+
+    external fun getVersion(path: String): String
+
+    external fun getIcon(path: String): ByteArray
+
+    external fun getIsHomebrew(path: String): Boolean
+
+    external fun resetMetadata()
+}

+ 2 - 0
src/android/app/src/main/jni/CMakeLists.txt

@@ -14,8 +14,10 @@ add_library(yuzu-android SHARED
     id_cache.cpp
     id_cache.h
     native.cpp
+    native.h
     native_config.cpp
     uisettings.cpp
+    game_metadata.cpp
 )
 
 set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})

+ 112 - 0
src/android/app/src/main/jni/game_metadata.cpp

@@ -0,0 +1,112 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <core/core.h>
+#include <core/file_sys/patch_manager.h>
+#include <core/loader/nro.h>
+#include <jni.h>
+#include "core/loader/loader.h"
+#include "jni/android_common/android_common.h"
+#include "native.h"
+
+struct RomMetadata {
+    std::string title;
+    u64 programId;
+    std::string developer;
+    std::string version;
+    std::vector<u8> icon;
+    bool isHomebrew;
+};
+
+std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
+
+RomMetadata CacheRomMetadata(const std::string& path) {
+    const auto file =
+        Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
+    auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
+
+    RomMetadata entry;
+    loader->ReadTitle(entry.title);
+    loader->ReadProgramId(entry.programId);
+    loader->ReadIcon(entry.icon);
+
+    const FileSys::PatchManager pm{
+        entry.programId, EmulationSession::GetInstance().System().GetFileSystemController(),
+        EmulationSession::GetInstance().System().GetContentProvider()};
+    const auto control = pm.GetControlMetadata();
+
+    if (control.first != nullptr) {
+        entry.developer = control.first->GetDeveloperName();
+        entry.version = control.first->GetVersionString();
+    } else {
+        FileSys::NACP nacp;
+        if (loader->ReadControlData(nacp) == Loader::ResultStatus::Success) {
+            entry.developer = nacp.GetDeveloperName();
+        } else {
+            entry.developer = "";
+        }
+
+        entry.version = "1.0.0";
+    }
+
+    if (loader->GetFileType() == Loader::FileType::NRO) {
+        auto loader_nro = reinterpret_cast<Loader::AppLoader_NRO*>(loader.get());
+        entry.isHomebrew = loader_nro->IsHomebrew();
+    } else {
+        entry.isHomebrew = false;
+    }
+
+    m_rom_metadata_cache[path] = entry;
+
+    return entry;
+}
+
+RomMetadata GetRomMetadata(const std::string& path) {
+    if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
+        return search->second;
+    }
+
+    return CacheRomMetadata(path);
+}
+
+extern "C" {
+
+jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
+                                                            jstring jpath) {
+    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title);
+}
+
+jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getProgramId(JNIEnv* env, jobject obj,
+                                                                jstring jpath) {
+    return ToJString(env, std::to_string(GetRomMetadata(GetJString(env, jpath)).programId));
+}
+
+jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, jobject obj,
+                                                                jstring jpath) {
+    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).developer);
+}
+
+jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
+                                                              jstring jpath) {
+    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version);
+}
+
+jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj,
+                                                              jstring jpath) {
+    auto icon_data = GetRomMetadata(GetJString(env, jpath)).icon;
+    jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
+    env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
+                            reinterpret_cast<jbyte*>(icon_data.data()));
+    return icon;
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, jobject obj,
+                                                                  jstring jpath) {
+    return static_cast<jboolean>(GetRomMetadata(GetJString(env, jpath)).isHomebrew);
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) {
+    return m_rom_metadata_cache.clear();
+}
+
+} // extern "C"

+ 307 - 450
src/android/app/src/main/jni/native.cpp

@@ -33,7 +33,6 @@
 #include "core/crypto/key_manager.h"
 #include "core/file_sys/card_image.h"
 #include "core/file_sys/content_archive.h"
-#include "core/file_sys/registered_cache.h"
 #include "core/file_sys/submission_package.h"
 #include "core/file_sys/vfs.h"
 #include "core/file_sys/vfs_real.h"
@@ -48,514 +47,416 @@
 #include "core/hid/emulated_controller.h"
 #include "core/hid/hid_core.h"
 #include "core/hid/hid_types.h"
-#include "core/hle/service/acc/profile_manager.h"
 #include "core/hle/service/am/applet_ae.h"
 #include "core/hle/service/am/applet_oe.h"
 #include "core/hle/service/am/applets/applets.h"
 #include "core/hle/service/filesystem/filesystem.h"
 #include "core/loader/loader.h"
-#include "core/perf_stats.h"
 #include "jni/android_common/android_common.h"
-#include "jni/applets/software_keyboard.h"
 #include "jni/config.h"
-#include "jni/emu_window/emu_window.h"
 #include "jni/id_cache.h"
-#include "video_core/rasterizer_interface.h"
+#include "jni/native.h"
 #include "video_core/renderer_base.h"
 
 #define jconst [[maybe_unused]] const auto
 #define jauto [[maybe_unused]] auto
 
-namespace {
+static EmulationSession s_instance;
 
-class EmulationSession final {
-public:
-    EmulationSession() {
-        m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
-    }
-
-    ~EmulationSession() = default;
-
-    static EmulationSession& GetInstance() {
-        return s_instance;
-    }
-
-    const Core::System& System() const {
-        return m_system;
-    }
+EmulationSession::EmulationSession() {
+    m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
+}
 
-    Core::System& System() {
-        return m_system;
-    }
+EmulationSession& EmulationSession::GetInstance() {
+    return s_instance;
+}
 
-    const EmuWindow_Android& Window() const {
-        return *m_window;
-    }
+const Core::System& EmulationSession::System() const {
+    return m_system;
+}
 
-    EmuWindow_Android& Window() {
-        return *m_window;
-    }
+Core::System& EmulationSession::System() {
+    return m_system;
+}
 
-    ANativeWindow* NativeWindow() const {
-        return m_native_window;
-    }
+const EmuWindow_Android& EmulationSession::Window() const {
+    return *m_window;
+}
 
-    void SetNativeWindow(ANativeWindow* native_window) {
-        m_native_window = native_window;
-    }
+EmuWindow_Android& EmulationSession::Window() {
+    return *m_window;
+}
 
-    int InstallFileToNand(std::string filename, std::string file_extension) {
-        jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
-                              std::size_t block_size) {
-            if (src == nullptr || dest == nullptr) {
-                return false;
-            }
-            if (!dest->Resize(src->GetSize())) {
-                return false;
-            }
+ANativeWindow* EmulationSession::NativeWindow() const {
+    return m_native_window;
+}
 
-            using namespace Common::Literals;
-            [[maybe_unused]] std::vector<u8> buffer(1_MiB);
+void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
+    m_native_window = native_window;
+}
 
-            for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
-                jconst read = src->Read(buffer.data(), buffer.size(), i);
-                dest->Write(buffer.data(), read, i);
-            }
-            return true;
-        };
-
-        enum InstallResult {
-            Success = 0,
-            SuccessFileOverwritten = 1,
-            InstallError = 2,
-            ErrorBaseGame = 3,
-            ErrorFilenameExtension = 4,
-        };
-
-        m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
-        m_system.GetFileSystemController().CreateFactories(*m_vfs);
-
-        [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
-        if (file_extension == "nsp") {
-            nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
-            if (nsp->IsExtractedType()) {
-                return InstallError;
-            }
-        } else {
-            return ErrorFilenameExtension;
+int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
+    jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
+                          std::size_t block_size) {
+        if (src == nullptr || dest == nullptr) {
+            return false;
         }
-
-        if (!nsp) {
-            return InstallError;
+        if (!dest->Resize(src->GetSize())) {
+            return false;
         }
 
-        if (nsp->GetStatus() != Loader::ResultStatus::Success) {
-            return InstallError;
-        }
+        using namespace Common::Literals;
+        [[maybe_unused]] std::vector<u8> buffer(1_MiB);
 
-        jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(
-            *nsp, true, copy_func);
-
-        switch (res) {
-        case FileSys::InstallResult::Success:
-            return Success;
-        case FileSys::InstallResult::OverwriteExisting:
-            return SuccessFileOverwritten;
-        case FileSys::InstallResult::ErrorBaseInstall:
-            return ErrorBaseGame;
-        default:
-            return InstallError;
+        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
+            jconst read = src->Read(buffer.data(), buffer.size(), i);
+            dest->Write(buffer.data(), read, i);
         }
-    }
+        return true;
+    };
 
-    void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
-                             const std::string& custom_driver_name,
-                             const std::string& file_redirect_dir) {
-#ifdef ARCHITECTURE_arm64
-        void* handle{};
-        const char* file_redirect_dir_{};
-        int featureFlags{};
-
-        // Enable driver file redirection when renderer debugging is enabled.
-        if (Settings::values.renderer_debug && file_redirect_dir.size()) {
-            featureFlags |= ADRENOTOOLS_DRIVER_FILE_REDIRECT;
-            file_redirect_dir_ = file_redirect_dir.c_str();
-        }
+    enum InstallResult {
+        Success = 0,
+        SuccessFileOverwritten = 1,
+        InstallError = 2,
+        ErrorBaseGame = 3,
+        ErrorFilenameExtension = 4,
+    };
 
-        // Try to load a custom driver.
-        if (custom_driver_name.size()) {
-            handle = adrenotools_open_libvulkan(
-                RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
-                custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
-        }
+    m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
+    m_system.GetFileSystemController().CreateFactories(*m_vfs);
 
-        // Try to load the system driver.
-        if (!handle) {
-            handle =
-                adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
-                                           nullptr, nullptr, file_redirect_dir_, nullptr);
+    [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
+    if (file_extension == "nsp") {
+        nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
+        if (nsp->IsExtractedType()) {
+            return InstallError;
         }
-
-        m_vulkan_library = std::make_shared<Common::DynamicLibrary>(handle);
-#endif
+    } else {
+        return ErrorFilenameExtension;
     }
 
-    bool IsRunning() const {
-        return m_is_running;
+    if (!nsp) {
+        return InstallError;
     }
 
-    bool IsPaused() const {
-        return m_is_running && m_is_paused;
+    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
+        return InstallError;
     }
 
-    const Core::PerfStatsResults& PerfStats() const {
-        std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
-        return m_perf_stats;
-    }
+    jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
+                                                                                        copy_func);
 
-    void SurfaceChanged() {
-        if (!IsRunning()) {
-            return;
-        }
-        m_window->OnSurfaceChanged(m_native_window);
+    switch (res) {
+    case FileSys::InstallResult::Success:
+        return Success;
+    case FileSys::InstallResult::OverwriteExisting:
+        return SuccessFileOverwritten;
+    case FileSys::InstallResult::ErrorBaseInstall:
+        return ErrorBaseGame;
+    default:
+        return InstallError;
     }
+}
 
-    void ConfigureFilesystemProvider(const std::string& filepath) {
-        const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read);
-        if (!file) {
-            return;
-        }
-
-        auto loader = Loader::GetLoader(m_system, file);
-        if (!loader) {
-            return;
-        }
-
-        const auto file_type = loader->GetFileType();
-        if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
-            return;
-        }
+void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
+                                           const std::string& custom_driver_dir,
+                                           const std::string& custom_driver_name,
+                                           const std::string& file_redirect_dir) {
+#ifdef ARCHITECTURE_arm64
+    void* handle{};
+    const char* file_redirect_dir_{};
+    int featureFlags{};
 
-        u64 program_id = 0;
-        const auto res2 = loader->ReadProgramId(program_id);
-        if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
-            m_manual_provider->AddEntry(FileSys::TitleType::Application,
-                                        FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
-                                        program_id, file);
-        } else if (res2 == Loader::ResultStatus::Success &&
-                   (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
-            const auto nsp = file_type == Loader::FileType::NSP
-                                 ? std::make_shared<FileSys::NSP>(file)
-                                 : FileSys::XCI{file}.GetSecurePartitionNSP();
-            for (const auto& title : nsp->GetNCAs()) {
-                for (const auto& entry : title.second) {
-                    m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
-                                                entry.second->GetBaseFile());
-                }
-            }
-        }
+    // Enable driver file redirection when renderer debugging is enabled.
+    if (Settings::values.renderer_debug && file_redirect_dir.size()) {
+        featureFlags |= ADRENOTOOLS_DRIVER_FILE_REDIRECT;
+        file_redirect_dir_ = file_redirect_dir.c_str();
     }
 
-    Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
-        std::scoped_lock lock(m_mutex);
-
-        // Create the render window.
-        m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window,
-                                                       m_vulkan_library);
-
-        m_system.SetFilesystem(m_vfs);
-        m_system.GetUserChannel().clear();
-
-        // Initialize system.
-        jauto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
-        m_software_keyboard = android_keyboard.get();
-        m_system.SetShuttingDown(false);
-        m_system.ApplySettings();
-        Settings::LogSettings();
-        m_system.HIDCore().ReloadInputDevices();
-        m_system.SetAppletFrontendSet({
-            nullptr,                     // Amiibo Settings
-            nullptr,                     // Controller Selector
-            nullptr,                     // Error Display
-            nullptr,                     // Mii Editor
-            nullptr,                     // Parental Controls
-            nullptr,                     // Photo Viewer
-            nullptr,                     // Profile Selector
-            std::move(android_keyboard), // Software Keyboard
-            nullptr,                     // Web Browser
-        });
-
-        // Initialize filesystem.
-        m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
-        m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
-        m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
-                                         m_manual_provider.get());
-        m_system.GetFileSystemController().CreateFactories(*m_vfs);
-        ConfigureFilesystemProvider(filepath);
-
-        // Initialize account manager
-        m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
-
-        // Load the ROM.
-        m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
-        if (m_load_result != Core::SystemResultStatus::Success) {
-            return m_load_result;
-        }
-
-        // Complete initialization.
-        m_system.GPU().Start();
-        m_system.GetCpuManager().OnGpuReady();
-        m_system.RegisterExitCallback([&] { HaltEmulation(); });
+    // Try to load a custom driver.
+    if (custom_driver_name.size()) {
+        handle = adrenotools_open_libvulkan(
+            RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
+            custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
+    }
 
-        return Core::SystemResultStatus::Success;
+    // Try to load the system driver.
+    if (!handle) {
+        handle = adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
+                                            nullptr, nullptr, file_redirect_dir_, nullptr);
     }
 
-    void ShutdownEmulation() {
-        std::scoped_lock lock(m_mutex);
+    m_vulkan_library = std::make_shared<Common::DynamicLibrary>(handle);
+#endif
+}
 
-        m_is_running = false;
+bool EmulationSession::IsRunning() const {
+    return m_is_running;
+}
 
-        // Unload user input.
-        m_system.HIDCore().UnloadInputDevices();
+bool EmulationSession::IsPaused() const {
+    return m_is_running && m_is_paused;
+}
 
-        // Shutdown the main emulated process
-        if (m_load_result == Core::SystemResultStatus::Success) {
-            m_system.DetachDebugger();
-            m_system.ShutdownMainProcess();
-            m_detached_tasks.WaitForAllTasks();
-            m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
-            m_window.reset();
-            OnEmulationStopped(Core::SystemResultStatus::Success);
-            return;
-        }
+const Core::PerfStatsResults& EmulationSession::PerfStats() const {
+    std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
+    return m_perf_stats;
+}
 
-        // Tear down the render window.
-        m_window.reset();
+void EmulationSession::SurfaceChanged() {
+    if (!IsRunning()) {
+        return;
     }
+    m_window->OnSurfaceChanged(m_native_window);
+}
 
-    void PauseEmulation() {
-        std::scoped_lock lock(m_mutex);
-        m_system.Pause();
-        m_is_paused = true;
+void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) {
+    const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read);
+    if (!file) {
+        return;
     }
 
-    void UnPauseEmulation() {
-        std::scoped_lock lock(m_mutex);
-        m_system.Run();
-        m_is_paused = false;
+    auto loader = Loader::GetLoader(m_system, file);
+    if (!loader) {
+        return;
     }
 
-    void HaltEmulation() {
-        std::scoped_lock lock(m_mutex);
-        m_is_running = false;
-        m_cv.notify_one();
+    const auto file_type = loader->GetFileType();
+    if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
+        return;
     }
 
-    void RunEmulation() {
-        {
-            std::scoped_lock lock(m_mutex);
-            m_is_running = true;
-        }
-
-        // Load the disk shader cache.
-        if (Settings::values.use_disk_shader_cache.GetValue()) {
-            LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
-            m_system.Renderer().ReadRasterizer()->LoadDiskResources(
-                m_system.GetApplicationProcessProgramID(), std::stop_token{},
-                LoadDiskCacheProgress);
-            LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
+    u64 program_id = 0;
+    const auto res2 = loader->ReadProgramId(program_id);
+    if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
+        m_manual_provider->AddEntry(FileSys::TitleType::Application,
+                                    FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
+                                    program_id, file);
+    } else if (res2 == Loader::ResultStatus::Success &&
+               (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
+        const auto nsp = file_type == Loader::FileType::NSP
+                             ? std::make_shared<FileSys::NSP>(file)
+                             : FileSys::XCI{file}.GetSecurePartitionNSP();
+        for (const auto& title : nsp->GetNCAs()) {
+            for (const auto& entry : title.second) {
+                m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
+                                            entry.second->GetBaseFile());
+            }
         }
+    }
+}
 
-        void(m_system.Run());
-
-        if (m_system.DebuggerEnabled()) {
-            m_system.InitializeDebugger();
-        }
+Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath) {
+    std::scoped_lock lock(m_mutex);
+
+    // Create the render window.
+    m_window =
+        std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library);
+
+    m_system.SetFilesystem(m_vfs);
+    m_system.GetUserChannel().clear();
+
+    // Initialize system.
+    jauto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
+    m_software_keyboard = android_keyboard.get();
+    m_system.SetShuttingDown(false);
+    m_system.ApplySettings();
+    Settings::LogSettings();
+    m_system.HIDCore().ReloadInputDevices();
+    m_system.SetAppletFrontendSet({
+        nullptr,                     // Amiibo Settings
+        nullptr,                     // Controller Selector
+        nullptr,                     // Error Display
+        nullptr,                     // Mii Editor
+        nullptr,                     // Parental Controls
+        nullptr,                     // Photo Viewer
+        nullptr,                     // Profile Selector
+        std::move(android_keyboard), // Software Keyboard
+        nullptr,                     // Web Browser
+    });
+
+    // Initialize filesystem.
+    m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
+    m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
+    m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
+                                     m_manual_provider.get());
+    m_system.GetFileSystemController().CreateFactories(*m_vfs);
+    ConfigureFilesystemProvider(filepath);
+
+    // Initialize account manager
+    m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
+
+    // Load the ROM.
+    m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
+    if (m_load_result != Core::SystemResultStatus::Success) {
+        return m_load_result;
+    }
+
+    // Complete initialization.
+    m_system.GPU().Start();
+    m_system.GetCpuManager().OnGpuReady();
+    m_system.RegisterExitCallback([&] { HaltEmulation(); });
 
-        OnEmulationStarted();
+    return Core::SystemResultStatus::Success;
+}
 
-        while (true) {
-            {
-                [[maybe_unused]] std::unique_lock lock(m_mutex);
-                if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
-                                  [&]() { return !m_is_running; })) {
-                    // Emulation halted.
-                    break;
-                }
-            }
-            {
-                // Refresh performance stats.
-                std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
-                m_perf_stats = m_system.GetAndResetPerfStats();
-            }
-        }
-    }
+void EmulationSession::ShutdownEmulation() {
+    std::scoped_lock lock(m_mutex);
 
-    std::string GetRomTitle(const std::string& path) {
-        return GetRomMetadata(path).title;
-    }
+    m_is_running = false;
 
-    std::vector<u8> GetRomIcon(const std::string& path) {
-        return GetRomMetadata(path).icon;
-    }
+    // Unload user input.
+    m_system.HIDCore().UnloadInputDevices();
 
-    bool GetIsHomebrew(const std::string& path) {
-        return GetRomMetadata(path).isHomebrew;
+    // Shutdown the main emulated process
+    if (m_load_result == Core::SystemResultStatus::Success) {
+        m_system.DetachDebugger();
+        m_system.ShutdownMainProcess();
+        m_detached_tasks.WaitForAllTasks();
+        m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
+        m_window.reset();
+        OnEmulationStopped(Core::SystemResultStatus::Success);
+        return;
     }
 
-    void ResetRomMetadata() {
-        m_rom_metadata_cache.clear();
-    }
+    // Tear down the render window.
+    m_window.reset();
+}
 
-    bool IsHandheldOnly() {
-        jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
+void EmulationSession::PauseEmulation() {
+    std::scoped_lock lock(m_mutex);
+    m_system.Pause();
+    m_is_paused = true;
+}
 
-        if (npad_style_set.fullkey == 1) {
-            return false;
-        }
+void EmulationSession::UnPauseEmulation() {
+    std::scoped_lock lock(m_mutex);
+    m_system.Run();
+    m_is_paused = false;
+}
 
-        if (npad_style_set.handheld == 0) {
-            return false;
-        }
+void EmulationSession::HaltEmulation() {
+    std::scoped_lock lock(m_mutex);
+    m_is_running = false;
+    m_cv.notify_one();
+}
 
-        return !Settings::IsDockedMode();
+void EmulationSession::RunEmulation() {
+    {
+        std::scoped_lock lock(m_mutex);
+        m_is_running = true;
     }
 
-    void SetDeviceType([[maybe_unused]] int index, int type) {
-        jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
-        controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
+    // Load the disk shader cache.
+    if (Settings::values.use_disk_shader_cache.GetValue()) {
+        LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
+        m_system.Renderer().ReadRasterizer()->LoadDiskResources(
+            m_system.GetApplicationProcessProgramID(), std::stop_token{}, LoadDiskCacheProgress);
+        LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
     }
 
-    void OnGamepadConnectEvent([[maybe_unused]] int index) {
-        jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
-
-        // Ensure that player1 is configured correctly and handheld disconnected
-        if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
-            jauto handheld =
-                m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
+    void(m_system.Run());
 
-            if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
-                handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
-                controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
-                handheld->Disconnect();
-            }
-        }
+    if (m_system.DebuggerEnabled()) {
+        m_system.InitializeDebugger();
+    }
 
-        // Ensure that handheld is configured correctly and player 1 disconnected
-        if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
-            jauto player1 =
-                m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
+    OnEmulationStarted();
 
-            if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
-                player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
-                controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
-                player1->Disconnect();
+    while (true) {
+        {
+            [[maybe_unused]] std::unique_lock lock(m_mutex);
+            if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
+                              [&]() { return !m_is_running; })) {
+                // Emulation halted.
+                break;
             }
         }
-
-        if (!controller->IsConnected()) {
-            controller->Connect();
+        {
+            // Refresh performance stats.
+            std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
+            m_perf_stats = m_system.GetAndResetPerfStats();
         }
     }
+}
+
+bool EmulationSession::IsHandheldOnly() {
+    jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
 
-    void OnGamepadDisconnectEvent([[maybe_unused]] int index) {
-        jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
-        controller->Disconnect();
+    if (npad_style_set.fullkey == 1) {
+        return false;
     }
 
-    SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard() {
-        return m_software_keyboard;
+    if (npad_style_set.handheld == 0) {
+        return false;
     }
 
-private:
-    struct RomMetadata {
-        std::string title;
-        std::vector<u8> icon;
-        bool isHomebrew;
-    };
+    return !Settings::IsDockedMode();
+}
 
-    RomMetadata GetRomMetadata(const std::string& path) {
-        if (jauto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
-            return search->second;
-        }
+void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
+    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
+    controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
+}
 
-        return CacheRomMetadata(path);
-    }
+void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
+    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
 
-    RomMetadata CacheRomMetadata(const std::string& path) {
-        jconst file = Core::GetGameFileFromPath(m_vfs, path);
-        jauto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
+    // Ensure that player1 is configured correctly and handheld disconnected
+    if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
+        jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
 
-        RomMetadata entry;
-        loader->ReadTitle(entry.title);
-        loader->ReadIcon(entry.icon);
-        if (loader->GetFileType() == Loader::FileType::NRO) {
-            jauto loader_nro = reinterpret_cast<Loader::AppLoader_NRO*>(loader.get());
-            entry.isHomebrew = loader_nro->IsHomebrew();
-        } else {
-            entry.isHomebrew = false;
+        if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
+            handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
+            controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
+            handheld->Disconnect();
         }
-
-        m_rom_metadata_cache[path] = entry;
-
-        return entry;
     }
 
-private:
-    static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) {
-        JNIEnv* env = IDCache::GetEnvForThread();
-        env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
-                                  IDCache::GetDiskCacheLoadProgress(), static_cast<jint>(stage),
-                                  static_cast<jint>(progress), static_cast<jint>(max));
-    }
+    // Ensure that handheld is configured correctly and player 1 disconnected
+    if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
+        jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
 
-    static void OnEmulationStarted() {
-        JNIEnv* env = IDCache::GetEnvForThread();
-        env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
-                                  IDCache::GetOnEmulationStarted());
+        if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
+            player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
+            controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
+            player1->Disconnect();
+        }
     }
 
-    static void OnEmulationStopped(Core::SystemResultStatus result) {
-        JNIEnv* env = IDCache::GetEnvForThread();
-        env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
-                                  IDCache::GetOnEmulationStopped(), static_cast<jint>(result));
+    if (!controller->IsConnected()) {
+        controller->Connect();
     }
+}
 
-private:
-    static EmulationSession s_instance;
-
-    // Frontend management
-    std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
-
-    // Window management
-    std::unique_ptr<EmuWindow_Android> m_window;
-    ANativeWindow* m_native_window{};
-
-    // Core emulation
-    Core::System m_system;
-    InputCommon::InputSubsystem m_input_subsystem;
-    Common::DetachedTasks m_detached_tasks;
-    Core::PerfStatsResults m_perf_stats{};
-    std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
-    Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
-    std::atomic<bool> m_is_running = false;
-    std::atomic<bool> m_is_paused = false;
-    SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
-    std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
-    std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
+void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
+    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
+    controller->Disconnect();
+}
 
-    // GPU driver parameters
-    std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
+SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {
+    return m_software_keyboard;
+}
 
-    // Synchronization
-    std::condition_variable_any m_cv;
-    mutable std::mutex m_perf_stats_mutex;
-    mutable std::mutex m_mutex;
-};
+void EmulationSession::LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress,
+                                             int max) {
+    JNIEnv* env = IDCache::GetEnvForThread();
+    env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
+                              IDCache::GetDiskCacheLoadProgress(), static_cast<jint>(stage),
+                              static_cast<jint>(progress), static_cast<jint>(max));
+}
 
-/*static*/ EmulationSession EmulationSession::s_instance;
+void EmulationSession::OnEmulationStarted() {
+    JNIEnv* env = IDCache::GetEnvForThread();
+    env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnEmulationStarted());
+}
 
-} // Anonymous namespace
+void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
+    JNIEnv* env = IDCache::GetEnvForThread();
+    env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnEmulationStopped(),
+                              static_cast<jint>(result));
+}
 
 static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
     Common::Log::Initialize();
@@ -657,10 +558,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_stopEmulation(JNIEnv* env, jclass cla
     EmulationSession::GetInstance().HaltEmulation();
 }
 
-void Java_org_yuzu_yuzu_1emu_NativeLibrary_resetRomMetadata(JNIEnv* env, jclass clazz) {
-    EmulationSession::GetInstance().ResetRomMetadata();
-}
-
 jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning(JNIEnv* env, jclass clazz) {
     return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
 }
@@ -766,46 +663,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass c
     }
 }
 
-jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon(JNIEnv* env, jclass clazz,
-                                                         jstring j_filename) {
-    jauto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
-    jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
-    env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
-                            reinterpret_cast<jbyte*>(icon_data.data()));
-    return icon;
-}
-
-jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getTitle(JNIEnv* env, jclass clazz,
-                                                       jstring j_filename) {
-    jauto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
-    return env->NewStringUTF(title.c_str());
-}
-
-jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDescription(JNIEnv* env, jclass clazz,
-                                                             jstring j_filename) {
-    return j_filename;
-}
-
-jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGameId(JNIEnv* env, jclass clazz,
-                                                        jstring j_filename) {
-    return j_filename;
-}
-
-jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRegions(JNIEnv* env, jclass clazz,
-                                                         jstring j_filename) {
-    return env->NewStringUTF("");
-}
-
-jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany(JNIEnv* env, jclass clazz,
-                                                         jstring j_filename) {
-    return env->NewStringUTF("");
-}
-
-jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew(JNIEnv* env, jclass clazz,
-                                                          jstring j_filename) {
-    return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename));
-}
-
 void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation(JNIEnv* env, jclass clazz) {
     // Create the default config.ini.
     Config{};

+ 84 - 0
src/android/app/src/main/jni/native.h

@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <android/native_window_jni.h>
+#include "common/detached_tasks.h"
+#include "core/core.h"
+#include "core/file_sys/registered_cache.h"
+#include "core/hle/service/acc/profile_manager.h"
+#include "core/perf_stats.h"
+#include "jni/applets/software_keyboard.h"
+#include "jni/emu_window/emu_window.h"
+#include "video_core/rasterizer_interface.h"
+
+#pragma once
+
+class EmulationSession final {
+public:
+    explicit EmulationSession();
+    ~EmulationSession() = default;
+
+    static EmulationSession& GetInstance();
+    const Core::System& System() const;
+    Core::System& System();
+
+    const EmuWindow_Android& Window() const;
+    EmuWindow_Android& Window();
+    ANativeWindow* NativeWindow() const;
+    void SetNativeWindow(ANativeWindow* native_window);
+    void SurfaceChanged();
+
+    int InstallFileToNand(std::string filename, std::string file_extension);
+    void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
+                             const std::string& custom_driver_name,
+                             const std::string& file_redirect_dir);
+
+    bool IsRunning() const;
+    bool IsPaused() const;
+    void PauseEmulation();
+    void UnPauseEmulation();
+    void HaltEmulation();
+    void RunEmulation();
+    void ShutdownEmulation();
+
+    const Core::PerfStatsResults& PerfStats() const;
+    void ConfigureFilesystemProvider(const std::string& filepath);
+    Core::SystemResultStatus InitializeEmulation(const std::string& filepath);
+
+    bool IsHandheldOnly();
+    void SetDeviceType([[maybe_unused]] int index, int type);
+    void OnGamepadConnectEvent([[maybe_unused]] int index);
+    void OnGamepadDisconnectEvent([[maybe_unused]] int index);
+    SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard();
+
+private:
+    static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
+    static void OnEmulationStarted();
+    static void OnEmulationStopped(Core::SystemResultStatus result);
+
+private:
+    // Window management
+    std::unique_ptr<EmuWindow_Android> m_window;
+    ANativeWindow* m_native_window{};
+
+    // Core emulation
+    Core::System m_system;
+    InputCommon::InputSubsystem m_input_subsystem;
+    Common::DetachedTasks m_detached_tasks;
+    Core::PerfStatsResults m_perf_stats{};
+    std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
+    Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
+    std::atomic<bool> m_is_running = false;
+    std::atomic<bool> m_is_paused = false;
+    SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
+    std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
+    std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
+
+    // GPU driver parameters
+    std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
+
+    // Synchronization
+    std::condition_variable_any m_cv;
+    mutable std::mutex m_perf_stats_mutex;
+    mutable std::mutex m_mutex;
+};

+ 33 - 0
src/common/fs/fs_android.cpp

@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 
 #include "common/fs/fs_android.h"
+#include "common/string_util.h"
 
 namespace Common::FS::Android {
 
@@ -28,28 +29,35 @@ void RegisterCallbacks(JNIEnv* env, jclass clazz) {
     env->GetJavaVM(&g_jvm);
     native_library = clazz;
 
+#define FH(FunctionName, JMethodID, Caller, JMethodName, Signature)                                \
+    F(JMethodID, JMethodName, Signature)
 #define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature)                   \
     F(JMethodID, JMethodName, Signature)
 #define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature)               \
     F(JMethodID, JMethodName, Signature)
 #define F(JMethodID, JMethodName, Signature)                                                       \
     JMethodID = env->GetStaticMethodID(native_library, JMethodName, Signature);
+    ANDROID_SINGLE_PATH_HELPER_FUNCTIONS(FH)
     ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
     ANDROID_STORAGE_FUNCTIONS(FS)
 #undef F
 #undef FS
 #undef FR
+#undef FH
 }
 
 void UnRegisterCallbacks() {
+#define FH(FunctionName, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
 #define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
 #define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
 #define F(JMethodID) JMethodID = nullptr;
+    ANDROID_SINGLE_PATH_HELPER_FUNCTIONS(FH)
     ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
     ANDROID_STORAGE_FUNCTIONS(FS)
 #undef F
 #undef FS
 #undef FR
+#undef FH
 }
 
 bool IsContentUri(const std::string& path) {
@@ -95,4 +103,29 @@ ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
 #undef F
 #undef FR
 
+#define FH(FunctionName, JMethodID, Caller, JMethodName, Signature)                                \
+    F(FunctionName, JMethodID, Caller)
+#define F(FunctionName, JMethodID, Caller)                                                         \
+    std::string FunctionName(const std::string& filepath) {                                        \
+        if (JMethodID == nullptr) {                                                                \
+            return 0;                                                                              \
+        }                                                                                          \
+        auto env = GetEnvForThread();                                                              \
+        jstring j_filepath = env->NewStringUTF(filepath.c_str());                                  \
+        jstring j_return =                                                                         \
+            static_cast<jstring>(env->Caller(native_library, JMethodID, j_filepath));              \
+        if (!j_return) {                                                                           \
+            return {};                                                                             \
+        }                                                                                          \
+        const jchar* jchars = env->GetStringChars(j_return, nullptr);                              \
+        const jsize length = env->GetStringLength(j_return);                                       \
+        const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);  \
+        const std::string converted_string = Common::UTF16ToUTF8(string_view);                     \
+        env->ReleaseStringChars(j_return, jchars);                                                 \
+        return converted_string;                                                                   \
+    }
+ANDROID_SINGLE_PATH_HELPER_FUNCTIONS(FH)
+#undef F
+#undef FH
+
 } // namespace Common::FS::Android

+ 15 - 0
src/common/fs/fs_android.h

@@ -17,19 +17,28 @@
       "(Ljava/lang/String;)Z")                                                                     \
     V(Exists, bool, file_exists, CallStaticBooleanMethod, "exists", "(Ljava/lang/String;)Z")
 
+#define ANDROID_SINGLE_PATH_HELPER_FUNCTIONS(V)                                                    \
+    V(GetParentDirectory, get_parent_directory, CallStaticObjectMethod, "getParentDirectory",      \
+      "(Ljava/lang/String;)Ljava/lang/String;")                                                    \
+    V(GetFilename, get_filename, CallStaticObjectMethod, "getFilename",                            \
+      "(Ljava/lang/String;)Ljava/lang/String;")
+
 namespace Common::FS::Android {
 
 static JavaVM* g_jvm = nullptr;
 static jclass native_library = nullptr;
 
+#define FH(FunctionName, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
 #define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
 #define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
 #define F(JMethodID) static jmethodID JMethodID = nullptr;
+ANDROID_SINGLE_PATH_HELPER_FUNCTIONS(FH)
 ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
 ANDROID_STORAGE_FUNCTIONS(FS)
 #undef F
 #undef FS
 #undef FR
+#undef FH
 
 enum class OpenMode {
     Read,
@@ -62,4 +71,10 @@ ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
 #undef F
 #undef FR
 
+#define FH(FunctionName, JMethodID, Caller, JMethodName, Signature) F(FunctionName)
+#define F(FunctionName) std::string FunctionName(const std::string& filepath);
+ANDROID_SINGLE_PATH_HELPER_FUNCTIONS(FH)
+#undef F
+#undef FH
+
 } // namespace Common::FS::Android

+ 10 - 0
src/common/fs/path_util.cpp

@@ -401,6 +401,16 @@ std::string SanitizePath(std::string_view path_, DirectorySeparator directory_se
 }
 
 std::string_view GetParentPath(std::string_view path) {
+    if (path.empty()) {
+        return path;
+    }
+
+#ifdef ANDROID
+    if (path[0] != '/') {
+        std::string path_string{path};
+        return FS::Android::GetParentDirectory(path_string);
+    }
+#endif
     const auto name_bck_index = path.rfind('\\');
     const auto name_fwd_index = path.rfind('/');
     std::size_t name_index;

+ 12 - 0
src/common/string_util.cpp

@@ -14,6 +14,10 @@
 #include <windows.h>
 #endif
 
+#ifdef ANDROID
+#include <common/fs/fs_android.h>
+#endif
+
 namespace Common {
 
 /// Make a string lowercase
@@ -63,6 +67,14 @@ bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _
     if (full_path.empty())
         return false;
 
+#ifdef ANDROID
+    if (full_path[0] != '/') {
+        *_pPath = Common::FS::Android::GetParentDirectory(full_path);
+        *_pFilename = Common::FS::Android::GetFilename(full_path);
+        return true;
+    }
+#endif
+
     std::size_t dir_end = full_path.find_last_of("/"
 // windows needs the : included for something like just "C:" to be considered a directory
 #ifdef _WIN32