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 00000000..e27d7167 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png differ 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 00000000..3a9da621 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png differ 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 00000000..3a8b2257 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b863faa2 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ 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 00000000..94f9c720 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..c0977e6a Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1bd79c40 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ 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 @@ + + + +