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.
This commit is contained in:
JosJuice 2023-05-06 11:10:11 +02:00 committed by GitHub
parent 2b8610fcc4
commit 62792b6b0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 127 additions and 168 deletions

View File

@ -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();
/**

View File

@ -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.

View File

@ -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();
}

View File

@ -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) {
int[] vector;
try {
String url = request.uri.toString();
int[] vector = NativeLibrary.GetIcon(url);
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);

View File

@ -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

View File

@ -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<u8> GetSMDHData(std::string physical_name) {
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(physical_name);
std::vector<u8> GetSMDHData(const std::string& path) {
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(path);
if (!loader) {
return {};
}
@ -51,55 +52,55 @@ std::vector<u8> GetSMDHData(std::string physical_name) {
return smdh;
}
std::u16string GetTitle(std::string physical_name) {
Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English;
std::vector<u8> 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<Loader::SMDH*>(env->GetLongField(obj, IDCache::GetGameInfoPointer()));
}
Loader::SMDH smdh;
memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
JNIEXPORT jlong JNICALL Java_org_citra_citra_1emu_model_GameInfo_initialize(JNIEnv* env, jclass,
jstring j_path) {
std::vector<u8> 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<jlong>(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<char16_t*>(smdh.titles[static_cast<int>(language)].long_title.data())};
reinterpret_cast<char16_t*>(smdh->titles[static_cast<size_t>(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<u8> 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<char16_t*>(smdh.titles[static_cast<int>(language)].publisher.data());
reinterpret_cast<char16_t*>(smdh->titles[static_cast<size_t>(language)].publisher.data());
return publisher;
return ToJString(env, Common::UTF16ToUTF8(publisher).data());
}
std::string GetRegions(std::string physical_name) {
std::vector<u8> 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<GameRegion, const char*> 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<GameRegion> regions = smdh.GetRegions();
std::vector<GameRegion> 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<u16> GetIcon(std::string physical_name) {
std::vector<u8> smdh_data = GetSMDHData(physical_name);
if (!Loader::IsValidSMDH(smdh_data)) {
// SMDH is not valid, return null
return std::vector<u16>(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<u16> icon_data = smdh.GetIcon(true);
return icon_data;
std::vector<u16> icon_data = smdh->GetIcon(true);
if (icon_data.empty()) {
return nullptr;
}
} // namespace GameInfo
jintArray icon = env->NewIntArray(static_cast<jsize>(icon_data.size() / 2));
env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon),
reinterpret_cast<jint*>(icon_data.data()));
return icon;
}
}

View File

@ -1,19 +0,0 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <string>
#include "common/common_types.h"
namespace GameInfo {
std::vector<u8> 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<u16> GetIcon(std::string physical_name);
} // namespace GameInfo

View File

@ -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<VideoCore::LoadCallbackStage, jobject> 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, "<init>", "(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();

View File

@ -34,6 +34,8 @@ jclass GetCheatClass();
jfieldID GetCheatPointer();
jmethodID GetCheatConstructor();
jfieldID GetGameInfoPointer();
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
} // namespace IDCache

View File

@ -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<u16> icon_data = GameInfo::GetIcon(filepath);
if (icon_data.size() == 0) {
return 0;
}
jintArray icon = env->NewIntArray(static_cast<jsize>(icon_data.size() / 2));
env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon),
reinterpret_cast<jint*>(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;