diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml new file mode 100644 index 00000000..c94140d7 --- /dev/null +++ b/.github/workflows/Android_Build.yml @@ -0,0 +1,89 @@ +name: Android Build + +on: + push: + branches: + - master + pull_request: + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + x64: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Setup Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' # See 'Supported distributions' for available options + java-version: '17' + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DBUILD_HYDRA_CORE=1 -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=x86_64 -DENABLE_VULKAN=0 + + - name: Build + run: | + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/x86_64/ + cd src/pandroid + ./gradlew assembleDebug + cd ../.. + + - name: Upload executable + uses: actions/upload-artifact@v2 + with: + name: Android APK (x86-64) + path: './src/pandroid/app/build/outputs/apk/debug/app-debug.apk' + + arm64: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Setup Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' # See 'Supported distributions' for available options + java-version: '17' + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DBUILD_HYDRA_CORE=1 -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DENABLE_VULKAN=0 -DCMAKE_CXX_FLAGS="-march=armv8.1-a+crypto" + + - name: Build + run: | + cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/arm64-v8a/ + cd src/pandroid + ./gradlew assembleDebug + cd ../.. + + - name: Upload executable + uses: actions/upload-artifact@v2 + with: + name: Android APK (arm64) + path: './src/pandroid/app/build/outputs/apk/debug/app-debug.apk' + diff --git a/CMakeLists.txt b/CMakeLists.txt index c202f80c..4aa4456a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,16 +184,18 @@ set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selecto set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files -if(ENABLE_QT_GUI) - set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp) - set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp) +if(NOT ANDROID) + if(ENABLE_QT_GUI) + set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp) - source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) - source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) - include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) -else() - set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) - set(FRONTEND_HEADER_FILES "") + source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) + source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) + include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) + else() + set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) + set(FRONTEND_HEADER_FILES "") + endif() endif() set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp @@ -378,6 +380,10 @@ if(ENABLE_VULKAN) set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_VK_SOURCE_FILES}) endif() +if(ANDROID) + set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp) +endif() + if(BUILD_HYDRA_CORE) include_directories(third_party/hydra_core/include) add_library(Alber SHARED ${ALL_SOURCES} src/hydra_core.cpp) @@ -386,11 +392,19 @@ else() add_executable(Alber ${ALL_SOURCES}) endif() +if(ANDROID) + target_link_libraries(Alber PRIVATE EGL log) +endif() + if(ENABLE_LTO OR ENABLE_USER_BUILD) set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) endif() -target_link_libraries(Alber PRIVATE dynarmic SDL2-static cryptopp glad resources_console_fonts) +target_link_libraries(Alber PRIVATE dynarmic cryptopp glad resources_console_fonts) + +if(NOT ANDROID) + target_link_libraries(Alber PRIVATE SDL2-static) +endif() if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") diff --git a/src/emulator.cpp b/src/emulator.cpp index bcd74966..656aaebd 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,6 +1,8 @@ #include "emulator.hpp" +#ifndef __ANDROID__ #include +#endif #include diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp new file mode 100644 index 00000000..8f5c352e --- /dev/null +++ b/src/jni_driver.cpp @@ -0,0 +1,76 @@ +#include +#include +#include + +#include + +#include "emulator.hpp" +#include "renderer_gl/renderer_gl.hpp" +#include "services/hid.hpp" + +std::unique_ptr emulator = nullptr; +HIDService* hidService = nullptr; +RendererGL* renderer = nullptr; +bool romLoaded = false; + +#define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name + +void throwException(JNIEnv* env, const char* message) { + jclass exceptionClass = env->FindClass("java/lang/RuntimeException"); + env->ThrowNew(exceptionClass, message); +} + +extern "C" { +AlberFunction(void, Initialize)(JNIEnv* env, jobject obj) { + emulator = std::make_unique(); + + if (emulator->getRendererType() != RendererType::OpenGL) { + return throwException(env, "Renderer type is not OpenGL"); + } + + renderer = static_cast(emulator->getRenderer()); + hidService = &emulator->getServiceManager().getHID(); + + if (!gladLoadGLES2Loader(reinterpret_cast(eglGetProcAddress))) { + return throwException(env, "Failed to load OpenGL ES 2.0"); + } + + __android_log_print(ANDROID_LOG_INFO, "AlberDriver", "OpenGL ES %d.%d", GLVersion.major, GLVersion.minor); + emulator->initGraphicsContext(nullptr); +} + +AlberFunction(void, RunFrame)(JNIEnv* env, jobject obj, jint fbo) { + renderer->setFBO(fbo); + // TODO: don't reset entire state manager + renderer->resetStateManager(); + emulator->runFrame(); + + hidService->updateInputs(emulator->getTicks()); +} + +AlberFunction(void, Finalize)(JNIEnv* env, jobject obj) { + emulator = nullptr; + hidService = nullptr; + renderer = nullptr; +} + +AlberFunction(jboolean, HasRomLoaded)(JNIEnv* env, jobject obj) { return romLoaded; } + +AlberFunction(void, LoadRom)(JNIEnv* env, jobject obj, jstring path) { + const char* pathStr = env->GetStringUTFChars(path, nullptr); + romLoaded = emulator->loadROM(pathStr); + env->ReleaseStringUTFChars(path, pathStr); +} + +AlberFunction(void, TouchScreenDown)(JNIEnv* env, jobject obj, jint x, jint y) { hidService->setTouchScreenPress((u16)x, (u16)y); } +AlberFunction(void, TouchScreenUp)(JNIEnv* env, jobject obj) { hidService->releaseTouchScreen(); } +AlberFunction(void, KeyUp)(JNIEnv* env, jobject obj, jint keyCode) { hidService->releaseKey((u32)keyCode); } +AlberFunction(void, KeyDown)(JNIEnv* env, jobject obj, jint keyCode) { hidService->pressKey((u32)keyCode); } + +AlberFunction(void, SetCirclepadAxis)(JNIEnv* env, jobject obj, jint x, jint y) { + hidService->setCirclepadX((s16)x); + hidService->setCirclepadY((s16)y); +} +} + +#undef AlberFunction \ No newline at end of file diff --git a/src/pandroid/.gitignore b/src/pandroid/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/src/pandroid/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts new file mode 100644 index 00000000..276eb552 --- /dev/null +++ b/src/pandroid/app/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("com.android.application") +} + +android { + namespace = "com.panda3ds.pandroid" + compileSdk = 33 + + defaultConfig { + applicationId = "com.panda3ds.pandroid" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters += listOf("x86_64", "arm64-v8a") + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.8.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") +} \ No newline at end of file diff --git a/src/pandroid/app/proguard-rules.pro b/src/pandroid/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/src/pandroid/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src/pandroid/app/src/main/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..291496f4 --- /dev/null +++ b/src/pandroid/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..81bf29a5 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java @@ -0,0 +1,19 @@ +package com.panda3ds.pandroid; + +public class AlberDriver { + AlberDriver() { super(); } + + public static native void Initialize(); + public static native void RunFrame(int fbo); + public static native boolean HasRomLoaded(); + public static native void LoadRom(String path); + public static native void Finalize(); + + public static native void KeyDown(int code); + public static native void KeyUp(int code); + public static native void SetCirclepadAxis(int x, int y); + public static native void TouchScreenUp(); + public static native void TouchScreenDown(int x, int y); + + static { System.loadLibrary("Alber"); } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java new file mode 100644 index 00000000..72926b07 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java @@ -0,0 +1,5 @@ +package com.panda3ds.pandroid.app; + +import androidx.appcompat.app.AppCompatActivity; + +public class BaseActivity extends AppCompatActivity {} 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 new file mode 100644 index 00000000..2da73b97 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java @@ -0,0 +1,50 @@ +package com.panda3ds.pandroid.app; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.view.PandaGlSurfaceView; +import com.panda3ds.pandroid.view.PandaLayoutController; + +public class GameActivity extends BaseActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_PATH)) { + setContentView(new FrameLayout(this)); + Toast.makeText(this, "Invalid rom path!", Toast.LENGTH_LONG).show(); + finish(); + return; + } + + PandaGlSurfaceView pandaSurface = new PandaGlSurfaceView(this, intent.getStringExtra(Constants.ACTIVITY_PARAMETER_PATH)); + setContentView(R.layout.game_activity); + + ((FrameLayout) findViewById(R.id.panda_gl_frame)) + .addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + PandaLayoutController controllerLayout = findViewById(R.id.controller_layout); + controllerLayout.initialize(); + + ((CheckBox) findViewById(R.id.hide_screen_controller)).setOnCheckedChangeListener((buttonView, isChecked) -> findViewById(R.id.overlay_controller).setVisibility(isChecked ? View.VISIBLE : View.INVISIBLE)); + } + + @Override + protected void onResume() { + super.onResume(); + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java new file mode 100644 index 00000000..f4fc27bf --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java @@ -0,0 +1,57 @@ +package com.panda3ds.pandroid.app; + +import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.PathUtils; + +public class MainActivity extends BaseActivity { + private static final int PICK_ROM = 2; + private static final int PERMISSION_REQUEST_CODE = 3; + + private void openFile() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, PICK_ROM); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + Intent intent = new Intent(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + startActivity(intent); + } + } else { + ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); + ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); + } + + setContentView(R.layout.activity_main); + findViewById(R.id.load_rom).setOnClickListener(v -> { openFile(); }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == PICK_ROM) { + if (resultCode == RESULT_OK) { + String path = PathUtils.getPath(getApplicationContext(), data.getData()); + Toast.makeText(getApplicationContext(), "pandroid opening " + path, Toast.LENGTH_LONG).show(); + startActivity(new Intent(this, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path)); + } + super.onActivityResult(requestCode, resultCode, data); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/math/Vector2.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/math/Vector2.java new file mode 100644 index 00000000..becec9e1 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/math/Vector2.java @@ -0,0 +1,16 @@ +package com.panda3ds.pandroid.math; + +public class Vector2 { + public float x, y; + public Vector2(float x, float y) { + this.x = x; + this.y = y; + } + + public static float distance(float x, float y, float x2, float y2) { return (float) Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); } + + public void set(float x, float y) { + this.x = x; + this.y = y; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java new file mode 100644 index 00000000..d94164c7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java @@ -0,0 +1,23 @@ +package com.panda3ds.pandroid.utils; + +public class Constants { + public static final int INPUT_KEY_A = 1 << 0; + public static final int INPUT_KEY_B = 1 << 1; + public static final int INPUT_KEY_SELECT = 1 << 2; + public static final int INPUT_KEY_START = 1 << 3; + public static final int INPUT_KEY_RIGHT = 1 << 4; + public static final int INPUT_KEY_LEFT = 1 << 5; + public static final int INPUT_KEY_UP = 1 << 6; + public static final int INPUT_KEY_DOWN = 1 << 7; + public static final int INPUT_KEY_R = 1 << 8; + public static final int INPUT_KEY_L = 1 << 9; + public static final int INPUT_KEY_X = 1 << 10; + public static final int INPUT_KEY_Y = 1 << 11; + + public static final int N3DS_WIDTH = 400; + public static final int N3DS_FULL_HEIGHT = 480; + public static final int N3DS_HALF_HEIGHT = N3DS_FULL_HEIGHT / 2; + + public static final String ACTIVITY_PARAMETER_PATH = "path"; + public static final String LOG_TAG = "pandroid"; +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java new file mode 100644 index 00000000..9bfaa0e4 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java @@ -0,0 +1,89 @@ +package com.panda3ds.pandroid.utils; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +public class PathUtils { + public static String getPath(final Context context, final Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + String storageDefinition; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + + } else { + if (Environment.isExternalStorageRemovable()) { + storageDefinition = "EXTERNAL_STORAGE"; + + } else { + storageDefinition = "SECONDARY_STORAGE"; + } + + return System.getenv(storageDefinition) + "/" + split[1]; + } + + } else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] {split[1]}; + return getDataColumn(context, contentUri, selection, selectionArgs); + } + + } else if ("content".equalsIgnoreCase(uri.getScheme())) { + if (isGooglePhotosUri(uri)) return uri.getLastPathSegment(); + return getDataColumn(context, uri, null, null); + } else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = "_data"; + final String[] projection = {column}; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) cursor.close(); + } + return null; + } + + public static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } + public static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } + public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } + public static boolean isGooglePhotosUri(Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java new file mode 100644 index 00000000..8dd350ce --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java @@ -0,0 +1,127 @@ +package com.panda3ds.pandroid.view; + +import static android.opengl.GLES32.*; + +import android.content.res.Resources; +import android.graphics.Rect; +import android.opengl.GLSurfaceView; +import android.util.Log; +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; +import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; +import com.panda3ds.pandroid.view.renderer.layout.DefaultScreenLayout; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer { + private final String romPath; + private ConsoleLayout displayLayout; + private int screenWidth, screenHeight; + private int screenTexture; + public int screenFbo; + + PandaGlRenderer(String romPath) { + super(); + this.romPath = romPath; + + screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + setLayout(new DefaultScreenLayout()); + } + + @Override + protected void finalize() throws Throwable { + if (screenTexture != 0) { + glDeleteTextures(1, new int[] {screenTexture}, 0); + } + if (screenFbo != 0) { + glDeleteFramebuffers(1, new int[] {screenFbo}, 0); + } + super.finalize(); + } + + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + Log.i(Constants.LOG_TAG, glGetString(GL_EXTENSIONS)); + Log.w(Constants.LOG_TAG, glGetString(GL_VERSION)); + + int[] version = new int[2]; + glGetIntegerv(GL_MAJOR_VERSION, version, 0); + glGetIntegerv(GL_MINOR_VERSION, version, 1); + + if (version[0] < 3 || (version[0] == 3 && version[1] < 1)) { + Log.e(Constants.LOG_TAG, "OpenGL 3.1 or higher is required"); + } + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + int[] generateBuffer = new int[1]; + glGenTextures(1, generateBuffer, 0); + screenTexture = generateBuffer[0]; + glBindTexture(GL_TEXTURE_2D, screenTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, screenWidth, screenHeight); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, 0); + + glGenFramebuffers(1, generateBuffer, 0); + screenFbo = generateBuffer[0]; + glBindFramebuffer(GL_FRAMEBUFFER, screenFbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + Log.e(Constants.LOG_TAG, "Framebuffer is not complete"); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + AlberDriver.Initialize(); + AlberDriver.LoadRom(romPath); + } + + public void onDrawFrame(GL10 unused) { + if (AlberDriver.HasRomLoaded()) { + AlberDriver.RunFrame(screenFbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, screenFbo); + + Rect topScreen = displayLayout.getTopDisplayBounds(); + Rect bottomScreen = displayLayout.getBottomDisplayBounds(); + + glBlitFramebuffer( + 0, Constants.N3DS_FULL_HEIGHT, Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT, topScreen.left, screenHeight - topScreen.top, + topScreen.right, screenHeight - topScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + + // Remove the black bars on the bottom screen + glBlitFramebuffer( + 40, Constants.N3DS_HALF_HEIGHT, Constants.N3DS_WIDTH - 40, 0, bottomScreen.left, screenHeight - bottomScreen.top, bottomScreen.right, + screenHeight - bottomScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + } + } + + public void onSurfaceChanged(GL10 unused, int width, int height) { + screenWidth = width; + screenHeight = height; + + displayLayout.update(screenWidth, screenHeight); + } + + @Override + public void setLayout(ConsoleLayout layout) { + displayLayout = layout; + displayLayout.setTopDisplaySourceSize(Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT); + displayLayout.setBottomDisplaySourceSize(Constants.N3DS_WIDTH - 40 - 40, Constants.N3DS_HALF_HEIGHT); + displayLayout.update(screenWidth, screenHeight); + } + + @Override + public ConsoleLayout getLayout() { + return displayLayout; + } + + @Override + public String getBackendName() { + return "OpenGL"; + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java new file mode 100644 index 00000000..c813294c --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java @@ -0,0 +1,47 @@ +package com.panda3ds.pandroid.view; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.os.Debug; + +import androidx.annotation.NonNull; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.nodes.TouchScreenNodeImpl; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; + +public class PandaGlSurfaceView extends GLSurfaceView implements TouchScreenNodeImpl { + final PandaGlRenderer renderer; + private int width; + private int height; + + public PandaGlSurfaceView(Context context, String romPath) { + super(context); + setEGLContextClientVersion(3); + if (Debug.isDebuggerConnected()) { + setDebugFlags(DEBUG_LOG_GL_CALLS); + } + renderer = new PandaGlRenderer(romPath); + setRenderer(renderer); + } + + public ConsoleRenderer getRenderer() { return renderer; } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + width = getMeasuredWidth(); + height = getMeasuredHeight(); + } + + @NonNull + @Override + public Vector2 getSize() { + return new Vector2(width, height); + } + + @Override + public void onTouch(TouchEvent event) { + onTouchScreenPress(renderer, event); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java new file mode 100644 index 00000000..617b407e --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java @@ -0,0 +1,48 @@ +package com.panda3ds.pandroid.view; + +import android.content.Context; +import android.util.AttributeSet; +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.view.controller.ControllerLayout; +import com.panda3ds.pandroid.view.controller.nodes.Button; +import com.panda3ds.pandroid.view.controller.nodes.Joystick; + +public class PandaLayoutController extends ControllerLayout { + public PandaLayoutController(Context context) { super(context); } + public PandaLayoutController(Context context, AttributeSet attrs) { super(context, attrs); } + public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } + + public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void initialize() { + int[] keyButtonList = {R.id.button_a, Constants.INPUT_KEY_A, R.id.button_b, Constants.INPUT_KEY_B, + R.id.button_y, Constants.INPUT_KEY_Y, R.id.button_x, Constants.INPUT_KEY_X, + + R.id.button_left, Constants.INPUT_KEY_LEFT, R.id.button_right, Constants.INPUT_KEY_RIGHT, + R.id.button_up, Constants.INPUT_KEY_UP, R.id.button_down, Constants.INPUT_KEY_DOWN, + + R.id.button_start, Constants.INPUT_KEY_START, R.id.button_select, Constants.INPUT_KEY_SELECT, + + R.id.button_l, Constants.INPUT_KEY_L, R.id.button_r, Constants.INPUT_KEY_R}; + + for (int i = 0; i < keyButtonList.length; i += 2) { + final int keyCode = keyButtonList[i + 1]; + ((Button) findViewById(keyButtonList[i])).setStateListener((btn, pressed) -> { + if (pressed) + AlberDriver.KeyDown(keyCode); + else + AlberDriver.KeyUp(keyCode); + }); + } + + ((Joystick) findViewById(R.id.left_analog)).setJoystickListener((joystick, axisX, axisY) -> { + AlberDriver.SetCirclepadAxis((int) (axisX * 0x9C), (int) (axisY * 0x9C) * -1); + }); + + refreshChildren(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerLayout.java new file mode 100644 index 00000000..32451bc7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerLayout.java @@ -0,0 +1,143 @@ +package com.panda3ds.pandroid.view.controller; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.utils.Constants; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; + +public class ControllerLayout extends RelativeLayout { + private final HashMap activeTouchEvents = new HashMap<>(); + private final ArrayList controllerNodes = new ArrayList<>(); + + public ControllerLayout(Context context) { this(context, null); } + public ControllerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } + public ControllerLayout(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } + + public ControllerLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void refreshChildren() { + ArrayList nodes = new ArrayList<>(); + populateNodesArray(this, nodes); + + // Need Reverse: First view is in back and last view is in front for respect android View hierarchy + Collections.reverse(nodes); + + controllerNodes.clear(); + controllerNodes.addAll(nodes); + } + + private void populateNodesArray(ViewGroup group, ArrayList list) { + for (int i = 0; i < group.getChildCount(); i++) { + View view = group.getChildAt(i); + if (view instanceof ControllerNode) { + list.add((ControllerNode) view); + } else if (view instanceof ViewGroup) { + populateNodesArray((ViewGroup) view, list); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int index = event.getActionIndex(); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_UP: { + int id = event.getPointerId(index); + processTouch(true, event.getX(index), event.getY(index), id); + } break; + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + int id = event.getPointerId(index); + processTouch(false, event.getX(index), event.getY(index), id); + } break; + case MotionEvent.ACTION_MOVE: + for (int id = 0; id < event.getPointerCount(); id++) { + processTouch(false, event.getX(id), event.getY(id), id); + } + break; + } + return true; + } + + private void processTouch(boolean up, float x, float y, int index) { + int[] globalPosition = new int[2]; + getLocationInWindow(globalPosition); + + TouchType action = TouchType.ACTION_MOVE; + if ((!activeTouchEvents.containsKey(index))) { + if (up) return; + ControllerNode node = null; + for (ControllerNode item : controllerNodes) { + Vector2 pos = item.getPosition(); + Vector2 size = item.getSize(); + + float cx = (pos.x - globalPosition[0]); + float cy = (pos.y - globalPosition[1]); + if (item.isVisible() && x > cx && x < cx + size.x && y > cy && y < cy + size.y) { + node = item; + break; + } + } + if (node != null) { + activeTouchEvents.put(index, node); + action = TouchType.ACTION_DOWN; + } else { + return; + } + } + + if (up) action = TouchType.ACTION_UP; + + ControllerNode node = activeTouchEvents.get(index); + Vector2 pos = node.getPosition(); + pos.x -= globalPosition[0]; + pos.y -= globalPosition[1]; + + x -= pos.x; + y -= pos.y; + + node.onTouch(new TouchEvent(x, y, action)); + + if (up) { + activeTouchEvents.remove(index); + } + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + refreshChildren(); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + refreshChildren(); + } + + // TODO: Need to replace these methods to prevent Android sending events directly to children + + @Override + public ArrayList getTouchables() { + return new ArrayList<>(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return true; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerNode.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerNode.java new file mode 100644 index 00000000..7d45550a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerNode.java @@ -0,0 +1,21 @@ +package com.panda3ds.pandroid.view.controller; + +import android.view.View; +import androidx.annotation.NonNull; +import com.panda3ds.pandroid.math.Vector2; + +public interface ControllerNode { + @NonNull + default Vector2 getPosition() { + View view = (View) this; + + int[] position = new int[2]; + view.getLocationInWindow(position); + return new Vector2(position[0], position[1]); + } + + default boolean isVisible() { return ((View) this).isShown(); } + + @NonNull Vector2 getSize(); + void onTouch(TouchEvent event); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchEvent.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchEvent.java new file mode 100644 index 00000000..6b82d201 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchEvent.java @@ -0,0 +1,16 @@ +package com.panda3ds.pandroid.view.controller; + +public class TouchEvent { + private final TouchType action; + private final float x, y; + + public float getX() { return x; } + public float getY() { return y; } + public TouchType getAction() { return action; } + + public TouchEvent(float x, float y, TouchType action) { + this.x = x; + this.y = y; + this.action = action; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchType.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchType.java new file mode 100644 index 00000000..69772915 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchType.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.controller; + +public enum TouchType { + ACTION_DOWN, + ACTION_MOVE, + ACTION_UP +}; \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/ButtonStateListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/ButtonStateListener.java new file mode 100644 index 00000000..eb5a693a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/ButtonStateListener.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.controller.listeners; + +import com.panda3ds.pandroid.view.controller.nodes.Button; + +public interface ButtonStateListener { + void onButtonPressedChange(Button button, boolean pressed); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/JoystickListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/JoystickListener.java new file mode 100644 index 00000000..77225223 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/JoystickListener.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.controller.listeners; + +import com.panda3ds.pandroid.view.controller.nodes.Joystick; + +public interface JoystickListener { + void onJoystickAxisChange(Joystick joystick, float axisX, float axisY); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/BasicControllerNode.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/BasicControllerNode.java new file mode 100644 index 00000000..d196c7ec --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/BasicControllerNode.java @@ -0,0 +1,14 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import com.panda3ds.pandroid.view.controller.ControllerNode; + +public abstract class BasicControllerNode extends AppCompatTextView implements ControllerNode { + public BasicControllerNode(@NonNull Context context) { super(context); } + public BasicControllerNode(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } + public BasicControllerNode(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Button.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Button.java new file mode 100644 index 00000000..83c38d48 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Button.java @@ -0,0 +1,68 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.Gravity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.controller.ControllerNode; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.TouchType; +import com.panda3ds.pandroid.view.controller.listeners.ButtonStateListener; + +public class Button extends BasicControllerNode { + private boolean pressed = false; + private int width, height; + + private ButtonStateListener stateListener; + + public Button(@NonNull Context context) { + super(context); + init(); + } + + public Button(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public Button(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + width = getWidth(); + height = getHeight(); + } + + private void init() { + setTextAlignment(TEXT_ALIGNMENT_CENTER); + setGravity(Gravity.CENTER); + } + + public void setStateListener(ButtonStateListener stateListener) { this.stateListener = stateListener; } + + public boolean isPressed() { return pressed; } + + @NonNull + @Override + public Vector2 getSize() { + return new Vector2(width, height); + } + + @Override + public void onTouch(TouchEvent event) { + pressed = event.getAction() != TouchType.ACTION_UP; + setAlpha(pressed ? 0.2f : 1.0f); + if (stateListener != null) { + stateListener.onButtonPressedChange(this, pressed); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Joystick.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Joystick.java new file mode 100644 index 00000000..cf33afb6 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Joystick.java @@ -0,0 +1,113 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import androidx.annotation.NonNull; + +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.controller.ControllerNode; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.TouchType; +import com.panda3ds.pandroid.view.controller.listeners.JoystickListener; + +public class Joystick extends BasicControllerNode implements ControllerNode { + private float axisX = 0; + private float axisY = 0; + + private int width = 0; + private int height = 0; + private JoystickListener joystickListener; + + public Joystick(Context context) { this(context, null); } + public Joystick(Context context, AttributeSet attrs) { this(context, attrs, 0); } + + public Joystick(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + paint.setColor(Color.RED); + invalidate(); + } + + private final Paint paint = new Paint(); + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + } + + @Override + public void onDrawForeground(Canvas canvas) { + width = getWidth(); + height = getHeight(); + + int analogIconSize = width - getPaddingLeft(); + + float middleIconSize = analogIconSize / 2.0f; + float middle = width / 2.0f; + + float maxDistance = (middle - middleIconSize) * 0.9f; + + float tx = maxDistance * axisX; + float ty = maxDistance * axisY; + + float radius = Vector2.distance(0.0f, 0.0f, Math.abs(tx), Math.abs(ty)); + radius = Math.min(maxDistance, radius); + + double deg = Math.atan2(ty, tx) * (180.0 / Math.PI); + float rx = (float) (radius * Math.cos(2 * Math.PI * deg / 360.0)); + float ry = (float) (radius * Math.sin(2 * Math.PI * deg / 360.0)); + + axisX = Math.max(-1.0f, Math.min(1.0f, axisX)); + axisY = Math.max(-1.0f, Math.min(1.0f, axisY)); + + float x = middle - middleIconSize + rx; + float y = middle - middleIconSize + ry; + + Drawable foreground = getForeground(); + if (foreground != null) { + foreground.setBounds((int) x, (int) y, (int) (x + analogIconSize), (int) (y + analogIconSize)); + foreground.draw(canvas); + } else { + canvas.drawOval(x, y, x + analogIconSize, y + analogIconSize, paint); + } + } + + public Vector2 getAxis() { return new Vector2(Math.max(-1.0f, Math.min(1.0f, axisX)), Math.max(-1.0f, Math.min(1.0f, axisY))); } + + public void setJoystickListener(JoystickListener joystickListener) { this.joystickListener = joystickListener; } + + @NonNull + @Override + public Vector2 getSize() { + return new Vector2(width, height); + } + + @Override + public void onTouch(TouchEvent event) { + float middle = width / 2.0f; + + float x = event.getX(); + float y = event.getY(); + + x = Math.max(0, Math.min(middle * 2, x)); + y = Math.max(0, Math.min(middle * 2, y)); + + axisX = ((x - middle) / middle); + axisY = ((y - middle) / middle); + + if (event.getAction() == TouchType.ACTION_UP) { + axisX = 0; + axisY = 0; + } + + if (joystickListener != null) { + joystickListener.onJoystickAxisChange(this, axisX, axisY); + } + + invalidate(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/TouchScreenNodeImpl.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/TouchScreenNodeImpl.java new file mode 100644 index 00000000..bf51d4fe --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/TouchScreenNodeImpl.java @@ -0,0 +1,37 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.graphics.Rect; +import android.view.View; + +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.view.controller.ControllerNode; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.TouchType; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; + +public interface TouchScreenNodeImpl extends ControllerNode { + default void onTouchScreenPress(ConsoleRenderer renderer, TouchEvent event) { + View view = (View) this; + boolean hasDownEvent = view.getTag(R.id.TagEventHasDown) != null && (boolean) view.getTag(R.id.TagEventHasDown); + + Rect bounds = renderer.getLayout().getBottomDisplayBounds(); + + if (event.getX() >= bounds.left && event.getY() >= bounds.top && event.getX() <= bounds.right && event.getY() <= bounds.bottom) { + int x = (int) (event.getX() - bounds.left); + int y = (int) (event.getY() - bounds.top); + + x = Math.round((x / (float) bounds.width()) * 320); + y = Math.round((y / (float) bounds.height()) * 240); + + AlberDriver.TouchScreenDown(x, y); + + view.setTag(R.id.TagEventHasDown, true); + } + + if (hasDownEvent && event.getAction() == TouchType.ACTION_UP) { + AlberDriver.TouchScreenUp(); + view.setTag(R.id.TagEventHasDown, false); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/ConsoleRenderer.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/ConsoleRenderer.java new file mode 100644 index 00000000..d5c99e8d --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/ConsoleRenderer.java @@ -0,0 +1,9 @@ +package com.panda3ds.pandroid.view.renderer; + +import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; + +public interface ConsoleRenderer { + void setLayout(ConsoleLayout layout); + ConsoleLayout getLayout(); + String getBackendName(); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/ConsoleLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/ConsoleLayout.java new file mode 100644 index 00000000..7ec00974 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/ConsoleLayout.java @@ -0,0 +1,13 @@ +package com.panda3ds.pandroid.view.renderer.layout; + +import android.graphics.Rect; + +public interface ConsoleLayout { + void update(int screenWidth, int screenHeight); + + void setBottomDisplaySourceSize(int width, int height); + void setTopDisplaySourceSize(int width, int height); + + Rect getBottomDisplayBounds(); + Rect getTopDisplayBounds(); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java new file mode 100644 index 00000000..a726b2e6 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java @@ -0,0 +1,77 @@ +package com.panda3ds.pandroid.view.renderer.layout; + +import android.graphics.Rect; + +import com.panda3ds.pandroid.math.Vector2; + +public class DefaultScreenLayout implements ConsoleLayout { + private final Rect topDisplay = new Rect(); + private final Rect bottomDisplay = new Rect(); + + private final Vector2 screenSize = new Vector2(1.0f, 1.0f); + private final Vector2 topSourceSize = new Vector2(1.0f, 1.0f); + private final Vector2 bottomSourceSize = new Vector2(1.0f, 1.0f); + + @Override + public void update(int screenWidth, int screenHeight) { + screenSize.set(screenWidth, screenHeight); + updateBounds(); + } + + @Override + public void setBottomDisplaySourceSize(int width, int height) { + bottomSourceSize.set(width, height); + updateBounds(); + } + @Override + public void setTopDisplaySourceSize(int width, int height) { + topSourceSize.set(width, height); + updateBounds(); + } + + private void updateBounds() { + int screenWidth = (int) screenSize.x; + int screenHeight = (int) screenSize.y; + + if (screenWidth > screenHeight) { + int topDisplayWidth = (int) ((screenHeight / topSourceSize.y) * topSourceSize.x); + int topDisplayHeight = screenHeight; + + if (topDisplayWidth > (screenWidth * 0.7)) { + topDisplayWidth = (int) (screenWidth * 0.7); + topDisplayHeight = (int) ((topDisplayWidth / topSourceSize.x) * topSourceSize.y); + } + + int bottomDisplayHeight = (int) (((screenWidth - topDisplayWidth) / bottomSourceSize.x) * bottomSourceSize.y); + + topDisplay.set(0, 0, topDisplayWidth, topDisplayHeight); + bottomDisplay.set(topDisplayWidth, 0, topDisplayWidth + (screenWidth - topDisplayWidth), bottomDisplayHeight); + } else { + int topScreenHeight = (int) ((screenWidth / topSourceSize.x) * topSourceSize.y); + topDisplay.set(0, 0, screenWidth, topScreenHeight); + + int bottomDisplayHeight = (int) ((screenWidth / bottomSourceSize.x) * bottomSourceSize.y); + int bottomDisplayWidth = screenWidth; + int bottomDisplayX = 0; + + if (topScreenHeight + bottomDisplayHeight > screenHeight) { + bottomDisplayHeight = (screenHeight - topScreenHeight); + bottomDisplayWidth = (int) ((bottomDisplayHeight / bottomSourceSize.y) * bottomSourceSize.x); + bottomDisplayX = (screenWidth - bottomDisplayX) / 2; + } + + topDisplay.set(0, 0, screenWidth, topScreenHeight); + bottomDisplay.set(bottomDisplayX, topScreenHeight, bottomDisplayX + bottomDisplayWidth, topScreenHeight + bottomDisplayHeight); + } + } + + @Override + public Rect getBottomDisplayBounds() { + return bottomDisplay; + } + + @Override + public Rect getTopDisplayBounds() { + return topDisplay; + } +} diff --git a/src/pandroid/app/src/main/jniLibs/.gitignore b/src/pandroid/app/src/main/jniLibs/.gitignore new file mode 100644 index 00000000..0b469178 --- /dev/null +++ b/src/pandroid/app/src/main/jniLibs/.gitignore @@ -0,0 +1,2 @@ +# Prebuilt Alber libraries will be placed in this directory, but we don't want to push them to the repo +libAlber.so \ No newline at end of file diff --git a/src/pandroid/app/src/main/jniLibs/arm64-v8a/.gitkeep b/src/pandroid/app/src/main/jniLibs/arm64-v8a/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/pandroid/app/src/main/jniLibs/x86_64/.gitkeep b/src/pandroid/app/src/main/jniLibs/x86_64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/pandroid/app/src/main/res/drawable/ic_launcher_background.xml b/src/pandroid/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_launcher_foreground.xml b/src/pandroid/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/screen_gamepad_checkbox.xml b/src/pandroid/app/src/main/res/drawable/screen_gamepad_checkbox.xml new file mode 100644 index 00000000..8c61f4c8 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/screen_gamepad_checkbox.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/screen_gamepad_hide.xml b/src/pandroid/app/src/main/res/drawable/screen_gamepad_hide.xml new file mode 100644 index 00000000..f22e8c9d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/screen_gamepad_hide.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/screen_gamepad_show.xml b/src/pandroid/app/src/main/res/drawable/screen_gamepad_show.xml new file mode 100644 index 00000000..133f2e70 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/screen_gamepad_show.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/simple_analog_background.xml b/src/pandroid/app/src/main/res/drawable/simple_analog_background.xml new file mode 100644 index 00000000..81855e14 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/simple_analog_background.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_button.xml b/src/pandroid/app/src/main/res/drawable/simple_card_button.xml new file mode 100644 index 00000000..d58e9c4f --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/simple_card_button.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml b/src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml new file mode 100644 index 00000000..baf1f293 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml b/src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml new file mode 100644 index 00000000..2f69341c --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/simple_circle_button.xml b/src/pandroid/app/src/main/res/drawable/simple_circle_button.xml new file mode 100644 index 00000000..15879540 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/simple_circle_button.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/activity_main.xml b/src/pandroid/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..89a17ce9 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + + +