diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml new file mode 100644 index 000000000..c94140d70 --- /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 cea727859..5e49c27c4 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 src/panda_qt/config_window.cpp) - set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_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 "") +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 src/panda_qt/config_window.cpp) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_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 "") + 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/readme.md b/readme.md index 8667862d8..9e27194c4 100644 --- a/readme.md +++ b/readme.md @@ -13,14 +13,23 @@ Join our Discord server by pressing on the banner below, or find us on other pla ![screenshot1](docs/img/KirbyRobobot.png) ![screenshot2](docs/img/OoT_Title.png) ![screenshot3](docs/img/pokegang.png) # Download -You can download stable builds from the Releases tab, or you can download the latest build from the table below +You can download stable builds from the Releases tab, or you can download the latest build from the tables below. Additionally, Panda3DS comes in 2 flavours on PC: A minimal SDL frontend, which does not have a GUI, and an experimental Qt 6 frontend with a proper user interface. +SDL builds (No GUI): |Platform|Status|Download| |--------|------------|--------| |Windows build|[![Windows Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Windows_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Windows_Build.yml)|[Windows Executable](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Windows_Build/master/Windows%20executable.zip)| |MacOS build|[![MacOS Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/MacOS_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/MacOS_Build.yml)|[MacOS App Bundle](https://nightly.link/wheremyfoodat/Panda3DS/workflows/MacOS_Build/master/MacOS%20Alber%20App%20Bundle.zip)| |Linux build|[![Linux Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Linux_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Linux_Build.yml)|[Linux AppImage](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Linux_AppImage_Build/master/Linux%20executable.zip)| +Qt builds: +|Platform|Status|Download| +|--------|------------|--------| +|Windows build|[![Qt Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml/badge.svg)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml)|[Windows Executable](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Qt_Build/master/Windows%20executable.zip)| +|MacOS build|[![Qt Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml/badge.svg)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml)|[MacOS App Bundle](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Qt_Build/master/MacOS%20Alber%20App%20Bundle.zip)| +|Linux build|[![Qt Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml/badge.svg)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml)|[Linux AppImage](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Qt_Build/master/Linux%20executable.zip)| + + # Compatibility Panda3DS is still in the early stages of development. Many games boot, many don't. Lots of games have at least some hilariously broken graphics, audio is not supported, and some QoL features (including a GUI) are missing. However, even more things are implemented, such as most of the 3DS core required to play games, and various neat features, such as Lua scripting, discord bot support, support for some system apps, cheats, controller support, WIP amiibo support and many more! The emulator is constantly evolving, so make sure to take a peek every now and then! diff --git a/src/emulator.cpp b/src/emulator.cpp index bcd74966c..656aaebdb 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 000000000..8f5c352ed --- /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/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 3dcb246f8..8dbfc7aa4 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -23,7 +23,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto aboutMenu = menuBar->addMenu(tr("About")); // Create and bind actions for them - auto pandaAction = fileMenu->addAction(tr("panda...")); + auto pandaAction = fileMenu->addAction(tr("Load game")); connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); auto pauseAction = emulationMenu->addAction(tr("Pause")); @@ -252,4 +252,4 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { case Qt::Key_Return: releaseKey(HID::Keys::Start); break; case Qt::Key_Backspace: releaseKey(HID::Keys::Select); break; } -} \ No newline at end of file +} diff --git a/src/pandroid/.gitignore b/src/pandroid/.gitignore new file mode 100644 index 000000000..aa724b770 --- /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 000000000..276eb552e --- /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 000000000..481bb4348 --- /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 000000000..291496f4b --- /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 000000000..81bf29a52 --- /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 000000000..72926b076 --- /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 000000000..2da73b972 --- /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 000000000..f4fc27bf1 --- /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 000000000..becec9e1b --- /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 000000000..d94164c71 --- /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 000000000..9bfaa0e4a --- /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 000000000..8dd350ced --- /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 000000000..c813294c7 --- /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 000000000..617b407e9 --- /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 000000000..32451bc76 --- /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 000000000..7d45550a9 --- /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 000000000..6b82d2019 --- /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 000000000..697729158 --- /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 000000000..eb5a693a5 --- /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 000000000..772252237 --- /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 000000000..d196c7ecd --- /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 000000000..83c38d483 --- /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 000000000..cf33afb6f --- /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 000000000..bf51d4fe7 --- /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 000000000..d5c99e8d6 --- /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 000000000..7ec00974c --- /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 000000000..a726b2e68 --- /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 000000000..0b469178c --- /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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..07d5da9cb --- /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 000000000..2b068d114 --- /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 000000000..8c61f4c88 --- /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 000000000..f22e8c9d6 --- /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 000000000..133f2e70b --- /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 000000000..81855e147 --- /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 000000000..d58e9c4fb --- /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 000000000..baf1f2931 --- /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 000000000..2f69341c2 --- /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 000000000..15879540b --- /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 000000000..89a17ce98 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + + +