diff --git a/CMakeLists.txt b/CMakeLists.txt index 732ec793e..4b0410fd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -389,6 +389,7 @@ if(ENABLE_VULKAN) endif() if(ANDROID) + set(HEADER_FILES ${HEADER_FILES} include/jni_driver.hpp) set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp) endif() diff --git a/include/jni_driver.hpp b/include/jni_driver.hpp new file mode 100644 index 000000000..ff6230f12 --- /dev/null +++ b/include/jni_driver.hpp @@ -0,0 +1,7 @@ +#include +#include "helpers.hpp" + +class Pandroid { +public: + static void onSmdhLoaded(const std::vector &smdh); +}; diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 2546aa01f..3e17790ce 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -6,6 +6,10 @@ #include "loader/ncch.hpp" #include "memory.hpp" +#ifdef __ANDROID__ +#include "jni_driver.hpp" +#endif + #include bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) { @@ -255,6 +259,11 @@ bool NCCH::parseSMDH(const std::vector& smdh) { return false; } + + #ifdef __ANDROID__ + Pandroid::onSmdhLoaded(smdh); + #endif + // Bitmask showing which regions are allowed. // https://www.3dbrew.org/wiki/SMDH#Region_Lockout const u32 regionMasks = *(u32*)&smdh[0x2018]; diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp index 8f5c352ed..37d15787f 100644 --- a/src/jni_driver.cpp +++ b/src/jni_driver.cpp @@ -8,10 +8,14 @@ #include "renderer_gl/renderer_gl.hpp" #include "services/hid.hpp" +#include "jni_driver.hpp" + std::unique_ptr emulator = nullptr; HIDService* hidService = nullptr; RendererGL* renderer = nullptr; bool romLoaded = false; +JavaVM* jvm = nullptr; +const char* alberClass = "com/panda3ds/pandroid/AlberDriver"; #define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name @@ -20,7 +24,47 @@ void throwException(JNIEnv* env, const char* message) { env->ThrowNew(exceptionClass, message); } +JNIEnv* jniEnv(){ + JNIEnv* env; + auto status = jvm->GetEnv((void **)&env, JNI_VERSION_1_6); + if(status == JNI_EDETACHED){ + jvm->AttachCurrentThread(&env, nullptr); + } else if(status != JNI_OK){ + throw std::runtime_error("Failed to obtain JNIEnv from JVM!!"); + } + return env; +} + + +void Pandroid::onSmdhLoaded(const std::vector &smdh){ + JNIEnv* env = jniEnv(); + int size = smdh.size(); + + jbyteArray result = env->NewByteArray(size); + + jbyte buffer[size]; + + for(int i = 0; i < size; i++){ + buffer[i] = (jbyte) smdh[i]; + } + env->SetByteArrayRegion(result, 0, size, buffer); + + + auto clazz = env->FindClass(alberClass); + auto method = env->GetStaticMethodID(clazz, "OnSmdhLoaded", "([B)V"); + + env->CallStaticVoidMethod(clazz, method, result); + + env->DeleteLocalRef(result); +} + + extern "C" { + +AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { + env->GetJavaVM(&jvm); +} + AlberFunction(void, Initialize)(JNIEnv* env, jobject obj) { emulator = std::make_unique(); @@ -73,4 +117,4 @@ AlberFunction(void, SetCirclepadAxis)(JNIEnv* env, jobject obj, jint x, jint y) } } -#undef AlberFunction \ No newline at end of file +#undef AlberFunction diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java index 81bf29a52..fe808548d 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java @@ -1,8 +1,16 @@ package com.panda3ds.pandroid; +import android.util.Log; + +import com.panda3ds.pandroid.data.SMDH; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.GameUtils; + public class AlberDriver { AlberDriver() { super(); } + public static native void Setup(); public static native void Initialize(); public static native void RunFrame(int fbo); public static native boolean HasRomLoaded(); @@ -15,5 +23,13 @@ public class AlberDriver { public static native void TouchScreenUp(); public static native void TouchScreenDown(int x, int y); + public static void OnSmdhLoaded(byte[] buffer) { + Log.i(Constants.LOG_TAG, "Loaded rom smdh"); + SMDH smdh = new SMDH(buffer); + GameMetadata game = GameUtils.getCurrentGame(); + GameUtils.removeGame(game); + GameUtils.addGame(GameMetadata.applySMDH(game, smdh)); + } + static { System.loadLibrary("Alber"); } } \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java index 286a7ed32..fc622cd62 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java @@ -23,7 +23,7 @@ public class GameActivity extends BaseActivity { - private final AlberInputListener inputListener = new AlberInputListener(); + private final AlberInputListener inputListener = new AlberInputListener(this); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java index 246decec7..770e5e81b 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java @@ -3,6 +3,7 @@ import android.app.Application; import android.content.Context; +import com.panda3ds.pandroid.AlberDriver; import com.panda3ds.pandroid.data.config.GlobalConfig; import com.panda3ds.pandroid.input.InputMap; import com.panda3ds.pandroid.utils.GameUtils; @@ -17,6 +18,7 @@ public void onCreate() { GlobalConfig.initialize(); GameUtils.initialize(); InputMap.initialize(); + AlberDriver.Setup(); } public static Context getAppContext() { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java index c500d9701..101676647 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java @@ -1,5 +1,8 @@ package com.panda3ds.pandroid.app.game; +import android.app.Activity; +import android.view.KeyEvent; + import com.panda3ds.pandroid.AlberDriver; import com.panda3ds.pandroid.input.InputEvent; import com.panda3ds.pandroid.input.InputMap; @@ -7,7 +10,13 @@ import com.panda3ds.pandroid.lang.Function; import com.panda3ds.pandroid.math.Vector2; +import java.util.Objects; + public class AlberInputListener implements Function { + private final Activity activity; + public AlberInputListener(Activity activity){ + this.activity = activity; + } private final Vector2 axis = new Vector2(0.0f, 0.0f); @@ -15,6 +24,11 @@ public class AlberInputListener implements Function { public void run(InputEvent event) { KeyName key = InputMap.relative(event.getName()); + if (Objects.equals(event.getName(), "KEYCODE_BACK")){ + activity.onBackPressed(); + return; + } + if (key == KeyName.NULL) return; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java index ef49f6f15..c5f1aceed 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java @@ -51,7 +51,7 @@ public void onActivityResult(Uri result) { String uri = result.toString(); if (GameUtils.findByRomPath(uri) == null) { FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ); - GameMetadata game = new GameMetadata(FileUtils.getName(uri).split("\\.")[0], uri, "Unknown"); + GameMetadata game = new GameMetadata(uri, FileUtils.getName(uri).split("\\.")[0],"Unknown"); GameUtils.addGame(game); GameUtils.launch(requireActivity(), game); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java new file mode 100644 index 000000000..ae61cf4bf --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java @@ -0,0 +1,167 @@ +package com.panda3ds.pandroid.data; + +import android.graphics.Bitmap; + +import com.panda3ds.pandroid.data.game.GameRegion; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class SMDH { + private static final int ICON_SIZE = 48; + private static final int META_OFFSET = 0x8 ; + private static final int META_REGION_OFFSET = 0x2018; + private static final int IMAGE_OFFSET = 0x24C0; + + private int metaIndex = 1; + private final ByteBuffer smdh; + private final String[] title = new String[12]; + private final String[] publisher = new String[12]; + private final int[] icon; + + private final GameRegion region; + + public SMDH(byte[] source){ + smdh = ByteBuffer.allocate(source.length); + smdh.position(0); + smdh.put(source); + smdh.position(0); + + region = parseRegion(); + icon = parseIcon(); + parseMeta(); + } + + private GameRegion parseRegion(){ + GameRegion region; + smdh.position(META_REGION_OFFSET); + + int regionMasks = smdh.get() & 0xFF; + + final boolean japan = (regionMasks & 0x1) != 0; + final boolean northAmerica = (regionMasks & 0x2) != 0; + final boolean europe = (regionMasks & 0x4) != 0; + final boolean australia = (regionMasks & 0x8) != 0; + final boolean china = (regionMasks & 0x10) != 0; + final boolean korea = (regionMasks & 0x20) != 0; + + final boolean taiwan = (regionMasks & 0x40) != 0; + if (northAmerica) { + region = GameRegion.NorthAmerican; + } else if (europe) { + region = GameRegion.Europe; + } else if (australia) { + region = GameRegion.Australia; + } else if (japan) { + region = GameRegion.Japan; + metaIndex = 0; + } else if (korea) { + metaIndex = 7; + region = GameRegion.Korean; + } else if (china) { + metaIndex = 6; + region = GameRegion.China; + } else if (taiwan) { + metaIndex = 6; + region = GameRegion.Taiwan; + } else { + region = GameRegion.None; + } + + return region; + } + + private void parseMeta(){ + + for (int i = 0; i < 12; i++){ + smdh.position(META_OFFSET + (512*i) + 0x80); + byte[] data = new byte[0x100]; + smdh.get(data); + title[i] = convertString(data).replaceAll("\n", " "); + } + + for (int i = 0; i < 12; i++){ + smdh.position(META_OFFSET + (512 * i) + 0x180); + byte[] data = new byte[0x80]; + smdh.get(data); + publisher[i] = convertString(data); + } + } + + private int[] parseIcon() { + int[] icon = new int[ICON_SIZE*ICON_SIZE]; + smdh.position(0); + + for (int x = 0; x < ICON_SIZE; x++) { + for (int y = 0; y < ICON_SIZE; y++) { + int curseY = y & ~7; + int curseX = x & ~7; + + int i = mortonInterleave(x, y); + int offset = (i + (curseX * 8)) * 2; + + offset = offset + curseY * 48 * 2; + + smdh.position(offset + IMAGE_OFFSET); + + int bit1 = smdh.get() & 0xFF; + int bit2 = smdh.get() & 0xFF; + + int pixel = bit1 + (bit2 << 8); + + int r = (((pixel & 0xF800) >> 11) << 3); + int g = (((pixel & 0x7E0) >> 5) << 2); + int b = (((pixel & 0x1F)) << 3); + + //Convert to ARGB8888 + icon[x + 48 * y] = 255 << 24 | (r & 255) << 16 | (g & 255) << 8 | (b & 255); + } + } + return icon; + } + + + public GameRegion getRegion() { + return region; + } + + public Bitmap getBitmapIcon(){ + Bitmap bitmap = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Bitmap.Config.RGB_565); + bitmap.setPixels(icon,0,ICON_SIZE,0,0,ICON_SIZE,ICON_SIZE); + return bitmap; + } + + public int[] getIcon() { + return icon; + } + + public String getTitle(){ + return title[metaIndex]; + } + + public String getPublisher(){ + return publisher[metaIndex]; + } + + // SMDH stores string in UTF-16LE format + private static String convertString(byte[] buffer){ + try { + return new String(buffer,0, buffer.length, StandardCharsets.UTF_16LE) + .replaceAll("\0",""); + } catch (Exception e){ + return ""; + } + } + + // u and v are the UVs of the relevant texel + // Texture data is stored interleaved in Morton order, ie in a Z - order curve as shown here + // https://en.wikipedia.org/wiki/Z-order_curve + // Textures are split into 8x8 tiles.This function returns the in - tile offset depending on the u & v of the texel + // The in - tile offset is the sum of 2 offsets, one depending on the value of u % 8 and the other on the value of y % 8 + // As documented in this picture https ://en.wikipedia.org/wiki/File:Moser%E2%80%93de_Bruijn_addition.svg + private static int mortonInterleave(int u, int v) { + int[] xlut = {0, 1, 4, 5, 16, 17, 20, 21}; + int[] ylut = {0, 2, 8, 10, 32, 34, 40, 42}; + return xlut[u % 8] + ylut[v % 8]; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java index 325ce1e71..7e73872d0 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java @@ -1,23 +1,42 @@ package com.panda3ds.pandroid.data.game; import android.graphics.Bitmap; +import android.util.Log; +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.data.SMDH; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.GameUtils; + +import java.util.Objects; import java.util.UUID; public class GameMetadata { - private final String id; private final String romPath; private final String title; - private transient final Bitmap icon = Bitmap.createBitmap(48,48, Bitmap.Config.RGB_565); private final String publisher; - private final GameRegion[] regions = new GameRegion[]{GameRegion.None}; + private final GameRegion[] regions; + private transient Bitmap icon; - public GameMetadata(String title, String romPath, String publisher) { - this.id = UUID.randomUUID().toString(); + private GameMetadata(String id, String romPath, String title, String publisher, Bitmap icon, GameRegion[] regions){ + this.id = id; this.title = title; this.publisher = publisher; this.romPath = romPath; + this.regions = regions; + if (icon != null) { + GameUtils.setGameIcon(id, icon); + } + } + + public GameMetadata(String romPath,String title, String publisher, GameRegion[] regions) { + this(UUID.randomUUID().toString(), romPath, title, publisher, null, regions); + } + + public GameMetadata(String romPath,String title, String publisher){ + this(romPath,title, publisher, new GameRegion[]{GameRegion.None}); } public String getRomPath() { @@ -37,10 +56,28 @@ public String getPublisher() { } public Bitmap getIcon() { + if (icon == null){ + icon = GameUtils.loadGameIcon(id); + } return icon; } public GameRegion[] getRegions() { return regions; } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof GameMetadata){ + return Objects.equals(((GameMetadata) obj).id, id); + } + return false; + } + + public static GameMetadata applySMDH(GameMetadata meta, SMDH smdh){ + Bitmap icon = smdh.getBitmapIcon(); + GameMetadata newMeta = new GameMetadata(meta.getId(), meta.getRomPath(), smdh.getTitle(), smdh.getPublisher(), icon, new GameRegion[]{smdh.getRegion()}); + icon.recycle(); + return newMeta; + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java index a22c4842f..652eb6fd4 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java @@ -8,12 +8,16 @@ import com.panda3ds.pandroid.app.PandroidApplication; -public class FileUtils { +import java.io.File; +public class FileUtils { public static final String MODE_READ = "r"; - - private static Uri parseUri(String value) { - return Uri.parse(value); + private static DocumentFile parseFile(String path){ + if (path.startsWith("/")){ + return DocumentFile.fromFile(new File(path)); + } + Uri uri = Uri.parse(path); + return DocumentFile.fromSingleUri(getContext(), uri); } private static Context getContext() { @@ -21,8 +25,21 @@ private static Context getContext() { } public static String getName(String path) { - DocumentFile file = DocumentFile.fromSingleUri(getContext(), parseUri(path)); - return file.getName(); + return parseFile(path).getName(); + } + + public static boolean createFolder(String path, String name){ + DocumentFile folder = parseFile(path); + + if (folder.findFile(name) != null){ + return true; + } + + return folder.createDirectory(name) != null; + } + + public static String getPrivatePath(){ + return getContext().getFilesDir().getAbsolutePath(); } public static void makeUriPermanent(String uri, String mode) { @@ -31,6 +48,6 @@ public static void makeUriPermanent(String uri, String mode) { if (mode.toLowerCase().contains("w")) flags &= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - getContext().getContentResolver().takePersistableUriPermission(parseUri(uri), flags); + getContext().getContentResolver().takePersistableUriPermission(Uri.parse(uri), flags); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java index e4c2f53ec..49d4f6f2d 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java @@ -3,27 +3,39 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; +import android.util.Log; import com.google.gson.Gson; import com.panda3ds.pandroid.app.GameActivity; import com.panda3ds.pandroid.app.PandroidApplication; import com.panda3ds.pandroid.data.game.GameMetadata; +import java.io.File; +import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; public class GameUtils { + private static final Bitmap DEFAULT_ICON = Bitmap.createBitmap(48,48, Bitmap.Config.ARGB_8888); private static final String KEY_GAME_LIST = "gameList"; private static final ArrayList games = new ArrayList<>(); private static SharedPreferences data; private static final Gson gson = new Gson(); + private static GameMetadata currentGame; + public static void initialize() { data = PandroidApplication.getAppContext().getSharedPreferences(Constants.PREF_GAME_UTILS, Context.MODE_PRIVATE); GameMetadata[] list = gson.fromJson(data.getString(KEY_GAME_LIST, "[]"), GameMetadata[].class); + + for (GameMetadata game: list) + game.getIcon(); + games.clear(); games.addAll(Arrays.asList(list)); } @@ -38,17 +50,22 @@ public static GameMetadata findByRomPath(String romPath) { } public static void launch(Context context, GameMetadata game) { + currentGame = game; String path = PathUtils.getPath(Uri.parse(game.getRomPath())); context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path)); } + public static GameMetadata getCurrentGame() { + return currentGame; + } + public static void removeGame(GameMetadata game) { games.remove(game); saveAll(); } public static void addGame(GameMetadata game) { - games.add(game); + games.add(0,game); saveAll(); } @@ -61,4 +78,27 @@ private static synchronized void saveAll() { .putString(KEY_GAME_LIST, gson.toJson(games.toArray(new GameMetadata[0]))) .apply(); } + + public static void setGameIcon(String id, Bitmap icon) { + try { + File file = new File(FileUtils.getPrivatePath()+"/cache_icons/", id+".png"); + file.getParentFile().mkdirs(); + FileOutputStream o = new FileOutputStream(file); + icon.compress(Bitmap.CompressFormat.PNG, 100, o); + o.close(); + } catch (Exception e){ + Log.e(Constants.LOG_TAG, "Error on save game icon: ", e); + } + } + + public static Bitmap loadGameIcon(String id) { + try { + File file = new File(FileUtils.getPrivatePath()+"/cache_icons/"+id+".png"); + if (file.exists()) + return BitmapFactory.decodeFile(file.getAbsolutePath()); + } catch (Exception e){ + Log.e(Constants.LOG_TAG, "Error on load game icon: ", e); + } + return DEFAULT_ICON; + } }