diff --git a/CMakeLists.txt b/CMakeLists.txt index 732ec793..4b0410fd 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 00000000..ff6230f1 --- /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 2546aa01..3e17790c 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 8f5c352e..37d15787 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 81bf29a5..fe808548 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 286a7ed3..fc622cd6 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 @@ import com.panda3ds.pandroid.view.PandaLayoutController; 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 246decec..770e5e81 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 @@ package com.panda3ds.pandroid.app; 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 class PandroidApplication extends Application { 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 c500d970..10167664 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.input.KeyName; 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 ef49f6f1..c5f1acee 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 class GamesFragment extends Fragment implements ActivityResultCallback> 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 325ce1e7..7e73872d 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 class GameMetadata { } 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 a22c4842..652eb6fd 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 androidx.documentfile.provider.DocumentFile; import com.panda3ds.pandroid.app.PandroidApplication; +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 @@ public class FileUtils { } 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 class FileUtils { 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 e4c2f53e..49d4f6f2 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 @@ package com.panda3ds.pandroid.utils; 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 class GameUtils { } 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 @@ public class GameUtils { .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; + } }