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