diff --git a/.gitignore b/.gitignore index 37b419d07..6b56addcb 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,5 @@ PublishProfiles/ # Glade backup files *.glade~ +/src/RyujinxAndroid/.idea +/src/RyujinxAndroid/app/src/main/jniLibs/arm64-v8a diff --git a/src/RyujinxAndroid/.gitignore b/src/RyujinxAndroid/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/src/RyujinxAndroid/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/src/RyujinxAndroid/app/.gitignore b/src/RyujinxAndroid/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/src/RyujinxAndroid/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle new file mode 100644 index 000000000..b790abf66 --- /dev/null +++ b/src/RyujinxAndroid/app/build.gradle @@ -0,0 +1,95 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'org.ryujinx.android' + compileSdk 33 + + defaultConfig { + applicationId "org.ryujinx.android" + minSdk 28 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + + ndk { + 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' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + prefab true + } + composeOptions { + kotlinCompilerExtensionVersion '1.3.2' + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } + externalNativeBuild { + cmake { + path file('src/main/cpp/CMakeLists.txt') + version '3.22.1' + } + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.10.1' + implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation "androidx.navigation:navigation-compose:2.6.0" + implementation 'androidx.activity:activity-compose:1.7.2' + implementation platform('androidx.compose:compose-bom:2023.06.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.google.oboe:oboe:1.7.0' + implementation "com.anggrayudi:storage:1.5.5" + implementation "androidx.preference:preference-ktx:1.2.0" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' + implementation 'com.google.code.gson:gson:2.10.1' + implementation "io.coil-kt:coil-compose:2.4.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:2023.06.00') + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/proguard-rules.pro b/src/RyujinxAndroid/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /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 000000000..d6e1198c7 --- /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 000000000..778370752 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 000000000..3060d2fb4 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,55 @@ + +# 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") + +# 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 + oboe.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 ) + +find_package (oboe REQUIRED CONFIG) + +# 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. + oboe::oboe + ${log-lib} + -lvulkan + -landroid) diff --git a/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp b/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp new file mode 100644 index 000000000..61b605ab5 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp @@ -0,0 +1,137 @@ +// +// Created by Emmanuel Hansen on 6/27/2023. +// + +#include "oboe.h" + +static int s_device_id = 0; + +void AudioSession::initialize() { +} + +void AudioSession::destroy() { + stream->close(); + delete stream; +} + +void AudioSession::start() { + isStarted = true; + + stream->requestStart(); +} + +void AudioSession::stop() { + isStarted = false; + stream->requestStop(); +} + + +void AudioSession::read(uint64_t data, uint64_t samples) { + int timeout = INT32_MAX; + + stream->write((void*)data, samples, timeout); +} + +extern "C" +{ +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_setDeviceId( + JNIEnv *env, +jobject instance, + jint device_id){ + s_device_id = device_id; +} + + AudioSession* create_session(int sample_format, + uint sample_rate, + uint channel_count) + { + using namespace oboe; + + AudioStreamBuilder builder; + + AudioFormat format; + + switch (sample_format) { + case 0: + format = AudioFormat::Invalid; + break; + case 1: + case 2: + format = AudioFormat::I16; + break; + case 3: + format = AudioFormat::I24; + break; + case 4: + format = AudioFormat::I32; + break; + case 5: + format = AudioFormat::Float; + break; + default: + std::ostringstream string; + string << "Invalid Format" << sample_format; + + throw std::runtime_error(string.str()); + } + + auto session = new AudioSession(); + session->initialize(); + + session->format = format; + session->channelCount = channel_count; + + builder.setDirection(Direction::Output) + ->setPerformanceMode(PerformanceMode::LowLatency) + ->setSharingMode(SharingMode::Shared) + ->setFormat(format) + ->setChannelCount(channel_count) + ->setSampleRate(sample_rate); + AudioStream* stream; + if(builder.openStream(&stream) != oboe::Result::OK) + { + delete session; + return nullptr; + } + session->stream = stream; + + return session; + } + + void start_session(AudioSession* session) + { + session->start(); + } + + void stop_session(AudioSession* session) + { + session->stop(); + } + + void set_session_volume(AudioSession* session, float volume) + { + session->volume = volume; + } + + float get_session_volume(AudioSession* session) + { + return session->volume; + } + + void close_session(AudioSession* session) + { + session->destroy(); + + delete session; + } + + bool is_playing(AudioSession* session) { + return session->isStarted; + } + + void write_to_session(AudioSession* session, uint64_t data, uint64_t samples) + { + session->read(data, samples); + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/cpp/oboe.h b/src/RyujinxAndroid/app/src/main/cpp/oboe.h new file mode 100644 index 000000000..0d6559417 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/oboe.h @@ -0,0 +1,29 @@ +// +// Created by Emmanuel Hansen on 6/27/2023. +// + +#ifndef RYUJINXNATIVE_OBOE_H +#define RYUJINXNATIVE_OBOE_H + +#include +#include +#include +#include +#include + +class AudioSession { +public: + oboe::AudioStream* stream; + float volume = 1.0f; + bool isStarted; + oboe::AudioFormat format; + uint channelCount; + + void initialize(); + void destroy(); + void start(); + void stop(); + void read(uint64_t data, uint64_t samples); +}; + +#endif //RYUJINXNATIVE_OBOE_H 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 000000000..2077c6fef --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -0,0 +1,39 @@ +// +// 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 "vulkan_wrapper.h" +#include +#include + +// 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; + +#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 000000000..0c072f8f3 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -0,0 +1,123 @@ +// 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" + +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); + } + +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_createSurface( + JNIEnv *env, + jobject instance, + jlong vulkanInstance, + jlong window) { + auto nativeWindow = (ANativeWindow *) window; + + if (nativeWindow != NULL) + return -1; + VkSurfaceKHR surface; + auto vkInstance = VkInstance(vulkanInstance); + 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 (jlong)surface; +} + +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_attachCurrentThread( + JNIEnv *env, + jobject instance) { + JavaVM* jvm = NULL; + env->GetJavaVM(&jvm); + + if(jvm != NULL) + jvm->AttachCurrentThread(&env, NULL); +} + +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_detachCurrentThread( + JNIEnv *env, + jobject instance) { + JavaVM* jvm = NULL; + env->GetJavaVM(&jvm); + + if(jvm != NULL) + jvm->DetachCurrentThread(); +} + +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; +} + + +} \ No newline at end of file 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 000000000..f186c8504 --- /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 000000000..5d34c0c8d --- /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/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt new file mode 100644 index 000000000..08c3af72f --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -0,0 +1,374 @@ +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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.flowWithLifecycle +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.RadialGamePadTheme +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import com.swordfish.radialgamepad.library.event.GestureType +import com.swordfish.radialgamepad.library.haptics.HapticConfig +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.subscribe +import kotlinx.coroutines.launch + +typealias GamePad = RadialGamePad +typealias GamePadConfig = RadialGamePadConfig + +class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) { + var leftGamePad: GamePad + var rightGamePad: GamePad + var controllerId: String? = null + + 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 + } + + @Composable + fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit + { + + AndroidView( + modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)}) + + lifecycleScope.apply { + lifecycleScope.launch { + var events = merge(leftGamePad.events(),rightGamePad.events()) + .shareIn(lifecycleScope, SharingStarted.Lazily) + + events.safeCollect { + handleEvent(it) + }; + } + } + } + + private fun Create(context: Context) : View + { + var inflator = LayoutInflater.from(context); + var view = inflator.inflate(R.layout.game_layout, null) + view.findViewById(R.id.leftcontainer)!!.addView(leftGamePad); + view.findViewById(R.id.rightcontainer)!!.addView(rightGamePad); + + return view as View + } + + fun connect(){ + if(controllerId.isNullOrEmpty()) + controllerId = ryujinxNative.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if(controllerId.isNullOrEmpty()) + controllerId = ryujinxNative.inputConnectGamepad(0) + + controllerId?.apply { + when (ev) { + is Event.Button -> { + var action = ev.action + when (action) { + KeyEvent.ACTION_UP -> { + ryujinxNative.inputSetButtonReleased(ev.id, this) + } + KeyEvent.ACTION_DOWN -> { + ryujinxNative.inputSetButtonPressed(ev.id, this) + } + } + } + is Event.Direction -> { + var direction = ev.id + + when(direction) { + GamePadButtonInputId.DpadUp.ordinal -> { + if (ev.xAxis > 0) + { + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + } + else if (ev.xAxis < 0) + { + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + else + { + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + if (ev.yAxis < 0) + { + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + } + else if (ev.yAxis > 0) + { + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + else + { + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + } + + GamePadButtonInputId.LeftStick.ordinal -> { + ryujinxNative.inputSetStickAxis(1, ev.xAxis, -ev.yAxis ,this) + } + GamePadButtonInputId.RightStick.ordinal -> { + ryujinxNative.inputSetStickAxis(2, ev.xAxis, -ev.yAxis ,this) + } + } + } + } + } + } +} + +suspend fun Flow.safeCollect( + block: suspend (T) -> Unit +) { + this.catch {} + .collect { + block(it) + } +} + +private fun generateConfig(isLeft: Boolean): GamePadConfig { + var distance = 0.05f + + if (isLeft) { + return GamePadConfig( + 12, + PrimaryDialConfig.Stick( + GamePadButtonInputId.LeftStick.ordinal, + GamePadButtonInputId.LeftStickButton.ordinal, + setOf(), + "LeftStick", + null + ), + listOf( + SecondaryDialConfig.Cross( + 9, + 3, + 1.8f, + distance, + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 0, + 1f, + 0.05f, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.DoubleButton( + 2, + 0.05f, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 8, + 1f, + 0.05f, + 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, + 3f, + 0.05f, + GamePadButtonInputId.RightStick.ordinal, + GamePadButtonInputId.RightStickButton.ordinal, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 6, + 1f, + 0.05f, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.DoubleButton( + 3, + 0.05f, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + SecondaryDialConfig.SingleButton( + 9, + 1f, + 0.05f, + 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 000000000..d9a804dfe --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -0,0 +1,160 @@ +package org.ryujinx.android + +import android.content.Context +import android.os.ParcelFileDescriptor +import android.view.MotionEvent +import android.view.SurfaceHolder +import android.view.SurfaceView +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.ryujinx.android.viewmodels.GameModel +import org.ryujinx.android.viewmodels.MainViewModel +import kotlin.concurrent.thread +import kotlin.math.roundToInt + +class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { + private var _height: Int = 0 + private var _width: Int = 0 + private var _updateThread: Thread? = null + private var nativeInterop: NativeGraphicsInterop? = null + private var _guestThread: Thread? = null + private var _isInit: Boolean = false + private var _isStarted: Boolean = false + private var _nativeWindow: Long = 0 + private var _nativeHelper: NativeHelpers = NativeHelpers() + + private var _nativeRyujinx: RyujinxNative = RyujinxNative() + + companion object { + var gameModel: GameModel? = null + } + + init { + holder.addCallback(this) + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (_isStarted) + return when (event!!.actionMasked) { + MotionEvent.ACTION_MOVE -> { + _nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt()) + true + } + MotionEvent.ACTION_DOWN -> { + _nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt()) + true + } + MotionEvent.ACTION_UP -> { + _nativeRyujinx.inputReleaseTouchPoint() + true + } + else -> super.onTouchEvent(event) + } + return super.onTouchEvent(event) + } + + override fun surfaceCreated(holder: SurfaceHolder) { + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + var isStarted = _isStarted; + + start(holder) + + if(isStarted && (_width != width || _height != height)) + { + var nativeHelpers = NativeHelpers() + var window = nativeHelpers.getNativeWindow(holder.surface); + _nativeRyujinx.graphicsSetSurface(window); + } + + _width = width; + _height = height; + + if(_isStarted) + { + _nativeRyujinx.inputSetClientSize(width, height) + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + + } + + private fun start(surfaceHolder: SurfaceHolder) : Unit { + var game = gameModel ?: return + var path = game.getPath() ?: return + if (_isStarted) + return + + var surface = surfaceHolder.surface; + + var success = _nativeRyujinx.graphicsInitialize(GraphicsConfiguration()) + + + var nativeHelpers = NativeHelpers() + var window = nativeHelpers.getNativeWindow(surfaceHolder.surface); + nativeInterop = NativeGraphicsInterop() + nativeInterop!!.VkRequiredExtensions = arrayOf( + "VK_KHR_surface", "VK_KHR_android_surface" + ); + nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() + nativeInterop!!.SurfaceHandle = window; + + success = _nativeRyujinx.graphicsInitializeRenderer( + nativeInterop!!.VkRequiredExtensions!!, + window + ); + success = _nativeRyujinx.deviceInitialize(true, true, + SystemLanguage.AmericanEnglish.ordinal, + RegionCode.USA.ordinal, + true, + true, + true, + false, + "UTC", + false); + + success = _nativeRyujinx.deviceLoad(path) + + _nativeRyujinx.inputInitialize(width, height) + + controller.connect() + + _nativeRyujinx.graphicsRendererSetSize( + surfaceHolder.surfaceFrame.width(), + surfaceHolder.surfaceFrame.height() + ); + + _guestThread = thread(start = true) { + runBlocking { + this.launch { + delay(5000) + _nativeRyujinx.graphicsRendererSetVsync(false); + } + } + runGame() + } + _isStarted = success; + + _updateThread = thread(start = true) { + var c = 0 + while (_isStarted) { + _nativeRyujinx.inputUpdate() + Thread.sleep(1) + c++ + if (c >= 1000) { + c = 0 + var stats = _nativeRyujinx.deviceGetGameStats() + mainViewModel.updateStats(stats.Fifo, stats.GameFps, stats.GameTime) + } + } + } + } + + private fun runGame() : Unit{ + _nativeRyujinx.graphicsRendererRunLoop() + } + +} \ No newline at end of file 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 000000000..05119474a --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GamePadButtonInputId.kt @@ -0,0 +1,26 @@ +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/GraphicsConfiguration.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GraphicsConfiguration.kt new file mode 100644 index 000000000..a52048441 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GraphicsConfiguration.kt @@ -0,0 +1,22 @@ +package org.ryujinx.android + +import android.R.bool + +class GraphicsConfiguration { + var ResScale = 1f + var MaxAnisotropy = -1f + var FastGpuTime: Boolean = true + var Fast2DCopy: Boolean = true + var EnableMacroJit: Boolean = false + var EnableMacroHLE: Boolean = true + var EnableShaderCache: Boolean = true + var EnableTextureRecompression: Boolean = false + var BackendThreading: Int = org.ryujinx.android.BackendThreading.Auto.ordinal +} + +enum class BackendThreading +{ + Auto, + Off, + On +} \ 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 000000000..f08e44d8d --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -0,0 +1,84 @@ +package org.ryujinx.android + +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore + +class Helpers { + companion object{ + fun getPath(context: Context, uri: Uri): String? { + val isKitKatorAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + + // DocumentProvider + if (isKitKatorAbove && 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 + if ("image" == type) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else if ("video" == type) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } else if ("audio" == type) { + 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 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.getContentResolver().query(it, projection, selection, selectionArgs,null) } + if (cursor != null && cursor.moveToFirst()) { + val column_index: Int = cursor.getColumnIndexOrThrow(column) + return cursor.getString(column_index) + } + } finally { + if (cursor != null) cursor.close() + } + return null + } + + fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + } +} \ No newline at end of file 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 000000000..422b05fed --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -0,0 +1,153 @@ +package org.ryujinx.android + +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.anggrayudi.storage.SimpleStorageHelper +import org.ryujinx.android.ui.theme.RyujinxAndroidTheme +import org.ryujinx.android.viewmodels.HomeViewModel +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.views.HomeViews +import org.ryujinx.android.views.MainView + + +class MainActivity : ComponentActivity() { + private var mainViewModel: MainViewModel? = null + private var _isInit: Boolean = false + var storageHelper: SimpleStorageHelper? = null + companion object { + var AppPath : String? + var StorageHelper: SimpleStorageHelper? = null + init { + AppPath = "" + } + } + + init { + storageHelper = SimpleStorageHelper(this) + StorageHelper = storageHelper + } + + fun setFullScreen() :Unit { + requestedOrientation = + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + + WindowCompat.setDecorFitsSystemWindows(window,false) + + var insets = WindowCompat.getInsetsController(window, window.decorView) + + insets?.apply { + insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + private fun getAudioDevice () : Int { + var audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + + var devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + + + return if (devices.isEmpty()) + 0 + else { + var speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } + var earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET } + if(earPiece != null) + return earPiece.id + if(speaker != null) + return speaker.id + devices.first().id + } + } + + private fun initialize() : Unit + { + if(_isInit) + return + + var appPath: String = AppPath ?: return + var success = RyujinxNative().initialize(appPath, false) + _isInit = success + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AppPath = this.getExternalFilesDir(null)!!.absolutePath + + initialize() + + if(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + !Environment.isExternalStorageManager() + } else { + false + } + ) { + storageHelper?.storage?.requestFullStorageAccess() + } + mainViewModel = MainViewModel(this) + + mainViewModel?.apply { + setContent { + RyujinxAndroidTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + /*Box { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + GameHost(context) + } + ) + controller.Compose(lifecycleScope, lifecycle) + }*/ + 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) + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + RyujinxAndroidTheme { + HomeViews.Home() + } +} \ No newline at end of file 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 000000000..262b551c3 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt @@ -0,0 +1,9 @@ +package org.ryujinx.android + +import android.view.Surface + +class NativeGraphicsInterop { + var VkCreateSurface: Long = 0 + var SurfaceHandle: Long = 0 + var VkRequiredExtensions: Array? = null +} \ No newline at end of file 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 000000000..32ff5f1d3 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -0,0 +1,18 @@ +package org.ryujinx.android + +import android.view.Surface + +class NativeHelpers { + + companion object { + init { + System.loadLibrary("ryujinxjni") + } + } + external fun releaseNativeWindow(window:Long) : Unit + external fun createSurface(vkInstance:Long, window:Long) : Long + external fun getCreateSurfacePtr() : Long + external fun getNativeWindow(surface:Surface) : Long + external fun attachCurrentThread() : Unit + external fun detachCurrentThread() : Unit +} \ No newline at end of file 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 000000000..f9dfec477 --- /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/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt new file mode 100644 index 000000000..437f16ed7 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -0,0 +1,51 @@ +package org.ryujinx.android + +import android.view.Surface +import org.ryujinx.android.viewmodels.GameInfo +import org.ryujinx.android.viewmodels.GameStats +import java.io.FileDescriptor + +class RyujinxNative { + + external fun initialize(appPath: String, enableDebugLogs : Boolean): Boolean + + companion object { + init { + System.loadLibrary("ryujinx") + } + } + + external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean, + systemLanguage : Int, + regionCode : Int, + enableVsync : Boolean, + enableDockedMode : Boolean, + enablePtc : Boolean, + enableInternetAccess : Boolean, + timeZone : String, + ignoreMissingServices : Boolean): Boolean + external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean + external fun graphicsInitializeRenderer( + extensions: Array, + surface: Long + ): Boolean + + external fun deviceLoad(game: String): Boolean + external fun deviceGetGameStats(): GameStats + external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo + external fun deviceGetGameInfoFromPath(path: String): GameInfo + external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean + external fun graphicsRendererSetSize(width: Int, height: Int): Unit + external fun graphicsRendererSetVsync(enabled: Boolean): Unit + external fun graphicsRendererRunLoop(): Unit + external fun inputInitialize(width: Int, height: Int): Unit + external fun inputSetClientSize(width: Int, height: Int): Unit + external fun inputSetTouchPoint(x: Int, y: Int): Unit + external fun inputReleaseTouchPoint(): Unit + external fun inputUpdate(): Unit + external fun inputSetButtonPressed(button: Int, id: String): Unit + external fun inputSetButtonReleased(button: Int, id: String): Unit + external fun inputConnectGamepad(index: Int): String + external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: String): Unit + external fun graphicsSetSurface(surface: Long): String +} \ No newline at end of file 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 000000000..f1af8e151 --- /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/ui/theme/Color.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Color.kt new file mode 100644 index 000000000..243ab3667 --- /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 000000000..38fbf2513 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/ui/theme/Theme.kt @@ -0,0 +1,80 @@ +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.tooling.preview.Preview +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 000000000..c9097e38a --- /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/GameModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt new file mode 100644 index 000000000..2853a1c12 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt @@ -0,0 +1,55 @@ +package org.ryujinx.android.viewmodels + +import android.R.string +import android.content.ContentResolver +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.Helpers +import org.ryujinx.android.RyujinxNative + + +class GameModel(var file: DocumentFile, val context: Context) { + var fileName: String? + var fileSize = 0.0 + var titleName: String? = null + var titleId: String? = null + var developer: String? = null + var version: String? = null + var iconCache: String? = null + + init { + fileName = file.name + var absPath = getPath() + var gameInfo = RyujinxNative().deviceGetGameInfoFromPath(absPath ?: "") + + fileSize = gameInfo.FileSize + titleId = gameInfo.TitleId + titleName = gameInfo.TitleName + developer = gameInfo.Developer + version = gameInfo.Version + iconCache = gameInfo.IconCache + } + + fun getPath() : String? { + var uri = file.uri + if (uri.scheme != "file") + uri = Uri.parse("file://" + Helpers.getPath(context, file.uri)) + return uri.path; + } + + fun getIsXci() : Boolean { + return file.extension == "xci" + } +} + +class GameInfo { + var FileSize = 0.0 + var TitleName: String? = null + var TitleId: String? = null + var Developer: String? = null + var Version: String? = null + var IconCache: String? = null +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameStats.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameStats.kt new file mode 100644 index 000000000..e4d954312 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameStats.kt @@ -0,0 +1,7 @@ +package org.ryujinx.android.viewmodels + +class GameStats { + var Fifo = 0.0 + var GameFps = 0.0 + var GameTime = 0.0 +} \ No newline at end of file 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 000000000..0fffb7081 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt @@ -0,0 +1,97 @@ +package org.ryujinx.android.viewmodels + +import android.content.SharedPreferences +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.FileFullPath +import com.anggrayudi.storage.file.extension +import com.anggrayudi.storage.file.fullName +import com.anggrayudi.storage.file.getAbsolutePath +import com.anggrayudi.storage.file.search +import org.ryujinx.android.MainActivity + +class HomeViewModel( + val activity: MainActivity? = null, + val mainViewModel: MainViewModel? = null +) { + private var gameList: SnapshotStateList? = null + private var loadedCache: List = listOf() + private var gameFolderPath: DocumentFile? = null + private var sharedPref: SharedPreferences? = null; + + init { + if (activity != null) { + sharedPref = PreferenceManager.getDefaultSharedPreferences(activity) + activity.storageHelper!!.onFolderSelected = { requestCode, folder -> + run { + gameFolderPath = folder + var p = folder.getAbsolutePath(activity!!) + var editor = sharedPref?.edit() + editor?.putString("gameFolder", p); + editor?.apply() + reloadGameList() + } + } + + var savedFolder = sharedPref?.getString("gameFolder", "") ?: "" + + if (savedFolder.isNotEmpty()) { + try { + gameFolderPath = DocumentFileCompat.fromFullPath( + activity, + savedFolder, + documentType = DocumentFileType.FOLDER, + requiresWriteAccess = true + ) + + reloadGameList() + } catch (e: Exception) { + + } + } + } + } + + fun openGameFolder() { + var path = sharedPref?.getString("gameFolder", "") ?: "" + + if (path.isNullOrEmpty()) + activity?.storageHelper?.storage?.openFolderPicker(); + else + activity?.storageHelper?.storage?.openFolderPicker( + activity!!.storageHelper!!.storage.requestCodeFolderPicker, + FileFullPath(activity, path) + ) + } + + fun reloadGameList() { + var storage = activity?.storageHelper ?: return + var folder = gameFolderPath ?: return + + var files = mutableListOf() + + for (file in folder.search(false, DocumentFileType.FILE)) { + if (file.extension == "xci" || file.extension == "nsp") + activity?.let { + files.add(GameModel(file, it)) + } + } + + loadedCache = files.toList() + + applyFilter() + } + + private fun applyFilter() { + gameList?.clear() + gameList?.addAll(loadedCache) + } + + fun setViewList(list: SnapshotStateList) { + gameList = list; + applyFilter() + } +} \ No newline at end of file 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 000000000..c8d3664b2 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -0,0 +1,53 @@ +package org.ryujinx.android.viewmodels + +import androidx.compose.runtime.MutableState +import androidx.navigation.NavHostController +import org.ryujinx.android.GameHost +import org.ryujinx.android.MainActivity + +class MainViewModel(val activity: MainActivity) { + var selected: GameModel? = null + private var gameTimeState: MutableState? = null + private var gameFpsState: MutableState? = null + private var fifoState: MutableState? = null + private var navController : NavHostController? = null + + var homeViewModel: HomeViewModel = HomeViewModel(activity, this,) + + fun loadGame(game:GameModel) { + var controller = navController?: return; + activity.setFullScreen() + GameHost.gameModel = game + controller.navigate("game") + } + + fun setNavController(controller: NavHostController) { + navController = controller + } + + fun setStatStates( + fifo: MutableState, + gameFps: MutableState, + gameTime: MutableState + ) { + fifoState = fifo + gameFpsState = gameFps + gameTimeState = gameTime + } + + fun updateStats( + fifo: Double, + gameFps: Double, + gameTime: Double + ){ + fifoState?.apply { + this.value = fifo + } + gameFpsState?.apply { + this.value = gameFps + } + gameTimeState?.apply { + this.value = gameTime + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt new file mode 100644 index 000000000..670871572 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt @@ -0,0 +1,116 @@ +package org.ryujinx.android.viewmodels + +import androidx.appcompat.widget.ThemedSpinnerAdapter.Helper +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import com.anggrayudi.storage.SimpleStorageHelper +import com.google.gson.Gson +import org.ryujinx.android.Helpers +import org.ryujinx.android.MainActivity +import java.io.File +import kotlin.math.max + +class TitleUpdateViewModel(val titleId: String) { + private var storageHelper: SimpleStorageHelper + var pathsState: SnapshotStateList? = null + + companion object { + const val UpdateRequestCode = 1002 + } + + fun Remove(index: Int) { + if (index <= 0) + return + + data?.paths?.apply { + removeAt(index - 1); + pathsState?.clear() + pathsState?.addAll(this) + } + } + + fun Add() { + storageHelper.openFilePicker(UpdateRequestCode) + } + + fun save(index: Int) { + data?.apply { + this.selected = "" + if(paths.isNotEmpty() && index > 0) + { + var ind = max(index - 1, paths.count() - 1) + this.selected = paths[ind] + } + var gson = Gson() + var json = gson.toJson(this) + jsonPath = (MainActivity.AppPath + ?: "") + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json" + File(jsonPath).writeText(json) + } + } + + fun setPaths(paths: SnapshotStateList) { + pathsState = paths; + data?.apply { + pathsState?.clear() + pathsState?.addAll(this.paths) + } + } + + var data: TitleUpdateMetadata? = null + private var jsonPath: String + + init { + jsonPath = (MainActivity.AppPath + ?: "") + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json" + if (File(jsonPath).exists()) { + var gson = Gson() + data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) + + data?.apply { + var existingPaths = mutableListOf() + for (path in paths) { + if (File(path).exists()) { + existingPaths.add(path) + } + } + + if(!existingPaths.contains(selected)){ + selected = "" + } + pathsState?.clear() + pathsState?.addAll(existingPaths) + paths = existingPaths + } + } + + storageHelper = MainActivity.StorageHelper!! + + storageHelper.onFileSelected = { requestCode, files -> + run { + if(requestCode == UpdateRequestCode) + { + var file = files.firstOrNull() + file?.apply { + var path = Helpers.getPath(storageHelper.storage.context, file.uri) + if(!path.isNullOrEmpty()){ + data?.apply { + if(!paths.contains(path)) { + paths.add(path) + pathsState?.clear() + pathsState?.addAll(paths) + } + } + } + } + } + } + } + } +} + +data class TitleUpdateMetadata( + var selected: String = "", + var paths: MutableList = mutableListOf() +) \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt new file mode 100644 index 000000000..ea21ecd5f --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -0,0 +1,274 @@ +package org.ryujinx.android.views + +import android.content.res.Resources +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.DockedSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.AsyncImage +import org.ryujinx.android.MainActivity +import org.ryujinx.android.R +import org.ryujinx.android.viewmodels.GameModel +import org.ryujinx.android.viewmodels.HomeViewModel +import java.io.File +import kotlin.math.roundToInt + +class HomeViews { + companion object { + const val ImageSize = 150 + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun MainTopBar() { + TopAppBar( + modifier = Modifier + .zIndex(1f) + .padding(top = 10.dp), + title = { + DockedSearchBar( + shape = SearchBarDefaults.inputFieldShape, + query = "", + onQueryChange = {}, + onSearch = {}, + active = false, + onActiveChange = {}, + leadingIcon = { + Icon( + Icons.Filled.Search, + contentDescription = "Search Games" + ) + }, + placeholder = { + Text(text = "Search Games") + } + ) { + + } + }, + actions = { + IconButton( + onClick = { } + ) { + Icon( + Icons.Filled.MoreVert, + contentDescription = "More" + ) + } + } + ) + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Home(viewModel: HomeViewModel = HomeViewModel()) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + var showBottomSheet = remember { mutableStateOf(false) } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + MainTopBar() + }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + FloatingActionButton(onClick = { + viewModel.openGameFolder() + }, + shape = CircleShape) { + Icon( + Icons.Filled.Add, + contentDescription = "Options" + ) + } + } + + ) { contentPadding -> + Box(modifier = Modifier.padding(contentPadding)) { + var list = remember { + mutableStateListOf() + } + viewModel.setViewList(list) + LazyColumn(Modifier.fillMaxSize()) { + items(list) { + GameItem(it, viewModel, showBottomSheet) + } + } + } + + if(showBottomSheet.value) { + ModalBottomSheet(onDismissRequest = { + showBottomSheet.value = false + }, + sheetState = sheetState) { + val openDialog = remember { mutableStateOf(false) } + + if(openDialog.value) { + AlertDialog(onDismissRequest = { + openDialog.value = false + }) { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + var titleId = viewModel.mainViewModel?.selected?.titleId ?: "" + var name = viewModel.mainViewModel?.selected?.titleName ?: "" + TitleUpdateViews.Main(titleId, name, openDialog) + } + + } + } + Surface(color = MaterialTheme.colorScheme.surface, + modifier = Modifier.padding(10.dp)) { + Column(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { + Card( + onClick = { + openDialog.value = true + } + ) { + Column(modifier = Modifier.padding(10.dp)) { + Icon( + painter = painterResource(R.drawable.app_update), + contentDescription = "More", + tint = Color.Green, + modifier = Modifier + .width(48.dp) + .height(48.dp) + .align(Alignment.CenterHorizontally) + ) + Text(text = "Game Updates", + modifier = Modifier.align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.onSurface) + + } + } + } + } + } + } + } + } + } + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + @Composable + fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState) { + Card(shape = MaterialTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + .combinedClickable( + onClick = { + if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { + viewModel.mainViewModel?.loadGame(gameModel) + } + }, + onLongClick = { + viewModel.mainViewModel?.selected = gameModel + showSheet.value = true + })) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween) { + Row { + if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") + { + var iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache + var imageFile = File(iconSource) + if(imageFile.exists()) { + val size = ImageSize / Resources.getSystem().displayMetrics.density + AsyncImage(model = imageFile, + contentDescription = gameModel.titleName + " icon", + modifier = Modifier + .padding(end = 5.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp)) + } + else NotAvailableIcon() + } else NotAvailableIcon() + Column{ + Text(text = gameModel.titleName ?: "") + Text(text = gameModel.developer ?: "") + Text(text = gameModel.titleId ?: "") + } + } + Column{ + Text(text = gameModel.version ?: "") + Text(text = String.format("%.3f", gameModel.fileSize)) + } + } + } + } + + @Composable + fun NotAvailableIcon() { + var size = ImageSize / Resources.getSystem().displayMetrics.density + Icon( + Icons.Filled.Add, + contentDescription = "Options", + modifier = Modifier + .padding(end = 5.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } + + } + + @Preview + @Composable + fun HomePreview() { + Home() + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt new file mode 100644 index 000000000..0392cac3f --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -0,0 +1,79 @@ +package org.ryujinx.android.views + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.lifecycleScope +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.ryujinx.android.GameController +import org.ryujinx.android.GameHost +import org.ryujinx.android.viewmodels.MainViewModel + +class MainView { + companion object { + @Composable + fun Main(mainViewModel: MainViewModel){ + val navController = rememberNavController() + mainViewModel.setNavController(navController) + + NavHost(navController = navController, startDestination = "home") { + composable("home") {HomeViews.Home(mainViewModel.homeViewModel)} + composable("game") { GameView(mainViewModel)} + } + } + + @Composable + fun GameView(mainViewModel: MainViewModel){ + Box { + var controller = GameController(mainViewModel.activity) + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + GameHost(context, controller, mainViewModel) + } + ) + GameStats(mainViewModel) + controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle) + } + } + + @Composable + fun GameStats(mainViewModel: MainViewModel){ + var fifo = remember { + mutableStateOf(0.0) + } + var gameFps = remember { + mutableStateOf(0.0) + } + var gameTime = remember { + mutableStateOf(0.0) + } + + Surface(modifier = Modifier.padding(10.dp), + color = MaterialTheme.colorScheme.surface.copy(0.4f)) { + Column { + var gameTimeVal = 0.0; + if (!gameTime.value.isInfinite()) + gameTimeVal = gameTime.value + Text(text = "${String.format("%.3f", fifo.value)} %") + Text(text = "${String.format("%.3f", gameFps.value)} FPS") + Text(text = "${String.format("%.3f", gameTimeVal)} ms") + } + } + + mainViewModel.setStatStates(fifo, gameFps, gameTime) + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt new file mode 100644 index 000000000..42b9d3ce5 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -0,0 +1,131 @@ +package org.ryujinx.android.views + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.ryujinx.android.viewmodels.TitleUpdateViewModel + +class TitleUpdateViews { + companion object { + @Composable + fun Main(titleId: String, name: String, openDialog: MutableState) { + val viewModel = TitleUpdateViewModel(titleId) + + var selected = remember { mutableStateOf(0) } + viewModel.data?.apply { + selected.value = paths.indexOf(this.selected) + 1 + } + + Column(modifier = Modifier.padding(16.dp)) { + + Column { + Text(text = "Updates for ${name}", textAlign = TextAlign.Center) + Surface( + modifier = Modifier + .padding(5.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .height(300.dp) + .fillMaxWidth() + ) { + Row(modifier = Modifier.padding(5.dp)) { + RadioButton( + selected = (selected.value == 0), + onClick = { selected.value = 0 + }) + Text( + text = "None", + modifier = Modifier.fillMaxWidth() + .align(Alignment.CenterVertically) + ) + } + + var paths = remember { + mutableStateListOf() + } + + viewModel.setPaths(paths) + var index = 1 + for (path in paths) { + var i = index + Row(modifier = Modifier.padding(5.dp)) { + RadioButton( + selected = (selected.value == i), + onClick = { selected.value = i }) + Text( + text = path, + modifier = Modifier.fillMaxWidth() + .align(Alignment.CenterVertically) + ) + } + + index++ + } + } + } + Row(modifier = Modifier.align(Alignment.End)) { + IconButton( + onClick = { + viewModel.Remove(selected.value) + } + ) { + Icon( + Icons.Filled.Delete, + contentDescription = "Remove" + ) + } + + IconButton( + onClick = { + viewModel.Add() + } + ) { + Icon( + Icons.Filled.Add, + contentDescription = "Remove" + ) + } + } + + } + Spacer(modifier = Modifier.height(15.dp)) + TextButton( + modifier = Modifier.align(Alignment.End), + onClick = { + openDialog.value = false + viewModel.save(selected.value) + }, + ) { + Text("Save") + } + } + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml b/src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml new file mode 100644 index 000000000..e5de70f7a --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/drawable/app_update.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml b/src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml new file mode 100644 index 000000000..d30d7c303 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/layout/game_layout.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/src/RyujinxAndroid/app/src/main/res/values/colors.xml b/src/RyujinxAndroid/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/values/strings.xml b/src/RyujinxAndroid/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..ea3527878 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RyujinxAndroid + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/values/themes.xml b/src/RyujinxAndroid/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..9b1784b45 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +