From 62792b6b0ee2c3191e4a03991e64043ec085ac6a Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 6 May 2023 11:10:11 +0200 Subject: [PATCH] android: Turn GameInfo into a class (#6494) By only loading data from disk when creating an instance of this new class instead of on every method call, we save a lot of file open operations, which due to SAF are very expensive. This should noticeably speed up game list scanning. No intended change in what metadata is shown. --- .../org/citra/citra_emu/NativeLibrary.java | 24 ---- .../citra/citra_emu/model/GameDatabase.java | 26 ++--- .../org/citra/citra_emu/model/GameInfo.java | 37 ++++++ .../utils/GameIconRequestHandler.java | 12 +- src/android/app/src/main/jni/CMakeLists.txt | 1 - src/android/app/src/main/jni/game_info.cpp | 108 +++++++++--------- src/android/app/src/main/jni/game_info.h | 19 --- src/android/app/src/main/jni/id_cache.cpp | 11 ++ src/android/app/src/main/jni/id_cache.h | 2 + src/android/app/src/main/jni/native.cpp | 55 --------- 10 files changed, 127 insertions(+), 168 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java delete mode 100644 src/android/app/src/main/jni/game_info.h diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java index 3aa73f99e..08041f790 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -128,30 +128,6 @@ public final class NativeLibrary { public static native void InitGameIni(String gameID); - /** - * Gets the embedded icon within the given ROM. - * - * @param filename the file path to the ROM. - * @return an integer array containing the color data for the icon. - */ - public static native int[] GetIcon(String filename); - - /** - * 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. - */ - public static native String GetTitle(String filename); - - public static native String GetDescription(String filename); - - public static native String GetGameId(String filename); - - public static native String GetRegions(String filename); - - public static native String GetCompany(String filename); - public static native String GetGitRevision(); /** diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java index 4a67c9a1a..cbbd8e32e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java @@ -12,6 +12,7 @@ import org.citra.citra_emu.utils.FileUtil; import org.citra.citra_emu.utils.Log; import java.io.File; +import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; import java.util.HashSet; @@ -206,27 +207,26 @@ public final class GameDatabase extends SQLiteOpenHelper { } private static void attemptToAddGame(SQLiteDatabase database, String filePath) { - String name = NativeLibrary.GetTitle(filePath); + GameInfo gameInfo; + try { + gameInfo = new GameInfo(filePath); + } catch (IOException e) { + gameInfo = null; + } + + String name = gameInfo != null ? gameInfo.getTitle() : ""; // If the game's title field is empty, use the filename. if (name.isEmpty()) { name = filePath.substring(filePath.lastIndexOf("/") + 1); } - String gameId = NativeLibrary.GetGameId(filePath); - - // If the game's ID field is empty, use the filename without extension. - if (gameId.isEmpty()) { - gameId = filePath.substring(filePath.lastIndexOf("/") + 1, - filePath.lastIndexOf(".")); - } - ContentValues game = Game.asContentValues(name, - NativeLibrary.GetDescription(filePath).replace("\n", " "), - NativeLibrary.GetRegions(filePath), + filePath.replace("\n", " "), + gameInfo != null ? gameInfo.getRegions() : "Invalid region", filePath, - gameId, - NativeLibrary.GetCompany(filePath)); + filePath, + gameInfo != null ? gameInfo.getCompany() : ""); // Try to update an existing game first. int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java new file mode 100644 index 000000000..35ce9947c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.java @@ -0,0 +1,37 @@ +package org.citra.citra_emu.model; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; + +public class GameInfo { + @Keep + private final long mPointer; + + @Keep + public GameInfo(String path) throws IOException { + mPointer = initialize(path); + if (mPointer == 0L) { + throw new IOException(); + } + } + + private static native long initialize(String path); + + @Override + protected native void finalize(); + + @NonNull + public native String getTitle(); + + @NonNull + public native String getRegions(); + + @NonNull + public native String getCompany(); + + @Nullable + public native int[] getIcon(); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java index 6ebe70161..7057c07ad 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java @@ -7,7 +7,9 @@ import com.squareup.picasso.Request; import com.squareup.picasso.RequestHandler; import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.model.GameInfo; +import java.io.IOException; import java.nio.IntBuffer; public class GameIconRequestHandler extends RequestHandler { @@ -18,8 +20,14 @@ public class GameIconRequestHandler extends RequestHandler { @Override public Result load(Request request, int networkPolicy) { - String url = request.uri.toString(); - int[] vector = NativeLibrary.GetIcon(url); + int[] vector; + try { + String url = request.uri.toString(); + vector = new GameInfo(url).getIcon(); + } catch (IOException e) { + vector = null; + } + Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); return new Result(bitmap, Picasso.LoadedFrom.DISK); diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 5a1abc35d..531704490 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -20,7 +20,6 @@ add_library(citra-android SHARED emu_window/emu_window.cpp emu_window/emu_window.h game_info.cpp - game_info.h game_settings.cpp game_settings.h id_cache.cpp diff --git a/src/android/app/src/main/jni/game_info.cpp b/src/android/app/src/main/jni/game_info.cpp index 80c0379d6..ca0ff355c 100644 --- a/src/android/app/src/main/jni/game_info.cpp +++ b/src/android/app/src/main/jni/game_info.cpp @@ -12,12 +12,13 @@ #include "core/hle/service/fs/archive.h" #include "core/loader/loader.h" #include "core/loader/smdh.h" -#include "jni/game_info.h" +#include "jni/android_common/android_common.h" +#include "jni/id_cache.h" -namespace GameInfo { +namespace { -std::vector GetSMDHData(std::string physical_name) { - std::unique_ptr loader = Loader::GetLoader(physical_name); +std::vector GetSMDHData(const std::string& path) { + std::unique_ptr loader = Loader::GetLoader(path); if (!loader) { return {}; } @@ -51,55 +52,55 @@ std::vector GetSMDHData(std::string physical_name) { return smdh; } -std::u16string GetTitle(std::string physical_name) { - Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; - std::vector smdh_data = GetSMDHData(physical_name); +} // namespace - if (!Loader::IsValidSMDH(smdh_data)) { - // SMDH is not valid, return null - return {}; +extern "C" { + +static Loader::SMDH* GetPointer(JNIEnv* env, jobject obj) { + return reinterpret_cast(env->GetLongField(obj, IDCache::GetGameInfoPointer())); +} + +JNIEXPORT jlong JNICALL Java_org_citra_citra_1emu_model_GameInfo_initialize(JNIEnv* env, jclass, + jstring j_path) { + std::vector smdh_data = GetSMDHData(GetJString(env, j_path)); + + Loader::SMDH* smdh = nullptr; + if (Loader::IsValidSMDH(smdh_data)) { + smdh = new Loader::SMDH; + memcpy(smdh, smdh_data.data(), sizeof(Loader::SMDH)); } + return reinterpret_cast(smdh); +} - Loader::SMDH smdh; - memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_model_GameInfo_finalize(JNIEnv* env, jobject obj) { + delete GetPointer(env, obj); +} + +jstring Java_org_citra_citra_1emu_model_GameInfo_getTitle(JNIEnv* env, jobject obj) { + Loader::SMDH* smdh = GetPointer(env, obj); + Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; // Get the title from SMDH in UTF-16 format std::u16string title{ - reinterpret_cast(smdh.titles[static_cast(language)].long_title.data())}; + reinterpret_cast(smdh->titles[static_cast(language)].long_title.data())}; - return title; + return ToJString(env, Common::UTF16ToUTF8(title).data()); } -std::u16string GetPublisher(std::string physical_name) { +jstring Java_org_citra_citra_1emu_model_GameInfo_getCompany(JNIEnv* env, jobject obj) { + Loader::SMDH* smdh = GetPointer(env, obj); Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; - std::vector smdh_data = GetSMDHData(physical_name); - - if (!Loader::IsValidSMDH(smdh_data)) { - // SMDH is not valid, return null - return {}; - } - - Loader::SMDH smdh; - memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); // Get the Publisher's name from SMDH in UTF-16 format char16_t* publisher; publisher = - reinterpret_cast(smdh.titles[static_cast(language)].publisher.data()); + reinterpret_cast(smdh->titles[static_cast(language)].publisher.data()); - return publisher; + return ToJString(env, Common::UTF16ToUTF8(publisher).data()); } -std::string GetRegions(std::string physical_name) { - std::vector smdh_data = GetSMDHData(physical_name); - - if (!Loader::IsValidSMDH(smdh_data)) { - // SMDH is not valid, return "Invalid region" - return "Invalid region"; - } - - Loader::SMDH smdh; - memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); +jstring Java_org_citra_citra_1emu_model_GameInfo_getRegions(JNIEnv* env, jobject obj) { + Loader::SMDH* smdh = GetPointer(env, obj); using GameRegion = Loader::SMDH::GameRegion; static const std::map regions_map = { @@ -107,10 +108,10 @@ std::string GetRegions(std::string physical_name) { {GameRegion::Europe, "Europe"}, {GameRegion::Australia, "Australia"}, {GameRegion::China, "China"}, {GameRegion::Korea, "Korea"}, {GameRegion::Taiwan, "Taiwan"}}; - std::vector regions = smdh.GetRegions(); + std::vector regions = smdh->GetRegions(); if (regions.empty()) { - return "Invalid region"; + return ToJString(env, "Invalid region"); } const bool region_free = @@ -119,7 +120,7 @@ std::string GetRegions(std::string physical_name) { }); if (region_free) { - return "Region free"; + return ToJString(env, "Region free"); } const std::string separator = ", "; @@ -128,23 +129,22 @@ std::string GetRegions(std::string physical_name) { result += separator + regions_map.at(*region); } - return result; + return ToJString(env, result); } -std::vector GetIcon(std::string physical_name) { - std::vector smdh_data = GetSMDHData(physical_name); - - if (!Loader::IsValidSMDH(smdh_data)) { - // SMDH is not valid, return null - return std::vector(0, 0); - } - - Loader::SMDH smdh; - memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); +jintArray Java_org_citra_citra_1emu_model_GameInfo_getIcon(JNIEnv* env, jobject obj) { + Loader::SMDH* smdh = GetPointer(env, obj); // Always get a 48x48(large) icon - std::vector icon_data = smdh.GetIcon(true); - return icon_data; -} + std::vector icon_data = smdh->GetIcon(true); + if (icon_data.empty()) { + return nullptr; + } -} // namespace GameInfo + jintArray icon = env->NewIntArray(static_cast(icon_data.size() / 2)); + env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon), + reinterpret_cast(icon_data.data())); + + return icon; +} +} diff --git a/src/android/app/src/main/jni/game_info.h b/src/android/app/src/main/jni/game_info.h deleted file mode 100644 index 7b9750fe2..000000000 --- a/src/android/app/src/main/jni/game_info.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include - -#include "common/common_types.h" - -namespace GameInfo { -std::vector GetSMDHData(std::string physical_name); - -std::u16string GetTitle(std::string physical_name); - -std::u16string GetPublisher(std::string physical_name); - -std::string GetRegions(std::string physical_name); - -std::vector GetIcon(std::string physical_name); -} // namespace GameInfo diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 255e1ae0a..f90c046c2 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -40,6 +40,8 @@ static jclass s_cheat_class; static jfieldID s_cheat_pointer; static jmethodID s_cheat_constructor; +static jfieldID s_game_info_pointer; + static std::unordered_map s_java_load_callback_stages; namespace IDCache { @@ -135,6 +137,10 @@ jmethodID GetCheatConstructor() { return s_cheat_constructor; } +jfieldID GetGameInfoPointer() { + return s_game_info_pointer; +} + jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) { const auto it = s_java_load_callback_stages.find(stage); ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage); @@ -205,6 +211,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_cheat_constructor = env->GetMethodID(cheat_class, "", "(J)V"); env->DeleteLocalRef(cheat_class); + // Initialize GameInfo + const jclass game_info_class = env->FindClass("org/citra/citra_emu/model/GameInfo"); + s_game_info_pointer = env->GetFieldID(game_info_class, "mPointer", "J"); + env->DeleteLocalRef(game_info_class); + // Initialize LoadCallbackStage map const auto to_java_load_callback_stage = [env](const std::string& stage) { jclass load_callback_stage_class = IDCache::GetDiskCacheLoadCallbackStageClass(); diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index 87bebed0e..0fd687666 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -34,6 +34,8 @@ jclass GetCheatClass(); jfieldID GetCheatPointer(); jmethodID GetCheatConstructor(); +jfieldID GetGameInfoPointer(); + jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage); } // namespace IDCache diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 9c0e022fb..48d37666a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -32,7 +32,6 @@ #include "jni/camera/still_image_camera.h" #include "jni/config.h" #include "jni/emu_window/emu_window.h" -#include "jni/game_info.h" #include "jni/game_settings.h" #include "jni/id_cache.h" #include "jni/input_manager.h" @@ -436,60 +435,6 @@ void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, window->OnTouchMoved((int)x, (int)y); } -jintArray Java_org_citra_citra_1emu_NativeLibrary_GetIcon(JNIEnv* env, - [[maybe_unused]] jclass clazz, - jstring j_file) { - std::string filepath = GetJString(env, j_file); - - std::vector icon_data = GameInfo::GetIcon(filepath); - if (icon_data.size() == 0) { - return 0; - } - - jintArray icon = env->NewIntArray(static_cast(icon_data.size() / 2)); - env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon), - reinterpret_cast(icon_data.data())); - - return icon; -} - -jstring Java_org_citra_citra_1emu_NativeLibrary_GetTitle(JNIEnv* env, [[maybe_unused]] jclass clazz, - jstring j_filename) { - std::string filepath = GetJString(env, j_filename); - auto Title = GameInfo::GetTitle(filepath); - return env->NewStringUTF(Common::UTF16ToUTF8(Title).data()); -} - -jstring Java_org_citra_citra_1emu_NativeLibrary_GetDescription(JNIEnv* env, - [[maybe_unused]] jclass clazz, - jstring j_filename) { - return j_filename; -} - -jstring Java_org_citra_citra_1emu_NativeLibrary_GetGameId(JNIEnv* env, - [[maybe_unused]] jclass clazz, - jstring j_filename) { - return j_filename; -} - -jstring Java_org_citra_citra_1emu_NativeLibrary_GetRegions(JNIEnv* env, - [[maybe_unused]] jclass clazz, - jstring j_filename) { - std::string filepath = GetJString(env, j_filename); - - std::string regions = GameInfo::GetRegions(filepath); - - return env->NewStringUTF(regions.c_str()); -} - -jstring Java_org_citra_citra_1emu_NativeLibrary_GetCompany(JNIEnv* env, - [[maybe_unused]] jclass clazz, - jstring j_filename) { - std::string filepath = GetJString(env, j_filename); - auto publisher = GameInfo::GetPublisher(filepath); - return env->NewStringUTF(Common::UTF16ToUTF8(publisher).data()); -} - jstring Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, [[maybe_unused]] jclass clazz) { return nullptr;