From 09d96310fc4dccffd7b0d6ae3f35a0d9551be351 Mon Sep 17 00:00:00 2001 From: Dan Ware Date: Wed, 2 Oct 2024 16:47:04 +0100 Subject: [PATCH] add android project to branch --- src/RyujinxAndroid/.gitignore | 12 + src/RyujinxAndroid/app/build.gradle | 117 ++ src/RyujinxAndroid/app/proguard-rules.pro | 21 + .../android/ExampleInstrumentedTest.kt | 24 + .../app/src/main/AndroidManifest.xml | 65 + .../app/src/main/cpp/CMakeLists.txt | 77 + .../app/src/main/cpp/native_window.h | 305 ++++ src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h | 49 + .../app/src/main/cpp/ryujinx.cpp | 250 ++++ .../app/src/main/cpp/vulkan_wrapper.cpp | 404 ++++++ .../app/src/main/cpp/vulkan_wrapper.h | 236 +++ .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 12207 bytes .../org/ryujinx/android/BackendThreading.kt | 7 + .../java/org/ryujinx/android/BaseActivity.kt | 9 + .../java/org/ryujinx/android/CrashHandler.kt | 15 + .../org/ryujinx/android/GameController.kt | 435 ++++++ .../main/java/org/ryujinx/android/GameHost.kt | 179 +++ .../ryujinx/android/GamePadButtonInputId.kt | 27 + .../main/java/org/ryujinx/android/Helpers.kt | 223 +++ .../main/java/org/ryujinx/android/Icons.kt | 826 +++++++++++ .../main/java/org/ryujinx/android/Logging.kt | 63 + .../java/org/ryujinx/android/MainActivity.kt | 227 +++ .../ryujinx/android/MotionSensorManager.kt | 137 ++ .../ryujinx/android/NativeGraphicsInterop.kt | 7 + .../java/org/ryujinx/android/NativeHelpers.kt | 31 + .../java/org/ryujinx/android/NativeWindow.kt | 42 + .../org/ryujinx/android/PerformanceManager.kt | 31 + .../org/ryujinx/android/PerformanceMonitor.kt | 45 + .../android/PhysicalControllerManager.kt | 158 ++ .../java/org/ryujinx/android/RegionCode.kt | 11 + .../org/ryujinx/android/RyujinxApplication.kt | 20 + .../java/org/ryujinx/android/RyujinxNative.kt | 158 ++ .../org/ryujinx/android/SystemLanguage.kt | 22 + .../java/org/ryujinx/android/UiHandler.kt | 211 +++ .../android/providers/DocumentProvider.kt | 300 ++++ .../org/ryujinx/android/ui/theme/Color.kt | 11 + .../org/ryujinx/android/ui/theme/Theme.kt | 79 + .../java/org/ryujinx/android/ui/theme/Type.kt | 34 + .../android/viewmodels/DlcViewModel.kt | 157 ++ .../ryujinx/android/viewmodels/GameInfo.java | 20 + .../ryujinx/android/viewmodels/GameModel.kt | 90 ++ .../android/viewmodels/HomeViewModel.kt | 101 ++ .../android/viewmodels/MainViewModel.kt | 414 ++++++ .../android/viewmodels/QuickSettings.kt | 97 ++ .../android/viewmodels/SettingsViewModel.kt | 287 ++++ .../viewmodels/TitleUpdateViewModel.kt | 171 +++ .../android/viewmodels/UserViewModel.kt | 77 + .../viewmodels/VulkanDriverViewModel.kt | 167 +++ .../org/ryujinx/android/views/DlcViews.kt | 142 ++ .../org/ryujinx/android/views/GameViews.kt | 403 ++++++ .../org/ryujinx/android/views/HomeViews.kt | 803 +++++++++++ .../org/ryujinx/android/views/MainView.kt | 32 + .../org/ryujinx/android/views/SettingViews.kt | 1280 +++++++++++++++++ .../ryujinx/android/views/TitleUpdateViews.kt | 148 ++ .../org/ryujinx/android/views/UserViews.kt | 175 +++ .../app/src/main/jniLibs/arm64-v8a/.gitkeep | 1 + .../app/src/main/res/drawable/app_update.xml | 9 + .../res/drawable/ic_launcher_foreground.xml | 35 + .../app/src/main/res/drawable/icon_nro.png | Bin 0 -> 10254 bytes .../app/src/main/res/layout/game_layout.xml | 34 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2366 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1460 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3566 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5356 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7524 bytes .../app/src/main/res/values/colors.xml | 10 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../app/src/main/res/xml/provider_paths.xml | 12 + .../org/ryujinx/android/ExampleUnitTest.kt | 17 + src/RyujinxAndroid/build.gradle | 6 + src/RyujinxAndroid/gradle.properties | 36 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + src/RyujinxAndroid/gradlew | 185 +++ src/RyujinxAndroid/gradlew.bat | 89 ++ src/RyujinxAndroid/libryujinx/README.md | 12 + src/RyujinxAndroid/libryujinx/build.gradle | 139 ++ .../libryujinx/libs/OpenSSL.cmake | 54 + src/RyujinxAndroid/settings.gradle | 21 + 85 files changed, 10152 insertions(+) create mode 100644 src/RyujinxAndroid/.gitignore create mode 100644 src/RyujinxAndroid/app/build.gradle create mode 100644 src/RyujinxAndroid/app/proguard-rules.pro create mode 100644 src/RyujinxAndroid/app/src/androidTest/java/org/ryujinx/android/ExampleInstrumentedTest.kt create mode 100644 src/RyujinxAndroid/app/src/main/AndroidManifest.xml create mode 100644 src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt create mode 100644 src/RyujinxAndroid/app/src/main/cpp/native_window.h create mode 100644 src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h create mode 100644 src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp create mode 100644 src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.cpp create mode 100644 src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.h create mode 100644 src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BackendThreading.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GamePadButtonInputId.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceMonitor.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RegionCode.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/SystemLanguage.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Color.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Theme.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Type.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameInfo.java create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt create mode 100644 src/RyujinxAndroid/app/src/main/jniLibs/arm64-v8a/.gitkeep create mode 100644 src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png create mode 100644 src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 src/RyujinxAndroid/app/src/main/res/values/colors.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/values/strings.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/values/themes.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/xml/backup_rules.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/xml/provider_paths.xml create mode 100644 src/RyujinxAndroid/app/src/test/java/org/ryujinx/android/ExampleUnitTest.kt create mode 100644 src/RyujinxAndroid/build.gradle create mode 100644 src/RyujinxAndroid/gradle.properties create mode 100644 src/RyujinxAndroid/gradle/wrapper/gradle-wrapper.jar create mode 100644 src/RyujinxAndroid/gradle/wrapper/gradle-wrapper.properties create mode 100644 src/RyujinxAndroid/gradlew create mode 100644 src/RyujinxAndroid/gradlew.bat create mode 100644 src/RyujinxAndroid/libryujinx/README.md create mode 100644 src/RyujinxAndroid/libryujinx/build.gradle create mode 100644 src/RyujinxAndroid/libryujinx/libs/OpenSSL.cmake create mode 100644 src/RyujinxAndroid/settings.gradle diff --git a/src/RyujinxAndroid/.gitignore b/src/RyujinxAndroid/.gitignore new file mode 100644 index 00000000..314e02c2 --- /dev/null +++ b/src/RyujinxAndroid/.gitignore @@ -0,0 +1,12 @@ +.idea/ +*.iml +.gradle +local.properties +.DS_Store +build/ +captures +.externalNativeBuild +.cxx/ + +app/src/main/jniLibs/arm64-v8a/** +!app/src/main/jniLibs/arm64-v8a/.gitkeep diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle new file mode 100644 index 00000000..88976992 --- /dev/null +++ b/src/RyujinxAndroid/app/build.gradle @@ -0,0 +1,117 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'org.ryujinx.android' + compileSdk 34 + + defaultConfig { + applicationId "org.ryujinx.android" + minSdk 30 + targetSdk 34 + versionCode 10044 + versionName '1.0.44' + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + + ndk { + //noinspection ChromeOsAbiSupport + abiFilters 'arm64-v8a' + } + + externalNativeBuild { + cmake { + cppFlags "-std=c++11" + arguments "-DANDROID_STL=c++_shared" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + buildFeatures { + compose true + prefab true + buildConfig true + } + composeOptions { + kotlinCompilerExtensionVersion '1.5.13' + } + packagingOptions { + jniLibs { + keepDebugSymbols += '**/libryujinx.so' + useLegacyPackaging true + } + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } + externalNativeBuild { + cmake { + path file('src/main/cpp/CMakeLists.txt') + version '3.22.1' + } + } +} + +tasks.named("preBuild") { + dependsOn ':libryujinx:assemble' +} + +dependencies { + implementation group: 'net.java.dev.jna', name: 'jna', version: '5.14.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.12.0' + implementation platform('androidx.compose:compose-bom:2024.05.00') + implementation platform('androidx.compose:compose-bom:2024.05.00') + androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00') + androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00') + runtimeOnly project(":libryujinx") + implementation 'androidx.core:core-ktx:1.13.1' + implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24') + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + implementation "androidx.navigation:navigation-compose:2.7.7" + implementation 'androidx.activity:activity-compose:1.9.0' + implementation platform('androidx.compose:compose-bom:2024.05.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + implementation 'com.github.swordfish90:radialgamepad:2.0.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation "com.anggrayudi:storage:1.5.5" + implementation "androidx.preference:preference-ktx:1.2.1" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'net.lingala.zip4j:zip4j:2.11.5' + implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0") + implementation 'io.coil-kt:coil-compose:2.6.0' + implementation("com.halilibo.compose-richtext:richtext-ui:0.20.0") + implementation("com.halilibo.compose-richtext:richtext-commonmark:0.20.0") + implementation("com.halilibo.compose-richtext:richtext-ui-material3:0.20.0") + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00') + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0' + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' +} diff --git a/src/RyujinxAndroid/app/proguard-rules.pro b/src/RyujinxAndroid/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/src/RyujinxAndroid/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/RyujinxAndroid/app/src/androidTest/java/org/ryujinx/android/ExampleInstrumentedTest.kt b/src/RyujinxAndroid/app/src/androidTest/java/org/ryujinx/android/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..d6e1198c --- /dev/null +++ b/src/RyujinxAndroid/app/src/androidTest/java/org/ryujinx/android/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.ryujinx.android + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.ryujinx.android", appContext.packageName) + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0e457759 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..07f54ced --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,77 @@ +include(FetchContent) + +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.22.1) + +# Declares and names the project. + +project("ryujinxjni") + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) + +FetchContent_Declare( + adrenotools + GIT_REPOSITORY https://github.com/bylaws/libadrenotools.git + GIT_TAG deec5f75ee1a8ccbe32c8780b1d17284fc87b0f1 # v1.0-14-gdeec5f7 +) + +FetchContent_MakeAvailable(adrenotools) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +add_library( # Sets the name of the library. + ryujinxjni + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + vulkan_wrapper.cpp + ryujinx.cpp) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log ) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + ryujinxjni + # Links the target library to the log library + # included in the NDK. + ${log-lib} + -lvulkan + -landroid + adrenotools + ) + +# Build external libraries if prebuilt files don't exist +set(JNI_PATH ../jniLibs/${CMAKE_ANDROID_ARCH_ABI}) +cmake_path(ABSOLUTE_PATH JNI_PATH NORMALIZE) + +cmake_path(APPEND JNI_PATH libcrypto.so OUTPUT_VARIABLE LIBCRYPTO_JNI_PATH) +cmake_path(APPEND JNI_PATH libssl.so OUTPUT_VARIABLE LIBSSL_JNI_PATH) + +if (NOT (EXISTS ${LIBCRYPTO_JNI_PATH} AND EXISTS ${LIBSSL_JNI_PATH})) + include(../../../../libryujinx/libs/OpenSSL.cmake) + add_dependencies(ryujinxjni openssl) +endif () diff --git a/src/RyujinxAndroid/app/src/main/cpp/native_window.h b/src/RyujinxAndroid/app/src/main/cpp/native_window.h new file mode 100644 index 00000000..354cd6cc --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/native_window.h @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/) +// Copyright © 2021 The Android Open Source Project + +#pragma once + +/* A collection of various types from AOSP that allow us to access private APIs for Native Window which we utilize for emulating the guest SF more accurately */ + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativebase/include/nativebase/nativebase.h;l=29;drc=cb496acbe593326e8d5d563847067d02b2df40ec + */ +#define ANDROID_NATIVE_UNSIGNED_CAST(x) static_cast(x) + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativebase/include/nativebase/nativebase.h;l=34-38;drc=cb496acbe593326e8d5d563847067d02b2df40ec + */ +#define ANDROID_NATIVE_MAKE_CONSTANT(a, b, c, d) \ + ((ANDROID_NATIVE_UNSIGNED_CAST(a) << 24) | \ + (ANDROID_NATIVE_UNSIGNED_CAST(b) << 16) | \ + (ANDROID_NATIVE_UNSIGNED_CAST(c) << 8) | \ + (ANDROID_NATIVE_UNSIGNED_CAST(d))) + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=60;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d + */ +#define ANDROID_NATIVE_WINDOW_MAGIC ANDROID_NATIVE_MAKE_CONSTANT('_','w','n','d') + +constexpr int AndroidNativeWindowMagic{ANDROID_NATIVE_WINDOW_MAGIC}; + +#undef ANDROID_NATIVE_WINDOW_MAGIC +#undef ANDROID_NATIVE_MAKE_CONSTANT +#undef ANDROID_NATIVE_UNSIGNED_CAST + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=325-331;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d + */ +constexpr int64_t NativeWindowTimestampAuto{-9223372036854775807LL - 1}; + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=198-259;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d + */ +enum { + NATIVE_WINDOW_CONNECT = 1, /* deprecated */ + NATIVE_WINDOW_DISCONNECT = 2, /* deprecated */ + NATIVE_WINDOW_SET_CROP = 3, /* private */ + NATIVE_WINDOW_SET_BUFFER_COUNT = 4, + NATIVE_WINDOW_SET_BUFFERS_TRANSFORM = 6, + NATIVE_WINDOW_SET_BUFFERS_TIMESTAMP = 7, + NATIVE_WINDOW_SET_BUFFERS_DIMENSIONS = 8, + NATIVE_WINDOW_SET_SCALING_MODE = 10, /* private */ + NATIVE_WINDOW_LOCK = 11, /* private */ + NATIVE_WINDOW_UNLOCK_AND_POST = 12, /* private */ + NATIVE_WINDOW_API_CONNECT = 13, /* private */ + NATIVE_WINDOW_API_DISCONNECT = 14, /* private */ + NATIVE_WINDOW_SET_BUFFERS_USER_DIMENSIONS = 15, /* private */ + NATIVE_WINDOW_SET_POST_TRANSFORM_CROP = 16, /* deprecated, unimplemented */ + NATIVE_WINDOW_SET_BUFFERS_STICKY_TRANSFORM = 17, /* private */ + NATIVE_WINDOW_SET_SIDEBAND_STREAM = 18, + NATIVE_WINDOW_SET_BUFFERS_DATASPACE = 19, + NATIVE_WINDOW_SET_SURFACE_DAMAGE = 20, /* private */ + NATIVE_WINDOW_SET_SHARED_BUFFER_MODE = 21, + NATIVE_WINDOW_SET_AUTO_REFRESH = 22, + NATIVE_WINDOW_GET_REFRESH_CYCLE_DURATION = 23, + NATIVE_WINDOW_GET_NEXT_FRAME_ID = 24, + NATIVE_WINDOW_ENABLE_FRAME_TIMESTAMPS = 25, + NATIVE_WINDOW_GET_COMPOSITOR_TIMING = 26, + NATIVE_WINDOW_GET_FRAME_TIMESTAMPS = 27, + NATIVE_WINDOW_GET_WIDE_COLOR_SUPPORT = 28, + NATIVE_WINDOW_GET_HDR_SUPPORT = 29, + NATIVE_WINDOW_GET_CONSUMER_USAGE64 = 31, + NATIVE_WINDOW_SET_BUFFERS_SMPTE2086_METADATA = 32, + NATIVE_WINDOW_SET_BUFFERS_CTA861_3_METADATA = 33, + NATIVE_WINDOW_SET_BUFFERS_HDR10_PLUS_METADATA = 34, + NATIVE_WINDOW_SET_AUTO_PREROTATION = 35, + NATIVE_WINDOW_GET_LAST_DEQUEUE_START = 36, /* private */ + NATIVE_WINDOW_SET_DEQUEUE_TIMEOUT = 37, /* private */ + NATIVE_WINDOW_GET_LAST_DEQUEUE_DURATION = 38, /* private */ + NATIVE_WINDOW_GET_LAST_QUEUE_DURATION = 39, /* private */ + NATIVE_WINDOW_SET_FRAME_RATE = 40, + NATIVE_WINDOW_SET_CANCEL_INTERCEPTOR = 41, /* private */ + NATIVE_WINDOW_SET_DEQUEUE_INTERCEPTOR = 42, /* private */ + NATIVE_WINDOW_SET_PERFORM_INTERCEPTOR = 43, /* private */ + NATIVE_WINDOW_SET_QUEUE_INTERCEPTOR = 44, /* private */ + NATIVE_WINDOW_ALLOCATE_BUFFERS = 45, /* private */ + NATIVE_WINDOW_GET_LAST_QUEUED_BUFFER = 46, /* private */ + NATIVE_WINDOW_SET_QUERY_INTERCEPTOR = 47, /* private */ + NATIVE_WINDOW_GET_LAST_QUEUED_BUFFER2 = 50, /* private */ +}; + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativebase/include/nativebase/nativebase.h;l=43-56;drc=cb496acbe593326e8d5d563847067d02b2df40ec + */ +struct android_native_base_t { + int magic; + int version; + void *reserved[4]; + + void (*incRef)(android_native_base_t *); + + void (*decRef)(android_native_base_t *); +}; + +/** + * @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=341-560;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d + */ +struct ANativeWindow { + struct android_native_base_t common; + + /* flags describing some attributes of this surface or its updater */ + const uint32_t flags; + + /* min swap interval supported by this updated */ + const int minSwapInterval; + + /* max swap interval supported by this updated */ + const int maxSwapInterval; + + /* horizontal and vertical resolution in DPI */ + const float xdpi; + const float ydpi; + + /* Some storage reserved for the OEM's driver. */ + intptr_t oem[4]; + + /* + * Set the swap interval for this surface. + * + * Returns 0 on success or -errno on error. + */ + int (*setSwapInterval)(struct ANativeWindow *window, + int interval); + + /* + * Hook called by EGL to acquire a buffer. After this call, the buffer + * is not locked, so its content cannot be modified. This call may block if + * no buffers are available. + * + * The window holds a reference to the buffer between dequeueBuffer and + * either queueBuffer or cancelBuffer, so clients only need their own + * reference if they might use the buffer after queueing or canceling it. + * Holding a reference to a buffer after queueing or canceling it is only + * allowed if a specific buffer count has been set. + * + * Returns 0 on success or -errno on error. + * + * XXX: This function is deprecated. It will continue to work for some + * time for binary compatibility, but the new dequeueBuffer function that + * outputs a fence file descriptor should be used in its place. + */ + int (*dequeueBuffer_DEPRECATED)(struct ANativeWindow *window, + struct ANativeWindowBuffer **buffer); + + /* + * hook called by EGL to lock a buffer. This MUST be called before modifying + * the content of a buffer. The buffer must have been acquired with + * dequeueBuffer first. + * + * Returns 0 on success or -errno on error. + * + * XXX: This function is deprecated. It will continue to work for some + * time for binary compatibility, but it is essentially a no-op, and calls + * to it should be removed. + */ + int (*lockBuffer_DEPRECATED)(struct ANativeWindow *window, + struct ANativeWindowBuffer *buffer); + + /* + * Hook called by EGL when modifications to the render buffer are done. + * This unlocks and post the buffer. + * + * The window holds a reference to the buffer between dequeueBuffer and + * either queueBuffer or cancelBuffer, so clients only need their own + * reference if they might use the buffer after queueing or canceling it. + * Holding a reference to a buffer after queueing or canceling it is only + * allowed if a specific buffer count has been set. + * + * Buffers MUST be queued in the same order than they were dequeued. + * + * Returns 0 on success or -errno on error. + * + * XXX: This function is deprecated. It will continue to work for some + * time for binary compatibility, but the new queueBuffer function that + * takes a fence file descriptor should be used in its place (pass a value + * of -1 for the fence file descriptor if there is no valid one to pass). + */ + int (*queueBuffer_DEPRECATED)(struct ANativeWindow *window, + struct ANativeWindowBuffer *buffer); + + /* + * hook used to retrieve information about the native window. + * + * Returns 0 on success or -errno on error. + */ + int (*query)(const struct ANativeWindow *window, + int what, int *value); + + /* + * hook used to perform various operations on the surface. + * (*perform)() is a generic mechanism to add functionality to + * ANativeWindow while keeping backward binary compatibility. + * + * DO NOT CALL THIS HOOK DIRECTLY. Instead, use the helper functions + * defined below. + * + * (*perform)() returns -ENOENT if the 'what' parameter is not supported + * by the surface's implementation. + * + * See above for a list of valid operations, such as + * NATIVE_WINDOW_SET_USAGE or NATIVE_WINDOW_CONNECT + */ + int (*perform)(struct ANativeWindow *window, + int operation, ...); + + /* + * Hook used to cancel a buffer that has been dequeued. + * No synchronization is performed between dequeue() and cancel(), so + * either external synchronization is needed, or these functions must be + * called from the same thread. + * + * The window holds a reference to the buffer between dequeueBuffer and + * either queueBuffer or cancelBuffer, so clients only need their own + * reference if they might use the buffer after queueing or canceling it. + * Holding a reference to a buffer after queueing or canceling it is only + * allowed if a specific buffer count has been set. + * + * XXX: This function is deprecated. It will continue to work for some + * time for binary compatibility, but the new cancelBuffer function that + * takes a fence file descriptor should be used in its place (pass a value + * of -1 for the fence file descriptor if there is no valid one to pass). + */ + int (*cancelBuffer_DEPRECATED)(struct ANativeWindow *window, + struct ANativeWindowBuffer *buffer); + + /* + * Hook called by EGL to acquire a buffer. This call may block if no + * buffers are available. + * + * The window holds a reference to the buffer between dequeueBuffer and + * either queueBuffer or cancelBuffer, so clients only need their own + * reference if they might use the buffer after queueing or canceling it. + * Holding a reference to a buffer after queueing or canceling it is only + * allowed if a specific buffer count has been set. + * + * The libsync fence file descriptor returned in the int pointed to by the + * fenceFd argument will refer to the fence that must signal before the + * dequeued buffer may be written to. A value of -1 indicates that the + * caller may access the buffer immediately without waiting on a fence. If + * a valid file descriptor is returned (i.e. any value except -1) then the + * caller is responsible for closing the file descriptor. + * + * Returns 0 on success or -errno on error. + */ + int (*dequeueBuffer)(struct ANativeWindow *window, + struct ANativeWindowBuffer **buffer, int *fenceFd); + + /* + * Hook called by EGL when modifications to the render buffer are done. + * This unlocks and post the buffer. + * + * The window holds a reference to the buffer between dequeueBuffer and + * either queueBuffer or cancelBuffer, so clients only need their own + * reference if they might use the buffer after queueing or canceling it. + * Holding a reference to a buffer after queueing or canceling it is only + * allowed if a specific buffer count has been set. + * + * The fenceFd argument specifies a libsync fence file descriptor for a + * fence that must signal before the buffer can be accessed. If the buffer + * can be accessed immediately then a value of -1 should be used. The + * caller must not use the file descriptor after it is passed to + * queueBuffer, and the ANativeWindow implementation is responsible for + * closing it. + * + * Returns 0 on success or -errno on error. + */ + int (*queueBuffer)(struct ANativeWindow *window, + struct ANativeWindowBuffer *buffer, int fenceFd); + + /* + * Hook used to cancel a buffer that has been dequeued. + * No synchronization is performed between dequeue() and cancel(), so + * either external synchronization is needed, or these functions must be + * called from the same thread. + * + * The window holds a reference to the buffer between dequeueBuffer and + * either queueBuffer or cancelBuffer, so clients only need their own + * reference if they might use the buffer after queueing or canceling it. + * Holding a reference to a buffer after queueing or canceling it is only + * allowed if a specific buffer count has been set. + * + * The fenceFd argument specifies a libsync fence file decsriptor for a + * fence that must signal before the buffer can be accessed. If the buffer + * can be accessed immediately then a value of -1 should be used. + * + * Note that if the client has not waited on the fence that was returned + * from dequeueBuffer, that same fence should be passed to cancelBuffer to + * ensure that future uses of the buffer are preceded by a wait on that + * fence. The caller must not use the file descriptor after it is passed + * to cancelBuffer, and the ANativeWindow implementation is responsible for + * closing it. + * + * Returns 0 on success or -errno on error. + */ + int (*cancelBuffer)(struct ANativeWindow *window, + struct ANativeWindowBuffer *buffer, int fenceFd); +}; diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h new file mode 100644 index 00000000..cbcbe0cb --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -0,0 +1,49 @@ +// +// Created by Emmanuel Hansen on 6/19/2023. +// + +#ifndef RYUJINXNATIVE_RYUIJNX_H +#define RYUJINXNATIVE_RYUIJNX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vulkan_wrapper.h" +#include +#include +#include +#include "adrenotools/driver.h" +#include "native_window.h" + +// A macro to pass call to Vulkan and check for return value for success +#define CALL_VK(func) \ + if (VK_SUCCESS != (func)) { \ + __android_log_print(ANDROID_LOG_ERROR, "Tutorial ", \ + "Vulkan error. File[%s], line[%d]", __FILE__, \ + __LINE__); \ + assert(false); \ + } + +// A macro to check value is VK_SUCCESS +// Used also for non-vulkan functions but return VK_SUCCESS +#define VK_CHECK(x) CALL_VK(x) + +#define LoadLib(a) dlopen(a, RTLD_NOW) + +void *_ryujinxNative = NULL; + +// Ryujinx imported functions +bool (*initialize)(char *) = NULL; + +long _renderingThreadId = 0; +JavaVM *_vm = nullptr; +jobject _mainActivity = nullptr; +jclass _mainActivityClass = nullptr; + +#endif //RYUJINXNATIVE_RYUIJNX_H diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp new file mode 100644 index 00000000..3272c314 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -0,0 +1,250 @@ +// Write C++ code here. +// +// Do not forget to dynamically load the C++ library into your application. +// +// For instance, +// +// In MainActivity.java: +// static { +// System.loadLibrary("ryuijnx"); +// } +// +// Or, in MainActivity.kt: +// companion object { +// init { +// System.loadLibrary("ryuijnx") +// } +// } + +#include "ryuijnx.h" +#include "pthread.h" +#include +#include + + +std::chrono::time_point _currentTimePoint; + +extern "C" +{ +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getNativeWindow( + JNIEnv *env, + jobject instance, + jobject surface) { + auto nativeWindow = ANativeWindow_fromSurface(env, surface); + return nativeWindow == NULL ? -1 : (jlong) nativeWindow; +} + +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_releaseNativeWindow( + JNIEnv *env, + jobject instance, + jlong window) { + auto nativeWindow = (ANativeWindow *) window; + + if (nativeWindow != NULL) + ANativeWindow_release(nativeWindow); +} + +long createSurface(long native_surface, long instance) { + auto nativeWindow = (ANativeWindow *) native_surface; + VkSurfaceKHR surface; + auto vkInstance = (VkInstance) instance; + auto fpCreateAndroidSurfaceKHR = + reinterpret_cast(vkGetInstanceProcAddr(vkInstance, + "vkCreateAndroidSurfaceKHR")); + if (!fpCreateAndroidSurfaceKHR) + return -1; + VkAndroidSurfaceCreateInfoKHR info = {VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR}; + info.window = nativeWindow; + VK_CHECK(fpCreateAndroidSurfaceKHR(vkInstance, &info, nullptr, &surface)); + return (long) surface; +} + +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getCreateSurfacePtr( + JNIEnv *env, + jobject instance) { + return (jlong) createSurface; +} + +char *getStringPointer( + JNIEnv *env, + jstring jS) { + const char *cparam = env->GetStringUTFChars(jS, 0); + auto len = env->GetStringUTFLength(jS); + char *s = new char[len]; + strcpy(s, cparam); + env->ReleaseStringUTFChars(jS, cparam); + + return s; +} + +jstring createString( + JNIEnv *env, + char *ch) { + auto str = env->NewStringUTF(ch); + + return str; +} + +jstring createStringFromStdString( + JNIEnv *env, + std::string s) { + auto str = env->NewStringUTF(s.c_str()); + + return str; +} + + +} +extern "C" +void setRenderingThread() { + auto currentId = pthread_self(); + + _renderingThreadId = currentId; + + _currentTimePoint = std::chrono::high_resolution_clock::now(); +} +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) { + JavaVM *vm = nullptr; + auto success = env->GetJavaVM(&vm); + _vm = vm; + _mainActivity = thiz; + _mainActivityClass = env->GetObjectClass(thiz); +} + +bool isInitialOrientationFlipped = true; + +extern "C" +void setCurrentTransform(long native_window, int transform) { + if (native_window == 0 || native_window == -1) + return; + auto nativeWindow = (ANativeWindow *) native_window; + + auto nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY; + + transform = transform >> 1; + + // transform is a valid VkSurfaceTransformFlagBitsKHR + switch (transform) { + case 0x1: + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY; + break; + case 0x2: + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90; + break; + case 0x4: + nativeTransform = isInitialOrientationFlipped + ? ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY + : ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180; + break; + case 0x8: + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270; + break; + case 0x10: + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_HORIZONTAL; + break; + case 0x20: + nativeTransform = static_cast( + ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_HORIZONTAL | + ANATIVEWINDOW_TRANSFORM_ROTATE_90); + break; + case 0x40: + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_VERTICAL; + break; + case 0x80: + nativeTransform = static_cast( + ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_VERTICAL | + ANATIVEWINDOW_TRANSFORM_ROTATE_90); + break; + case 0x100: + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY; + break; + } + + nativeWindow->perform(nativeWindow, NATIVE_WINDOW_SET_BUFFERS_TRANSFORM, + static_cast(nativeTransform)); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_loadDriver(JNIEnv *env, jobject thiz, + jstring native_lib_path, + jstring private_apps_path, + jstring driver_name) { + auto libPath = getStringPointer(env, native_lib_path); + auto privateAppsPath = getStringPointer(env, private_apps_path); + auto driverName = getStringPointer(env, driver_name); + + auto handle = adrenotools_open_libvulkan( + RTLD_NOW, + ADRENOTOOLS_DRIVER_CUSTOM, + nullptr, + libPath, + privateAppsPath, + driverName, + nullptr, + nullptr + ); + + delete libPath; + delete privateAppsPath; + delete driverName; + + return (jlong) handle; +} + +extern "C" +void debug_break(int code) { + if (code >= 3) + int r = 0; +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_setTurboMode(JNIEnv *env, jobject thiz, jboolean enable) { + adrenotools_set_turbo(enable); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getMaxSwapInterval(JNIEnv *env, jobject thiz, + jlong native_window) { + auto nativeWindow = (ANativeWindow *) native_window; + + return nativeWindow->maxSwapInterval; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getMinSwapInterval(JNIEnv *env, jobject thiz, + jlong native_window) { + auto nativeWindow = (ANativeWindow *) native_window; + + return nativeWindow->minSwapInterval; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_setSwapInterval(JNIEnv *env, jobject thiz, + jlong native_window, jint swap_interval) { + auto nativeWindow = (ANativeWindow *) native_window; + + return nativeWindow->setSwapInterval(nativeWindow, swap_interval); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong ptr) { + return createString(env, (char*)ptr); +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_setIsInitialOrientationFlipped(JNIEnv *env, jobject thiz, + jboolean is_flipped) { + isInitialOrientationFlipped = is_flipped; +} diff --git a/src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.cpp b/src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.cpp new file mode 100644 index 00000000..f186c850 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.cpp @@ -0,0 +1,404 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// This file is generated. +#include "vulkan_wrapper.h" +#include + +int InitVulkan(void) { + void* libvulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); + if (!libvulkan) + return 0; + + // Vulkan supported, set function addresses + vkCreateInstance = reinterpret_cast(dlsym(libvulkan, "vkCreateInstance")); + vkDestroyInstance = reinterpret_cast(dlsym(libvulkan, "vkDestroyInstance")); + vkEnumeratePhysicalDevices = reinterpret_cast(dlsym(libvulkan, "vkEnumeratePhysicalDevices")); + vkGetPhysicalDeviceFeatures = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceFeatures")); + vkGetPhysicalDeviceFormatProperties = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceFormatProperties")); + vkGetPhysicalDeviceImageFormatProperties = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceImageFormatProperties")); + vkGetPhysicalDeviceProperties = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceProperties")); + vkGetPhysicalDeviceQueueFamilyProperties = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceQueueFamilyProperties")); + vkGetPhysicalDeviceMemoryProperties = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceMemoryProperties")); + vkGetInstanceProcAddr = reinterpret_cast(dlsym(libvulkan, "vkGetInstanceProcAddr")); + vkGetDeviceProcAddr = reinterpret_cast(dlsym(libvulkan, "vkGetDeviceProcAddr")); + vkCreateDevice = reinterpret_cast(dlsym(libvulkan, "vkCreateDevice")); + vkDestroyDevice = reinterpret_cast(dlsym(libvulkan, "vkDestroyDevice")); + vkEnumerateInstanceExtensionProperties = reinterpret_cast(dlsym(libvulkan, "vkEnumerateInstanceExtensionProperties")); + vkEnumerateDeviceExtensionProperties = reinterpret_cast(dlsym(libvulkan, "vkEnumerateDeviceExtensionProperties")); + vkEnumerateInstanceLayerProperties = reinterpret_cast(dlsym(libvulkan, "vkEnumerateInstanceLayerProperties")); + vkEnumerateDeviceLayerProperties = reinterpret_cast(dlsym(libvulkan, "vkEnumerateDeviceLayerProperties")); + vkGetDeviceQueue = reinterpret_cast(dlsym(libvulkan, "vkGetDeviceQueue")); + vkQueueSubmit = reinterpret_cast(dlsym(libvulkan, "vkQueueSubmit")); + vkQueueWaitIdle = reinterpret_cast(dlsym(libvulkan, "vkQueueWaitIdle")); + vkDeviceWaitIdle = reinterpret_cast(dlsym(libvulkan, "vkDeviceWaitIdle")); + vkAllocateMemory = reinterpret_cast(dlsym(libvulkan, "vkAllocateMemory")); + vkFreeMemory = reinterpret_cast(dlsym(libvulkan, "vkFreeMemory")); + vkMapMemory = reinterpret_cast(dlsym(libvulkan, "vkMapMemory")); + vkUnmapMemory = reinterpret_cast(dlsym(libvulkan, "vkUnmapMemory")); + vkFlushMappedMemoryRanges = reinterpret_cast(dlsym(libvulkan, "vkFlushMappedMemoryRanges")); + vkInvalidateMappedMemoryRanges = reinterpret_cast(dlsym(libvulkan, "vkInvalidateMappedMemoryRanges")); + vkGetDeviceMemoryCommitment = reinterpret_cast(dlsym(libvulkan, "vkGetDeviceMemoryCommitment")); + vkBindBufferMemory = reinterpret_cast(dlsym(libvulkan, "vkBindBufferMemory")); + vkBindImageMemory = reinterpret_cast(dlsym(libvulkan, "vkBindImageMemory")); + vkGetBufferMemoryRequirements = reinterpret_cast(dlsym(libvulkan, "vkGetBufferMemoryRequirements")); + vkGetImageMemoryRequirements = reinterpret_cast(dlsym(libvulkan, "vkGetImageMemoryRequirements")); + vkGetImageSparseMemoryRequirements = reinterpret_cast(dlsym(libvulkan, "vkGetImageSparseMemoryRequirements")); + vkGetPhysicalDeviceSparseImageFormatProperties = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceSparseImageFormatProperties")); + vkQueueBindSparse = reinterpret_cast(dlsym(libvulkan, "vkQueueBindSparse")); + vkCreateFence = reinterpret_cast(dlsym(libvulkan, "vkCreateFence")); + vkDestroyFence = reinterpret_cast(dlsym(libvulkan, "vkDestroyFence")); + vkResetFences = reinterpret_cast(dlsym(libvulkan, "vkResetFences")); + vkGetFenceStatus = reinterpret_cast(dlsym(libvulkan, "vkGetFenceStatus")); + vkWaitForFences = reinterpret_cast(dlsym(libvulkan, "vkWaitForFences")); + vkCreateSemaphore = reinterpret_cast(dlsym(libvulkan, "vkCreateSemaphore")); + vkDestroySemaphore = reinterpret_cast(dlsym(libvulkan, "vkDestroySemaphore")); + vkCreateEvent = reinterpret_cast(dlsym(libvulkan, "vkCreateEvent")); + vkDestroyEvent = reinterpret_cast(dlsym(libvulkan, "vkDestroyEvent")); + vkGetEventStatus = reinterpret_cast(dlsym(libvulkan, "vkGetEventStatus")); + vkSetEvent = reinterpret_cast(dlsym(libvulkan, "vkSetEvent")); + vkResetEvent = reinterpret_cast(dlsym(libvulkan, "vkResetEvent")); + vkCreateQueryPool = reinterpret_cast(dlsym(libvulkan, "vkCreateQueryPool")); + vkDestroyQueryPool = reinterpret_cast(dlsym(libvulkan, "vkDestroyQueryPool")); + vkGetQueryPoolResults = reinterpret_cast(dlsym(libvulkan, "vkGetQueryPoolResults")); + vkCreateBuffer = reinterpret_cast(dlsym(libvulkan, "vkCreateBuffer")); + vkDestroyBuffer = reinterpret_cast(dlsym(libvulkan, "vkDestroyBuffer")); + vkCreateBufferView = reinterpret_cast(dlsym(libvulkan, "vkCreateBufferView")); + vkDestroyBufferView = reinterpret_cast(dlsym(libvulkan, "vkDestroyBufferView")); + vkCreateImage = reinterpret_cast(dlsym(libvulkan, "vkCreateImage")); + vkDestroyImage = reinterpret_cast(dlsym(libvulkan, "vkDestroyImage")); + vkGetImageSubresourceLayout = reinterpret_cast(dlsym(libvulkan, "vkGetImageSubresourceLayout")); + vkCreateImageView = reinterpret_cast(dlsym(libvulkan, "vkCreateImageView")); + vkDestroyImageView = reinterpret_cast(dlsym(libvulkan, "vkDestroyImageView")); + vkCreateShaderModule = reinterpret_cast(dlsym(libvulkan, "vkCreateShaderModule")); + vkDestroyShaderModule = reinterpret_cast(dlsym(libvulkan, "vkDestroyShaderModule")); + vkCreatePipelineCache = reinterpret_cast(dlsym(libvulkan, "vkCreatePipelineCache")); + vkDestroyPipelineCache = reinterpret_cast(dlsym(libvulkan, "vkDestroyPipelineCache")); + vkGetPipelineCacheData = reinterpret_cast(dlsym(libvulkan, "vkGetPipelineCacheData")); + vkMergePipelineCaches = reinterpret_cast(dlsym(libvulkan, "vkMergePipelineCaches")); + vkCreateGraphicsPipelines = reinterpret_cast(dlsym(libvulkan, "vkCreateGraphicsPipelines")); + vkCreateComputePipelines = reinterpret_cast(dlsym(libvulkan, "vkCreateComputePipelines")); + vkDestroyPipeline = reinterpret_cast(dlsym(libvulkan, "vkDestroyPipeline")); + vkCreatePipelineLayout = reinterpret_cast(dlsym(libvulkan, "vkCreatePipelineLayout")); + vkDestroyPipelineLayout = reinterpret_cast(dlsym(libvulkan, "vkDestroyPipelineLayout")); + vkCreateSampler = reinterpret_cast(dlsym(libvulkan, "vkCreateSampler")); + vkDestroySampler = reinterpret_cast(dlsym(libvulkan, "vkDestroySampler")); + vkCreateDescriptorSetLayout = reinterpret_cast(dlsym(libvulkan, "vkCreateDescriptorSetLayout")); + vkDestroyDescriptorSetLayout = reinterpret_cast(dlsym(libvulkan, "vkDestroyDescriptorSetLayout")); + vkCreateDescriptorPool = reinterpret_cast(dlsym(libvulkan, "vkCreateDescriptorPool")); + vkDestroyDescriptorPool = reinterpret_cast(dlsym(libvulkan, "vkDestroyDescriptorPool")); + vkResetDescriptorPool = reinterpret_cast(dlsym(libvulkan, "vkResetDescriptorPool")); + vkAllocateDescriptorSets = reinterpret_cast(dlsym(libvulkan, "vkAllocateDescriptorSets")); + vkFreeDescriptorSets = reinterpret_cast(dlsym(libvulkan, "vkFreeDescriptorSets")); + vkUpdateDescriptorSets = reinterpret_cast(dlsym(libvulkan, "vkUpdateDescriptorSets")); + vkCreateFramebuffer = reinterpret_cast(dlsym(libvulkan, "vkCreateFramebuffer")); + vkDestroyFramebuffer = reinterpret_cast(dlsym(libvulkan, "vkDestroyFramebuffer")); + vkCreateRenderPass = reinterpret_cast(dlsym(libvulkan, "vkCreateRenderPass")); + vkDestroyRenderPass = reinterpret_cast(dlsym(libvulkan, "vkDestroyRenderPass")); + vkGetRenderAreaGranularity = reinterpret_cast(dlsym(libvulkan, "vkGetRenderAreaGranularity")); + vkCreateCommandPool = reinterpret_cast(dlsym(libvulkan, "vkCreateCommandPool")); + vkDestroyCommandPool = reinterpret_cast(dlsym(libvulkan, "vkDestroyCommandPool")); + vkResetCommandPool = reinterpret_cast(dlsym(libvulkan, "vkResetCommandPool")); + vkAllocateCommandBuffers = reinterpret_cast(dlsym(libvulkan, "vkAllocateCommandBuffers")); + vkFreeCommandBuffers = reinterpret_cast(dlsym(libvulkan, "vkFreeCommandBuffers")); + vkBeginCommandBuffer = reinterpret_cast(dlsym(libvulkan, "vkBeginCommandBuffer")); + vkEndCommandBuffer = reinterpret_cast(dlsym(libvulkan, "vkEndCommandBuffer")); + vkResetCommandBuffer = reinterpret_cast(dlsym(libvulkan, "vkResetCommandBuffer")); + vkCmdBindPipeline = reinterpret_cast(dlsym(libvulkan, "vkCmdBindPipeline")); + vkCmdSetViewport = reinterpret_cast(dlsym(libvulkan, "vkCmdSetViewport")); + vkCmdSetScissor = reinterpret_cast(dlsym(libvulkan, "vkCmdSetScissor")); + vkCmdSetLineWidth = reinterpret_cast(dlsym(libvulkan, "vkCmdSetLineWidth")); + vkCmdSetDepthBias = reinterpret_cast(dlsym(libvulkan, "vkCmdSetDepthBias")); + vkCmdSetBlendConstants = reinterpret_cast(dlsym(libvulkan, "vkCmdSetBlendConstants")); + vkCmdSetDepthBounds = reinterpret_cast(dlsym(libvulkan, "vkCmdSetDepthBounds")); + vkCmdSetStencilCompareMask = reinterpret_cast(dlsym(libvulkan, "vkCmdSetStencilCompareMask")); + vkCmdSetStencilWriteMask = reinterpret_cast(dlsym(libvulkan, "vkCmdSetStencilWriteMask")); + vkCmdSetStencilReference = reinterpret_cast(dlsym(libvulkan, "vkCmdSetStencilReference")); + vkCmdBindDescriptorSets = reinterpret_cast(dlsym(libvulkan, "vkCmdBindDescriptorSets")); + vkCmdBindIndexBuffer = reinterpret_cast(dlsym(libvulkan, "vkCmdBindIndexBuffer")); + vkCmdBindVertexBuffers = reinterpret_cast(dlsym(libvulkan, "vkCmdBindVertexBuffers")); + vkCmdDraw = reinterpret_cast(dlsym(libvulkan, "vkCmdDraw")); + vkCmdDrawIndexed = reinterpret_cast(dlsym(libvulkan, "vkCmdDrawIndexed")); + vkCmdDrawIndirect = reinterpret_cast(dlsym(libvulkan, "vkCmdDrawIndirect")); + vkCmdDrawIndexedIndirect = reinterpret_cast(dlsym(libvulkan, "vkCmdDrawIndexedIndirect")); + vkCmdDispatch = reinterpret_cast(dlsym(libvulkan, "vkCmdDispatch")); + vkCmdDispatchIndirect = reinterpret_cast(dlsym(libvulkan, "vkCmdDispatchIndirect")); + vkCmdCopyBuffer = reinterpret_cast(dlsym(libvulkan, "vkCmdCopyBuffer")); + vkCmdCopyImage = reinterpret_cast(dlsym(libvulkan, "vkCmdCopyImage")); + vkCmdBlitImage = reinterpret_cast(dlsym(libvulkan, "vkCmdBlitImage")); + vkCmdCopyBufferToImage = reinterpret_cast(dlsym(libvulkan, "vkCmdCopyBufferToImage")); + vkCmdCopyImageToBuffer = reinterpret_cast(dlsym(libvulkan, "vkCmdCopyImageToBuffer")); + vkCmdUpdateBuffer = reinterpret_cast(dlsym(libvulkan, "vkCmdUpdateBuffer")); + vkCmdFillBuffer = reinterpret_cast(dlsym(libvulkan, "vkCmdFillBuffer")); + vkCmdClearColorImage = reinterpret_cast(dlsym(libvulkan, "vkCmdClearColorImage")); + vkCmdClearDepthStencilImage = reinterpret_cast(dlsym(libvulkan, "vkCmdClearDepthStencilImage")); + vkCmdClearAttachments = reinterpret_cast(dlsym(libvulkan, "vkCmdClearAttachments")); + vkCmdResolveImage = reinterpret_cast(dlsym(libvulkan, "vkCmdResolveImage")); + vkCmdSetEvent = reinterpret_cast(dlsym(libvulkan, "vkCmdSetEvent")); + vkCmdResetEvent = reinterpret_cast(dlsym(libvulkan, "vkCmdResetEvent")); + vkCmdWaitEvents = reinterpret_cast(dlsym(libvulkan, "vkCmdWaitEvents")); + vkCmdPipelineBarrier = reinterpret_cast(dlsym(libvulkan, "vkCmdPipelineBarrier")); + vkCmdBeginQuery = reinterpret_cast(dlsym(libvulkan, "vkCmdBeginQuery")); + vkCmdEndQuery = reinterpret_cast(dlsym(libvulkan, "vkCmdEndQuery")); + vkCmdResetQueryPool = reinterpret_cast(dlsym(libvulkan, "vkCmdResetQueryPool")); + vkCmdWriteTimestamp = reinterpret_cast(dlsym(libvulkan, "vkCmdWriteTimestamp")); + vkCmdCopyQueryPoolResults = reinterpret_cast(dlsym(libvulkan, "vkCmdCopyQueryPoolResults")); + vkCmdPushConstants = reinterpret_cast(dlsym(libvulkan, "vkCmdPushConstants")); + vkCmdBeginRenderPass = reinterpret_cast(dlsym(libvulkan, "vkCmdBeginRenderPass")); + vkCmdNextSubpass = reinterpret_cast(dlsym(libvulkan, "vkCmdNextSubpass")); + vkCmdEndRenderPass = reinterpret_cast(dlsym(libvulkan, "vkCmdEndRenderPass")); + vkCmdExecuteCommands = reinterpret_cast(dlsym(libvulkan, "vkCmdExecuteCommands")); + vkDestroySurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkDestroySurfaceKHR")); + vkGetPhysicalDeviceSurfaceSupportKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceSurfaceSupportKHR")); + vkGetPhysicalDeviceSurfaceCapabilitiesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR")); + vkGetPhysicalDeviceSurfaceFormatsKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceSurfaceFormatsKHR")); + vkGetPhysicalDeviceSurfacePresentModesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceSurfacePresentModesKHR")); + vkCreateSwapchainKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateSwapchainKHR")); + vkDestroySwapchainKHR = reinterpret_cast(dlsym(libvulkan, "vkDestroySwapchainKHR")); + vkGetSwapchainImagesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetSwapchainImagesKHR")); + vkAcquireNextImageKHR = reinterpret_cast(dlsym(libvulkan, "vkAcquireNextImageKHR")); + vkQueuePresentKHR = reinterpret_cast(dlsym(libvulkan, "vkQueuePresentKHR")); + vkGetPhysicalDeviceDisplayPropertiesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceDisplayPropertiesKHR")); + vkGetPhysicalDeviceDisplayPlanePropertiesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR")); + vkGetDisplayPlaneSupportedDisplaysKHR = reinterpret_cast(dlsym(libvulkan, "vkGetDisplayPlaneSupportedDisplaysKHR")); + vkGetDisplayModePropertiesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetDisplayModePropertiesKHR")); + vkCreateDisplayModeKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateDisplayModeKHR")); + vkGetDisplayPlaneCapabilitiesKHR = reinterpret_cast(dlsym(libvulkan, "vkGetDisplayPlaneCapabilitiesKHR")); + vkCreateDisplayPlaneSurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateDisplayPlaneSurfaceKHR")); + vkCreateSharedSwapchainsKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateSharedSwapchainsKHR")); + +#ifdef VK_USE_PLATFORM_XLIB_KHR + vkCreateXlibSurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateXlibSurfaceKHR")); + vkGetPhysicalDeviceXlibPresentationSupportKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceXlibPresentationSupportKHR")); +#endif + +#ifdef VK_USE_PLATFORM_XCB_KHR + vkCreateXcbSurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateXcbSurfaceKHR")); + vkGetPhysicalDeviceXcbPresentationSupportKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceXcbPresentationSupportKHR")); +#endif + +#ifdef VK_USE_PLATFORM_WAYLAND_KHR + vkCreateWaylandSurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateWaylandSurfaceKHR")); + vkGetPhysicalDeviceWaylandPresentationSupportKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceWaylandPresentationSupportKHR")); +#endif + +#ifdef VK_USE_PLATFORM_MIR_KHR + vkCreateMirSurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateMirSurfaceKHR")); + vkGetPhysicalDeviceMirPresentationSupportKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceMirPresentationSupportKHR")); +#endif + +#ifdef VK_USE_PLATFORM_ANDROID_KHR + vkCreateAndroidSurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateAndroidSurfaceKHR")); +#endif + +#ifdef VK_USE_PLATFORM_WIN32_KHR + vkCreateWin32SurfaceKHR = reinterpret_cast(dlsym(libvulkan, "vkCreateWin32SurfaceKHR")); + vkGetPhysicalDeviceWin32PresentationSupportKHR = reinterpret_cast(dlsym(libvulkan, "vkGetPhysicalDeviceWin32PresentationSupportKHR")); +#endif +#ifdef USE_DEBUG_EXTENTIONS + vkCreateDebugReportCallbackEXT = reinterpret_cast(dlsym(libvulkan, "vkCreateDebugReportCallbackEXT")); + vkDestroyDebugReportCallbackEXT = reinterpret_cast(dlsym(libvulkan, "vkDestroyDebugReportCallbackEXT")); + vkDebugReportMessageEXT = reinterpret_cast(dlsym(libvulkan, "vkDebugReportMessageEXT")); +#endif + return 1; +} + +// No Vulkan support, do not set function addresses +PFN_vkCreateInstance vkCreateInstance; +PFN_vkDestroyInstance vkDestroyInstance; +PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices; +PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures; +PFN_vkGetPhysicalDeviceFormatProperties vkGetPhysicalDeviceFormatProperties; +PFN_vkGetPhysicalDeviceImageFormatProperties vkGetPhysicalDeviceImageFormatProperties; +PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties; +PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties; +PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties; +PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr; +PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr; +PFN_vkCreateDevice vkCreateDevice; +PFN_vkDestroyDevice vkDestroyDevice; +PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties; +PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties; +PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties; +PFN_vkEnumerateDeviceLayerProperties vkEnumerateDeviceLayerProperties; +PFN_vkGetDeviceQueue vkGetDeviceQueue; +PFN_vkQueueSubmit vkQueueSubmit; +PFN_vkQueueWaitIdle vkQueueWaitIdle; +PFN_vkDeviceWaitIdle vkDeviceWaitIdle; +PFN_vkAllocateMemory vkAllocateMemory; +PFN_vkFreeMemory vkFreeMemory; +PFN_vkMapMemory vkMapMemory; +PFN_vkUnmapMemory vkUnmapMemory; +PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges; +PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges; +PFN_vkGetDeviceMemoryCommitment vkGetDeviceMemoryCommitment; +PFN_vkBindBufferMemory vkBindBufferMemory; +PFN_vkBindImageMemory vkBindImageMemory; +PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements; +PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements; +PFN_vkGetImageSparseMemoryRequirements vkGetImageSparseMemoryRequirements; +PFN_vkGetPhysicalDeviceSparseImageFormatProperties vkGetPhysicalDeviceSparseImageFormatProperties; +PFN_vkQueueBindSparse vkQueueBindSparse; +PFN_vkCreateFence vkCreateFence; +PFN_vkDestroyFence vkDestroyFence; +PFN_vkResetFences vkResetFences; +PFN_vkGetFenceStatus vkGetFenceStatus; +PFN_vkWaitForFences vkWaitForFences; +PFN_vkCreateSemaphore vkCreateSemaphore; +PFN_vkDestroySemaphore vkDestroySemaphore; +PFN_vkCreateEvent vkCreateEvent; +PFN_vkDestroyEvent vkDestroyEvent; +PFN_vkGetEventStatus vkGetEventStatus; +PFN_vkSetEvent vkSetEvent; +PFN_vkResetEvent vkResetEvent; +PFN_vkCreateQueryPool vkCreateQueryPool; +PFN_vkDestroyQueryPool vkDestroyQueryPool; +PFN_vkGetQueryPoolResults vkGetQueryPoolResults; +PFN_vkCreateBuffer vkCreateBuffer; +PFN_vkDestroyBuffer vkDestroyBuffer; +PFN_vkCreateBufferView vkCreateBufferView; +PFN_vkDestroyBufferView vkDestroyBufferView; +PFN_vkCreateImage vkCreateImage; +PFN_vkDestroyImage vkDestroyImage; +PFN_vkGetImageSubresourceLayout vkGetImageSubresourceLayout; +PFN_vkCreateImageView vkCreateImageView; +PFN_vkDestroyImageView vkDestroyImageView; +PFN_vkCreateShaderModule vkCreateShaderModule; +PFN_vkDestroyShaderModule vkDestroyShaderModule; +PFN_vkCreatePipelineCache vkCreatePipelineCache; +PFN_vkDestroyPipelineCache vkDestroyPipelineCache; +PFN_vkGetPipelineCacheData vkGetPipelineCacheData; +PFN_vkMergePipelineCaches vkMergePipelineCaches; +PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines; +PFN_vkCreateComputePipelines vkCreateComputePipelines; +PFN_vkDestroyPipeline vkDestroyPipeline; +PFN_vkCreatePipelineLayout vkCreatePipelineLayout; +PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout; +PFN_vkCreateSampler vkCreateSampler; +PFN_vkDestroySampler vkDestroySampler; +PFN_vkCreateDescriptorSetLayout vkCreateDescriptorSetLayout; +PFN_vkDestroyDescriptorSetLayout vkDestroyDescriptorSetLayout; +PFN_vkCreateDescriptorPool vkCreateDescriptorPool; +PFN_vkDestroyDescriptorPool vkDestroyDescriptorPool; +PFN_vkResetDescriptorPool vkResetDescriptorPool; +PFN_vkAllocateDescriptorSets vkAllocateDescriptorSets; +PFN_vkFreeDescriptorSets vkFreeDescriptorSets; +PFN_vkUpdateDescriptorSets vkUpdateDescriptorSets; +PFN_vkCreateFramebuffer vkCreateFramebuffer; +PFN_vkDestroyFramebuffer vkDestroyFramebuffer; +PFN_vkCreateRenderPass vkCreateRenderPass; +PFN_vkDestroyRenderPass vkDestroyRenderPass; +PFN_vkGetRenderAreaGranularity vkGetRenderAreaGranularity; +PFN_vkCreateCommandPool vkCreateCommandPool; +PFN_vkDestroyCommandPool vkDestroyCommandPool; +PFN_vkResetCommandPool vkResetCommandPool; +PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers; +PFN_vkFreeCommandBuffers vkFreeCommandBuffers; +PFN_vkBeginCommandBuffer vkBeginCommandBuffer; +PFN_vkEndCommandBuffer vkEndCommandBuffer; +PFN_vkResetCommandBuffer vkResetCommandBuffer; +PFN_vkCmdBindPipeline vkCmdBindPipeline; +PFN_vkCmdSetViewport vkCmdSetViewport; +PFN_vkCmdSetScissor vkCmdSetScissor; +PFN_vkCmdSetLineWidth vkCmdSetLineWidth; +PFN_vkCmdSetDepthBias vkCmdSetDepthBias; +PFN_vkCmdSetBlendConstants vkCmdSetBlendConstants; +PFN_vkCmdSetDepthBounds vkCmdSetDepthBounds; +PFN_vkCmdSetStencilCompareMask vkCmdSetStencilCompareMask; +PFN_vkCmdSetStencilWriteMask vkCmdSetStencilWriteMask; +PFN_vkCmdSetStencilReference vkCmdSetStencilReference; +PFN_vkCmdBindDescriptorSets vkCmdBindDescriptorSets; +PFN_vkCmdBindIndexBuffer vkCmdBindIndexBuffer; +PFN_vkCmdBindVertexBuffers vkCmdBindVertexBuffers; +PFN_vkCmdDraw vkCmdDraw; +PFN_vkCmdDrawIndexed vkCmdDrawIndexed; +PFN_vkCmdDrawIndirect vkCmdDrawIndirect; +PFN_vkCmdDrawIndexedIndirect vkCmdDrawIndexedIndirect; +PFN_vkCmdDispatch vkCmdDispatch; +PFN_vkCmdDispatchIndirect vkCmdDispatchIndirect; +PFN_vkCmdCopyBuffer vkCmdCopyBuffer; +PFN_vkCmdCopyImage vkCmdCopyImage; +PFN_vkCmdBlitImage vkCmdBlitImage; +PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage; +PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer; +PFN_vkCmdUpdateBuffer vkCmdUpdateBuffer; +PFN_vkCmdFillBuffer vkCmdFillBuffer; +PFN_vkCmdClearColorImage vkCmdClearColorImage; +PFN_vkCmdClearDepthStencilImage vkCmdClearDepthStencilImage; +PFN_vkCmdClearAttachments vkCmdClearAttachments; +PFN_vkCmdResolveImage vkCmdResolveImage; +PFN_vkCmdSetEvent vkCmdSetEvent; +PFN_vkCmdResetEvent vkCmdResetEvent; +PFN_vkCmdWaitEvents vkCmdWaitEvents; +PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier; +PFN_vkCmdBeginQuery vkCmdBeginQuery; +PFN_vkCmdEndQuery vkCmdEndQuery; +PFN_vkCmdResetQueryPool vkCmdResetQueryPool; +PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp; +PFN_vkCmdCopyQueryPoolResults vkCmdCopyQueryPoolResults; +PFN_vkCmdPushConstants vkCmdPushConstants; +PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass; +PFN_vkCmdNextSubpass vkCmdNextSubpass; +PFN_vkCmdEndRenderPass vkCmdEndRenderPass; +PFN_vkCmdExecuteCommands vkCmdExecuteCommands; +PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR; +PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR; +PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vkGetPhysicalDeviceSurfaceCapabilitiesKHR; +PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR; +PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR; +PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR; +PFN_vkDestroySwapchainKHR vkDestroySwapchainKHR; +PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR; +PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR; +PFN_vkQueuePresentKHR vkQueuePresentKHR; +PFN_vkGetPhysicalDeviceDisplayPropertiesKHR vkGetPhysicalDeviceDisplayPropertiesKHR; +PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR vkGetPhysicalDeviceDisplayPlanePropertiesKHR; +PFN_vkGetDisplayPlaneSupportedDisplaysKHR vkGetDisplayPlaneSupportedDisplaysKHR; +PFN_vkGetDisplayModePropertiesKHR vkGetDisplayModePropertiesKHR; +PFN_vkCreateDisplayModeKHR vkCreateDisplayModeKHR; +PFN_vkGetDisplayPlaneCapabilitiesKHR vkGetDisplayPlaneCapabilitiesKHR; +PFN_vkCreateDisplayPlaneSurfaceKHR vkCreateDisplayPlaneSurfaceKHR; +PFN_vkCreateSharedSwapchainsKHR vkCreateSharedSwapchainsKHR; + +#ifdef VK_USE_PLATFORM_XLIB_KHR +PFN_vkCreateXlibSurfaceKHR vkCreateXlibSurfaceKHR; +PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR vkGetPhysicalDeviceXlibPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_XCB_KHR +PFN_vkCreateXcbSurfaceKHR vkCreateXcbSurfaceKHR; +PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR vkGetPhysicalDeviceXcbPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_WAYLAND_KHR +PFN_vkCreateWaylandSurfaceKHR vkCreateWaylandSurfaceKHR; +PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR vkGetPhysicalDeviceWaylandPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_MIR_KHR +PFN_vkCreateMirSurfaceKHR vkCreateMirSurfaceKHR; +PFN_vkGetPhysicalDeviceMirPresentationSupportKHR vkGetPhysicalDeviceMirPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_ANDROID_KHR +PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR; +#endif + +#ifdef VK_USE_PLATFORM_WIN32_KHR +PFN_vkCreateWin32SurfaceKHR vkCreateWin32SurfaceKHR; +PFN_vkGetPhysicalDeviceWin32PresentationSupportKHR vkGetPhysicalDeviceWin32PresentationSupportKHR; +#endif +PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT; +PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT; +PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT; + diff --git a/src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.h b/src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.h new file mode 100644 index 00000000..5d34c0c8 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/vulkan_wrapper.h @@ -0,0 +1,236 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file is generated. +#ifndef VULKAN_WRAPPER_H +#define VULKAN_WRAPPER_H + +#define VK_NO_PROTOTYPES 1 +#include + +/* Initialize the Vulkan function pointer variables declared in this header. + * Returns 0 if vulkan is not available, non-zero if it is available. + */ +int InitVulkan(void); + +// VK_core +extern PFN_vkCreateInstance vkCreateInstance; +extern PFN_vkDestroyInstance vkDestroyInstance; +extern PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices; +extern PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures; +extern PFN_vkGetPhysicalDeviceFormatProperties vkGetPhysicalDeviceFormatProperties; +extern PFN_vkGetPhysicalDeviceImageFormatProperties vkGetPhysicalDeviceImageFormatProperties; +extern PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties; +extern PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties; +extern PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties; +extern PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr; +extern PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr; +extern PFN_vkCreateDevice vkCreateDevice; +extern PFN_vkDestroyDevice vkDestroyDevice; +extern PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties; +extern PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties; +extern PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties; +extern PFN_vkEnumerateDeviceLayerProperties vkEnumerateDeviceLayerProperties; +extern PFN_vkGetDeviceQueue vkGetDeviceQueue; +extern PFN_vkQueueSubmit vkQueueSubmit; +extern PFN_vkQueueWaitIdle vkQueueWaitIdle; +extern PFN_vkDeviceWaitIdle vkDeviceWaitIdle; +extern PFN_vkAllocateMemory vkAllocateMemory; +extern PFN_vkFreeMemory vkFreeMemory; +extern PFN_vkMapMemory vkMapMemory; +extern PFN_vkUnmapMemory vkUnmapMemory; +extern PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges; +extern PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges; +extern PFN_vkGetDeviceMemoryCommitment vkGetDeviceMemoryCommitment; +extern PFN_vkBindBufferMemory vkBindBufferMemory; +extern PFN_vkBindImageMemory vkBindImageMemory; +extern PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements; +extern PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements; +extern PFN_vkGetImageSparseMemoryRequirements vkGetImageSparseMemoryRequirements; +extern PFN_vkGetPhysicalDeviceSparseImageFormatProperties vkGetPhysicalDeviceSparseImageFormatProperties; +extern PFN_vkQueueBindSparse vkQueueBindSparse; +extern PFN_vkCreateFence vkCreateFence; +extern PFN_vkDestroyFence vkDestroyFence; +extern PFN_vkResetFences vkResetFences; +extern PFN_vkGetFenceStatus vkGetFenceStatus; +extern PFN_vkWaitForFences vkWaitForFences; +extern PFN_vkCreateSemaphore vkCreateSemaphore; +extern PFN_vkDestroySemaphore vkDestroySemaphore; +extern PFN_vkCreateEvent vkCreateEvent; +extern PFN_vkDestroyEvent vkDestroyEvent; +extern PFN_vkGetEventStatus vkGetEventStatus; +extern PFN_vkSetEvent vkSetEvent; +extern PFN_vkResetEvent vkResetEvent; +extern PFN_vkCreateQueryPool vkCreateQueryPool; +extern PFN_vkDestroyQueryPool vkDestroyQueryPool; +extern PFN_vkGetQueryPoolResults vkGetQueryPoolResults; +extern PFN_vkCreateBuffer vkCreateBuffer; +extern PFN_vkDestroyBuffer vkDestroyBuffer; +extern PFN_vkCreateBufferView vkCreateBufferView; +extern PFN_vkDestroyBufferView vkDestroyBufferView; +extern PFN_vkCreateImage vkCreateImage; +extern PFN_vkDestroyImage vkDestroyImage; +extern PFN_vkGetImageSubresourceLayout vkGetImageSubresourceLayout; +extern PFN_vkCreateImageView vkCreateImageView; +extern PFN_vkDestroyImageView vkDestroyImageView; +extern PFN_vkCreateShaderModule vkCreateShaderModule; +extern PFN_vkDestroyShaderModule vkDestroyShaderModule; +extern PFN_vkCreatePipelineCache vkCreatePipelineCache; +extern PFN_vkDestroyPipelineCache vkDestroyPipelineCache; +extern PFN_vkGetPipelineCacheData vkGetPipelineCacheData; +extern PFN_vkMergePipelineCaches vkMergePipelineCaches; +extern PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines; +extern PFN_vkCreateComputePipelines vkCreateComputePipelines; +extern PFN_vkDestroyPipeline vkDestroyPipeline; +extern PFN_vkCreatePipelineLayout vkCreatePipelineLayout; +extern PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout; +extern PFN_vkCreateSampler vkCreateSampler; +extern PFN_vkDestroySampler vkDestroySampler; +extern PFN_vkCreateDescriptorSetLayout vkCreateDescriptorSetLayout; +extern PFN_vkDestroyDescriptorSetLayout vkDestroyDescriptorSetLayout; +extern PFN_vkCreateDescriptorPool vkCreateDescriptorPool; +extern PFN_vkDestroyDescriptorPool vkDestroyDescriptorPool; +extern PFN_vkResetDescriptorPool vkResetDescriptorPool; +extern PFN_vkAllocateDescriptorSets vkAllocateDescriptorSets; +extern PFN_vkFreeDescriptorSets vkFreeDescriptorSets; +extern PFN_vkUpdateDescriptorSets vkUpdateDescriptorSets; +extern PFN_vkCreateFramebuffer vkCreateFramebuffer; +extern PFN_vkDestroyFramebuffer vkDestroyFramebuffer; +extern PFN_vkCreateRenderPass vkCreateRenderPass; +extern PFN_vkDestroyRenderPass vkDestroyRenderPass; +extern PFN_vkGetRenderAreaGranularity vkGetRenderAreaGranularity; +extern PFN_vkCreateCommandPool vkCreateCommandPool; +extern PFN_vkDestroyCommandPool vkDestroyCommandPool; +extern PFN_vkResetCommandPool vkResetCommandPool; +extern PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers; +extern PFN_vkFreeCommandBuffers vkFreeCommandBuffers; +extern PFN_vkBeginCommandBuffer vkBeginCommandBuffer; +extern PFN_vkEndCommandBuffer vkEndCommandBuffer; +extern PFN_vkResetCommandBuffer vkResetCommandBuffer; +extern PFN_vkCmdBindPipeline vkCmdBindPipeline; +extern PFN_vkCmdSetViewport vkCmdSetViewport; +extern PFN_vkCmdSetScissor vkCmdSetScissor; +extern PFN_vkCmdSetLineWidth vkCmdSetLineWidth; +extern PFN_vkCmdSetDepthBias vkCmdSetDepthBias; +extern PFN_vkCmdSetBlendConstants vkCmdSetBlendConstants; +extern PFN_vkCmdSetDepthBounds vkCmdSetDepthBounds; +extern PFN_vkCmdSetStencilCompareMask vkCmdSetStencilCompareMask; +extern PFN_vkCmdSetStencilWriteMask vkCmdSetStencilWriteMask; +extern PFN_vkCmdSetStencilReference vkCmdSetStencilReference; +extern PFN_vkCmdBindDescriptorSets vkCmdBindDescriptorSets; +extern PFN_vkCmdBindIndexBuffer vkCmdBindIndexBuffer; +extern PFN_vkCmdBindVertexBuffers vkCmdBindVertexBuffers; +extern PFN_vkCmdDraw vkCmdDraw; +extern PFN_vkCmdDrawIndexed vkCmdDrawIndexed; +extern PFN_vkCmdDrawIndirect vkCmdDrawIndirect; +extern PFN_vkCmdDrawIndexedIndirect vkCmdDrawIndexedIndirect; +extern PFN_vkCmdDispatch vkCmdDispatch; +extern PFN_vkCmdDispatchIndirect vkCmdDispatchIndirect; +extern PFN_vkCmdCopyBuffer vkCmdCopyBuffer; +extern PFN_vkCmdCopyImage vkCmdCopyImage; +extern PFN_vkCmdBlitImage vkCmdBlitImage; +extern PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage; +extern PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer; +extern PFN_vkCmdUpdateBuffer vkCmdUpdateBuffer; +extern PFN_vkCmdFillBuffer vkCmdFillBuffer; +extern PFN_vkCmdClearColorImage vkCmdClearColorImage; +extern PFN_vkCmdClearDepthStencilImage vkCmdClearDepthStencilImage; +extern PFN_vkCmdClearAttachments vkCmdClearAttachments; +extern PFN_vkCmdResolveImage vkCmdResolveImage; +extern PFN_vkCmdSetEvent vkCmdSetEvent; +extern PFN_vkCmdResetEvent vkCmdResetEvent; +extern PFN_vkCmdWaitEvents vkCmdWaitEvents; +extern PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier; +extern PFN_vkCmdBeginQuery vkCmdBeginQuery; +extern PFN_vkCmdEndQuery vkCmdEndQuery; +extern PFN_vkCmdResetQueryPool vkCmdResetQueryPool; +extern PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp; +extern PFN_vkCmdCopyQueryPoolResults vkCmdCopyQueryPoolResults; +extern PFN_vkCmdPushConstants vkCmdPushConstants; +extern PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass; +extern PFN_vkCmdNextSubpass vkCmdNextSubpass; +extern PFN_vkCmdEndRenderPass vkCmdEndRenderPass; +extern PFN_vkCmdExecuteCommands vkCmdExecuteCommands; + +// VK_KHR_surface +extern PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR; +extern PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR; +extern PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vkGetPhysicalDeviceSurfaceCapabilitiesKHR; +extern PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR; +extern PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR; + +// VK_KHR_swapchain +extern PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR; +extern PFN_vkDestroySwapchainKHR vkDestroySwapchainKHR; +extern PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR; +extern PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR; +extern PFN_vkQueuePresentKHR vkQueuePresentKHR; + +// VK_KHR_display +extern PFN_vkGetPhysicalDeviceDisplayPropertiesKHR vkGetPhysicalDeviceDisplayPropertiesKHR; +extern PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR vkGetPhysicalDeviceDisplayPlanePropertiesKHR; +extern PFN_vkGetDisplayPlaneSupportedDisplaysKHR vkGetDisplayPlaneSupportedDisplaysKHR; +extern PFN_vkGetDisplayModePropertiesKHR vkGetDisplayModePropertiesKHR; +extern PFN_vkCreateDisplayModeKHR vkCreateDisplayModeKHR; +extern PFN_vkGetDisplayPlaneCapabilitiesKHR vkGetDisplayPlaneCapabilitiesKHR; +extern PFN_vkCreateDisplayPlaneSurfaceKHR vkCreateDisplayPlaneSurfaceKHR; + +// VK_KHR_display_swapchain +extern PFN_vkCreateSharedSwapchainsKHR vkCreateSharedSwapchainsKHR; + +#ifdef VK_USE_PLATFORM_XLIB_KHR +// VK_KHR_xlib_surface +extern PFN_vkCreateXlibSurfaceKHR vkCreateXlibSurfaceKHR; +extern PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR vkGetPhysicalDeviceXlibPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_XCB_KHR +// VK_KHR_xcb_surface +extern PFN_vkCreateXcbSurfaceKHR vkCreateXcbSurfaceKHR; +extern PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR vkGetPhysicalDeviceXcbPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_WAYLAND_KHR +// VK_KHR_wayland_surface +extern PFN_vkCreateWaylandSurfaceKHR vkCreateWaylandSurfaceKHR; +extern PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR vkGetPhysicalDeviceWaylandPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_MIR_KHR +// VK_KHR_mir_surface +extern PFN_vkCreateMirSurfaceKHR vkCreateMirSurfaceKHR; +extern PFN_vkGetPhysicalDeviceMirPresentationSupportKHR vkGetPhysicalDeviceMirPresentationSupportKHR; +#endif + +#ifdef VK_USE_PLATFORM_ANDROID_KHR +// VK_KHR_android_surface +extern PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR; +#endif + +#ifdef VK_USE_PLATFORM_WIN32_KHR +// VK_KHR_win32_surface +extern PFN_vkCreateWin32SurfaceKHR vkCreateWin32SurfaceKHR; +extern PFN_vkGetPhysicalDeviceWin32PresentationSupportKHR vkGetPhysicalDeviceWin32PresentationSupportKHR; +#endif + +#ifdef USE_DEBUG_EXTENTIONS +#include +// VK_EXT_debug_report +extern PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT; +extern PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT; +extern PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT; +#endif + + +#endif // VULKAN_WRAPPER_H diff --git a/src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png b/src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..e27d7167c2099994cb0e6e022d04ba5e46feec66 GIT binary patch literal 12207 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 T{;2};4shJ?v_Zy^3%C9U6p*$} literal 0 HcmV?d00001 diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BackendThreading.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BackendThreading.kt new file mode 100644 index 00000000..f3dd5ed9 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BackendThreading.kt @@ -0,0 +1,7 @@ +package org.ryujinx.android + +enum class BackendThreading { + Auto, + Off, + On +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt new file mode 100644 index 00000000..50e35ff2 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt @@ -0,0 +1,9 @@ +package org.ryujinx.android + +import androidx.activity.ComponentActivity + +abstract class BaseActivity : ComponentActivity() { + companion object { + val crashHandler = CrashHandler() + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt new file mode 100644 index 00000000..be00d508 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt @@ -0,0 +1,15 @@ +package org.ryujinx.android + +import java.io.File +import java.lang.Thread.UncaughtExceptionHandler + +class CrashHandler : UncaughtExceptionHandler { + var crashLog: String = "" + override fun uncaughtException(t: Thread, e: Throwable) { + crashLog += e.toString() + "\n" + + File(MainActivity.AppPath + "${File.separator}Logs${File.separator}crash.log").writeText( + crashLog + ) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt new file mode 100644 index 00000000..a325ec8c --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -0,0 +1,435 @@ +package org.ryujinx.android + +import android.app.Activity +import android.content.Context +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.swordfish.radialgamepad.library.RadialGamePad +import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig +import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.PrimaryDialConfig +import com.swordfish.radialgamepad.library.config.RadialGamePadConfig +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings + +typealias GamePad = RadialGamePad +typealias GamePadConfig = RadialGamePadConfig + +class GameController(var activity: Activity) { + + companion object { + private fun Create(context: Context, controller: GameController): View { + val inflator = LayoutInflater.from(context) + val view = inflator.inflate(R.layout.game_layout, null) + view.findViewById(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(R.id.rightcontainer)!!.addView(controller.rightGamePad) + + return view + } + + @Composable + fun Compose(viewModel: MainViewModel): Unit { + AndroidView( + modifier = Modifier.fillMaxSize(), factory = { context -> + val controller = GameController(viewModel.activity) + val c = Create(context, controller) + viewModel.activity.lifecycleScope.apply { + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) + .shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily) + events.safeCollect { + controller.handleEvent(it) + } + } + } + controller.controllerView = c + viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) + c + }) + } + } + + private var controllerView: View? = null + var leftGamePad: GamePad + var rightGamePad: GamePad + var controllerId: Int = -1 + val isVisible: Boolean + get() { + controllerView?.apply { + return this.isVisible + } + + return false + } + + init { + leftGamePad = GamePad(generateConfig(true), 16f, activity) + rightGamePad = GamePad(generateConfig(false), 16f, activity) + + leftGamePad.primaryDialMaxSizeDp = 200f + rightGamePad.primaryDialMaxSizeDp = 200f + + leftGamePad.gravityX = -1f + leftGamePad.gravityY = 1f + rightGamePad.gravityX = 1f + rightGamePad.gravityY = 1f + } + + fun setVisible(isVisible: Boolean) { + controllerView?.apply { + this.isVisible = isVisible + + if (isVisible) + connect() + } + } + + fun connect() { + if (controllerId == -1) + controllerId = RyujinxNative.jnaInstance.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if (controllerId == -1) + controllerId = RyujinxNative.jnaInstance.inputConnectGamepad(0) + + controllerId.apply { + when (ev) { + is Event.Button -> { + val action = ev.action + when (action) { + KeyEvent.ACTION_UP -> { + RyujinxNative.jnaInstance.inputSetButtonReleased(ev.id, this) + } + + KeyEvent.ACTION_DOWN -> { + RyujinxNative.jnaInstance.inputSetButtonPressed(ev.id, this) + } + } + } + + is Event.Direction -> { + val direction = ev.id + + when (direction) { + GamePadButtonInputId.DpadUp.ordinal -> { + if (ev.xAxis > 0) { + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadRight.ordinal, + this + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadLeft.ordinal, + this + ) + } else if (ev.xAxis < 0) { + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadLeft.ordinal, + this + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadRight.ordinal, + this + ) + } else { + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadLeft.ordinal, + this + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadRight.ordinal, + this + ) + } + if (ev.yAxis < 0) { + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadUp.ordinal, + this + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadDown.ordinal, + this + ) + } else if (ev.yAxis > 0) { + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadDown.ordinal, + this + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadUp.ordinal, + this + ) + } else { + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadDown.ordinal, + this + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadUp.ordinal, + this + ) + } + } + + GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + RyujinxNative.jnaInstance.inputSetStickAxis( + 1, + x, + -y, + this + ) + } + + GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + RyujinxNative.jnaInstance.inputSetStickAxis( + 2, + x, + -y, + this + ) + } + } + } + } + } + } +} + +suspend fun Flow.safeCollect( + block: suspend (T) -> Unit +) { + this.catch {} + .collect { + block(it) + } +} + +private fun generateConfig(isLeft: Boolean): GamePadConfig { + val distance = 0.3f + val buttonScale = 1f + + if (isLeft) { + return GamePadConfig( + 12, + PrimaryDialConfig.Stick( + GamePadButtonInputId.LeftStick.ordinal, + GamePadButtonInputId.LeftStickButton.ordinal, + setOf(), + "LeftStick", + null + ), + listOf( + SecondaryDialConfig.Cross( + 10, + 3, + 2.5f, + distance, + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 1, + buttonScale, + distance, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.DoubleButton( + 2, + distance, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 9, + buttonScale, + distance, + ButtonConfig( + GamePadButtonInputId.LeftTrigger.ordinal, + "ZL", + true, + null, + "LeftTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } else { + return GamePadConfig( + 12, + PrimaryDialConfig.PrimaryButtons( + listOf( + ButtonConfig( + GamePadButtonInputId.A.ordinal, + "A", + true, + null, + "A", + setOf(), + true, + null + ), + ButtonConfig( + GamePadButtonInputId.X.ordinal, + "X", + true, + null, + "X", + setOf(), + true, + null + ), + ButtonConfig( + GamePadButtonInputId.Y.ordinal, + "Y", + true, + null, + "Y", + setOf(), + true, + null + ), + ButtonConfig( + GamePadButtonInputId.B.ordinal, + "B", + true, + null, + "B", + setOf(), + true, + null + ) + ), + null, + 0f, + true, + null + ), + listOf( + SecondaryDialConfig.Stick( + 7, + 2, + 2f, + distance, + GamePadButtonInputId.RightStick.ordinal, + GamePadButtonInputId.RightStickButton.ordinal, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 6, + buttonScale, + distance, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.DoubleButton( + 3, + distance, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 9, + buttonScale, + distance, + ButtonConfig( + GamePadButtonInputId.RightTrigger.ordinal, + "ZR", + true, + null, + "RightTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ) + ) + ) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt new file mode 100644 index 00000000..6fed55e8 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -0,0 +1,179 @@ +package org.ryujinx.android + +import android.annotation.SuppressLint +import android.content.Context +import android.view.SurfaceHolder +import android.view.SurfaceView +import androidx.compose.runtime.MutableState +import org.ryujinx.android.viewmodels.GameModel +import org.ryujinx.android.viewmodels.MainViewModel +import kotlin.concurrent.thread + +@SuppressLint("ViewConstructor") +class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), + SurfaceHolder.Callback { + private var _currentWindow: Long = -1 + private var isProgressHidden: Boolean = false + private var progress: MutableState? = null + private var progressValue: MutableState? = null + private var showLoading: MutableState? = null + private var game: GameModel? = null + private var _isClosed: Boolean = false + private var _renderingThreadWatcher: Thread? = null + private var _height: Int = 0 + private var _width: Int = 0 + private var _updateThread: Thread? = null + private var _guestThread: Thread? = null + private var _isInit: Boolean = false + private var _isStarted: Boolean = false + private val _nativeWindow: NativeWindow + + val currentSurface:Long + get() { + return _currentWindow + } + + val currentWindowhandle: Long + get() { + return _nativeWindow.nativePointer + } + + init { + holder.addCallback(this) + + _nativeWindow = NativeWindow(this) + + mainViewModel.gameHost = this + } + + override fun surfaceCreated(holder: SurfaceHolder) { + } + + fun setProgress(info : String, progressVal: Float) { + showLoading?.apply { + progressValue?.apply { + this.value = progressVal + } + + progress?.apply { + this.value = info + } + } + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + if (_isClosed) + return + + if (_width != width || _height != height) { + _currentWindow = _nativeWindow.requeryWindowHandle() + + _nativeWindow.swapInterval = 0 + } + + _width = width + _height = height + + start(holder) + + RyujinxNative.jnaInstance.graphicsRendererSetSize( + width, + height + ) + + if (_isStarted) { + RyujinxNative.jnaInstance.inputSetClientSize(width, height) + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + + } + + fun close() { + _isClosed = true + _isInit = false + _isStarted = false + + RyujinxNative.jnaInstance.uiHandlerSetResponse(false, "") + + _updateThread?.join() + _renderingThreadWatcher?.join() + } + + private fun start(surfaceHolder: SurfaceHolder) { + if (_isStarted) + return + + _isStarted = true + + game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel + + RyujinxNative.jnaInstance.inputInitialize(width, height) + + val id = mainViewModel.physicalControllerManager?.connect() + mainViewModel.motionSensorManager?.setControllerId(id ?: -1) + + RyujinxNative.jnaInstance.graphicsRendererSetSize( + surfaceHolder.surfaceFrame.width(), + surfaceHolder.surfaceFrame.height() + ) + + NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3) + + _guestThread = thread(start = true) { + runGame() + } + + _updateThread = thread(start = true) { + var c = 0 + val helper = NativeHelpers.instance + while (_isStarted) { + RyujinxNative.jnaInstance.inputUpdate() + Thread.sleep(1) + c++ + if (c >= 1000) { + if (progressValue?.value == -1f) + progress?.apply { + this.value = + "Loading ${if (mainViewModel.isMiiEditorLaunched) "Mii Editor" else game!!.titleName}" + } + c = 0 + mainViewModel.updateStats( + RyujinxNative.jnaInstance.deviceGetGameFifo(), + RyujinxNative.jnaInstance.deviceGetGameFrameRate(), + RyujinxNative.jnaInstance.deviceGetGameFrameTime() + ) + } + } + } + } + + private fun runGame() { + RyujinxNative.jnaInstance.graphicsRendererRunLoop() + + game?.close() + } + + fun setProgressStates( + showLoading: MutableState?, + progressValue: MutableState?, + progress: MutableState? + ) { + this.showLoading = showLoading + this.progressValue = progressValue + this.progress = progress + + showLoading?.apply { + showLoading.value = !isProgressHidden + } + } + + fun hideProgressIndicator() { + isProgressHidden = true + showLoading?.apply { + if (value == isProgressHidden) + value = !isProgressHidden + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GamePadButtonInputId.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GamePadButtonInputId.kt new file mode 100644 index 00000000..797be757 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GamePadButtonInputId.kt @@ -0,0 +1,27 @@ +package org.ryujinx.android + +enum class GamePadButtonInputId { + None, + + // Buttons + A, + B, + X, + Y, + LeftStickButton, + RightStickButton, + LeftShoulder, + RightShoulder, + LeftTrigger, + RightTrigger, + DpadUp, + DpadDown, + DpadLeft, + DpadRight, + Minus, + Plus, + + // Stick + LeftStick, + RightStick +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt new file mode 100644 index 00000000..990cea89 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -0,0 +1,223 @@ +package org.ryujinx.android + +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import androidx.compose.runtime.MutableState +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.copyFileTo +import com.anggrayudi.storage.file.openInputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.lingala.zip4j.io.inputstream.ZipInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream + +class Helpers { + companion object { + fun getPath(context: Context, uri: Uri): String? { + + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } + + } else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).toTypedArray() + val type = split[0] + var contentUri: Uri? = null + when (type) { + "image" -> { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + "video" -> { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + + "audio" -> { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { + return getDataColumn(context, uri, null, null) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + fun copyToData( + file: DocumentFile, path: String, storageHelper: SimpleStorageHelper, + isCopying: MutableState, + copyProgress: MutableState, + currentProgressName: MutableState, + finish: () -> Unit + ) { + var fPath = path + "/${file.name}" + var callback: FileCallback? = object : FileCallback() { + override fun onFailed(errorCode: ErrorCode) { + super.onFailed(errorCode) + File(fPath).delete() + finish() + } + + override fun onStart(file: Any, workerThread: Thread): Long { + copyProgress.value = 0f + + (file as DocumentFile).apply { + currentProgressName.value = "Copying ${file.name}" + } + return super.onStart(file, workerThread) + } + + override fun onReport(report: Report) { + super.onReport(report) + + if (!isCopying.value) { + Thread.currentThread().interrupt() + } + + copyProgress.value = report.progress / 100f + } + + override fun onCompleted(result: Any) { + super.onCompleted(result) + isCopying.value = false + finish() + } + } + val ioScope = CoroutineScope(Dispatchers.IO) + isCopying.value = true + File(fPath).delete() + file.apply { + val f = this + ioScope.launch { + f.copyFileTo( + storageHelper.storage.context, + File(path), + callback = callback!! + ) + + } + } + } + + private fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array? + ): String? { + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + try { + cursor = uri?.let { + context.contentResolver.query( + it, + projection, + selection, + selectionArgs, + null + ) + } + if (cursor != null && cursor.moveToFirst()) { + val column_index: Int = cursor.getColumnIndexOrThrow(column) + return cursor.getString(column_index) + } + } finally { + cursor?.close() + } + return null + } + + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + fun importAppData( + file: DocumentFile, + isImporting: MutableState + ) { + isImporting.value = true + try { + MainActivity.StorageHelper?.apply { + val stream = file.openInputStream(storage.context) + stream?.apply { + val folders = listOf("bis", "games", "profiles", "system") + for (f in folders) { + val dir = File(MainActivity.AppPath + "${File.separator}${f}") + if (dir.exists()) { + dir.deleteRecursively() + } + + dir.mkdirs() + } + ZipInputStream(stream).use { zip -> + while (true) { + val header = zip.nextEntry ?: break + if (!folders.any { header.fileName.startsWith(it) }) { + continue + } + val filePath = + MainActivity.AppPath + File.separator + header.fileName + + if (!header.isDirectory) { + val bos = BufferedOutputStream(FileOutputStream(filePath)) + val bytesIn = ByteArray(4096) + var read: Int = 0 + while (zip.read(bytesIn).also { read = it } > 0) { + bos.write(bytesIn, 0, read) + } + bos.close() + } else { + val dir = File(filePath) + dir.mkdir() + } + } + } + stream.close() + } + } + } finally { + isImporting.value = false + RyujinxNative.jnaInstance.deviceReloadFilesystem() + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt new file mode 100644 index 00000000..6b0e27a8 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -0,0 +1,826 @@ +package org.ryujinx.android + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import compose.icons.CssGgIcons +import compose.icons.cssggicons.Games + +class Icons { + companion object { + /// Icons exported from https://www.composables.com/icons + @Composable + fun circle(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "circle", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(20f, 36.375f) + quadToRelative(-3.375f, 0f, -6.375f, -1.292f) + quadToRelative(-3f, -1.291f, -5.208f, -3.521f) + quadToRelative(-2.209f, -2.229f, -3.5f, -5.208f) + quadTo(3.625f, 23.375f, 3.625f, 20f) + quadToRelative(0f, -3.417f, 1.292f, -6.396f) + quadToRelative(1.291f, -2.979f, 3.521f, -5.208f) + quadToRelative(2.229f, -2.229f, 5.208f, -3.5f) + reflectiveQuadTo(20f, 3.625f) + quadToRelative(3.417f, 0f, 6.396f, 1.292f) + quadToRelative(2.979f, 1.291f, 5.208f, 3.5f) + quadToRelative(2.229f, 2.208f, 3.5f, 5.187f) + reflectiveQuadTo(36.375f, 20f) + quadToRelative(0f, 3.375f, -1.292f, 6.375f) + quadToRelative(-1.291f, 3f, -3.5f, 5.208f) + quadToRelative(-2.208f, 2.209f, -5.187f, 3.5f) + quadToRelative(-2.979f, 1.292f, -6.396f, 1.292f) + close() + moveToRelative(0f, -2.625f) + quadToRelative(5.75f, 0f, 9.75f, -4.021f) + reflectiveQuadToRelative(4f, -9.729f) + quadToRelative(0f, -5.75f, -4f, -9.75f) + reflectiveQuadToRelative(-9.75f, -4f) + quadToRelative(-5.708f, 0f, -9.729f, 4f) + quadToRelative(-4.021f, 4f, -4.021f, 9.75f) + quadToRelative(0f, 5.708f, 4.021f, 9.729f) + quadTo(14.292f, 33.75f, 20f, 33.75f) + close() + moveTo(20f, 20f) + close() + } + }.build() + } + } + @Composable + fun listView(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "list", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(13.375f, 14.458f) + quadToRelative(-0.583f, 0f, -0.958f, -0.395f) + quadToRelative(-0.375f, -0.396f, -0.375f, -0.938f) + quadToRelative(0f, -0.542f, 0.375f, -0.937f) + quadToRelative(0.375f, -0.396f, 0.958f, -0.396f) + horizontalLineToRelative(20.083f) + quadToRelative(0.584f, 0f, 0.959f, 0.396f) + quadToRelative(0.375f, 0.395f, 0.375f, 0.937f) + reflectiveQuadToRelative(-0.375f, 0.938f) + quadToRelative(-0.375f, 0.395f, -0.959f, 0.395f) + close() + moveToRelative(0f, 6.834f) + quadToRelative(-0.583f, 0f, -0.958f, -0.375f) + reflectiveQuadTo(12.042f, 20f) + quadToRelative(0f, -0.583f, 0.375f, -0.958f) + reflectiveQuadToRelative(0.958f, -0.375f) + horizontalLineToRelative(20.083f) + quadToRelative(0.584f, 0f, 0.959f, 0.395f) + quadToRelative(0.375f, 0.396f, 0.375f, 0.938f) + quadToRelative(0f, 0.542f, -0.375f, 0.917f) + reflectiveQuadToRelative(-0.959f, 0.375f) + close() + moveToRelative(0f, 6.916f) + quadToRelative(-0.583f, 0f, -0.958f, -0.396f) + quadToRelative(-0.375f, -0.395f, -0.375f, -0.937f) + reflectiveQuadToRelative(0.375f, -0.937f) + quadToRelative(0.375f, -0.396f, 0.958f, -0.396f) + horizontalLineToRelative(20.083f) + quadToRelative(0.584f, 0f, 0.959f, 0.396f) + quadToRelative(0.375f, 0.395f, 0.375f, 0.937f) + reflectiveQuadToRelative(-0.375f, 0.937f) + quadToRelative(-0.375f, 0.396f, -0.959f, 0.396f) + close() + moveToRelative(-6.833f, -13.75f) + quadToRelative(-0.584f, 0f, -0.959f, -0.395f) + quadToRelative(-0.375f, -0.396f, -0.375f, -0.938f) + quadToRelative(0f, -0.583f, 0.375f, -0.958f) + reflectiveQuadToRelative(0.959f, -0.375f) + quadToRelative(0.583f, 0f, 0.958f, 0.375f) + reflectiveQuadToRelative(0.375f, 0.958f) + quadToRelative(0f, 0.542f, -0.375f, 0.938f) + quadToRelative(-0.375f, 0.395f, -0.958f, 0.395f) + close() + moveToRelative(0f, 6.875f) + quadToRelative(-0.584f, 0f, -0.959f, -0.375f) + reflectiveQuadTo(5.208f, 20f) + quadToRelative(0f, -0.583f, 0.375f, -0.958f) + reflectiveQuadToRelative(0.959f, -0.375f) + quadToRelative(0.583f, 0f, 0.958f, 0.375f) + reflectiveQuadToRelative(0.375f, 0.958f) + quadToRelative(0f, 0.583f, -0.375f, 0.958f) + reflectiveQuadToRelative(-0.958f, 0.375f) + close() + moveToRelative(0f, 6.875f) + quadToRelative(-0.584f, 0f, -0.959f, -0.375f) + reflectiveQuadToRelative(-0.375f, -0.958f) + quadToRelative(0f, -0.542f, 0.375f, -0.937f) + quadToRelative(0.375f, -0.396f, 0.959f, -0.396f) + quadToRelative(0.583f, 0f, 0.958f, 0.396f) + quadToRelative(0.375f, 0.395f, 0.375f, 0.937f) + quadToRelative(0f, 0.583f, -0.375f, 0.958f) + reflectiveQuadToRelative(-0.958f, 0.375f) + close() + } + }.build() + } + } + + @Composable + fun gridView(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "grid_view", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(7.875f, 18.667f) + quadToRelative(-1.083f, 0f, -1.854f, -0.771f) + quadToRelative(-0.771f, -0.771f, -0.771f, -1.854f) + verticalLineTo(7.875f) + quadToRelative(0f, -1.083f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(8.167f) + quadToRelative(1.083f, 0f, 1.875f, 0.771f) + quadToRelative(0.791f, 0.771f, 0.791f, 1.854f) + verticalLineToRelative(8.167f) + quadToRelative(0f, 1.083f, -0.791f, 1.854f) + quadToRelative(-0.792f, 0.771f, -1.875f, 0.771f) + close() + moveToRelative(0f, 16.083f) + quadToRelative(-1.083f, 0f, -1.854f, -0.771f) + quadToRelative(-0.771f, -0.771f, -0.771f, -1.854f) + verticalLineToRelative(-8.167f) + quadToRelative(0f, -1.083f, 0.771f, -1.875f) + quadToRelative(0.771f, -0.791f, 1.854f, -0.791f) + horizontalLineToRelative(8.167f) + quadToRelative(1.083f, 0f, 1.875f, 0.791f) + quadToRelative(0.791f, 0.792f, 0.791f, 1.875f) + verticalLineToRelative(8.167f) + quadToRelative(0f, 1.083f, -0.791f, 1.854f) + quadToRelative(-0.792f, 0.771f, -1.875f, 0.771f) + close() + moveToRelative(16.083f, -16.083f) + quadToRelative(-1.083f, 0f, -1.854f, -0.771f) + quadToRelative(-0.771f, -0.771f, -0.771f, -1.854f) + verticalLineTo(7.875f) + quadToRelative(0f, -1.083f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(8.167f) + quadToRelative(1.083f, 0f, 1.854f, 0.771f) + quadToRelative(0.771f, 0.771f, 0.771f, 1.854f) + verticalLineToRelative(8.167f) + quadToRelative(0f, 1.083f, -0.771f, 1.854f) + quadToRelative(-0.771f, 0.771f, -1.854f, 0.771f) + close() + moveToRelative(0f, 16.083f) + quadToRelative(-1.083f, 0f, -1.854f, -0.771f) + quadToRelative(-0.771f, -0.771f, -0.771f, -1.854f) + verticalLineToRelative(-8.167f) + quadToRelative(0f, -1.083f, 0.771f, -1.875f) + quadToRelative(0.771f, -0.791f, 1.854f, -0.791f) + horizontalLineToRelative(8.167f) + quadToRelative(1.083f, 0f, 1.854f, 0.791f) + quadToRelative(0.771f, 0.792f, 0.771f, 1.875f) + verticalLineToRelative(8.167f) + quadToRelative(0f, 1.083f, -0.771f, 1.854f) + quadToRelative(-0.771f, 0.771f, -1.854f, 0.771f) + close() + moveTo(7.875f, 16.042f) + horizontalLineToRelative(8.167f) + verticalLineTo(7.875f) + horizontalLineTo(7.875f) + close() + moveToRelative(16.083f, 0f) + horizontalLineToRelative(8.167f) + verticalLineTo(7.875f) + horizontalLineToRelative(-8.167f) + close() + moveToRelative(0f, 16.083f) + horizontalLineToRelative(8.167f) + verticalLineToRelative(-8.167f) + horizontalLineToRelative(-8.167f) + close() + moveToRelative(-16.083f, 0f) + horizontalLineToRelative(8.167f) + verticalLineToRelative(-8.167f) + horizontalLineTo(7.875f) + close() + moveToRelative(16.083f, -16.083f) + close() + moveToRelative(0f, 7.916f) + close() + moveToRelative(-7.916f, 0f) + close() + moveToRelative(0f, -7.916f) + close() + } + }.build() + } + } + + @Composable + fun applets(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "apps", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(9.708f, 33.125f) + quadToRelative(-1.208f, 0f, -2.02f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.02f) + quadToRelative(0f, -1.167f, 0.813f, -2f) + quadToRelative(0.812f, -0.834f, 2.02f, -0.834f) + quadToRelative(1.167f, 0f, 2f, 0.813f) + quadToRelative(0.834f, 0.812f, 0.834f, 2.021f) + quadToRelative(0f, 1.208f, -0.813f, 2.02f) + quadToRelative(-0.812f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -1.979f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.02f) + quadToRelative(0f, -1.167f, 0.813f, -2f) + quadToRelative(0.812f, -0.834f, 1.979f, -0.834f) + reflectiveQuadToRelative(2f, 0.813f) + quadToRelative(0.833f, 0.812f, 0.833f, 2.021f) + quadToRelative(0f, 1.208f, -0.812f, 2.02f) + quadToRelative(-0.813f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -2f, -0.813f) + quadToRelative(-0.834f, -0.812f, -0.834f, -2.02f) + quadToRelative(0f, -1.167f, 0.813f, -2f) + quadToRelative(0.812f, -0.834f, 2.021f, -0.834f) + quadToRelative(1.208f, 0f, 2.02f, 0.813f) + quadToRelative(0.813f, 0.812f, 0.813f, 2.021f) + quadToRelative(0f, 1.208f, -0.813f, 2.02f) + quadToRelative(-0.812f, 0.813f, -2.02f, 0.813f) + close() + moveTo(9.708f, 22.792f) + quadToRelative(-1.208f, 0f, -2.02f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -1.979f) + reflectiveQuadToRelative(0.813f, -2f) + quadToRelative(0.812f, -0.833f, 2.02f, -0.833f) + quadToRelative(1.167f, 0f, 2f, 0.812f) + quadToRelative(0.834f, 0.813f, 0.834f, 2.021f) + quadToRelative(0f, 1.167f, -0.813f, 1.979f) + quadToRelative(-0.812f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -1.979f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -1.979f) + reflectiveQuadToRelative(0.813f, -2f) + quadToRelative(0.812f, -0.833f, 1.979f, -0.833f) + reflectiveQuadToRelative(2f, 0.812f) + quadToRelative(0.833f, 0.813f, 0.833f, 2.021f) + quadToRelative(0f, 1.167f, -0.812f, 1.979f) + quadToRelative(-0.813f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -2f, -0.813f) + quadToRelative(-0.834f, -0.812f, -0.834f, -1.979f) + reflectiveQuadToRelative(0.813f, -2f) + quadToRelative(0.812f, -0.833f, 2.021f, -0.833f) + quadToRelative(1.208f, 0f, 2.02f, 0.812f) + quadToRelative(0.813f, 0.813f, 0.813f, 2.021f) + quadToRelative(0f, 1.167f, -0.813f, 1.979f) + quadToRelative(-0.812f, 0.813f, -2.02f, 0.813f) + close() + moveTo(9.708f, 12.542f) + quadToRelative(-1.208f, 0f, -2.02f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.021f) + quadToRelative(0f, -1.208f, 0.813f, -2.02f) + quadToRelative(0.812f, -0.813f, 2.02f, -0.813f) + quadToRelative(1.167f, 0f, 2f, 0.813f) + quadToRelative(0.834f, 0.812f, 0.834f, 2.02f) + quadToRelative(0f, 1.167f, -0.813f, 2f) + quadToRelative(-0.812f, 0.834f, -2.021f, 0.834f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -1.979f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.021f) + quadToRelative(0f, -1.208f, 0.813f, -2.02f) + quadToRelative(0.812f, -0.813f, 1.979f, -0.813f) + reflectiveQuadToRelative(2f, 0.813f) + quadToRelative(0.833f, 0.812f, 0.833f, 2.02f) + quadToRelative(0f, 1.167f, -0.812f, 2f) + quadToRelative(-0.813f, 0.834f, -2.021f, 0.834f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -2f, -0.813f) + quadToRelative(-0.834f, -0.812f, -0.834f, -2.021f) + quadToRelative(0f, -1.208f, 0.813f, -2.02f) + quadToRelative(0.812f, -0.813f, 2.021f, -0.813f) + quadToRelative(1.208f, 0f, 2.02f, 0.813f) + quadToRelative(0.813f, 0.812f, 0.813f, 2.02f) + quadToRelative(0f, 1.167f, -0.813f, 2f) + quadToRelative(-0.812f, 0.834f, -2.02f, 0.834f) + close() + } + }.build() + } + } + + @Composable + fun playArrow(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "play_arrow", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(15.542f, 30f) + quadToRelative(-0.667f, 0.458f, -1.334f, 0.062f) + quadToRelative(-0.666f, -0.395f, -0.666f, -1.187f) + verticalLineTo(10.917f) + quadToRelative(0f, -0.75f, 0.666f, -1.146f) + quadToRelative(0.667f, -0.396f, 1.334f, 0.062f) + lineToRelative(14.083f, 9f) + quadToRelative(0.583f, 0.375f, 0.583f, 1.084f) + quadToRelative(0f, 0.708f, -0.583f, 1.083f) + close() + moveToRelative(0.625f, -10.083f) + close() + moveToRelative(0f, 6.541f) + lineToRelative(10.291f, -6.541f) + lineToRelative(-10.291f, -6.542f) + close() + } + }.build() + } + } + + @Composable + fun folderOpen(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "folder_open", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 33.125f) + quadToRelative(-1.083f, 0f, -1.854f, -0.792f) + quadToRelative(-0.771f, -0.791f, -0.771f, -1.875f) + verticalLineTo(9.667f) + quadToRelative(0f, -1.084f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(10.042f) + quadToRelative(0.541f, 0f, 1.041f, 0.208f) + quadToRelative(0.5f, 0.208f, 0.834f, 0.583f) + lineToRelative(1.875f, 1.834f) + horizontalLineTo(33.75f) + quadToRelative(1.083f, 0f, 1.854f, 0.791f) + quadToRelative(0.771f, 0.792f, 0.771f, 1.834f) + horizontalLineTo(18.917f) + lineTo(16.25f, 9.667f) + horizontalLineToRelative(-10f) + verticalLineTo(30.25f) + lineToRelative(3.542f, -13.375f) + quadToRelative(0.25f, -0.875f, 0.979f, -1.396f) + quadToRelative(0.729f, -0.521f, 1.604f, -0.521f) + horizontalLineToRelative(23.25f) + quadToRelative(1.292f, 0f, 2.104f, 1.021f) + quadToRelative(0.813f, 1.021f, 0.438f, 2.271f) + lineToRelative(-3.459f, 12.833f) + quadToRelative(-0.291f, 1f, -1f, 1.521f) + quadToRelative(-0.708f, 0.521f, -1.75f, 0.521f) + close() + moveToRelative(2.708f, -2.667f) + horizontalLineToRelative(23.167f) + lineToRelative(3.417f, -12.875f) + horizontalLineTo(12.333f) + close() + moveToRelative(0f, 0f) + lineToRelative(3.375f, -12.875f) + lineToRelative(-3.375f, 12.875f) + close() + moveToRelative(-2.708f, -15.5f) + verticalLineTo(9.667f) + verticalLineToRelative(5.291f) + close() + } + }.build() + } + } + + @Composable + fun gameUpdate(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "game_update_alt", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), + fillAlpha = 1f, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 33.083f) + quadToRelative(-1.083f, 0f, -1.854f, -0.791f) + quadToRelative(-0.771f, -0.792f, -0.771f, -1.834f) + verticalLineTo(9.542f) + quadToRelative(0f, -1.042f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.813f, 1.854f, -0.813f) + horizontalLineToRelative(8.458f) + quadToRelative(0.584f, 0f, 0.959f, 0.396f) + reflectiveQuadToRelative(0.375f, 0.937f) + quadToRelative(0f, 0.584f, -0.375f, 0.959f) + reflectiveQuadToRelative(-0.959f, 0.375f) + horizontalLineTo(6.25f) + verticalLineToRelative(20.916f) + horizontalLineToRelative(27.542f) + verticalLineTo(9.542f) + horizontalLineToRelative(-8.5f) + quadToRelative(-0.584f, 0f, -0.959f, -0.375f) + reflectiveQuadToRelative(-0.375f, -0.959f) + quadToRelative(0f, -0.541f, 0.375f, -0.937f) + reflectiveQuadToRelative(0.959f, -0.396f) + horizontalLineToRelative(8.5f) + quadToRelative(1.041f, 0f, 1.833f, 0.813f) + quadToRelative(0.792f, 0.812f, 0.792f, 1.854f) + verticalLineToRelative(20.916f) + quadToRelative(0f, 1.042f, -0.792f, 1.834f) + quadToRelative(-0.792f, 0.791f, -1.833f, 0.791f) + close() + moveTo(20f, 25f) + quadToRelative(-0.25f, 0f, -0.479f, -0.083f) + quadToRelative(-0.229f, -0.084f, -0.396f, -0.292f) + lineTo(12.75f, 18.25f) + quadToRelative(-0.375f, -0.333f, -0.375f, -0.896f) + quadToRelative(0f, -0.562f, 0.417f, -0.979f) + quadToRelative(0.375f, -0.375f, 0.916f, -0.375f) + quadToRelative(0.542f, 0f, 0.959f, 0.375f) + lineToRelative(4.041f, 4.083f) + verticalLineTo(8.208f) + quadToRelative(0f, -0.541f, 0.375f, -0.937f) + reflectiveQuadTo(20f, 6.875f) + quadToRelative(0.542f, 0f, 0.938f, 0.396f) + quadToRelative(0.395f, 0.396f, 0.395f, 0.937f) + verticalLineToRelative(12.25f) + lineToRelative(4.084f, -4.083f) + quadToRelative(0.333f, -0.333f, 0.875f, -0.333f) + quadToRelative(0.541f, 0f, 0.916f, 0.375f) + quadToRelative(0.417f, 0.416f, 0.417f, 0.958f) + reflectiveQuadToRelative(-0.375f, 0.917f) + lineToRelative(-6.333f, 6.333f) + quadToRelative(-0.209f, 0.208f, -0.438f, 0.292f) + quadTo(20.25f, 25f, 20f, 25f) + close() + } + }.build() + } + } + + @Composable + fun download(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "download", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), + fillAlpha = 1f, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(20f, 26.25f) + quadToRelative(-0.25f, 0f, -0.479f, -0.083f) + quadToRelative(-0.229f, -0.084f, -0.438f, -0.292f) + lineToRelative(-6.041f, -6.083f) + quadToRelative(-0.417f, -0.375f, -0.396f, -0.917f) + quadToRelative(0.021f, -0.542f, 0.396f, -0.917f) + reflectiveQuadToRelative(0.916f, -0.396f) + quadToRelative(0.542f, -0.02f, 0.959f, 0.396f) + lineToRelative(3.791f, 3.792f) + verticalLineTo(8.292f) + quadToRelative(0f, -0.584f, 0.375f, -0.959f) + reflectiveQuadTo(20f, 6.958f) + quadToRelative(0.542f, 0f, 0.938f, 0.375f) + quadToRelative(0.395f, 0.375f, 0.395f, 0.959f) + verticalLineTo(21.75f) + lineToRelative(3.792f, -3.792f) + quadToRelative(0.375f, -0.416f, 0.917f, -0.396f) + quadToRelative(0.541f, 0.021f, 0.958f, 0.396f) + quadToRelative(0.375f, 0.375f, 0.375f, 0.917f) + reflectiveQuadToRelative(-0.375f, 0.958f) + lineToRelative(-6.083f, 6.042f) + quadToRelative(-0.209f, 0.208f, -0.438f, 0.292f) + quadToRelative(-0.229f, 0.083f, -0.479f, 0.083f) + close() + moveTo(9.542f, 32.958f) + quadToRelative(-1.042f, 0f, -1.834f, -0.791f) + quadToRelative(-0.791f, -0.792f, -0.791f, -1.834f) + verticalLineToRelative(-4.291f) + quadToRelative(0f, -0.542f, 0.395f, -0.938f) + quadToRelative(0.396f, -0.396f, 0.938f, -0.396f) + quadToRelative(0.542f, 0f, 0.917f, 0.396f) + reflectiveQuadToRelative(0.375f, 0.938f) + verticalLineToRelative(4.291f) + horizontalLineToRelative(20.916f) + verticalLineToRelative(-4.291f) + quadToRelative(0f, -0.542f, 0.375f, -0.938f) + quadToRelative(0.375f, -0.396f, 0.917f, -0.396f) + quadToRelative(0.583f, 0f, 0.958f, 0.396f) + reflectiveQuadToRelative(0.375f, 0.938f) + verticalLineToRelative(4.291f) + quadToRelative(0f, 1.042f, -0.791f, 1.834f) + quadToRelative(-0.792f, 0.791f, -1.834f, 0.791f) + close() + } + }.build() + } + } + + @Composable + fun vSync(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "60fps", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), + fillAlpha = 1f, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(7.292f, 31.458f) + quadToRelative(-1.542f, 0f, -2.625f, -1.041f) + quadToRelative(-1.084f, -1.042f, -1.084f, -2.625f) + verticalLineTo(12.208f) + quadToRelative(0f, -1.583f, 1.084f, -2.625f) + quadTo(5.75f, 8.542f, 7.292f, 8.542f) + horizontalLineTo(14f) + quadToRelative(0.75f, 0f, 1.292f, 0.541f) + quadToRelative(0.541f, 0.542f, 0.541f, 1.292f) + reflectiveQuadToRelative(-0.541f, 1.292f) + quadToRelative(-0.542f, 0.541f, -1.292f, 0.541f) + horizontalLineTo(7.208f) + verticalLineToRelative(5.084f) + horizontalLineToRelative(6.709f) + quadToRelative(1.541f, 0f, 2.583f, 1.041f) + quadToRelative(1.042f, 1.042f, 1.042f, 2.625f) + verticalLineToRelative(6.834f) + quadToRelative(0f, 1.583f, -1.042f, 2.625f) + quadToRelative(-1.042f, 1.041f, -2.583f, 1.041f) + close() + moveToRelative(-0.084f, -10.5f) + verticalLineToRelative(6.834f) + horizontalLineToRelative(6.709f) + verticalLineToRelative(-6.834f) + close() + moveToRelative(17.125f, 6.834f) + horizontalLineToRelative(8.459f) + verticalLineTo(12.208f) + horizontalLineToRelative(-8.459f) + verticalLineToRelative(15.584f) + close() + moveToRelative(0f, 3.666f) + quadToRelative(-1.541f, 0f, -2.583f, -1.041f) + quadToRelative(-1.042f, -1.042f, -1.042f, -2.625f) + verticalLineTo(12.208f) + quadToRelative(0f, -1.583f, 1.042f, -2.625f) + quadToRelative(1.042f, -1.041f, 2.583f, -1.041f) + horizontalLineToRelative(8.459f) + quadToRelative(1.541f, 0f, 2.583f, 1.041f) + quadToRelative(1.042f, 1.042f, 1.042f, 2.625f) + verticalLineToRelative(15.584f) + quadToRelative(0f, 1.583f, -1.042f, 2.625f) + quadToRelative(-1.042f, 1.041f, -2.583f, 1.041f) + close() + } + }.build() + } + } + + @Composable + fun videoGame(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "videogame_asset", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), + fillAlpha = 1f, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 29.792f) + quadToRelative(-1.083f, 0f, -1.854f, -0.792f) + quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f) + verticalLineTo(12.833f) + quadToRelative(0f, -1.083f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(27.5f) + quadToRelative(1.083f, 0f, 1.854f, 0.771f) + quadToRelative(0.771f, 0.771f, 0.771f, 1.854f) + verticalLineToRelative(14.334f) + quadToRelative(0f, 1.041f, -0.771f, 1.833f) + reflectiveQuadToRelative(-1.854f, 0.792f) + close() + moveToRelative(0f, -2.625f) + horizontalLineToRelative(27.5f) + verticalLineTo(12.833f) + horizontalLineTo(6.25f) + verticalLineToRelative(14.334f) + close() + moveToRelative(7.167f, -1.792f) + quadToRelative(0.541f, 0f, 0.916f, -0.375f) + reflectiveQuadToRelative(0.375f, -0.917f) + verticalLineToRelative(-2.791f) + horizontalLineToRelative(2.75f) + quadToRelative(0.584f, 0f, 0.959f, -0.375f) + reflectiveQuadToRelative(0.375f, -0.917f) + quadToRelative(0f, -0.542f, -0.375f, -0.938f) + quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f) + horizontalLineToRelative(-2.75f) + verticalLineToRelative(-2.75f) + quadToRelative(0f, -0.542f, -0.375f, -0.938f) + quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f) + quadToRelative(-0.584f, 0f, -0.959f, 0.396f) + reflectiveQuadToRelative(-0.375f, 0.938f) + verticalLineToRelative(2.75f) + horizontalLineToRelative(-2.75f) + quadToRelative(-0.541f, 0f, -0.937f, 0.395f) + quadTo(8f, 19.458f, 8f, 20f) + quadToRelative(0f, 0.542f, 0.396f, 0.917f) + reflectiveQuadToRelative(0.937f, 0.375f) + horizontalLineToRelative(2.75f) + verticalLineToRelative(2.791f) + quadToRelative(0f, 0.542f, 0.396f, 0.917f) + reflectiveQuadToRelative(0.938f, 0.375f) + close() + moveToRelative(11.125f, -0.5f) + quadToRelative(0.791f, 0f, 1.396f, -0.583f) + quadToRelative(0.604f, -0.584f, 0.604f, -1.375f) + quadToRelative(0f, -0.834f, -0.604f, -1.417f) + quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f) + quadToRelative(-0.834f, 0f, -1.417f, 0.583f) + quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) + quadToRelative(0f, 0.833f, 0.583f, 1.417f) + quadToRelative(0.583f, 0.583f, 1.417f, 0.583f) + close() + moveToRelative(3.916f, -5.833f) + quadToRelative(0.834f, 0f, 1.417f, -0.584f) + quadToRelative(0.583f, -0.583f, 0.583f, -1.416f) + quadToRelative(0f, -0.792f, -0.583f, -1.375f) + quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f) + quadToRelative(-0.791f, 0f, -1.375f, 0.584f) + quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) + quadToRelative(0f, 0.833f, 0.583f, 1.416f) + quadToRelative(0.584f, 0.584f, 1.375f, 0.584f) + close() + moveTo(6.25f, 27.167f) + verticalLineTo(12.833f) + verticalLineToRelative(14.334f) + close() + } + }.build() + } + } + } +} + +@Preview +@Composable +fun Preview() { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + }) { + Icon( + imageVector = CssGgIcons.Games, + contentDescription = "Open Panel" + ) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt new file mode 100644 index 00000000..4ce4e9ff --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt @@ -0,0 +1,63 @@ +package org.ryujinx.android + +import android.content.Intent +import androidx.core.content.FileProvider +import net.lingala.zip4j.ZipFile +import org.ryujinx.android.viewmodels.MainViewModel +import java.io.File +import java.net.URLConnection + +class Logging(private var viewModel: MainViewModel) { + val logPath = MainActivity.AppPath + "/Logs" + + init { + File(logPath).mkdirs() + } + + fun requestExport() { + val files = File(logPath).listFiles() + files?.apply { + val zipExportPath = MainActivity.AppPath + "/log.zip" + File(zipExportPath).delete() + var count = 0 + if (files.isNotEmpty()) { + val zipFile = ZipFile(zipExportPath) + for (file in files) { + if (file.isFile) { + zipFile.addFile(file) + count++ + } + } + zipFile.close() + } + if (count > 0) { + val zip = File(zipExportPath) + val uri = FileProvider.getUriForFile( + viewModel.activity, + viewModel.activity.packageName + ".fileprovider", + zip + ) + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.setDataAndType(uri, URLConnection.guessContentTypeFromName(zip.name)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val chooser = Intent.createChooser(intent, "Share logs") + viewModel.activity.startActivity(chooser) + } else { + File(zipExportPath).delete() + } + } + } + + fun clearLogs() { + if (File(logPath).exists()) { + File(logPath).deleteRecursively() + } + + File(logPath).mkdirs() + } +} + +internal enum class LogLevel { + Debug, Stub, Info, Warning, Error, Guest, AccessLog, Notice, Trace +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt new file mode 100644 index 00000000..c120913c --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -0,0 +1,227 @@ +package org.ryujinx.android + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.os.Environment +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.WindowManager +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.anggrayudi.storage.SimpleStorageHelper +import com.sun.jna.JNIEnv +import org.ryujinx.android.ui.theme.RyujinxAndroidTheme +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings +import org.ryujinx.android.views.MainView + + +class MainActivity : BaseActivity() { + private var physicalControllerManager: PhysicalControllerManager = + PhysicalControllerManager(this) + private lateinit var motionSensorManager: MotionSensorManager + private var _isInit: Boolean = false + var isGameRunning = false + var isActive = false + var storageHelper: SimpleStorageHelper? = null + lateinit var uiHandler: UiHandler + + companion object { + var mainViewModel: MainViewModel? = null + var AppPath: String = "" + var StorageHelper: SimpleStorageHelper? = null + val performanceMonitor = PerformanceMonitor() + + @JvmStatic + fun frameEnded() { + mainViewModel?.activity?.apply { + if (isActive && QuickSettings(this).enablePerformanceMode) { + mainViewModel?.performanceManager?.setTurboMode(true) + } + } + mainViewModel?.gameHost?.hideProgressIndicator() + } + } + + init { + storageHelper = SimpleStorageHelper(this) + StorageHelper = storageHelper + System.loadLibrary("ryujinxjni") + initVm() + } + + private external fun initVm() + + private fun initialize() { + if (_isInit) + return + + val appPath: String = AppPath + + var quickSettings = QuickSettings(this) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Debug.ordinal, + quickSettings.enableDebugLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Info.ordinal, + quickSettings.enableInfoLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Stub.ordinal, + quickSettings.enableStubLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Warning.ordinal, + quickSettings.enableWarningLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Error.ordinal, + quickSettings.enableErrorLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.AccessLog.ordinal, + quickSettings.enableAccessLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Guest.ordinal, + quickSettings.enableGuestLogs + ) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Trace.ordinal, + quickSettings.enableTraceLogs + ) + RyujinxNative.jnaInstance.loggingEnabledGraphicsLog( + quickSettings.enableTraceLogs + ) + val success = + RyujinxNative.jnaInstance.javaInitialize(appPath, JNIEnv.CURRENT) + + uiHandler = UiHandler() + _isInit = success + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + motionSensorManager = MotionSensorManager(this) + Thread.setDefaultUncaughtExceptionHandler(crashHandler) + + if ( + !Environment.isExternalStorageManager() + ) { + storageHelper?.storage?.requestFullStorageAccess() + } + + AppPath = this.getExternalFilesDir(null)!!.absolutePath + + initialize() + + window.attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + WindowCompat.setDecorFitsSystemWindows(window, false) + + mainViewModel = MainViewModel(this) + mainViewModel!!.physicalControllerManager = physicalControllerManager + mainViewModel!!.motionSensorManager = motionSensorManager + + mainViewModel!!.refreshFirmwareVersion() + + mainViewModel?.apply { + setContent { + RyujinxAndroidTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainView.Main(mainViewModel = this) + } + } + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + storageHelper?.onSaveInstanceState(outState) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + storageHelper?.onRestoreInstanceState(savedInstanceState) + } + + fun setFullScreen(fullscreen: Boolean) { + requestedOrientation = + if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER + + val insets = WindowCompat.getInsetsController(window, window.decorView) + + insets.apply { + if (fullscreen) { + insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } + } + } + + @SuppressLint("RestrictedApi") + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + event.apply { + if (physicalControllerManager.onKeyEvent(this)) + return true + } + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + ev?.apply { + physicalControllerManager.onMotionEvent(this) + } + return super.dispatchGenericMotionEvent(ev) + } + + override fun onStop() { + super.onStop() + isActive = false + + if (isGameRunning) { + mainViewModel?.performanceManager?.setTurboMode(false) + } + } + + override fun onResume() { + super.onResume() + isActive = true + + if (isGameRunning) { + setFullScreen(true) + if (QuickSettings(this).enableMotion) + motionSensorManager.register() + } + } + + override fun onPause() { + super.onPause() + isActive = true + + if (isGameRunning) { + mainViewModel?.performanceManager?.setTurboMode(false) + } + + motionSensorManager.unregister() + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt new file mode 100644 index 00000000..aaf2824f --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt @@ -0,0 +1,137 @@ +package org.ryujinx.android + +import android.app.Activity +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener2 +import android.hardware.SensorManager +import android.view.OrientationEventListener + +class MotionSensorManager(val activity: MainActivity) : SensorEventListener2 { + private var isRegistered: Boolean = false + private var gyro: Sensor? + private var accelerometer: Sensor? + private var sensorManager: SensorManager = + activity.getSystemService(Activity.SENSOR_SERVICE) as SensorManager + private var controllerId: Int = -1 + + private val motionGyroOrientation: FloatArray = FloatArray(3) + private val motionAcelOrientation: FloatArray = FloatArray(3) + + init { + accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + setOrientation90() + var orientationListener = object : OrientationEventListener(activity) { + override fun onOrientationChanged(orientation: Int) { + when { + isWithinOrientationRange(orientation, 270) -> { + setOrientation270() + } + + isWithinOrientationRange(orientation, 90) -> { + setOrientation90() + } + } + } + + private fun isWithinOrientationRange( + currentOrientation: Int, targetOrientation: Int, epsilon: Int = 90 + ): Boolean { + return currentOrientation > targetOrientation - epsilon + && currentOrientation < targetOrientation + epsilon + } + } + } + + fun setOrientation270() { + motionGyroOrientation[0] = -1.0f + motionGyroOrientation[1] = 1.0f + motionGyroOrientation[2] = 1.0f + motionAcelOrientation[0] = 1.0f + motionAcelOrientation[1] = -1.0f + motionAcelOrientation[2] = -1.0f + } + + fun setOrientation90() { + motionGyroOrientation[0] = 1.0f + motionGyroOrientation[1] = -1.0f + motionGyroOrientation[2] = 1.0f + motionAcelOrientation[0] = -1.0f + motionAcelOrientation[1] = 1.0f + motionAcelOrientation[2] = -1.0f + } + + fun setControllerId(id: Int) { + controllerId = id + } + + fun register() { + if (isRegistered) + return + gyro?.apply { + sensorManager.registerListener( + this@MotionSensorManager, + gyro, + SensorManager.SENSOR_DELAY_GAME + ) + } + accelerometer?.apply { + sensorManager.registerListener( + this@MotionSensorManager, + accelerometer, + SensorManager.SENSOR_DELAY_GAME + ) + } + + isRegistered = true + } + + fun unregister() { + sensorManager.unregisterListener(this) + isRegistered = false + + if (controllerId != -1) { + RyujinxNative.jnaInstance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId) + RyujinxNative.jnaInstance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId) + } + } + + override fun onSensorChanged(event: SensorEvent?) { + if (controllerId != -1) + if (isRegistered) + event?.apply { + when (sensor.type) { + Sensor.TYPE_ACCELEROMETER -> { + val x = motionAcelOrientation[0] * event.values[1] + val y = motionAcelOrientation[1] * event.values[0] + val z = motionAcelOrientation[2] * event.values[2] + + RyujinxNative.jnaInstance.inputSetAccelerometerData( + x, + y, + z, + controllerId + ) + } + + Sensor.TYPE_GYROSCOPE -> { + val x = motionGyroOrientation[0] * event.values[1] + val y = motionGyroOrientation[1] * event.values[0] + val z = motionGyroOrientation[2] * event.values[2] + RyujinxNative.jnaInstance.inputSetGyroData(x, y, z, controllerId) + } + } + } + else { + RyujinxNative.jnaInstance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId) + RyujinxNative.jnaInstance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId) + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + } + + override fun onFlushCompleted(sensor: Sensor?) { + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt new file mode 100644 index 00000000..e4dddff0 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt @@ -0,0 +1,7 @@ +package org.ryujinx.android + +class NativeGraphicsInterop { + var VkCreateSurface: Long = 0 + var SurfaceHandle: Long = 0 + var VkRequiredExtensions: Array? = null +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt new file mode 100644 index 00000000..d6e74932 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -0,0 +1,31 @@ +package org.ryujinx.android + +import android.view.Surface + +class NativeHelpers { + + companion object { + val instance = NativeHelpers() + + init { + System.loadLibrary("ryujinxjni") + } + } + + external fun releaseNativeWindow(window: Long) + external fun getCreateSurfacePtr(): Long + external fun getNativeWindow(surface: Surface): Long + + external fun loadDriver( + nativeLibPath: String, + privateAppsPath: String, + driverName: String + ): Long + + external fun setTurboMode(enable: Boolean) + external fun getMaxSwapInterval(nativeWindow: Long): Int + external fun getMinSwapInterval(nativeWindow: Long): Int + external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int + external fun getStringJava(ptr: Long): String + external fun setIsInitialOrientationFlipped(isFlipped: Boolean) +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt new file mode 100644 index 00000000..0fd7b1f8 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt @@ -0,0 +1,42 @@ +package org.ryujinx.android + +import android.view.SurfaceView + +class NativeWindow(val surface: SurfaceView) { + var nativePointer: Long + private val nativeHelpers: NativeHelpers = NativeHelpers.instance + private var _swapInterval: Int = 0 + + var maxSwapInterval: Int = 0 + get() { + return if (nativePointer == -1L) 0 else nativeHelpers.getMaxSwapInterval(nativePointer) + } + + var minSwapInterval: Int = 0 + get() { + return if (nativePointer == -1L) 0 else nativeHelpers.getMinSwapInterval(nativePointer) + } + + var swapInterval: Int + get() { + return _swapInterval + } + set(value) { + if (nativePointer == -1L || nativeHelpers.setSwapInterval(nativePointer, value) == 0) + _swapInterval = value + } + + init { + nativePointer = nativeHelpers.getNativeWindow(surface.holder.surface) + + swapInterval = maxOf(1, minSwapInterval) + } + + fun requeryWindowHandle(): Long { + nativePointer = nativeHelpers.getNativeWindow(surface.holder.surface) + + swapInterval = swapInterval + + return nativePointer + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt new file mode 100644 index 00000000..daa2acc7 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt @@ -0,0 +1,31 @@ +package org.ryujinx.android + +import android.content.Intent +import kotlin.math.abs + +class PerformanceManager(private val activity: MainActivity) { + companion object { + fun force60HzRefreshRate(enable: Boolean, activity: MainActivity) { + // Hack for MIUI devices since they don't support the standard Android APIs + try { + val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS") + setFpsIntent.putExtra("package_name", "org.ryujinx.android") + setFpsIntent.putExtra("isEnter", enable) + activity.sendBroadcast(setFpsIntent) + } catch (_: Exception) { + } + + if (enable) + activity.display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) } + ?.let { activity.window.attributes.preferredDisplayModeId = it.modeId } + else + activity.display?.supportedModes?.maxByOrNull { it.refreshRate } + ?.let { activity.window.attributes.preferredDisplayModeId = it.modeId } + } + } + + fun setTurboMode(enable: Boolean) { + NativeHelpers.instance.setTurboMode(enable) + force60HzRefreshRate(enable, activity) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceMonitor.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceMonitor.kt new file mode 100644 index 00000000..c0aaf05e --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceMonitor.kt @@ -0,0 +1,45 @@ +package org.ryujinx.android + +import android.app.ActivityManager +import android.content.Context.ACTIVITY_SERVICE +import androidx.compose.runtime.MutableState +import java.io.RandomAccessFile + +class PerformanceMonitor { + val numberOfCores = Runtime.getRuntime().availableProcessors() + + fun getFrequencies(frequencies: MutableList){ + frequencies.clear() + for (i in 0.., + totalMem: MutableState) { + MainActivity.mainViewModel?.activity?.apply { + val actManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager + val memInfo = ActivityManager.MemoryInfo() + actManager.getMemoryInfo(memInfo) + val availMemory = memInfo.availMem.toDouble() / (1024 * 1024) + val totalMemory = memInfo.totalMem.toDouble() / (1024 * 1024) + + usedMem.value = (totalMemory - availMemory).toInt() + totalMem.value = totalMemory.toInt() + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt new file mode 100644 index 00000000..3680c254 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt @@ -0,0 +1,158 @@ +package org.ryujinx.android + +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import org.ryujinx.android.viewmodels.QuickSettings + +class PhysicalControllerManager(val activity: MainActivity) { + private var controllerId: Int = -1 + + fun onKeyEvent(event: KeyEvent): Boolean { + val id = getGamePadButtonInputId(event.keyCode) + if (id != GamePadButtonInputId.None) { + val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0 + if (/*controllerId != -1 &&*/ isNotFallback) { + when (event.action) { + KeyEvent.ACTION_UP -> { + RyujinxNative.jnaInstance.inputSetButtonReleased(id.ordinal, controllerId) + } + + KeyEvent.ACTION_DOWN -> { + RyujinxNative.jnaInstance.inputSetButtonPressed(id.ordinal, controllerId) + } + } + return true + } else if (!isNotFallback) { + return true + } + } + + return false + } + + fun onMotionEvent(ev: MotionEvent) { + if (true) { + if (ev.action == MotionEvent.ACTION_MOVE) { + val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X) + val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y) + val rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z) + val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ) + RyujinxNative.jnaInstance.inputSetStickAxis( + 1, + leftStickX, + -leftStickY, + controllerId + ) + RyujinxNative.jnaInstance.inputSetStickAxis( + 2, + rightStickX, + -rightStickY, + controllerId + ) + + ev.device?.apply { + if (sources and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD) { + // Controller uses HAT + val dPadHor = ev.getAxisValue(MotionEvent.AXIS_HAT_X) + val dPadVert = ev.getAxisValue(MotionEvent.AXIS_HAT_Y) + if (dPadVert == 0.0f) { + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadUp.ordinal, + controllerId + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadDown.ordinal, + controllerId + ) + } + if (dPadHor == 0.0f) { + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadLeft.ordinal, + controllerId + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadRight.ordinal, + controllerId + ) + } + + if (dPadVert < 0.0f) { + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadUp.ordinal, + controllerId + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadDown.ordinal, + controllerId + ) + } + if (dPadHor < 0.0f) { + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadLeft.ordinal, + controllerId + ) + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadRight.ordinal, + controllerId + ) + } + + if (dPadVert > 0.0f) { + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadUp.ordinal, + controllerId + ) + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadDown.ordinal, + controllerId + ) + } + if (dPadHor > 0.0f) { + RyujinxNative.jnaInstance.inputSetButtonReleased( + GamePadButtonInputId.DpadLeft.ordinal, + controllerId + ) + RyujinxNative.jnaInstance.inputSetButtonPressed( + GamePadButtonInputId.DpadRight.ordinal, + controllerId + ) + } + } + } + } + } + } + + fun connect(): Int { + controllerId = RyujinxNative.jnaInstance.inputConnectGamepad(0) + return controllerId + } + + fun disconnect() { + controllerId = -1 + } + + private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId { + val quickSettings = QuickSettings(activity) + return when (keycode) { + KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B + KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A + KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y + KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X + KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder + KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger + KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder + KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger + KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStick + KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStick + KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp + KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown + KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft + KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight + KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus + KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus + else -> GamePadButtonInputId.None + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RegionCode.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RegionCode.kt new file mode 100644 index 00000000..f9dfec47 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RegionCode.kt @@ -0,0 +1,11 @@ +package org.ryujinx.android + +enum class RegionCode { + Japan, + USA, + Europe, + Australia, + China, + Korea, + Taiwan, +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt new file mode 100644 index 00000000..79142be3 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt @@ -0,0 +1,20 @@ +package org.ryujinx.android + +import android.app.Application +import android.content.Context +import java.io.File + +class RyujinxApplication : Application() { + init { + instance = this + } + + fun getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir + + companion object { + lateinit var instance: RyujinxApplication + private set + + val context: Context get() = instance.applicationContext + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt new file mode 100644 index 00000000..3787fa41 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -0,0 +1,158 @@ +package org.ryujinx.android + +import com.sun.jna.JNIEnv +import com.sun.jna.Library +import com.sun.jna.Native +import org.ryujinx.android.viewmodels.GameInfo +import java.util.Collections + +interface RyujinxNativeJna : Library { + fun deviceInitialize( + isHostMapped: Boolean, useNce: Boolean, + systemLanguage: Int, + regionCode: Int, + enableVsync: Boolean, + enableDockedMode: Boolean, + enablePtc: Boolean, + enableInternetAccess: Boolean, + timeZone: String, + ignoreMissingServices: Boolean + ): Boolean + + fun graphicsInitialize( + rescale: Float = 1f, + maxAnisotropy: Float = 1f, + fastGpuTime: Boolean = true, + fast2DCopy: Boolean = true, + enableMacroJit: Boolean = false, + enableMacroHLE: Boolean = true, + enableShaderCache: Boolean = true, + enableTextureRecompression: Boolean = false, + backendThreading: Int = BackendThreading.Auto.ordinal + ): Boolean + + fun graphicsInitializeRenderer( + extensions: Array, + extensionsLength: Int, + driver: Long + ): Boolean + + fun javaInitialize(appPath: String, env: JNIEnv): Boolean + fun deviceLaunchMiiEditor(): Boolean + fun deviceGetGameFrameRate(): Double + fun deviceGetGameFrameTime(): Double + fun deviceGetGameFifo(): Double + fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean + fun graphicsRendererSetSize(width: Int, height: Int) + fun graphicsRendererSetVsync(enabled: Boolean) + fun graphicsRendererRunLoop() + fun deviceReloadFilesystem() + fun inputInitialize(width: Int, height: Int) + fun inputSetClientSize(width: Int, height: Int) + fun inputSetTouchPoint(x: Int, y: Int) + fun inputReleaseTouchPoint() + fun inputUpdate() + fun inputSetButtonPressed(button: Int, id: Int) + fun inputSetButtonReleased(button: Int, id: Int) + fun inputConnectGamepad(index: Int): Int + fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int) + fun inputSetAccelerometerData(x: Float, y: Float, z: Float, id: Int) + fun inputSetGyroData(x: Float, y: Float, z: Float, id: Int) + fun deviceCloseEmulation() + fun deviceSignalEmulationClose() + fun userGetOpenedUser(): String + fun userGetUserPicture(userId: String): String + fun userSetUserPicture(userId: String, picture: String) + fun userGetUserName(userId: String): String + fun userSetUserName(userId: String, userName: String) + fun userAddUser(username: String, picture: String) + fun userDeleteUser(userId: String) + fun userOpenUser(userId: String) + fun userCloseUser(userId: String) + fun loggingSetEnabled(logLevel: Int, enabled: Boolean) + fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): String + fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean) + fun deviceGetInstalledFirmwareVersion(): String + fun uiHandlerSetup() + fun uiHandlerSetResponse(isOkPressed: Boolean, input: String) + fun deviceGetDlcTitleId(path: String, ncaPath: String): String + fun deviceGetGameInfo(fileDescriptor: Int, extension: String, info: GameInfo) + fun userGetAllUsers(): Array + fun deviceGetDlcContentList(path: String, titleId: Long): Array + fun loggingEnabledGraphicsLog(enabled: Boolean) +} + +class RyujinxNative { + + companion object { + val jnaInstance: RyujinxNativeJna = Native.load( + "ryujinx", + RyujinxNativeJna::class.java, + Collections.singletonMap(Library.OPTION_ALLOW_OBJECTS, true) + ) + + @JvmStatic + fun test() + { + val i = 0 + } + + @JvmStatic + fun frameEnded() + { + MainActivity.frameEnded() + } + + @JvmStatic + fun getSurfacePtr() : Long + { + return MainActivity.mainViewModel?.gameHost?.currentSurface ?: -1 + } + + @JvmStatic + fun getWindowHandle() : Long + { + return MainActivity.mainViewModel?.gameHost?.currentWindowhandle ?: -1 + } + + @JvmStatic + fun updateProgress(infoPtr : Long, progress: Float) + { + val info = NativeHelpers.instance.getStringJava(infoPtr); + MainActivity.mainViewModel?.gameHost?.setProgress(info, progress) + } + + @JvmStatic + fun updateUiHandler( + newTitlePointer: Long, + newMessagePointer: Long, + newWatermarkPointer: Long, + newType: Int, + min: Int, + max: Int, + nMode: Int, + newSubtitlePointer: Long, + newInitialTextPointer: Long + ) + { + var uiHandler = MainActivity.mainViewModel?.activity?.uiHandler + uiHandler?.apply { + val newTitle = NativeHelpers.instance.getStringJava(newTitlePointer) + val newMessage = NativeHelpers.instance.getStringJava(newMessagePointer) + val newWatermark = NativeHelpers.instance.getStringJava(newWatermarkPointer) + val newSubtitle = NativeHelpers.instance.getStringJava(newSubtitlePointer) + val newInitialText = NativeHelpers.instance.getStringJava(newInitialTextPointer) + val newMode = KeyboardMode.entries[nMode] + update(newTitle, + newMessage, + newWatermark, + newType, + min, + max, + newMode, + newSubtitle, + newInitialText); + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/SystemLanguage.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/SystemLanguage.kt new file mode 100644 index 00000000..f1af8e15 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/SystemLanguage.kt @@ -0,0 +1,22 @@ +package org.ryujinx.android + +enum class SystemLanguage { + Japanese, + AmericanEnglish, + French, + German, + Italian, + Spanish, + Chinese, + Korean, + Dutch, + Portuguese, + Russian, + Taiwanese, + BritishEnglish, + CanadianFrench, + LatinAmericanSpanish, + SimplifiedChinese, + TraditionalChinese, + BrazilianPortuguese +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt new file mode 100644 index 00000000..800fb0c4 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt @@ -0,0 +1,211 @@ +package org.ryujinx.android + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.material3.RichText + +enum class KeyboardMode { + Default, Numeric, ASCII, FullLatin, Alphabet, SimplifiedChinese, TraditionalChinese, Korean, LanguageSet2, LanguageSet2Latin +} + +class UiHandler { + private var initialText: String = "" + private var subtitle: String = "" + private var maxLength: Int = 0 + private var minLength: Int = 0 + private var watermark: String = "" + private var type: Int = -1 + private var mode: KeyboardMode = KeyboardMode.Default + val showMessage = mutableStateOf(false) + val inputText = mutableStateOf("") + var title: String = "" + var message: String = "" + + init { + RyujinxNative.jnaInstance.uiHandlerSetup() + } + + fun update( + newTitle: String, + newMessage: String, + newWatermark: String, + newType: Int, + min: Int, + max: Int, + newMode: KeyboardMode, + newSubtitle: String, + newInitialText: String + ) + { + title = newTitle + message = newMessage + watermark = newWatermark + type = newType + minLength = min + maxLength = max + mode = newMode + subtitle = newSubtitle + initialText = newInitialText + inputText.value = initialText + showMessage.value = type > 0 + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Compose() { + val showMessageListener = remember { + showMessage + } + + val inputListener = remember { + inputText + } + val validation = remember { + mutableStateOf("") + } + + fun validate(): Boolean { + if (inputText.value.isEmpty()) { + validation.value = "Must be between ${minLength} and ${maxLength} characters" + } else { + return inputText.value.length < minLength || inputText.value.length > maxLength + } + + return false + } + + fun getInputType(): KeyboardType { + return when (mode) { + KeyboardMode.Default -> KeyboardType.Text + KeyboardMode.Numeric -> KeyboardType.Decimal + KeyboardMode.ASCII -> KeyboardType.Ascii + else -> { + KeyboardType.Text + } + } + } + + fun submit() { + if (type == 2) { + if (inputListener.value.length < minLength || inputListener.value.length > maxLength) + return + } + RyujinxNative.jnaInstance.uiHandlerSetResponse( + true, + if (type == 2) inputListener.value else "" + ) + showMessageListener.value = false + } + + if (showMessageListener.value) { + BasicAlertDialog( + onDismissRequest = { }, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + properties = DialogProperties(dismissOnBackPress = false, false) + ) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column( + modifier = Modifier + .padding(16.dp) + ) { + Text(text = title) + Column( + modifier = Modifier + .height(128.dp) + .verticalScroll(rememberScrollState()) + .padding(8.dp), + verticalArrangement = Arrangement.Center + ) { + RichText { + Markdown(content = message) + } + if (type == 2) { + validate() + if (watermark.isNotEmpty()) + TextField( + value = inputListener.value, + onValueChange = { inputListener.value = it }, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + label = { + Text(text = watermark) + }, + keyboardOptions = KeyboardOptions(keyboardType = getInputType()), + isError = validate() + ) + else + TextField( + value = inputListener.value, + onValueChange = { inputListener.value = it }, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + keyboardOptions = KeyboardOptions( + keyboardType = getInputType(), + imeAction = ImeAction.Done + ), + isError = validate(), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { submit() }) + ) + if (subtitle.isNotEmpty()) + Text(text = subtitle) + Text(text = validation.value) + } + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + ) { + Button(onClick = { + submit() + }) { + Text(text = "OK") + } + } + } + } + } + } + } + } +} + diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt new file mode 100644 index 00000000..13d5f66e --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt @@ -0,0 +1,300 @@ +package org.ryujinx.android.providers + +import android.database.Cursor +import android.database.MatrixCursor +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import android.provider.DocumentsProvider +import android.webkit.MimeTypeMap +import org.ryujinx.android.BuildConfig +import org.ryujinx.android.R +import org.ryujinx.android.RyujinxApplication +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException + +class DocumentProvider : DocumentsProvider() { + private val baseDirectory = File(RyujinxApplication.instance.getPublicFilesDir().canonicalPath) + private val applicationName = "Ryujinx" + + companion object { + private val DEFAULT_ROOT_PROJECTION: Array = arrayOf( + DocumentsContract.Root.COLUMN_ROOT_ID, + DocumentsContract.Root.COLUMN_MIME_TYPES, + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.COLUMN_ICON, + DocumentsContract.Root.COLUMN_TITLE, + DocumentsContract.Root.COLUMN_SUMMARY, + DocumentsContract.Root.COLUMN_DOCUMENT_ID, + DocumentsContract.Root.COLUMN_AVAILABLE_BYTES + ) + + private val DEFAULT_DOCUMENT_PROJECTION: Array = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_SIZE + ) + + const val AUTHORITY: String = BuildConfig.APPLICATION_ID + ".providers" + + const val ROOT_ID: String = "root" + } + + override fun onCreate(): Boolean { + return true + } + + /** + * @return The [File] that corresponds to the document ID supplied by [getDocumentId] + */ + private fun getFile(documentId: String): File { + if (documentId.startsWith(ROOT_ID)) { + val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1)) + if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found") + return file + } else { + throw FileNotFoundException("'$documentId' is not in any known root") + } + } + + /** + * @return A unique ID for the provided [File] + */ + private fun getDocumentId(file: File): String { + return "$ROOT_ID/${file.toRelativeString(baseDirectory)}" + } + + override fun queryRoots(projection: Array?): Cursor { + val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION) + + cursor.newRow().apply { + add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID) + add(DocumentsContract.Root.COLUMN_SUMMARY, null) + add( + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD + ) + add(DocumentsContract.Root.COLUMN_TITLE, applicationName) + add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory)) + add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*") + add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace) + add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) + } + + return cursor + } + + override fun queryDocument(documentId: String?, projection: Array?): Cursor { + val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) + return includeFile(cursor, documentId, null) + } + + override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean { + return documentId?.startsWith(parentDocumentId!!) ?: false + } + + /** + * @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file + */ + fun File.resolveWithoutConflict(name: String): File { + var file = resolve(name) + if (file.exists()) { + var noConflictId = + 1 // Makes sure two files don't have the same name by adding a number to the end + val extension = name.substringAfterLast('.') + val baseName = name.substringBeforeLast('.') + while (file.exists()) + file = resolve("$baseName (${noConflictId++}).$extension") + } + return file + } + + override fun createDocument( + parentDocumentId: String?, + mimeType: String?, + displayName: String + ): String { + val parentFile = getFile(parentDocumentId!!) + val newFile = parentFile.resolveWithoutConflict(displayName) + + try { + if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) { + if (!newFile.mkdir()) + throw IOException("Failed to create directory") + } else { + if (!newFile.createNewFile()) + throw IOException("Failed to create file") + } + } catch (e: IOException) { + throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}") + } + + return getDocumentId(newFile) + } + + override fun deleteDocument(documentId: String?) { + val file = getFile(documentId!!) + if (!file.delete()) + throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } + + override fun removeDocument(documentId: String, parentDocumentId: String?) { + val parent = getFile(parentDocumentId!!) + val file = getFile(documentId) + + if (parent == file || file.parentFile == null || file.parentFile!! == parent) { + if (!file.delete()) + throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } else { + throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } + } + + override fun renameDocument(documentId: String?, displayName: String?): String { + if (displayName == null) + throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null") + + val sourceFile = getFile(documentId!!) + val sourceParentFile = sourceFile.parentFile + ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent") + val destFile = sourceParentFile.resolve(displayName) + + try { + if (!sourceFile.renameTo(destFile)) + throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'") + } catch (e: Exception) { + throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}") + } + + return getDocumentId(destFile) + } + + private fun copyDocument( + sourceDocumentId: String, sourceParentDocumentId: String, + targetParentDocumentId: String? + ): String? { + if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) + throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'") + + return copyDocument(sourceDocumentId, targetParentDocumentId) + } + + override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String { + val parent = getFile(targetParentDocumentId!!) + val oldFile = getFile(sourceDocumentId) + val newFile = parent.resolveWithoutConflict(oldFile.name) + + try { + if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true))) + throw IOException("Couldn't create new file") + + FileInputStream(oldFile).use { inStream -> + FileOutputStream(newFile).use { outStream -> + inStream.copyTo(outStream) + } + } + } catch (e: IOException) { + throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}") + } + + return getDocumentId(newFile) + } + + override fun moveDocument( + sourceDocumentId: String, sourceParentDocumentId: String?, + targetParentDocumentId: String? + ): String? { + try { + val newDocumentId = copyDocument( + sourceDocumentId, sourceParentDocumentId!!, + targetParentDocumentId + ) + removeDocument(sourceDocumentId, sourceParentDocumentId) + return newDocumentId + } catch (e: FileNotFoundException) { + throw FileNotFoundException("Couldn't move document '$sourceDocumentId'") + } + } + + private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor { + val localDocumentId = documentId ?: file?.let { getDocumentId(it) } + val localFile = file ?: getFile(documentId!!) + + var flags = 0 + if (localFile.isDirectory && localFile.canWrite()) { + flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE + } else if (localFile.canWrite()) { + flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE + + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME + } + + cursor.newRow().apply { + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId) + add( + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + if (localFile == baseDirectory) applicationName else localFile.name + ) + add(DocumentsContract.Document.COLUMN_SIZE, localFile.length()) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile)) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified()) + add(DocumentsContract.Document.COLUMN_FLAGS, flags) + if (localFile == baseDirectory) + add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) + } + + return cursor + } + + private fun getTypeForFile(file: File): Any? { + return if (file.isDirectory) + DocumentsContract.Document.MIME_TYPE_DIR + else + getTypeForName(file.name) + } + + private fun getTypeForName(name: String): Any { + val lastDot = name.lastIndexOf('.') + if (lastDot >= 0) { + val extension = name.substring(lastDot + 1) + val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + if (mime != null) + return mime + } + return "application/octect-stream" + } + + override fun queryChildDocuments( + parentDocumentId: String?, + projection: Array?, + sortOrder: String? + ): Cursor { + var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) + + val parent = getFile(parentDocumentId!!) + for (file in parent.listFiles()!!) + cursor = includeFile(cursor, null, file) + + return cursor + } + + override fun openDocument( + documentId: String?, + mode: String?, + signal: CancellationSignal? + ): ParcelFileDescriptor { + val file = documentId?.let { getFile(it) } + val accessMode = ParcelFileDescriptor.parseMode(mode) + return ParcelFileDescriptor.open(file, accessMode) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Color.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Color.kt new file mode 100644 index 00000000..243ab366 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package org.ryujinx.android.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Theme.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Theme.kt new file mode 100644 index 00000000..cf597653 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Theme.kt @@ -0,0 +1,79 @@ +package org.ryujinx.android.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), +) + +@Composable +fun RyujinxAndroidTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val shapes = Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(24.dp) + ) + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content, + shapes = shapes + ) +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Type.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Type.kt new file mode 100644 index 00000000..c9097e38 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package org.ryujinx.android.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt new file mode 100644 index 00000000..b1d78b81 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt @@ -0,0 +1,157 @@ +package org.ryujinx.android.viewmodels + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.file.getAbsolutePath +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.ryujinx.android.MainActivity +import org.ryujinx.android.RyujinxNative +import java.io.File + +class DlcViewModel(val titleId: String) { + private var storageHelper: SimpleStorageHelper + + companion object { + const val UpdateRequestCode = 1002 + } + + fun remove(item: DlcItem) { + data?.apply { + this.removeAll { it.path == item.containerPath } + } + } + + fun add(refresh: MutableState) { + val callBack = storageHelper.onFileSelected + + storageHelper.onFileSelected = { requestCode, files -> + run { + storageHelper.onFileSelected = callBack + if (requestCode == UpdateRequestCode) { + val file = files.firstOrNull() + file?.apply { + val path = file.getAbsolutePath(storageHelper.storage.context) + if (path.isNotEmpty()) { + data?.apply { + val contents = RyujinxNative.jnaInstance.deviceGetDlcContentList( + path, + titleId.toLong(16) + ) + + if (contents.isNotEmpty()) { + val contentPath = path + val container = DlcContainerList(contentPath) + + for (content in contents) + container.dlc_nca_list.add( + DlcContainer( + true, + titleId, + content + ) + ) + + this.add(container) + } + } + } + } + refresh.value = true + } + } + } + storageHelper.openFilePicker(UpdateRequestCode) + } + + fun save(items: List) { + data?.apply { + + val gson = Gson() + val json = gson.toJson(this) + jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + File(jsonPath).mkdirs() + File("$jsonPath/dlc.json").writeText(json) + } + } + + @Composable + fun getDlc(): List { + var items = mutableListOf() + + data?.apply { + for (container in this) { + val containerPath = container.path + + if (!File(containerPath).exists()) + continue + + for (dlc in container.dlc_nca_list) { + val enabled = remember { + mutableStateOf(dlc.enabled) + } + items.add( + DlcItem( + File(containerPath).name, + enabled, + containerPath, + dlc.fullPath, + RyujinxNative.jnaInstance.deviceGetDlcTitleId( + containerPath, + dlc.fullPath + ) + ) + ) + } + } + } + + return items.toList() + } + + var data: MutableList? = null + private var jsonPath: String + + init { + jsonPath = + MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json" + storageHelper = MainActivity.StorageHelper!! + + reloadFromDisk() + } + + private fun reloadFromDisk() { + data = mutableListOf() + if (File(jsonPath).exists()) { + val gson = Gson() + val typeToken = object : TypeToken>() {}.type + data = + gson.fromJson>(File(jsonPath).readText(), typeToken) + } + + } +} + +data class DlcContainerList( + var path: String = "", + var dlc_nca_list: MutableList = mutableListOf() +) + +data class DlcContainer( + var enabled: Boolean = false, + var titleId: String = "", + var fullPath: String = "" +) + +data class DlcItem( + var name: String = "", + var isEnabled: MutableState = mutableStateOf(false), + var containerPath: String = "", + var fullPath: String = "", + var titleId: String = "" +) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameInfo.java b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameInfo.java new file mode 100644 index 00000000..1f62ba81 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameInfo.java @@ -0,0 +1,20 @@ +package org.ryujinx.android.viewmodels; + +import com.sun.jna.Structure; + +import java.util.List; + + +public class GameInfo extends Structure { + public double FileSize = 0.0; + public String TitleName; + public String TitleId; + public String Developer; + public String Version; + public String Icon; + + @Override + protected List getFieldOrder() { + return List.of("FileSize", "TitleName", "TitleId", "Developer", "Version", "Icon"); + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt new file mode 100644 index 00000000..714b6587 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt @@ -0,0 +1,90 @@ +package org.ryujinx.android.viewmodels + +import android.content.Context +import android.net.Uri +import android.os.ParcelFileDescriptor +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.file.extension +import org.ryujinx.android.RyujinxNative + + +class GameModel(var file: DocumentFile, val context: Context) { + private var updateDescriptor: ParcelFileDescriptor? = null + var type: FileType + var descriptor: ParcelFileDescriptor? = null + var fileName: String? + var fileSize = 0.0 + var titleName: String? = null + var titleId: String? = null + var developer: String? = null + var version: String? = null + var icon: String? = null + + init { + fileName = file.name + val pid = open() + val gameInfo = GameInfo() + RyujinxNative.jnaInstance.deviceGetGameInfo(pid, file.extension, gameInfo) + close() + + fileSize = gameInfo.FileSize + titleId = gameInfo.TitleId + titleName = gameInfo.TitleName + developer = gameInfo.Developer + version = gameInfo.Version + icon = gameInfo.Icon + type = when { + (file.extension == "xci") -> FileType.Xci + (file.extension == "nsp") -> FileType.Nsp + (file.extension == "nro") -> FileType.Nro + else -> FileType.None + } + + if (type == FileType.Nro && (titleName.isNullOrEmpty() || titleName == "Unknown")) { + titleName = file.name + } + } + + fun open(): Int { + descriptor = context.contentResolver.openFileDescriptor(file.uri, "rw") + + return descriptor?.fd ?: 0 + } + + fun openUpdate(): Int { + if (titleId?.isNotEmpty() == true) { + val vm = TitleUpdateViewModel(titleId ?: "") + + if (vm.data?.selected?.isNotEmpty() == true) { + val uri = Uri.parse(vm.data?.selected) + val file = DocumentFile.fromSingleUri(context, uri) + if (file?.exists() == true) { + try { + updateDescriptor = + context.contentResolver.openFileDescriptor(file.uri, "rw") + + return updateDescriptor?.fd ?: -1 + } catch (e: Exception) { + return -2 + } + } + } + } + + return -1 + } + + fun close() { + descriptor?.close() + descriptor = null + updateDescriptor?.close() + updateDescriptor = null + } +} + +enum class FileType { + None, + Nsp, + Xci, + Nro +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt new file mode 100644 index 00000000..1a644c89 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt @@ -0,0 +1,101 @@ +package org.ryujinx.android.viewmodels + +import android.content.SharedPreferences +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.documentfile.provider.DocumentFile +import androidx.preference.PreferenceManager +import com.anggrayudi.storage.file.DocumentFileCompat +import com.anggrayudi.storage.file.DocumentFileType +import com.anggrayudi.storage.file.extension +import com.anggrayudi.storage.file.search +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.ryujinx.android.MainActivity +import java.util.Locale +import kotlin.concurrent.thread + +class HomeViewModel( + val activity: MainActivity? = null, + val mainViewModel: MainViewModel? = null +) { + private var shouldReload: Boolean = false + private var savedFolder: String = "" + private var loadedCache: MutableList = mutableListOf() + private var gameFolderPath: DocumentFile? = null + private var sharedPref: SharedPreferences? = null + val gameList: SnapshotStateList = SnapshotStateList() + val isLoading: MutableState = mutableStateOf(false) + + init { + if (activity != null) { + sharedPref = PreferenceManager.getDefaultSharedPreferences(activity) + } + } + + fun ensureReloadIfNecessary() { + val oldFolder = savedFolder + savedFolder = sharedPref?.getString("gameFolder", "") ?: "" + + if (savedFolder.isNotEmpty() && (shouldReload || savedFolder != oldFolder)) { + gameFolderPath = DocumentFileCompat.fromFullPath( + mainViewModel?.activity!!, + savedFolder, + documentType = DocumentFileType.FOLDER, + requiresWriteAccess = true + ) + + reloadGameList() + } + } + + fun filter(query: String) { + gameList.clear() + gameList.addAll(loadedCache.filter { + it.titleName != null && it.titleName!!.isNotEmpty() && (query.trim() + .isEmpty() || it.titleName!!.lowercase(Locale.getDefault()) + .contains(query)) + }) + } + + fun requestReload() { + shouldReload = true + } + + @OptIn(DelicateCoroutinesApi::class) + private fun reloadGameList() { + activity?.storageHelper ?: return + val folder = gameFolderPath ?: return + + shouldReload = false + if (isLoading.value) + return + + gameList.clear() + loadedCache.clear() + isLoading.value = true + + thread { + try { + for (file in folder.search(false, DocumentFileType.FILE)) { + if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro") + activity.let { + val item = GameModel(file, it) + + if (item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true && item.titleName != "Unknown") { + loadedCache.add(item) + } + } + } + } finally { + isLoading.value = false + GlobalScope.launch(Dispatchers.Main){ + filter("") + } + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt new file mode 100644 index 00000000..84d9ac8b --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -0,0 +1,414 @@ +package org.ryujinx.android.viewmodels + +import android.annotation.SuppressLint +import androidx.compose.runtime.MutableState +import androidx.navigation.NavHostController +import com.anggrayudi.storage.extension.launchOnUiThread +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Semaphore +import org.ryujinx.android.GameController +import org.ryujinx.android.GameHost +import org.ryujinx.android.Logging +import org.ryujinx.android.MainActivity +import org.ryujinx.android.MotionSensorManager +import org.ryujinx.android.NativeGraphicsInterop +import org.ryujinx.android.NativeHelpers +import org.ryujinx.android.PerformanceManager +import org.ryujinx.android.PhysicalControllerManager +import org.ryujinx.android.RegionCode +import org.ryujinx.android.RyujinxNative +import org.ryujinx.android.SystemLanguage +import java.io.File + +@SuppressLint("WrongConstant") +class MainViewModel(val activity: MainActivity) { + var physicalControllerManager: PhysicalControllerManager? = null + var motionSensorManager: MotionSensorManager? = null + var gameModel: GameModel? = null + var controller: GameController? = null + var performanceManager: PerformanceManager? = null + var selected: GameModel? = null + var isMiiEditorLaunched = false + val userViewModel = UserViewModel() + val logging = Logging(this) + var firmwareVersion = "" + private var gameTimeState: MutableState? = null + private var gameFpsState: MutableState? = null + private var fifoState: MutableState? = null + private var usedMemState: MutableState? = null + private var totalMemState: MutableState? = null + private var frequenciesState: MutableList? = null + private var progress: MutableState? = null + private var progressValue: MutableState? = null + private var showLoading: MutableState? = null + private var refreshUser: MutableState? = null + + var gameHost: GameHost? = null + set(value) { + field = value + field?.setProgressStates(showLoading, progressValue, progress) + } + var navController: NavHostController? = null + + var homeViewModel: HomeViewModel = HomeViewModel(activity, this) + + init { + performanceManager = PerformanceManager(activity) + } + + fun closeGame() { + RyujinxNative.jnaInstance.deviceSignalEmulationClose() + gameHost?.close() + RyujinxNative.jnaInstance.deviceCloseEmulation() + motionSensorManager?.unregister() + physicalControllerManager?.disconnect() + motionSensorManager?.setControllerId(-1) + } + + fun refreshFirmwareVersion() { + firmwareVersion = RyujinxNative.jnaInstance.deviceGetInstalledFirmwareVersion() + } + + fun loadGame(game: GameModel): Int { + val descriptor = game.open() + + if (descriptor == 0) + return 0 + + val update = game.openUpdate() + + if(update == -2) + { + return -2 + } + + gameModel = game + isMiiEditorLaunched = false + + val settings = QuickSettings(activity) + + var success = RyujinxNative.jnaInstance.graphicsInitialize( + enableShaderCache = settings.enableShaderCache, + enableTextureRecompression = settings.enableTextureRecompression, + rescale = settings.resScale, + backendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal + ) + + if (!success) + return 0 + + val nativeHelpers = NativeHelpers.instance + val nativeInterop = NativeGraphicsInterop() + nativeInterop.VkRequiredExtensions = arrayOf( + "VK_KHR_surface", "VK_KHR_android_surface" + ) + nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() + nativeInterop.SurfaceHandle = 0 + + val driverViewModel = VulkanDriverViewModel(activity) + val drivers = driverViewModel.getAvailableDrivers() + + var driverHandle = 0L + + if (driverViewModel.selected.isNotEmpty()) { + val metaData = drivers.find { it.driverPath == driverViewModel.selected } + + metaData?.apply { + val privatePath = activity.filesDir + val privateDriverPath = privatePath.canonicalPath + "/driver/" + val pD = File(privateDriverPath) + if (pD.exists()) + pD.deleteRecursively() + + pD.mkdirs() + + val driver = File(driverViewModel.selected) + val parent = driver.parentFile + if (parent != null) { + for (file in parent.walkTopDown()) { + if (file.absolutePath == parent.absolutePath) + continue + file.copyTo(File(privateDriverPath + file.name), true) + } + } + + driverHandle = NativeHelpers.instance.loadDriver( + activity.applicationInfo.nativeLibraryDir!! + "/", + privateDriverPath, + this.libraryName + ) + } + + } + + val extensions = nativeInterop.VkRequiredExtensions + + success = RyujinxNative.jnaInstance.graphicsInitializeRenderer( + extensions!!, + extensions.size, + driverHandle + ) + if (!success) + return 0 + + val semaphore = Semaphore(1, 0) + runBlocking { + semaphore.acquire() + launchOnUiThread { + // We are only able to initialize the emulation context on the main thread + success = RyujinxNative.jnaInstance.deviceInitialize( + settings.isHostMapped, + settings.useNce, + SystemLanguage.AmericanEnglish.ordinal, + RegionCode.USA.ordinal, + settings.enableVsync, + settings.enableDocked, + settings.enablePtc, + false, + "UTC", + settings.ignoreMissingServices + ) + + semaphore.release() + } + semaphore.acquire() + semaphore.release() + } + + if (!success) + return 0 + + success = + RyujinxNative.jnaInstance.deviceLoadDescriptor(descriptor, game.type.ordinal, update) + + return if (success) 1 else 0 + } + + fun loadMiiEditor(): Boolean { + gameModel = null + isMiiEditorLaunched = true + + val settings = QuickSettings(activity) + + var success = RyujinxNative.jnaInstance.graphicsInitialize( + enableShaderCache = settings.enableShaderCache, + enableTextureRecompression = settings.enableTextureRecompression, + rescale = settings.resScale, + backendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal + ) + + if (!success) + return false + + val nativeHelpers = NativeHelpers.instance + val nativeInterop = NativeGraphicsInterop() + nativeInterop.VkRequiredExtensions = arrayOf( + "VK_KHR_surface", "VK_KHR_android_surface" + ) + nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() + nativeInterop.SurfaceHandle = 0 + + val driverViewModel = VulkanDriverViewModel(activity) + val drivers = driverViewModel.getAvailableDrivers() + + var driverHandle = 0L + + if (driverViewModel.selected.isNotEmpty()) { + val metaData = drivers.find { it.driverPath == driverViewModel.selected } + + metaData?.apply { + val privatePath = activity.filesDir + val privateDriverPath = privatePath.canonicalPath + "/driver/" + val pD = File(privateDriverPath) + if (pD.exists()) + pD.deleteRecursively() + + pD.mkdirs() + + val driver = File(driverViewModel.selected) + val parent = driver.parentFile + if (parent != null) { + for (file in parent.walkTopDown()) { + if (file.absolutePath == parent.absolutePath) + continue + file.copyTo(File(privateDriverPath + file.name), true) + } + } + + driverHandle = NativeHelpers.instance.loadDriver( + activity.applicationInfo.nativeLibraryDir!! + "/", + privateDriverPath, + this.libraryName + ) + } + + } + + val extensions = nativeInterop.VkRequiredExtensions + + success = RyujinxNative.jnaInstance.graphicsInitializeRenderer( + extensions!!, + extensions.size, + driverHandle + ) + if (!success) + return false + + val semaphore = Semaphore(1, 0) + runBlocking { + semaphore.acquire() + launchOnUiThread { + // We are only able to initialize the emulation context on the main thread + success = RyujinxNative.jnaInstance.deviceInitialize( + settings.isHostMapped, + settings.useNce, + SystemLanguage.AmericanEnglish.ordinal, + RegionCode.USA.ordinal, + settings.enableVsync, + settings.enableDocked, + settings.enablePtc, + false, + "UTC", + settings.ignoreMissingServices + ) + + semaphore.release() + } + semaphore.acquire() + semaphore.release() + } + + if (!success) + return false + + success = RyujinxNative.jnaInstance.deviceLaunchMiiEditor() + + return success + } + + fun clearPptcCache(titleId: String) { + if (titleId.isNotEmpty()) { + val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu" + if (File(basePath).exists()) { + var caches = mutableListOf() + + val mainCache = basePath + "${File.separator}0" + File(mainCache).listFiles()?.forEach { + if (it.isFile && it.name.endsWith(".cache")) + caches.add(it.absolutePath) + } + val backupCache = basePath + "${File.separator}1" + File(backupCache).listFiles()?.forEach { + if (it.isFile && it.name.endsWith(".cache")) + caches.add(it.absolutePath) + } + for (path in caches) + File(path).delete() + } + } + } + + fun purgeShaderCache(titleId: String) { + if (titleId.isNotEmpty()) { + val basePath = MainActivity.AppPath + "/games/$titleId/cache/shader" + if (File(basePath).exists()) { + var caches = mutableListOf() + File(basePath).listFiles()?.forEach { + if (!it.isFile) + it.delete() + else { + if (it.name.endsWith(".toc") || it.name.endsWith(".data")) + caches.add(it.absolutePath) + } + } + for (path in caches) + File(path).delete() + } + } + } + + fun deleteCache(titleId: String) { + fun deleteDirectory(directory: File) { + if (directory.exists() && directory.isDirectory) { + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + deleteDirectory(file) + } else { + file.delete() + } + } + directory.delete() + } + } + if (titleId.isNotEmpty()) { + val basePath = MainActivity.AppPath + "/games/$titleId/cache" + if (File(basePath).exists()) { + deleteDirectory(File(basePath)) + } + } + } + + fun setStatStates( + fifo: MutableState, + gameFps: MutableState, + gameTime: MutableState, + usedMem: MutableState, + totalMem: MutableState, + frequencies: MutableList + ) { + fifoState = fifo + gameFpsState = gameFps + gameTimeState = gameTime + usedMemState = usedMem + totalMemState = totalMem + frequenciesState = frequencies + } + + fun updateStats( + fifo: Double, + gameFps: Double, + gameTime: Double + ) { + fifoState?.apply { + this.value = fifo + } + gameFpsState?.apply { + this.value = gameFps + } + gameTimeState?.apply { + this.value = gameTime + } + usedMemState?.let { usedMem -> + totalMemState?.let { totalMem -> + MainActivity.performanceMonitor.getMemoryUsage( + usedMem, + totalMem + ) + } + } + frequenciesState?.let { MainActivity.performanceMonitor.getFrequencies(it) } + } + + fun setGameController(controller: GameController) { + this.controller = controller + } + + fun navigateToGame() { + activity.setFullScreen(true) + navController?.navigate("game") + activity.isGameRunning = true + if (QuickSettings(activity).enableMotion) + motionSensorManager?.register() + } + + fun setProgressStates( + showLoading: MutableState, + progressValue: MutableState, + progress: MutableState + ) { + this.showLoading = showLoading + this.progressValue = progressValue + this.progress = progress + gameHost?.setProgressStates(showLoading, progressValue, progress) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt new file mode 100644 index 00000000..70f625e0 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt @@ -0,0 +1,97 @@ +package org.ryujinx.android.viewmodels + +import android.app.Activity +import android.content.SharedPreferences +import androidx.preference.PreferenceManager + +class QuickSettings(val activity: Activity) { + var ignoreMissingServices: Boolean + var enablePtc: Boolean + var enableDocked: Boolean + var enableVsync: Boolean + var useNce: Boolean + var useVirtualController: Boolean + var isHostMapped: Boolean + var enableShaderCache: Boolean + var enableTextureRecompression: Boolean + var resScale: Float + var isGrid: Boolean + var useSwitchLayout: Boolean + var enableMotion: Boolean + var enablePerformanceMode: Boolean + var controllerStickSensitivity: Float + + // Logs + var enableDebugLogs: Boolean + var enableStubLogs: Boolean + var enableInfoLogs: Boolean + var enableWarningLogs: Boolean + var enableErrorLogs: Boolean + var enableGuestLogs: Boolean + var enableAccessLogs: Boolean + var enableTraceLogs: Boolean + var enableGraphicsLogs: Boolean + + private var sharedPref: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(activity) + + init { + isHostMapped = sharedPref.getBoolean("isHostMapped", true) + useNce = sharedPref.getBoolean("useNce", true) + enableVsync = sharedPref.getBoolean("enableVsync", true) + enableDocked = sharedPref.getBoolean("enableDocked", true) + enablePtc = sharedPref.getBoolean("enablePtc", true) + ignoreMissingServices = sharedPref.getBoolean("ignoreMissingServices", false) + enableShaderCache = sharedPref.getBoolean("enableShaderCache", true) + enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) + resScale = sharedPref.getFloat("resScale", 1f) + useVirtualController = sharedPref.getBoolean("useVirtualController", true) + isGrid = sharedPref.getBoolean("isGrid", true) + useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true) + enableMotion = sharedPref.getBoolean("enableMotion", true) + enablePerformanceMode = sharedPref.getBoolean("enablePerformanceMode", true) + controllerStickSensitivity = sharedPref.getFloat("controllerStickSensitivity", 1.0f) + + enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false) + enableStubLogs = sharedPref.getBoolean("enableStubLogs", false) + enableInfoLogs = sharedPref.getBoolean("enableInfoLogs", true) + enableWarningLogs = sharedPref.getBoolean("enableWarningLogs", true) + enableErrorLogs = sharedPref.getBoolean("enableErrorLogs", true) + enableGuestLogs = sharedPref.getBoolean("enableGuestLogs", true) + enableAccessLogs = sharedPref.getBoolean("enableAccessLogs", false) + enableTraceLogs = sharedPref.getBoolean("enableStubLogs", false) + enableGraphicsLogs = sharedPref.getBoolean("enableGraphicsLogs", false) + } + + fun save() { + val editor = sharedPref.edit() + + editor.putBoolean("isHostMapped", isHostMapped) + editor.putBoolean("useNce", useNce) + editor.putBoolean("enableVsync", enableVsync) + editor.putBoolean("enableDocked", enableDocked) + editor.putBoolean("enablePtc", enablePtc) + editor.putBoolean("ignoreMissingServices", ignoreMissingServices) + editor.putBoolean("enableShaderCache", enableShaderCache) + editor.putBoolean("enableTextureRecompression", enableTextureRecompression) + editor.putFloat("resScale", resScale) + editor.putBoolean("useVirtualController", useVirtualController) + editor.putBoolean("isGrid", isGrid) + editor.putBoolean("useSwitchLayout", useSwitchLayout) + editor.putBoolean("enableMotion", enableMotion) + editor.putBoolean("enablePerformanceMode", enablePerformanceMode) + editor.putFloat("controllerStickSensitivity", controllerStickSensitivity) + + editor.putBoolean("enableDebugLogs", enableDebugLogs) + editor.putBoolean("enableStubLogs", enableStubLogs) + editor.putBoolean("enableInfoLogs", enableInfoLogs) + editor.putBoolean("enableWarningLogs", enableWarningLogs) + editor.putBoolean("enableErrorLogs", enableErrorLogs) + editor.putBoolean("enableGuestLogs", enableGuestLogs) + editor.putBoolean("enableAccessLogs", enableAccessLogs) + editor.putBoolean("enableTraceLogs", enableTraceLogs) + editor.putBoolean("enableGraphicsLogs", enableGraphicsLogs) + + editor.apply() + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt new file mode 100644 index 00000000..fdf28278 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt @@ -0,0 +1,287 @@ +package org.ryujinx.android.viewmodels + +import android.content.SharedPreferences +import androidx.compose.runtime.MutableState +import androidx.documentfile.provider.DocumentFile +import androidx.navigation.NavHostController +import androidx.preference.PreferenceManager +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.FileFullPath +import com.anggrayudi.storage.file.copyFileTo +import com.anggrayudi.storage.file.extension +import com.anggrayudi.storage.file.getAbsolutePath +import org.ryujinx.android.LogLevel +import org.ryujinx.android.MainActivity +import org.ryujinx.android.RyujinxNative +import java.io.File +import kotlin.concurrent.thread + +class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) { + var selectedFirmwareVersion: String = "" + private var previousFileCallback: ((requestCode: Int, files: List) -> Unit)? + private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)? + private var sharedPref: SharedPreferences + var selectedFirmwareFile: DocumentFile? = null + + init { + sharedPref = getPreferences() + previousFolderCallback = activity.storageHelper!!.onFolderSelected + previousFileCallback = activity.storageHelper!!.onFileSelected + activity.storageHelper!!.onFolderSelected = { _, folder -> + run { + val p = folder.getAbsolutePath(activity) + val editor = sharedPref.edit() + editor?.putString("gameFolder", p) + editor?.apply() + } + } + } + + private fun getPreferences(): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(activity) + } + + fun initializeState( + isHostMapped: MutableState, + useNce: MutableState, + enableVsync: MutableState, + enableDocked: MutableState, + enablePtc: MutableState, + ignoreMissingServices: MutableState, + enableShaderCache: MutableState, + enableTextureRecompression: MutableState, + resScale: MutableState, + useVirtualController: MutableState, + isGrid: MutableState, + useSwitchLayout: MutableState, + enableMotion: MutableState, + enablePerformanceMode: MutableState, + controllerStickSensitivity: MutableState, + enableDebugLogs: MutableState, + enableStubLogs: MutableState, + enableInfoLogs: MutableState, + enableWarningLogs: MutableState, + enableErrorLogs: MutableState, + enableGuestLogs: MutableState, + enableAccessLogs: MutableState, + enableTraceLogs: MutableState, + enableGraphicsLogs: MutableState + ) { + + isHostMapped.value = sharedPref.getBoolean("isHostMapped", true) + useNce.value = sharedPref.getBoolean("useNce", true) + enableVsync.value = sharedPref.getBoolean("enableVsync", true) + enableDocked.value = sharedPref.getBoolean("enableDocked", true) + enablePtc.value = sharedPref.getBoolean("enablePtc", true) + ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false) + enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true) + enableTextureRecompression.value = + sharedPref.getBoolean("enableTextureRecompression", false) + resScale.value = sharedPref.getFloat("resScale", 1f) + useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) + isGrid.value = sharedPref.getBoolean("isGrid", true) + useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true) + enableMotion.value = sharedPref.getBoolean("enableMotion", true) + enablePerformanceMode.value = sharedPref.getBoolean("enablePerformanceMode", false) + controllerStickSensitivity.value = sharedPref.getFloat("controllerStickSensitivity", 1.0f) + + enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false) + enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false) + enableInfoLogs.value = sharedPref.getBoolean("enableInfoLogs", true) + enableWarningLogs.value = sharedPref.getBoolean("enableWarningLogs", true) + enableErrorLogs.value = sharedPref.getBoolean("enableErrorLogs", true) + enableGuestLogs.value = sharedPref.getBoolean("enableGuestLogs", true) + enableAccessLogs.value = sharedPref.getBoolean("enableAccessLogs", false) + enableTraceLogs.value = sharedPref.getBoolean("enableStubLogs", false) + enableGraphicsLogs.value = sharedPref.getBoolean("enableGraphicsLogs", false) + } + + fun save( + isHostMapped: MutableState, + useNce: MutableState, + enableVsync: MutableState, + enableDocked: MutableState, + enablePtc: MutableState, + ignoreMissingServices: MutableState, + enableShaderCache: MutableState, + enableTextureRecompression: MutableState, + resScale: MutableState, + useVirtualController: MutableState, + isGrid: MutableState, + useSwitchLayout: MutableState, + enableMotion: MutableState, + enablePerformanceMode: MutableState, + controllerStickSensitivity: MutableState, + enableDebugLogs: MutableState, + enableStubLogs: MutableState, + enableInfoLogs: MutableState, + enableWarningLogs: MutableState, + enableErrorLogs: MutableState, + enableGuestLogs: MutableState, + enableAccessLogs: MutableState, + enableTraceLogs: MutableState, + enableGraphicsLogs: MutableState + ) { + val editor = sharedPref.edit() + + editor.putBoolean("isHostMapped", isHostMapped.value) + editor.putBoolean("useNce", useNce.value) + editor.putBoolean("enableVsync", enableVsync.value) + editor.putBoolean("enableDocked", enableDocked.value) + editor.putBoolean("enablePtc", enablePtc.value) + editor.putBoolean("ignoreMissingServices", ignoreMissingServices.value) + editor.putBoolean("enableShaderCache", enableShaderCache.value) + editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value) + editor.putFloat("resScale", resScale.value) + editor.putBoolean("useVirtualController", useVirtualController.value) + editor.putBoolean("isGrid", isGrid.value) + editor.putBoolean("useSwitchLayout", useSwitchLayout.value) + editor.putBoolean("enableMotion", enableMotion.value) + editor.putBoolean("enablePerformanceMode", enablePerformanceMode.value) + editor.putFloat("controllerStickSensitivity", controllerStickSensitivity.value) + + editor.putBoolean("enableDebugLogs", enableDebugLogs.value) + editor.putBoolean("enableStubLogs", enableStubLogs.value) + editor.putBoolean("enableInfoLogs", enableInfoLogs.value) + editor.putBoolean("enableWarningLogs", enableWarningLogs.value) + editor.putBoolean("enableErrorLogs", enableErrorLogs.value) + editor.putBoolean("enableGuestLogs", enableGuestLogs.value) + editor.putBoolean("enableAccessLogs", enableAccessLogs.value) + editor.putBoolean("enableTraceLogs", enableTraceLogs.value) + editor.putBoolean("enableGraphicsLogs", enableGraphicsLogs.value) + + editor.apply() + activity.storageHelper!!.onFolderSelected = previousFolderCallback + + RyujinxNative.jnaInstance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value) + RyujinxNative.jnaInstance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value) + RyujinxNative.jnaInstance.loggingSetEnabled(LogLevel.Stub.ordinal, enableStubLogs.value) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.Warning.ordinal, + enableWarningLogs.value + ) + RyujinxNative.jnaInstance.loggingSetEnabled(LogLevel.Error.ordinal, enableErrorLogs.value) + RyujinxNative.jnaInstance.loggingSetEnabled( + LogLevel.AccessLog.ordinal, + enableAccessLogs.value + ) + RyujinxNative.jnaInstance.loggingSetEnabled(LogLevel.Guest.ordinal, enableGuestLogs.value) + RyujinxNative.jnaInstance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value) + RyujinxNative.jnaInstance.loggingEnabledGraphicsLog(enableGraphicsLogs.value) + } + + fun openGameFolder() { + val path = sharedPref.getString("gameFolder", "") ?: "" + + if (path.isEmpty()) + activity.storageHelper?.storage?.openFolderPicker() + else + activity.storageHelper?.storage?.openFolderPicker( + activity.storageHelper!!.storage.requestCodeFolderPicker, + FileFullPath(activity, path) + ) + } + + fun importProdKeys() { + activity.storageHelper!!.onFileSelected = { _, files -> + run { + activity.storageHelper!!.onFileSelected = previousFileCallback + val file = files.firstOrNull() + file?.apply { + if (name == "prod.keys") { + val outputFile = File(MainActivity.AppPath + "/system") + outputFile.delete() + + thread { + file.copyFileTo( + activity, + outputFile, + callback = object : FileCallback() { + }) + } + } + } + } + } + activity.storageHelper?.storage?.openFilePicker() + } + + fun selectFirmware(installState: MutableState) { + if (installState.value != FirmwareInstallState.None) + return + activity.storageHelper!!.onFileSelected = { _, files -> + run { + activity.storageHelper!!.onFileSelected = previousFileCallback + val file = files.firstOrNull() + file?.apply { + if (extension == "xci" || extension == "zip") { + installState.value = FirmwareInstallState.Verifying + thread { + val descriptor = + activity.contentResolver.openFileDescriptor(file.uri, "rw") + descriptor?.use { d -> + selectedFirmwareVersion = + RyujinxNative.jnaInstance.deviceVerifyFirmware( + d.fd, + extension == "xci" + ) + selectedFirmwareFile = file + if (selectedFirmwareVersion.isEmpty()) { + installState.value = FirmwareInstallState.Query + } else { + installState.value = FirmwareInstallState.Cancelled + } + } + } + } else { + installState.value = FirmwareInstallState.Cancelled + } + } + } + } + activity.storageHelper?.storage?.openFilePicker() + } + + fun installFirmware(installState: MutableState) { + if (installState.value != FirmwareInstallState.Query) + return + if (selectedFirmwareFile == null) { + installState.value = FirmwareInstallState.None + return + } + selectedFirmwareFile?.apply { + val descriptor = + activity.contentResolver.openFileDescriptor(uri, "rw") + descriptor?.use { d -> + installState.value = FirmwareInstallState.Install + thread { + try { + RyujinxNative.jnaInstance.deviceInstallFirmware( + d.fd, + extension == "xci" + ) + } finally { + MainActivity.mainViewModel?.refreshFirmwareVersion() + installState.value = FirmwareInstallState.Done + } + } + } + } + } + + fun clearFirmwareSelection(installState: MutableState) { + selectedFirmwareFile = null + selectedFirmwareVersion = "" + installState.value = FirmwareInstallState.None + } +} + + +enum class FirmwareInstallState { + None, + Cancelled, + Verifying, + Query, + Install, + Done +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt new file mode 100644 index 00000000..1eea840a --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt @@ -0,0 +1,171 @@ +package org.ryujinx.android.viewmodels + +import android.content.Intent +import android.net.Uri +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.file.extension +import com.google.gson.Gson +import org.ryujinx.android.MainActivity +import java.io.File +import kotlin.math.max + +class TitleUpdateViewModel(val titleId: String) { + private var canClose: MutableState? = null + private var basePath: String + private var updateJsonName = "updates.json" + private var storageHelper: SimpleStorageHelper + private var currentPaths: MutableList = mutableListOf() + private var pathsState: SnapshotStateList? = null + + companion object { + const val UpdateRequestCode = 1002 + } + + fun remove(index: Int) { + if (index <= 0) + return + + data?.paths?.apply { + val str = removeAt(index - 1) + Uri.parse(str)?.apply { + storageHelper.storage.context.contentResolver.releasePersistableUriPermission( + this, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + pathsState?.clear() + pathsState?.addAll(this) + currentPaths = this + } + } + + fun add() { + val callBack = storageHelper.onFileSelected + + storageHelper.onFileSelected = { requestCode, files -> + run { + storageHelper.onFileSelected = callBack + if (requestCode == UpdateRequestCode) { + val file = files.firstOrNull() + file?.apply { + if (file.extension == "nsp") { + storageHelper.storage.context.contentResolver.takePersistableUriPermission( + file.uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + currentPaths.add(file.uri.toString()) + } + } + + refreshPaths() + } + } + } + storageHelper.openFilePicker(UpdateRequestCode) + } + + private fun refreshPaths() { + data?.apply { + val existingPaths = mutableListOf() + currentPaths.forEach { + val uri = Uri.parse(it) + val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri) + if (file?.exists() == true) { + existingPaths.add(it) + } + } + + if (!existingPaths.contains(selected)) { + selected = "" + } + pathsState?.clear() + pathsState?.addAll(existingPaths) + paths = existingPaths + canClose?.apply { + value = true + } + } + } + + fun save( + index: Int, + openDialog: MutableState + ) { + data?.apply { + this.selected = "" + if (paths.isNotEmpty() && index > 0) { + val ind = max(index - 1, paths.count() - 1) + this.selected = paths[ind] + } + val gson = Gson() + File(basePath).mkdirs() + + + val metadata = TitleUpdateMetadata() + val savedUpdates = mutableListOf() + currentPaths.forEach { + val uri = Uri.parse(it) + val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri) + if (file?.exists() == true) { + savedUpdates.add(it) + } + } + metadata.paths = savedUpdates + + if (selected.isNotEmpty()) { + val uri = Uri.parse(selected) + val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri) + if (file?.exists() == true) { + metadata.selected = selected + } + } else { + metadata.selected = selected + } + + val json = gson.toJson(metadata) + File("$basePath/$updateJsonName").writeText(json) + + openDialog.value = false + } + } + + fun setPaths(paths: SnapshotStateList, canClose: MutableState) { + pathsState = paths + this.canClose = canClose + data?.apply { + pathsState?.clear() + pathsState?.addAll(this.paths) + } + } + + var data: TitleUpdateMetadata? = null + private var jsonPath: String + + init { + basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + jsonPath = "${basePath}/${updateJsonName}" + + data = TitleUpdateMetadata() + if (File(jsonPath).exists()) { + val gson = Gson() + data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) + + } + currentPaths = data?.paths ?: mutableListOf() + storageHelper = MainActivity.StorageHelper!! + refreshPaths() + + File("$basePath/update").deleteRecursively() + + } +} + +data class TitleUpdateMetadata( + var selected: String = "", + var paths: MutableList = mutableListOf() +) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt new file mode 100644 index 00000000..50cccb6c --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt @@ -0,0 +1,77 @@ +package org.ryujinx.android.viewmodels + +import org.ryujinx.android.RyujinxNative +import java.util.Base64 + +class UserViewModel { + var openedUser = UserModel() + val userList = mutableListOf() + + init { + refreshUsers() + } + + fun refreshUsers() { + userList.clear() + val decoder = Base64.getDecoder() + openedUser = UserModel() + openedUser.id = RyujinxNative.jnaInstance.userGetOpenedUser() + if (openedUser.id.isNotEmpty()) { + openedUser.username = RyujinxNative.jnaInstance.userGetUserName(openedUser.id) + openedUser.userPicture = decoder.decode( + RyujinxNative.jnaInstance.userGetUserPicture( + openedUser.id + ) + ) + } + + val users = RyujinxNative.jnaInstance.userGetAllUsers() + for (user in users) { + userList.add( + UserModel( + user, + RyujinxNative.jnaInstance.userGetUserName(user), + decoder.decode( + RyujinxNative.jnaInstance.userGetUserPicture(user) + ) + ) + ) + } + } + + fun openUser(userModel: UserModel) { + RyujinxNative.jnaInstance.userOpenUser(userModel.id) + + refreshUsers() + } +} + + +data class UserModel( + var id: String = "", + var username: String = "", + var userPicture: ByteArray? = null +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserModel + + if (id != other.id) return false + if (username != other.username) return false + if (userPicture != null) { + if (other.userPicture == null) return false + if (!userPicture.contentEquals(other.userPicture)) return false + } else if (other.userPicture != null) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + (userPicture?.contentHashCode() ?: 0) + return result + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt new file mode 100644 index 00000000..a82dafe5 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt @@ -0,0 +1,167 @@ +package org.ryujinx.android.viewmodels + +import androidx.compose.runtime.MutableState +import com.anggrayudi.storage.file.extension +import com.anggrayudi.storage.file.openInputStream +import com.google.gson.Gson +import org.ryujinx.android.MainActivity +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipInputStream + +class VulkanDriverViewModel(val activity: MainActivity) { + var selected: String = "" + + companion object { + const val DriverRequestCode: Int = 1003 + const val DriverFolder: String = "drivers" + } + + private fun getAppPath(): String { + var appPath = + MainActivity.AppPath + appPath += "/" + + return appPath + } + + fun ensureDriverPath(): File { + val driverPath = getAppPath() + DriverFolder + + val driverFolder = File(driverPath) + + if (!driverFolder.exists()) + driverFolder.mkdirs() + + return driverFolder + } + + fun getAvailableDrivers(): MutableList { + val driverFolder = ensureDriverPath() + + val folders = driverFolder.walkTopDown() + + val drivers = mutableListOf() + + val selectedDriverFile = File(driverFolder.absolutePath + "/selected") + if (selectedDriverFile.exists()) { + selected = selectedDriverFile.readText() + + if (!File(selected).exists()) { + selected = "" + saveSelected() + } + } + + val gson = Gson() + + for (folder in folders) { + if (folder.isDirectory && folder.parent == driverFolder.absolutePath) { + val meta = File(folder.absolutePath + "/meta.json") + + if (meta.exists()) { + val metadata = gson.fromJson(meta.readText(), DriverMetadata::class.java) + if (metadata.name.isNotEmpty()) { + val driver = folder.absolutePath + "/${metadata.libraryName}" + metadata.driverPath = driver + if (File(driver).exists()) + drivers.add(metadata) + } + } + } + } + + return drivers + } + + fun saveSelected() { + val driverFolder = ensureDriverPath() + + val selectedDriverFile = File(driverFolder.absolutePath + "/selected") + selectedDriverFile.writeText(selected) + } + + fun removeSelected() { + if (selected.isNotEmpty()) { + val sel = File(selected) + if (sel.exists()) { + sel.parentFile?.deleteRecursively() + } + selected = "" + + saveSelected() + } + } + + fun add(refresh: MutableState) { + activity.storageHelper?.apply { + + val callBack = this.onFileSelected + + onFileSelected = { requestCode, files -> + run { + onFileSelected = callBack + if (requestCode == DriverRequestCode) { + val file = files.firstOrNull() + file?.apply { + val stream = file.openInputStream(storage.context) + stream?.apply { + val name = file.name?.removeSuffix("." + file.extension) ?: "" + val driverFolder = ensureDriverPath() + val extractionFolder = File(driverFolder.absolutePath + "/${name}") + extractionFolder.deleteRecursively() + extractionFolder.mkdirs() + ZipInputStream(stream).use { zip -> + var entry = zip.nextEntry + while (entry != null) { + val filePath = + extractionFolder.absolutePath + File.separator + entry.name + + if (!entry.isDirectory) { + File(filePath).delete() + val bos = + BufferedOutputStream(FileOutputStream(filePath)) + val bytesIn = ByteArray(4096) + var read: Int + while (zip.read(bytesIn) + .also { read = it } != -1 + ) { + bos.write(bytesIn, 0, read) + } + bos.close() + } else { + val dir = File(filePath) + dir.mkdir() + } + + entry = zip.nextEntry + } + } + } + } + + refresh.value = true + } + } + } + openFilePicker( + DriverRequestCode, + filterMimeTypes = arrayOf("application/zip") + ) + } + } +} + +data class DriverMetadata( + var schemaVersion: Int = 0, + var name: String = "", + var description: String = "", + var author: String = "", + var packageVersion: String = "", + var vendor: String = "", + var driverVersion: String = "", + var minApi: Int = 0, + var libraryName: String = "", + var driverPath: String = "" +) \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt new file mode 100644 index 00000000..529033c7 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt @@ -0,0 +1,142 @@ +package org.ryujinx.android.views + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.ryujinx.android.viewmodels.DlcItem +import org.ryujinx.android.viewmodels.DlcViewModel + +class DlcViews { + companion object { + @Composable + fun Main(titleId: String, name: String, openDialog: MutableState) { + val viewModel = DlcViewModel(titleId) + + var dlcList = remember { + mutableListOf() + } + + viewModel.data?.apply { + dlcList.clear() + } + + var refresh = remember { + mutableStateOf(true) + } + + Column(modifier = Modifier.padding(16.dp)) { + Column { + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "DLC for ${name}", + textAlign = TextAlign.Center, + modifier = Modifier.align( + Alignment.CenterVertically + ) + ) + } + Surface( + modifier = Modifier + .padding(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium + ) { + + if (refresh.value) { + dlcList.clear() + dlcList.addAll(viewModel.getDlc()) + refresh.value = false + } + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + ) { + items(dlcList) { dlcItem -> + dlcItem.apply { + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + Checkbox( + checked = (dlcItem.isEnabled.value), + onCheckedChange = { dlcItem.isEnabled.value = it }) + Text( + text = dlcItem.name, + modifier = Modifier + .align(Alignment.CenterVertically) + .wrapContentWidth(Alignment.Start) + .fillMaxWidth(0.9f) + ) + IconButton( + onClick = { + viewModel.remove(dlcItem) + refresh.value = true + }) { + Icon( + Icons.Filled.Delete, + contentDescription = "remove" + ) + } + } + } + } + } + } + + } + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.align(Alignment.End)) { + TextButton( + modifier = Modifier.padding(4.dp), + onClick = { + viewModel.add(refresh) + } + ) { + + Text("Add") + } + TextButton( + modifier = Modifier.padding(4.dp), + onClick = { + openDialog.value = false + viewModel.save(dlcList) + }, + ) { + Text("Save") + } + } + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt new file mode 100644 index 00000000..9fb01d87 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt @@ -0,0 +1,403 @@ +package org.ryujinx.android.views + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup +import compose.icons.CssGgIcons +import compose.icons.cssggicons.ToolbarBottom +import org.ryujinx.android.GameController +import org.ryujinx.android.GameHost +import org.ryujinx.android.Icons +import org.ryujinx.android.MainActivity +import org.ryujinx.android.RyujinxNative +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings +import kotlin.math.roundToInt + +class GameViews { + companion object { + @Composable + fun Main() { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + GameView(mainViewModel = MainActivity.mainViewModel!!) + } + } + + @Composable + fun GameView(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + GameHost(context, mainViewModel) + } + ) + GameOverlay(mainViewModel) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun GameOverlay(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + GameStats(mainViewModel) + + val showController = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController) + } + val enableVsync = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) + } + val enableMotion = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).enableMotion) + } + val showMore = remember { + mutableStateOf(false) + } + + val showLoading = remember { + mutableStateOf(true) + } + + val progressValue = remember { + mutableStateOf(0.0f) + } + + val progress = remember { + mutableStateOf("Loading") + } + + mainViewModel.setProgressStates(showLoading, progressValue, progress) + + // touch surface + Surface(color = Color.Transparent, modifier = Modifier + .fillMaxSize() + .padding(0.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (showController.value) + continue + + val change = event + .component1() + .firstOrNull() + change?.apply { + val position = this.position + + when (event.type) { + PointerEventType.Press -> { + RyujinxNative.jnaInstance.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + } + + PointerEventType.Release -> { + RyujinxNative.jnaInstance.inputReleaseTouchPoint() + + } + + PointerEventType.Move -> { + RyujinxNative.jnaInstance.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + + } + } + } + } + } + }) { + } + if (!showLoading.value) { + GameController.Compose(mainViewModel) + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp) + ) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = true + }) { + Icon( + imageVector = CssGgIcons.ToolbarBottom, + contentDescription = "Open Panel" + ) + } + } + + if (showMore.value) { + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = { showMore.value = false }) { + Surface( + modifier = Modifier.padding(16.dp), + shape = MaterialTheme.shapes.medium + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Motion", + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp) + ) + Switch(checked = enableMotion.value, onCheckedChange = { + showMore.value = false + enableMotion.value = !enableMotion.value + val settings = QuickSettings(mainViewModel.activity) + settings.enableMotion = enableMotion.value + settings.save() + if (enableMotion.value) + mainViewModel.motionSensorManager?.register() + else + mainViewModel.motionSensorManager?.unregister() + }) + } + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + showController.value = !showController.value + RyujinxNative.jnaInstance.inputReleaseTouchPoint() + mainViewModel.controller?.setVisible(showController.value) + }) { + Icon( + imageVector = Icons.videoGame(), + contentDescription = "Toggle Virtual Pad" + ) + } + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + enableVsync.value = !enableVsync.value + RyujinxNative.jnaInstance.graphicsRendererSetVsync( + enableVsync.value + ) + }) { + Icon( + imageVector = Icons.vSync(), + tint = if (enableVsync.value) Color.Green else Color.Red, + contentDescription = "Toggle VSync" + ) + } + } + } + } + } + } + } + + val showBackNotice = remember { + mutableStateOf(false) + } + + BackHandler { + showBackNotice.value = true + } + + if (showLoading.value) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(0.5f) + .align(Alignment.Center), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = progress.value) + + if (progressValue.value > -1) + LinearProgressIndicator( + progress = { + progressValue.value + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + ) + else + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } + + } + } + + if (showBackNotice.value) { + BasicAlertDialog(onDismissRequest = { showBackNotice.value = false }) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Are you sure you want to exit the game?") + Text(text = "All unsaved data will be lost!") + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Button(onClick = { + showBackNotice.value = false + mainViewModel.closeGame() + mainViewModel.activity.setFullScreen(false) + mainViewModel.navController?.popBackStack() + mainViewModel.activity.isGameRunning = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Exit Game") + } + + Button(onClick = { + showBackNotice.value = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Dismiss") + } + } + } + } + } + } + } + + mainViewModel.activity.uiHandler.Compose() + } + } + + @Composable + fun GameStats(mainViewModel: MainViewModel) { + val fifo = remember { + mutableDoubleStateOf(0.0) + } + val gameFps = remember { + mutableDoubleStateOf(0.0) + } + val gameTime = remember { + mutableDoubleStateOf(0.0) + } + val usedMem = remember { + mutableIntStateOf(0) + } + val totalMem = remember { + mutableIntStateOf(0) + } + val frequencies = remember { + mutableListOf() + } + + Surface( + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.background.copy(0.4f) + ) { + CompositionLocalProvider(LocalTextStyle provides TextStyle(fontSize = 10.sp)) { + Column { + var gameTimeVal = 0.0 + if (!gameTime.value.isInfinite()) + gameTimeVal = gameTime.value + Text(text = "${String.format("%.3f", fifo.value)} %") + Text(text = "${String.format("%.3f", gameFps.value)} FPS") + Text(text = "${String.format("%.3f", gameTimeVal)} ms") + Box(modifier = Modifier.width(96.dp)) { + Column { + LazyColumn { + itemsIndexed(frequencies) { i, t -> + Row { + Text( + modifier = Modifier.padding(2.dp), + text = "CPU $i" + ) + Spacer(Modifier.weight(1f)) + Text(text = "$t MHz") + } + } + } + Row { + Text(modifier = Modifier.padding(2.dp), text = "Used") + Spacer(Modifier.weight(1f)) + Text(text = "${usedMem.value} MB") + } + Row { + Text(modifier = Modifier.padding(2.dp), text = "Total") + Spacer(Modifier.weight(1f)) + Text(text = "${totalMem.value} MB") + } + } + } + } + } + } + + mainViewModel.setStatStates(fifo, gameFps, gameTime, usedMem, totalMem, frequencies) + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt new file mode 100644 index 00000000..aab0d2c0 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -0,0 +1,803 @@ +package org.ryujinx.android.views + +import android.content.res.Resources +import android.graphics.BitmapFactory +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.anggrayudi.storage.extension.launchOnUiThread +import org.ryujinx.android.R +import org.ryujinx.android.viewmodels.FileType +import org.ryujinx.android.viewmodels.GameModel +import org.ryujinx.android.viewmodels.HomeViewModel +import org.ryujinx.android.viewmodels.QuickSettings +import java.util.Base64 +import java.util.Locale +import kotlin.concurrent.thread +import kotlin.math.roundToInt + +class HomeViews { + companion object { + const val ListImageSize = 150 + const val GridImageSize = 300 + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + @Composable + fun Home( + viewModel: HomeViewModel = HomeViewModel(), + navController: NavHostController? = null, + isPreview: Boolean = false + ) { + viewModel.ensureReloadIfNecessary() + val showAppActions = remember { mutableStateOf(false) } + val showLoading = remember { mutableStateOf(false) } + val openTitleUpdateDialog = remember { mutableStateOf(false) } + val canClose = remember { mutableStateOf(true) } + val openDlcDialog = remember { mutableStateOf(false) } + var openAppBarExtra by remember { mutableStateOf(false) } + val showError = remember { + mutableStateOf("") + } + + val selectedModel = remember { + mutableStateOf(viewModel.mainViewModel?.selected) + } + val query = remember { + mutableStateOf("") + } + var refreshUser by remember { + mutableStateOf(true) + } + + var isFabVisible by remember { + mutableStateOf(true) + } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + if (available.y < -1) { + isFabVisible = false + } + if (available.y > 1) { + isFabVisible = true + } + return Offset.Zero + } + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + SearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + shape = SearchBarDefaults.inputFieldShape, + query = query.value, + onQueryChange = { + query.value = it + }, + onSearch = {}, + active = false, + onActiveChange = {}, + leadingIcon = { + Icon( + Icons.Filled.Search, + contentDescription = "Search Games" + ) + }, + placeholder = { + Text(text = "Ryujinx") + }, + trailingIcon = { + IconButton(onClick = { + openAppBarExtra = !openAppBarExtra + }) { + if (!refreshUser) { + refreshUser = true + } + if (refreshUser) + if (viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { + val pic = + viewModel.mainViewModel.userViewModel.openedUser.userPicture + Image( + bitmap = BitmapFactory.decodeByteArray( + pic, + 0, + pic?.size ?: 0 + ) + .asImageBitmap(), + contentDescription = "user image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(52.dp) + .clip(CircleShape) + ) + } else { + Icon( + Icons.Filled.Person, + contentDescription = "user" + ) + } + } + } + ) { + + } + }, + floatingActionButton = { + AnimatedVisibility(visible = isFabVisible, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 })) { + FloatingActionButton( + onClick = { + viewModel.requestReload() + viewModel.ensureReloadIfNecessary() + }, + shape = MaterialTheme.shapes.small + ) { + Icon(Icons.Default.Refresh, contentDescription = "refresh") + } + } + } + + ) { contentPadding -> + Column(modifier = Modifier.padding(contentPadding)) { + val iconSize = 52.dp + AnimatedVisibility( + visible = openAppBarExtra, + ) + { + Card( + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (refreshUser) { + Box( + modifier = Modifier + .border( + width = 2.dp, + color = Color(0xFF14bf00), + shape = CircleShape + ) + .size(iconSize) + .padding(2.dp), + contentAlignment = Alignment.Center + ) { + if (viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { + val pic = + viewModel.mainViewModel.userViewModel.openedUser.userPicture + Image( + bitmap = BitmapFactory.decodeByteArray( + pic, + 0, + pic?.size ?: 0 + ) + .asImageBitmap(), + contentDescription = "user image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(iconSize) + .clip(CircleShape) + ) + } else { + Icon( + Icons.Filled.Person, + contentDescription = "user" + ) + } + } + Card( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(0.7f), + shape = MaterialTheme.shapes.small, + ) { + LazyRow { + if (viewModel.mainViewModel?.userViewModel?.userList?.isNotEmpty() == true) { + items(viewModel.mainViewModel.userViewModel.userList) { user -> + if (user.id != viewModel.mainViewModel.userViewModel.openedUser.id) { + Image( + bitmap = BitmapFactory.decodeByteArray( + user.userPicture, + 0, + user.userPicture?.size ?: 0 + ) + .asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(iconSize) + .clip(CircleShape) + .combinedClickable( + onClick = { + viewModel.mainViewModel.userViewModel.openUser( + user + ) + refreshUser = + false + }) + ) + } + } + } + } + } + Box( + modifier = Modifier + .size(iconSize) + ) { + IconButton( + modifier = Modifier.fillMaxSize(), + onClick = { + openAppBarExtra = false + navController?.navigate("user") + }) { + Icon( + Icons.Filled.Add, + contentDescription = "N/A" + ) + } + } + } + } + TextButton(modifier = Modifier.fillMaxWidth(), + onClick = { + navController?.navigate("settings") + } + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + Icons.Filled.Settings, + contentDescription = "Settings" + ) + Text( + text = "Settings", + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp) + ) + } + } + } + } + } + Box { + val list = remember { + viewModel.gameList + } + val isLoading = remember { + viewModel.isLoading + } + viewModel.filter(query.value) + + if (!isPreview) { + var settings = QuickSettings(viewModel.activity!!) + + if (isLoading.value) { + Box(modifier = Modifier.fillMaxSize()) + { + CircularProgressIndicator( + modifier = Modifier + .width(64.dp) + .align(Alignment.Center), + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant + ) + } + } else { + if (settings.isGrid) { + val size = + GridImageSize / Resources.getSystem().displayMetrics.density + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = (size + 4).dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .nestedScroll(nestedScrollConnection), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase(Locale.getDefault()) + .contains(query.value)) + ) + GridGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel, + showError + ) + } + } + } + } else { + LazyColumn(Modifier.fillMaxSize()) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase( + Locale.getDefault() + ) + .contains(query.value)) + ) + Box(modifier = Modifier.animateItemPlacement()) { + ListGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel, + showError + ) + } + } + } + } + } + } + } + } + } + + if (showLoading.value) { + BasicAlertDialog(onDismissRequest = { }) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = "Loading") + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } + + } + } + } + if (openTitleUpdateDialog.value) { + BasicAlertDialog(onDismissRequest = { + openTitleUpdateDialog.value = false + }) { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" + val name = viewModel.mainViewModel?.selected?.titleName ?: "" + TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose) + } + + } + } + if (openDlcDialog.value) { + BasicAlertDialog(onDismissRequest = { + openDlcDialog.value = false + }) { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" + val name = viewModel.mainViewModel?.selected?.titleName ?: "" + DlcViews.Main(titleId, name, openDlcDialog) + } + + } + } + } + + if (showAppActions.value) + ModalBottomSheet( + content = { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + if (showAppActions.value) { + IconButton(onClick = { + if (viewModel.mainViewModel?.selected != null) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel.loadGame(viewModel.mainViewModel.selected!!) + if (success == 1) { + launchOnUiThread { + viewModel.mainViewModel.navigateToGame() + } + } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" + viewModel.mainViewModel.selected!!.close() + } + showLoading.value = false + } + } + }) { + Icon( + org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), + contentDescription = "Run" + ) + } + val showAppMenu = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { + showAppMenu.value = true + }) { + Icon( + Icons.Filled.Menu, + contentDescription = "Menu" + ) + } + DropdownMenu( + expanded = showAppMenu.value, + onDismissRequest = { showAppMenu.value = false }) { + DropdownMenuItem(text = { + Text(text = "Clear PPTC Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.clearPptcCache( + viewModel.mainViewModel.selected?.titleId ?: "" + ) + }) + DropdownMenuItem(text = { + Text(text = "Purge Shader Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.purgeShaderCache( + viewModel.mainViewModel.selected?.titleId ?: "" + ) + }) + DropdownMenuItem(text = { + Text(text = "Delete All Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.deleteCache( + viewModel.mainViewModel.selected?.titleId ?: "" + ) + }) + DropdownMenuItem(text = { + Text(text = "Manage Updates") + }, onClick = { + showAppMenu.value = false + openTitleUpdateDialog.value = true + }) + DropdownMenuItem(text = { + Text(text = "Manage DLC") + }, onClick = { + showAppMenu.value = false + openDlcDialog.value = true + }) + } + } + } + } + }, + onDismissRequest = { + showAppActions.value = false + selectedModel.value = null + } + ) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun ListGameItem( + gameModel: GameModel, + viewModel: HomeViewModel, + showAppActions: MutableState, + showLoading: MutableState, + selectedModel: MutableState, + showError: MutableState + ) { + remember { + selectedModel + } + val color = + if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + + val decoder = Base64.getDecoder() + Surface( + shape = MaterialTheme.shapes.medium, + color = color, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .combinedClickable( + onClick = { + if (viewModel.mainViewModel?.selected != null) { + showAppActions.value = false + viewModel.mainViewModel.apply { + selected = null + } + selectedModel.value = null + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(gameModel) ?: false + if (success == 1) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" + gameModel.close() + } + showLoading.value = false + } + } + }, + onLongClick = { + viewModel.mainViewModel?.selected = gameModel + showAppActions.value = true + selectedModel.value = gameModel + }) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row { + if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) { + if (gameModel.icon?.isNotEmpty() == true) { + val pic = decoder.decode(gameModel.icon) + val size = + ListImageSize / Resources.getSystem().displayMetrics.density + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), + contentDescription = gameModel.titleName + " icon", + modifier = Modifier + .padding(end = 8.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } else if (gameModel.type == FileType.Nro) + NROIcon() + else NotAvailableIcon() + } else NotAvailableIcon() + Column { + Text(text = gameModel.titleName ?: "") + Text(text = gameModel.developer ?: "") + Text(text = gameModel.titleId ?: "") + } + } + Column { + Text(text = gameModel.version ?: "") + Text(text = String.format("%.3f", gameModel.fileSize)) + } + } + } + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun GridGameItem( + gameModel: GameModel, + viewModel: HomeViewModel, + showAppActions: MutableState, + showLoading: MutableState, + selectedModel: MutableState, + showError: MutableState + ) { + remember { + selectedModel + } + val color = + if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + + val decoder = Base64.getDecoder() + Surface( + shape = MaterialTheme.shapes.medium, + color = color, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .combinedClickable( + onClick = { + if (viewModel.mainViewModel?.selected != null) { + showAppActions.value = false + viewModel.mainViewModel.apply { + selected = null + } + selectedModel.value = null + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(gameModel) ?: false + if (success == 1) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" + gameModel.close() + } + showLoading.value = false + } + } + }, + onLongClick = { + viewModel.mainViewModel?.selected = gameModel + showAppActions.value = true + selectedModel.value = gameModel + }) + ) { + Column(modifier = Modifier.padding(4.dp)) { + if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) { + if (gameModel.icon?.isNotEmpty() == true) { + val pic = decoder.decode(gameModel.icon) + val size = GridImageSize / Resources.getSystem().displayMetrics.density + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), + contentDescription = gameModel.titleName + " icon", + modifier = Modifier + .padding(0.dp) + .clip(RoundedCornerShape(16.dp)) + .align(Alignment.CenterHorizontally) + ) + } else if (gameModel.type == FileType.Nro) + NROIcon() + else NotAvailableIcon() + } else NotAvailableIcon() + Text( + text = gameModel.titleName ?: "N/A", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(vertical = 4.dp) + .basicMarquee() + ) + } + } + } + + @Composable + fun NotAvailableIcon() { + val size = ListImageSize / Resources.getSystem().displayMetrics.density + Icon( + Icons.Filled.Add, + contentDescription = "N/A", + modifier = Modifier + .padding(end = 8.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } + + @Composable + fun NROIcon() { + val size = ListImageSize / Resources.getSystem().displayMetrics.density + Image( + painter = painterResource(id = R.drawable.icon_nro), + contentDescription = "NRO", + modifier = Modifier + .padding(end = 8.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } + + } + + @Preview + @Composable + fun HomePreview() { + Home(isPreview = true) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt new file mode 100644 index 00000000..cd994de6 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -0,0 +1,32 @@ +package org.ryujinx.android.views + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.SettingsViewModel + +class MainView { + companion object { + @Composable + fun Main(mainViewModel: MainViewModel) { + val navController = rememberNavController() + mainViewModel.navController = navController + + NavHost(navController = navController, startDestination = "home") { + composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } + composable("user") { UserViews.Main(mainViewModel) } + composable("game") { GameViews.Main() } + composable("settings") { + SettingViews.Main( + SettingsViewModel( + navController, + mainViewModel.activity + ), mainViewModel + ) + } + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt new file mode 100644 index 00000000..7b322df0 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -0,0 +1,1280 @@ +package org.ryujinx.android.views + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Intent +import android.provider.DocumentsContract +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Label +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.file.extension +import org.ryujinx.android.Helpers +import org.ryujinx.android.MainActivity +import org.ryujinx.android.providers.DocumentProvider +import org.ryujinx.android.viewmodels.FirmwareInstallState +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.SettingsViewModel +import org.ryujinx.android.viewmodels.VulkanDriverViewModel +import kotlin.concurrent.thread + +class SettingViews { + companion object { + const val EXPANSTION_TRANSITION_DURATION = 450 + const val IMPORT_CODE = 12341 + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) + @Composable + fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) { + val loaded = remember { + mutableStateOf(false) + } + + val isHostMapped = remember { + mutableStateOf(false) + } + val useNce = remember { + mutableStateOf(false) + } + val enableVsync = remember { + mutableStateOf(false) + } + val enableDocked = remember { + mutableStateOf(false) + } + val enablePtc = remember { + mutableStateOf(false) + } + val ignoreMissingServices = remember { + mutableStateOf(false) + } + val enableShaderCache = remember { + mutableStateOf(false) + } + val enableTextureRecompression = remember { + mutableStateOf(false) + } + val resScale = remember { + mutableStateOf(1f) + } + val useVirtualController = remember { + mutableStateOf(true) + } + val showFirwmareDialog = remember { + mutableStateOf(false) + } + val firmwareInstallState = remember { + mutableStateOf(FirmwareInstallState.None) + } + val firmwareVersion = remember { + mutableStateOf(mainViewModel.firmwareVersion) + } + val isGrid = remember { mutableStateOf(true) } + val useSwitchLayout = remember { mutableStateOf(true) } + val enableMotion = remember { mutableStateOf(true) } + val enablePerformanceMode = remember { mutableStateOf(true) } + val controllerStickSensitivity = remember { mutableStateOf(1.0f) } + + val enableDebugLogs = remember { mutableStateOf(true) } + val enableStubLogs = remember { mutableStateOf(true) } + val enableInfoLogs = remember { mutableStateOf(true) } + val enableWarningLogs = remember { mutableStateOf(true) } + val enableErrorLogs = remember { mutableStateOf(true) } + val enableGuestLogs = remember { mutableStateOf(true) } + val enableAccessLogs = remember { mutableStateOf(true) } + val enableTraceLogs = remember { mutableStateOf(true) } + val enableGraphicsLogs = remember { mutableStateOf(true) } + + if (!loaded.value) { + settingsViewModel.initializeState( + isHostMapped, + useNce, + enableVsync, enableDocked, enablePtc, ignoreMissingServices, + enableShaderCache, + enableTextureRecompression, + resScale, + useVirtualController, + isGrid, + useSwitchLayout, + enableMotion, + enablePerformanceMode, + controllerStickSensitivity, + enableDebugLogs, + enableStubLogs, + enableInfoLogs, + enableWarningLogs, + enableErrorLogs, + enableGuestLogs, + enableAccessLogs, + enableTraceLogs, + enableGraphicsLogs + ) + loaded.value = true + } + Scaffold(modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(title = { + Text(text = "Settings") + }, + modifier = Modifier.padding(top = 16.dp), + navigationIcon = { + IconButton(onClick = { + settingsViewModel.save( + isHostMapped, + useNce, + enableVsync, + enableDocked, + enablePtc, + ignoreMissingServices, + enableShaderCache, + enableTextureRecompression, + resScale, + useVirtualController, + isGrid, + useSwitchLayout, + enableMotion, + enablePerformanceMode, + controllerStickSensitivity, + enableDebugLogs, + enableStubLogs, + enableInfoLogs, + enableWarningLogs, + enableErrorLogs, + enableGuestLogs, + enableAccessLogs, + enableTraceLogs, + enableGraphicsLogs + ) + settingsViewModel.navController.popBackStack() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }) + }) { contentPadding -> + Column( + modifier = Modifier + .padding(contentPadding) + .verticalScroll(rememberScrollState()) + ) { + ExpandableView(onCardArrowClick = { }, title = "App") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Use Grid", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = isGrid.value, onCheckedChange = { + isGrid.value = !isGrid.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Game Folder", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Button(onClick = { + settingsViewModel.openGameFolder() + }) { + Text(text = "Choose Folder") + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "System Firmware", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Text( + text = firmwareVersion.value, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Button(onClick = { + fun createIntent(action: String): Intent { + val intent = Intent(action) + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.data = DocumentsContract.buildRootUri( + DocumentProvider.AUTHORITY, + DocumentProvider.ROOT_ID + ) + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + return intent + } + try { + mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW)) + return@Button + } catch (_: ActivityNotFoundException) { + } + try { + mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE")) + return@Button + } catch (_: ActivityNotFoundException) { + } + try { + mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui")) + return@Button + } catch (_: ActivityNotFoundException) { + } + try { + mainViewModel.activity.startActivity(createIntent("com.android.documentsui")) + return@Button + } catch (_: ActivityNotFoundException) { + } + }) { + Text(text = "Open App Folder") + } + + Button(onClick = { + settingsViewModel.importProdKeys() + }) { + Text(text = "Import prod Keys") + } + + Button(onClick = { + showFirwmareDialog.value = true + }) { + Text(text = "Install Firmware") + } + } + } + } + + if (showFirwmareDialog.value) { + BasicAlertDialog(onDismissRequest = { + if (firmwareInstallState.value != FirmwareInstallState.Install) { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection(firmwareInstallState) + } + }) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + verticalArrangement = Arrangement.SpaceBetween + ) { + if (firmwareInstallState.value == FirmwareInstallState.None) { + Text(text = "Select a zip or XCI file to install from.") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + ) { + Button(onClick = { + settingsViewModel.selectFirmware( + firmwareInstallState + ) + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Select File") + } + Button(onClick = { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection( + firmwareInstallState + ) + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Cancel") + } + } + } else if (firmwareInstallState.value == FirmwareInstallState.Query) { + Text(text = "Firmware ${settingsViewModel.selectedFirmwareVersion} will be installed. Do you want to continue?") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + ) { + Button(onClick = { + settingsViewModel.installFirmware( + firmwareInstallState + ) + + if (firmwareInstallState.value == FirmwareInstallState.None) { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection( + firmwareInstallState + ) + } + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Yes") + } + Button(onClick = { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection( + firmwareInstallState + ) + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "No") + } + } + } else if (firmwareInstallState.value == FirmwareInstallState.Install) { + Text(text = "Installing Firmware ${settingsViewModel.selectedFirmwareVersion}...") + LinearProgressIndicator( + modifier = Modifier + .padding(top = 4.dp) + ) + } else if (firmwareInstallState.value == FirmwareInstallState.Verifying) { + Text(text = "Verifying selected file...") + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + ) + } else if (firmwareInstallState.value == FirmwareInstallState.Done) { + Text(text = "Installed Firmware ${settingsViewModel.selectedFirmwareVersion}") + firmwareVersion.value = mainViewModel.firmwareVersion + } else if (firmwareInstallState.value == FirmwareInstallState.Cancelled) { + val file = settingsViewModel.selectedFirmwareFile + if (file != null) { + if (file.extension == "xci" || file.extension == "zip") { + if (settingsViewModel.selectedFirmwareVersion.isEmpty()) { + Text(text = "Unable to find version in selected file") + } else { + Text(text = "Unknown Error has occurred. Please check logs") + } + } else { + Text(text = "File type is not supported") + } + } else { + Text(text = "File type is not supported") + } + } + } + } + } + } + ExpandableView(onCardArrowClick = { }, title = "System") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Use NCE", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = useNce.value, onCheckedChange = { + useNce.value = !useNce.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Is Host Mapped", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = isHostMapped.value, onCheckedChange = { + isHostMapped.value = !isHostMapped.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable VSync", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableVsync.value, onCheckedChange = { + enableVsync.value = !enableVsync.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable PTC", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enablePtc.value, onCheckedChange = { + enablePtc.value = !enablePtc.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Docked Mode", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableDocked.value, onCheckedChange = { + enableDocked.value = !enableDocked.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Ignore Missing Services", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = ignoreMissingServices.value, onCheckedChange = { + ignoreMissingServices.value = !ignoreMissingServices.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.align(Alignment.CenterVertically) + ) { + Text( + text = "Enable Performance Mode", + ) + Text( + text = "Forces CPU and GPU to run at max clocks if available.", + fontSize = 12.sp + ) + Text( + text = "OS power settings may override this.", + fontSize = 12.sp + ) + } + Switch(checked = enablePerformanceMode.value, onCheckedChange = { + enablePerformanceMode.value = !enablePerformanceMode.value + }) + } + val isImporting = remember { + mutableStateOf(false) + } + val showImportWarning = remember { + mutableStateOf(false) + } + val showImportCompletion = remember { + mutableStateOf(false) + } + var importFile = remember { + mutableStateOf(null) + } + Button(onClick = { + val storage = MainActivity.StorageHelper + storage?.apply { + val callBack = this.onFileSelected + onFileSelected = { requestCode, files -> + run { + onFileSelected = callBack + if (requestCode == IMPORT_CODE) { + val file = files.firstOrNull() + file?.apply { + if (this.extension == "zip") { + importFile.value = this + showImportWarning.value = true + } + } + } + } + } + openFilePicker( + IMPORT_CODE, + filterMimeTypes = arrayOf("application/zip") + ) + } + }) { + Text(text = "Import App Data") + } + + if (showImportWarning.value) { + BasicAlertDialog(onDismissRequest = { + showImportWarning.value = false + importFile.value = null + }) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = "Importing app data will delete your current profile. Do you still want to continue?") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + val file = importFile.value + showImportWarning.value = false + importFile.value = null + file?.apply { + thread { + Helpers.importAppData(this, isImporting) + showImportCompletion.value = true + mainViewModel.userViewModel.refreshUsers() + } + } + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Yes") + } + Button(onClick = { + showImportWarning.value = false + importFile.value = null + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "No") + } + } + } + + } + } + } + + if (showImportCompletion.value) { + BasicAlertDialog(onDismissRequest = { + showImportCompletion.value = false + importFile.value = null + mainViewModel.userViewModel.refreshUsers() + mainViewModel.homeViewModel.requestReload() + }) { + Card( + modifier = Modifier, + shape = MaterialTheme.shapes.medium + ) { + Text( + modifier = Modifier + .padding(24.dp), + text = "App Data import completed." + ) + } + } + } + + if (isImporting.value) { + Text(text = "Importing Files") + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } + } + ExpandableView(onCardArrowClick = { }, title = "Graphics") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Shader Cache", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableShaderCache.value, onCheckedChange = { + enableShaderCache.value = !enableShaderCache.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Resolution Scale", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Text(text = resScale.value.toString() + "x") + } + Slider(value = resScale.value, + valueRange = 0.5f..4f, + steps = 6, + onValueChange = { it -> + resScale.value = it + }) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Texture Recompression", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch( + checked = enableTextureRecompression.value, + onCheckedChange = { + enableTextureRecompression.value = + !enableTextureRecompression.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + var isDriverSelectorOpen = remember { + mutableStateOf(false) + } + var driverViewModel = + VulkanDriverViewModel(settingsViewModel.activity) + var isChanged = remember { + mutableStateOf(false) + } + var refresh = remember { + mutableStateOf(false) + } + var drivers = driverViewModel.getAvailableDrivers() + var selectedDriver = remember { + mutableStateOf(0) + } + + if (refresh.value) { + isChanged.value = true + refresh.value = false + } + + if (isDriverSelectorOpen.value) { + BasicAlertDialog(onDismissRequest = { + isDriverSelectorOpen.value = false + + if (isChanged.value) { + driverViewModel.saveSelected() + } + }) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + if (!isChanged.value) { + selectedDriver.value = + drivers.indexOfFirst { it.driverPath == driverViewModel.selected } + 1 + isChanged.value = true + } + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .height(350.dp) + .verticalScroll(rememberScrollState()) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedDriver.value == 0 || driverViewModel.selected.isEmpty(), + onClick = { + selectedDriver.value = 0 + isChanged.value = true + driverViewModel.selected = "" + }) + Column { + Text(text = "Default", + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedDriver.value = 0 + isChanged.value = true + driverViewModel.selected = + "" + }) + } + } + var driverIndex = 1 + for (driver in drivers) { + var ind = driverIndex + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedDriver.value == ind, + onClick = { + selectedDriver.value = ind + isChanged.value = true + driverViewModel.selected = + driver.driverPath + }) + Column(modifier = Modifier.clickable { + selectedDriver.value = + ind + isChanged.value = + true + driverViewModel.selected = + driver.driverPath + }) { + Text( + text = driver.libraryName, + modifier = Modifier + .fillMaxWidth() + ) + Text( + text = driver.driverVersion, + modifier = Modifier + .fillMaxWidth() + ) + Text( + text = driver.description, + modifier = Modifier + .fillMaxWidth() + ) + } + } + + driverIndex++ + } + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Button(onClick = { + driverViewModel.removeSelected() + refresh.value = true + }, modifier = Modifier.padding(8.dp)) { + Text(text = "Remove") + } + + Button(onClick = { + driverViewModel.add(refresh) + refresh.value = true + }, modifier = Modifier.padding(8.dp)) { + Text(text = "Add") + } + } + } + } + } + } + } + + TextButton( + { + isChanged.value = false + isDriverSelectorOpen.value = !isDriverSelectorOpen.value + }, + modifier = Modifier.align(Alignment.CenterVertically) + ) { + Text(text = "Drivers") + } + } + + } + } + ExpandableView(onCardArrowClick = { }, title = "Input") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Show virtual controller", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = useVirtualController.value, onCheckedChange = { + useVirtualController.value = !useVirtualController.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Motion", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableMotion.value, onCheckedChange = { + enableMotion.value = !enableMotion.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Use Switch Controller Layout", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = useSwitchLayout.value, onCheckedChange = { + useSwitchLayout.value = !useSwitchLayout.value + }) + } + + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Controller Stick Sensitivity", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Slider(modifier = Modifier.width(250.dp), value = controllerStickSensitivity.value, onValueChange = { + controllerStickSensitivity.value = it + }, valueRange = 0.1f..2f, + steps = 20, + interactionSource = interactionSource, + thumb = { + Label( + label = { + PlainTooltip(modifier = Modifier + .sizeIn(45.dp, 25.dp) + .wrapContentWidth()) { + Text("%.2f".format(controllerStickSensitivity.value)) + } + }, + interactionSource = interactionSource + ) { + Icon( + imageVector = org.ryujinx.android.Icons.circle( + color = MaterialTheme.colorScheme.primary + ), + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + tint = MaterialTheme.colorScheme.primary + ) + } + } + ) + } + + } + } + ExpandableView(onCardArrowClick = { }, title = "Log") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Debug Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableDebugLogs.value, onCheckedChange = { + enableDebugLogs.value = !enableDebugLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Stub Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableStubLogs.value, onCheckedChange = { + enableStubLogs.value = !enableStubLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Info Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableInfoLogs.value, onCheckedChange = { + enableInfoLogs.value = !enableInfoLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Warning Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableWarningLogs.value, onCheckedChange = { + enableWarningLogs.value = !enableWarningLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Error Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableErrorLogs.value, onCheckedChange = { + enableErrorLogs.value = !enableErrorLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Guest Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableGuestLogs.value, onCheckedChange = { + enableGuestLogs.value = !enableGuestLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Access Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableAccessLogs.value, onCheckedChange = { + enableAccessLogs.value = !enableAccessLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Trace Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableTraceLogs.value, onCheckedChange = { + enableTraceLogs.value = !enableTraceLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Graphics Debug Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableGraphicsLogs.value, onCheckedChange = { + enableGraphicsLogs.value = !enableGraphicsLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = { + mainViewModel.logging.requestExport() + }) { + Text(text = "Send Logs") + } + } + } + } + } + + BackHandler { + settingsViewModel.save( + isHostMapped, + useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices, + enableShaderCache, + enableTextureRecompression, + resScale, + useVirtualController, + isGrid, + useSwitchLayout, + enableMotion, + enablePerformanceMode, + controllerStickSensitivity, + enableDebugLogs, + enableStubLogs, + enableInfoLogs, + enableWarningLogs, + enableErrorLogs, + enableGuestLogs, + enableAccessLogs, + enableTraceLogs, + enableGraphicsLogs + ) + settingsViewModel.navController.popBackStack() + } + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + @SuppressLint("UnusedTransitionTargetStateParameter") + fun ExpandableView( + onCardArrowClick: () -> Unit, + title: String, + content: @Composable () -> Unit + ) { + val expanded = false + val mutableExpanded = remember { + mutableStateOf(expanded) + } + val transitionState = remember { + MutableTransitionState(expanded).apply { + targetState = !mutableExpanded.value + } + } + val transition = updateTransition(transitionState, label = "transition") + val arrowRotationDegree by transition.animateFloat({ + tween(durationMillis = EXPANSTION_TRANSITION_DURATION) + }, label = "rotationDegreeTransition") { + if (mutableExpanded.value) 0f else 180f + } + + Card( + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 24.dp, + vertical = 8.dp + ) + ) { + Column { + Card( + onClick = { + mutableExpanded.value = !mutableExpanded.value + onCardArrowClick() + }) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CardTitle(title = title) + CardArrow( + degrees = arrowRotationDegree, + ) + + } + } + ExpandableContent(visible = mutableExpanded.value, content = content) + } + } + } + + @Composable + fun CardArrow( + degrees: Float, + ) { + Icon( + Icons.Filled.KeyboardArrowUp, + contentDescription = "Expandable Arrow", + modifier = Modifier + .padding(8.dp) + .rotate(degrees), + ) + } + + @Composable + fun CardTitle(title: String) { + Text( + text = title, + modifier = Modifier + .padding(16.dp), + textAlign = TextAlign.Center, + ) + } + + @Composable + fun ExpandableContent( + visible: Boolean = true, + content: @Composable () -> Unit + ) { + val enterTransition = remember { + expandVertically( + expandFrom = Alignment.Top, + animationSpec = tween(EXPANSTION_TRANSITION_DURATION) + ) + fadeIn( + initialAlpha = 0.3f, + animationSpec = tween(EXPANSTION_TRANSITION_DURATION) + ) + } + val exitTransition = remember { + shrinkVertically( + // Expand from the top. + shrinkTowards = Alignment.Top, + animationSpec = tween(EXPANSTION_TRANSITION_DURATION) + ) + fadeOut( + // Fade in with the initial alpha of 0.3f. + animationSpec = tween(EXPANSTION_TRANSITION_DURATION) + ) + } + + AnimatedVisibility( + visible = visible, + enter = enterTransition, + exit = exitTransition + ) { + Column(modifier = Modifier.padding(8.dp)) { + content() + } + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt new file mode 100644 index 00000000..7eb8d62c --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -0,0 +1,148 @@ +package org.ryujinx.android.views + +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.documentfile.provider.DocumentFile +import org.ryujinx.android.MainActivity +import org.ryujinx.android.viewmodels.TitleUpdateViewModel + +class TitleUpdateViews { + companion object { + @Composable + fun Main( + titleId: String, + name: String, + openDialog: MutableState, + canClose: MutableState + ) { + val viewModel = TitleUpdateViewModel(titleId) + + val selected = remember { mutableStateOf(0) } + viewModel.data?.apply { + selected.value = paths.indexOf(this.selected) + 1 + } + + Column(modifier = Modifier.padding(16.dp)) { + Column { + Text(text = "Updates for ${name}", textAlign = TextAlign.Center) + Surface( + modifier = Modifier + .padding(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .height(250.dp) + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Row(modifier = Modifier.padding(8.dp)) { + RadioButton( + selected = (selected.value == 0), + onClick = { + selected.value = 0 + }) + Text( + text = "None", + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically) + ) + } + + val paths = remember { + mutableStateListOf() + } + + viewModel.setPaths(paths, canClose) + var index = 1 + for (path in paths) { + val i = index + val uri = Uri.parse(path) + val file = DocumentFile.fromSingleUri( + MainActivity.mainViewModel!!.activity, + uri + ) + file?.apply { + Row(modifier = Modifier.padding(8.dp)) { + RadioButton( + selected = (selected.value == i), + onClick = { selected.value = i }) + Text( + text = file.name ?: "", + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically) + ) + } + } + index++ + } + } + } + Row(modifier = Modifier.align(Alignment.End)) { + IconButton( + onClick = { + viewModel.remove(selected.value) + } + ) { + Icon( + Icons.Filled.Delete, + contentDescription = "Remove" + ) + } + + IconButton( + onClick = { + viewModel.add() + } + ) { + Icon( + Icons.Filled.Add, + contentDescription = "Add" + ) + } + } + + } + Spacer(modifier = Modifier.height(18.dp)) + TextButton( + modifier = Modifier.align(Alignment.End), + onClick = { + canClose.value = true + viewModel.save(selected.value, openDialog) + }, + ) { + Text("Save") + } + } + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt new file mode 100644 index 00000000..a16d5759 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt @@ -0,0 +1,175 @@ +package org.ryujinx.android.views + +import android.graphics.BitmapFactory +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import org.ryujinx.android.viewmodels.MainViewModel + +class UserViews { + companion object { + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + @Composable + fun Main(viewModel: MainViewModel? = null) { + val reload = remember { + mutableStateOf(true) + } + + fun refresh() { + viewModel?.userViewModel?.refreshUsers() + reload.value = true + } + LaunchedEffect(reload.value) { + reload.value = false + } + + Scaffold(modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(title = { + Text(text = "Users") + }, + navigationIcon = { + IconButton(onClick = { + viewModel?.navController?.popBackStack() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }) + }) { contentPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = "Selected user") + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (viewModel?.userViewModel?.openedUser?.id?.isNotEmpty() == true) { + val openUser = viewModel.userViewModel.openedUser + Image( + bitmap = BitmapFactory.decodeByteArray( + openUser.userPicture, + 0, + openUser.userPicture?.size ?: 0 + ).asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(96.dp) + .clip(CircleShape) + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = openUser.username) + Text(text = openUser.id) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Available Users") + IconButton(onClick = { + refresh() + }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = "refresh users" + ) + } + } + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + ) { + if (viewModel?.userViewModel?.userList?.isNotEmpty() == true) { + items(viewModel.userViewModel.userList) { user -> + Image( + bitmap = BitmapFactory.decodeByteArray( + user.userPicture, + 0, + user.userPicture?.size ?: 0 + ) + .asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally) + .combinedClickable( + onClick = { + viewModel.userViewModel.openUser(user) + reload.value = true + }) + ) + } + } + } + } + + } + } + } + + } + + @Preview + @Composable + fun Preview() { + Main() + } +} diff --git a/src/RyujinxAndroid/app/src/main/jniLibs/arm64-v8a/.gitkeep b/src/RyujinxAndroid/app/src/main/jniLibs/arm64-v8a/.gitkeep new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/jniLibs/arm64-v8a/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml b/src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml new file mode 100644 index 00000000..e5de70f7 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..c4dd1135 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png b/src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png new file mode 100644 index 0000000000000000000000000000000000000000..3a9da621834bc55b58978ffbc7b37cc50372bb1b GIT binary patch literal 10254 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 Utxt6oQ~_w*)mAB0vVQ(Q0RPcgegFUf literal 0 HcmV?d00001 diff --git a/src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml b/src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml new file mode 100644 index 00000000..d30d7c30 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..3a8b2257a2bcf4b7aa21eb1b0a81fc7b5fbcabf5 GIT binary patch literal 2366 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 Oh~)laszlw8L74%L+RcXm literal 0 HcmV?d00001 diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..94f9c720865b443506280d33cf11417d5453f9d2 GIT binary patch literal 3566 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 o?3NyV^wCEzZQtY6meY?t3Xi?ndR- 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 literal 0 HcmV?d00001 diff --git a/src/RyujinxAndroid/app/src/main/res/values/colors.xml b/src/RyujinxAndroid/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml b/src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..c5d5899f --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/values/strings.xml b/src/RyujinxAndroid/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..ea352787 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RyujinxAndroid + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/values/themes.xml b/src/RyujinxAndroid/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..9b1784b4 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +