diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index 255adb456..1d443f299 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -6,6 +6,7 @@ using LibRyujinx.Jni.Values; using LibRyujinx.Shared.Audio.Oboe; using Microsoft.Win32.SafeHandles; using Rxmxnx.PInvoke; +using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; @@ -21,7 +22,6 @@ using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; -using System.Security.Cryptography; using System.Text; using System.Threading; @@ -42,7 +42,23 @@ namespace LibRyujinx private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); [DllImport("libryujinxjni")] - private extern static void pushString(string ch); + private extern static long storeString(string ch); + [DllImport("libryujinxjni")] + private extern static IntPtr getString(long id); + + private static string GetStoredString(long id) + { + var pointer = getString(id); + if (pointer != IntPtr.Zero) + { + var str = Marshal.PtrToStringAnsi(pointer) ?? ""; + + Marshal.FreeHGlobal(pointer); + return str; + } + + return ""; + } [DllImport("libryujinxjni")] internal extern static void setRenderingThread(); @@ -68,8 +84,9 @@ namespace LibRyujinx } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")] - public static JBoolean JniInitialize(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef jpath, JBoolean enableDebugLogs) + public static JBoolean JniInitialize(JEnvRef jEnv, JObjectLocalRef jObj, JLong jpathId, JBoolean enableDebugLogs) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SystemInfo.IsBionic = true; Logger.AddTarget( @@ -79,7 +96,7 @@ namespace LibRyujinx AsyncLogTargetOverflowAction.Block )); - var path = GetString(jEnv, jpath); + var path = GetStoredString(jpathId); var init = Initialize(path, enableDebugLogs); @@ -102,6 +119,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")] public static void JniReloadFileSystem() { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SwitchDevice?.ReloadFileSystem(); } @@ -116,10 +134,11 @@ namespace LibRyujinx JBoolean enableDockedMode, JBoolean enablePtc, JBoolean enableInternetAccess, - JStringLocalRef timeZone, + JLong timeZoneId, JBoolean ignoreMissingServices) { - AudioDriver = new OboeHardwareDeviceDriver(); + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + AudioDriver = new OpenALHardwareDeviceDriver();//new OboeHardwareDeviceDriver(); return InitializeDevice(isHostMapped, useNce, (SystemLanguage)(int)systemLanguage, @@ -128,13 +147,14 @@ namespace LibRyujinx enableDockedMode, enablePtc, enableInternetAccess, - GetString(jEnv, timeZone), + GetStoredString(timeZoneId), ignoreMissingServices); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFifo")] public static JDouble JniGetGameFifo(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var stats = SwitchDevice.EmulationContext?.Statistics.GetFifoPercent() ?? 0; return stats; @@ -143,6 +163,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFrameTime")] public static JDouble JniGetGameFrameTime(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var stats = SwitchDevice.EmulationContext?.Statistics.GetGameFrameTime() ?? 0; return stats; @@ -151,6 +172,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFrameRate")] public static JDouble JniGetGameFrameRate(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var stats = SwitchDevice.EmulationContext?.Statistics.GetGameFrameRate() ?? 0; return stats; @@ -159,6 +181,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoad")] public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); if (SwitchDevice?.EmulationContext == null) { return false; @@ -169,10 +192,23 @@ namespace LibRyujinx return LoadApplication(path); } - [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcContentList")] - public static JArrayLocalRef JniGetDlcContentListNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JLong titleId) + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLaunchMiiEditor")] + public static JBoolean JniLaunchMiiEditApplet(JEnvRef jEnv, JObjectLocalRef jObj) { - var list = GetDlcContentList(GetString(jEnv, pathPtr), (ulong)(long)titleId); + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + if (SwitchDevice?.EmulationContext == null) + { + return false; + } + + return LaunchMiiEditApplet(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcContentList")] + public static JArrayLocalRef JniGetDlcContentListNative(JEnvRef jEnv, JObjectLocalRef jObj, JLong pathPtr, JLong titleId) + { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + var list = GetDlcContentList(GetStoredString(pathPtr), (ulong)(long)titleId); debug_break(4); @@ -180,26 +216,30 @@ namespace LibRyujinx } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcTitleId")] - public static JStringLocalRef JniGetDlcTitleIdNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JStringLocalRef ncaPath) + public static JLong JniGetDlcTitleIdNative(JEnvRef jEnv, JObjectLocalRef jObj, JLong pathPtr, JLong ncaPath) { - return CreateString(jEnv, GetDlcTitleId(GetString(jEnv, pathPtr), GetString(jEnv, ncaPath))); + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + return storeString(GetDlcTitleId(GetStoredString(pathPtr), GetStoredString(ncaPath))); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceSignalEmulationClose")] public static void JniSignalEmulationCloseNative(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SignalEmulationClose(); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceCloseEmulation")] public static void JniCloseEmulationNative(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); CloseEmulation(); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")] public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); if (SwitchDevice?.EmulationContext == null) { return false; @@ -213,6 +253,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")] public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); JEnvValue value = jEnv.Environment; ref JNativeInterface jInterface = ref value.Functions; IntPtr getObjectClassPtr = jInterface.GetObjectClassPointer; @@ -255,6 +296,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsSetSurface")] public static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr, JLong window) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); _surfacePtr = surfacePtr; _window = window; @@ -267,6 +309,7 @@ namespace LibRyujinx JArrayLocalRef extensionsArray, JLong driverHandle) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); if (Renderer != null) { return false; @@ -354,12 +397,14 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSize")] public static void JniSetRendererSizeNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); Renderer?.Window?.SetSize(width, height); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")] public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetSwapBuffersCallback(() => { var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime(); @@ -371,21 +416,24 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")] public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var info = GetGameInfo(GetString(jEnv, path)); - return GetInfo(jEnv, info, out SHA256 _); + return GetInfo(jEnv, info); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")] public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); using var stream = OpenFile(fileDescriptor); var info = GetGameInfo(stream, isXci); - return GetInfo(jEnv, info, out SHA256 _); + return GetInfo(jEnv, info); } - private static JObjectLocalRef GetInfo(JEnvRef jEnv, GameInfo? info, out SHA256 sha) + private static JObjectLocalRef GetInfo(JEnvRef jEnv, GameInfo? info) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var javaClassName = GetCCharSequence("org/ryujinx/android/viewmodels/GameInfo"); JEnvValue value = jEnv.Environment; @@ -411,24 +459,12 @@ namespace LibRyujinx var newGlobal = newGlobalRef(jEnv, javaClass._value); var constructor = getMethod(jEnv, javaClass, GetCCharSequence(""), GetCCharSequence("()V")); var newObj = newObject(jEnv, javaClass, constructor, 0); - sha = SHA256.Create(); - var iconCacheByte = sha.ComputeHash(info?.Icon ?? Array.Empty()); - var iconCache = BitConverter.ToString(iconCacheByte).Replace("-", ""); - - var cacheDirectory = Path.Combine(AppDataManager.BaseDirPath, "iconCache"); - Directory.CreateDirectory(cacheDirectory); - - var cachePath = Path.Combine(cacheDirectory, iconCache); - if (!File.Exists(cachePath)) - { - File.WriteAllBytes(cachePath, info?.Icon ?? Array.Empty()); - } setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("TitleName"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.TitleName)._value); setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("TitleId"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.TitleId)._value); setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Developer"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.Developer)._value); setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Version"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.Version)._value); - setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("IconCache"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, iconCache)._value); + setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Icon"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, Convert.ToBase64String(info?.Icon ?? Array.Empty()))._value); setDoubleField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("FileSize"), GetCCharSequence("D")), info?.FileSize ?? 0d); return newObj; @@ -450,88 +486,102 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetVsync")] public static void JniSetVsyncStateNative(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean enabled) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetVsyncState(enabled); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSwapBufferCallback")] public static void JniSetSwapBuffersCallbackNative(JEnvRef jEnv, JObjectLocalRef jObj, IntPtr swapBuffersCallback) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); _swapBuffersCallback = Marshal.GetDelegateForFunctionPointer(swapBuffersCallback); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputInitialize")] public static void JniInitializeInput(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); InitializeInput(width, height); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetClientSize")] public static void JniSetClientSize(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetClientSize(width, height); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetTouchPoint")] public static void JniSetTouchPoint(JEnvRef jEnv, JObjectLocalRef jObj, JInt x, JInt y) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetTouchPoint(x, y); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputReleaseTouchPoint")] public static void JniReleaseTouchPoint(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); ReleaseTouchPoint(); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputUpdate")] public static void JniUpdateInput(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); UpdateInput(); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonPressed")] public static void JniSetButtonPressed(JEnvRef jEnv, JObjectLocalRef jObj, JInt button, JInt id) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetButtonPressed((GamepadButtonInputId)(int)button, id); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonReleased")] public static void JniSetButtonReleased(JEnvRef jEnv, JObjectLocalRef jObj, JInt button, JInt id) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetButtonReleased((GamepadButtonInputId)(int)button, id); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")] public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); SetStickAxis((StickInputId)(int)stick, new Vector2(float.IsNaN(x) ? 0 : x, float.IsNaN(y) ? 0 : y), id); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputConnectGamepad")] public static JInt JniConnectGamepad(JEnvRef jEnv, JObjectLocalRef jObj, JInt index) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); return ConnectGamepad(index); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")] - public static void JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj) + public static JLong JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var userId = GetOpenedUser(); - pushString(userId); + return storeString(userId); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")] - public static JStringLocalRef JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + public static JLong JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr) { - var userId = GetString(jEnv, userIdPtr) ?? ""; + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + var userId = GetStoredString(userIdPtr) ?? ""; - return CreateString(jEnv, GetUserPicture(userId)); + return storeString(GetUserPicture(userId)); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserPicture")] public static void JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef picturePtr) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var userId = GetString(jEnv, userIdPtr) ?? ""; var picture = GetString(jEnv, picturePtr) ?? ""; @@ -539,16 +589,18 @@ namespace LibRyujinx } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserName")] - public static JStringLocalRef JniGetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + public static JLong JniGetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr) { - var userId = GetString(jEnv, userIdPtr) ?? ""; + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + var userId = GetStoredString(userIdPtr) ?? ""; - return CreateString(jEnv, GetUserName(userId)); + return storeString(GetUserName(userId)); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserName")] public static void JniSetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef userNamePtr) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var userId = GetString(jEnv, userIdPtr) ?? ""; var userName = GetString(jEnv, userNamePtr) ?? ""; @@ -558,6 +610,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetAllUsers")] public static JArrayLocalRef JniGetAllUsers(JEnvRef jEnv, JObjectLocalRef jObj) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var users = GetAllUsers(); return CreateStringArray(jEnv, users.ToList()); @@ -566,6 +619,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userAddUser")] public static void JniAddUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userNamePtr, JStringLocalRef picturePtr) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var userName = GetString(jEnv, userNamePtr) ?? ""; var picture = GetString(jEnv, picturePtr) ?? ""; @@ -575,15 +629,17 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userDeleteUser")] public static void JniDeleteUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var userId = GetString(jEnv, userIdPtr) ?? ""; DeleteUser(userId); } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")] - public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr) { - var userId = GetString(jEnv, userIdPtr) ?? ""; + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + var userId = GetStoredString(userIdPtr) ?? ""; OpenUser(userId); } @@ -591,6 +647,7 @@ namespace LibRyujinx [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userCloseUser")] public static void JniCloseUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); var userId = GetString(jEnv, userIdPtr) ?? ""; CloseUser(userId); diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs index be6967930..484580c93 100644 --- a/src/LibRyujinx/LibRyujinx.cs +++ b/src/LibRyujinx/LibRyujinx.cs @@ -26,6 +26,7 @@ using LibHac.FsSystem; using LibHac.Fs; using Path = System.IO.Path; using LibHac; +using OpenTK.Audio.OpenAL; using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.Common.Utilities; using System.Globalization; @@ -76,6 +77,8 @@ namespace LibRyujinx Console.WriteLine(ex); return false; } + + OpenALLibraryNameContainer.OverridePath = "libopenal.so"; return true; } diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index a2ecc7554..606440096 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "org.ryujinx.android" minSdk 30 targetSdk 33 - versionCode 10001 - versionName '1.0.1' + versionCode 10004 + versionName '1.0.4' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -94,7 +94,6 @@ dependencies { 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.2' diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index 73e7d70d3..549ed4ed2 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -27,14 +27,10 @@ android:supportsRtl="true" android:theme="@style/Theme.RyujinxAndroid" tools:targetApi="31"> - diff --git a/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt index 6ae2af030..5900c6212 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt +++ b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt @@ -28,7 +28,7 @@ add_library( # Sets the name of the library. # Provides a relative path to your source file(s). vulkan_wrapper.cpp - oboe.cpp + string_helper.cpp ryujinx.cpp) # Searches for a specified prebuilt library and stores the path as a @@ -44,8 +44,6 @@ find_library( # Sets the name of the path variable. # 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. @@ -54,7 +52,6 @@ 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 deleted file mode 100644 index f643a4fbe..000000000 --- a/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp +++ /dev/null @@ -1,147 +0,0 @@ -// -// Created by Emmanuel Hansen on 6/27/2023. -// - -#include "oboe.h" - -static int s_device_id = 0; - -void AudioSession::initialize() { -} - -void AudioSession::destroy() { - if(stream == nullptr) - return; - stream->close(); - - stream = nullptr; -} - -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; - session = nullptr; - return nullptr; - } - session->stream = stream; - - return session; -} - -void start_session(AudioSession *session) { - if (session == nullptr) - return; - session->start(); -} - -void stop_session(AudioSession *session) { - if (session == nullptr) - return; - session->stop(); -} - -void set_session_volume(AudioSession *session, float volume) { - if (session == nullptr) - return; - session->volume = volume; -} - -float get_session_volume(AudioSession *session) { - if (session == nullptr) - return 0; - return session->volume; -} - -void close_session(AudioSession *session) { - if (session == nullptr) - return; - session->destroy(); - - delete session; -} - -bool is_playing(AudioSession *session) { - if (session == nullptr) - return false; - return session->isStarted; -} - -void write_to_session(AudioSession *session, uint64_t data, uint64_t samples) { - if (session == nullptr) - return; - 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 deleted file mode 100644 index 0d6559417..000000000 --- a/src/RyujinxAndroid/app/src/main/cpp/oboe.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// 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 index 87e7c3124..836cb6ccc 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -19,6 +19,7 @@ #include #include "libraries/adrenotools/include/adrenotools/driver.h" #include "native_window.h" +#include "string_helper.h" // A macro to pass call to Vulkan and check for return value for success #define CALL_VK(func) \ @@ -46,5 +47,6 @@ JavaVM* _vm = nullptr; jobject _mainActivity = nullptr; jclass _mainActivityClass = nullptr; std::string _currentString = ""; +string_helper str_helper = string_helper(); #endif //RYUJINXNATIVE_RYUIJNX_H diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index 095f462db..391d71850 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -312,24 +312,28 @@ Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz return createStringFromStdString(env, progressInfo); } +extern "C" +long storeString(char* str){ + return str_helper.store_cstring(str); +} + +extern "C" +const char* getString(long id){ + auto str = str_helper.get_stored(id); + auto cstr = (char*)::malloc(str.length() + 1); + ::strcpy(cstr, str.c_str()); + return cstr; +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz, jstring string) { + auto str = getStringPointer(env, string); + return str_helper.store_cstring(str); +} + extern "C" JNIEXPORT jstring JNICALL -Java_org_ryujinx_android_NativeHelpers_popStringJava(JNIEnv *env, jobject thiz) { - return createStringFromStdString(env, _currentString); -} -extern "C" -JNIEXPORT void JNICALL -Java_org_ryujinx_android_NativeHelpers_pushStringJava(JNIEnv *env, jobject thiz, jstring string) { - _currentString = getStringPointer(env, string); -} - - -extern "C" -void pushString(char* str){ - _currentString = str; -} - -extern "C" -const char* popString(){ - return _currentString.c_str(); +Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) { + return createStringFromStdString(env, str_helper.get_stored(id)); } diff --git a/src/RyujinxAndroid/app/src/main/cpp/string_helper.cpp b/src/RyujinxAndroid/app/src/main/cpp/string_helper.cpp new file mode 100644 index 000000000..e8e8483b0 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/string_helper.cpp @@ -0,0 +1,24 @@ +// +// Created by Emmanuel Hansen on 10/30/2023. +// + +#include "string_helper.h" + +long string_helper::store_cstring(const char *cstr) { + auto id = ++current_id; + _map.insert({id, cstr}); + return id; +} + +long string_helper::store_string(const string& str) { + auto id = ++current_id; + _map.insert({id, str}); + return id; +} + +string string_helper::get_stored(long id) { + auto str = _map[id]; + _map.erase(id); + + return str; +} diff --git a/src/RyujinxAndroid/app/src/main/cpp/string_helper.h b/src/RyujinxAndroid/app/src/main/cpp/string_helper.h new file mode 100644 index 000000000..b43993d29 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/cpp/string_helper.h @@ -0,0 +1,29 @@ +// +// Created by Emmanuel Hansen on 10/30/2023. +// + +#ifndef RYUJINXANDROID_STRING_HELPER_H +#define RYUJINXANDROID_STRING_HELPER_H + +#include +#include +using namespace std; +class string_helper { +public: + long store_cstring(const char * cstr); + long store_string(const string& str); + + string get_stored(long id); + + string_helper(){ + _map = unordered_map(); + current_id = 0; + } + +private: + unordered_map _map; + long current_id; +}; + + +#endif //RYUJINXANDROID_STRING_HELPER_H diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt deleted file mode 100644 index 1670e879a..000000000 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt +++ /dev/null @@ -1,407 +0,0 @@ -package org.ryujinx.android - -import android.annotation.SuppressLint -import android.content.Intent -import android.content.pm.ActivityInfo -import android.os.Bundle -import android.view.KeyEvent -import android.view.MotionEvent -import androidx.activity.compose.BackHandler -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.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.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Popup -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat -import compose.icons.CssGgIcons -import compose.icons.cssggicons.ToolbarBottom -import org.ryujinx.android.ui.theme.RyujinxAndroidTheme -import org.ryujinx.android.viewmodels.MainViewModel -import org.ryujinx.android.viewmodels.QuickSettings -import kotlin.math.abs -import kotlin.math.roundToInt - -class GameActivity : BaseActivity() { - private var physicalControllerManager: PhysicalControllerManager = - PhysicalControllerManager(this) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - MainActivity.mainViewModel!!.physicalControllerManager = physicalControllerManager - setContent { - RyujinxAndroidTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - GameView(mainViewModel = MainActivity.mainViewModel!!) - } - } - } - } - - @SuppressLint("RestrictedApi") - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - event?.apply { - if (physicalControllerManager.onKeyEvent(this)) - return true - } - return super.dispatchKeyEvent(event) - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - ev?.apply { - physicalControllerManager.onMotionEvent(this) - } - return super.dispatchGenericMotionEvent(ev) - } - - override fun onStop() { - super.onStop() - - NativeHelpers().setTurboMode(false) - force60HzRefreshRate(false) - } - - override fun onResume() { - super.onResume() - - setFullScreen(true) - NativeHelpers().setTurboMode(true) - force60HzRefreshRate(true) - } - - override fun onPause() { - super.onPause() - - NativeHelpers().setTurboMode(false) - force60HzRefreshRate(false) - } - - private fun force60HzRefreshRate(enable: Boolean) { - // Hack for MIUI devices since they don't support the standard Android APIs - try { - val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS") - setFpsIntent.putExtra("package_name", "org.ryujinx.android") - setFpsIntent.putExtra("isEnter", enable) - sendBroadcast(setFpsIntent) - } catch (_: Exception) { - } - - if (enable) - display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) } - ?.let { window.attributes.preferredDisplayModeId = it.modeId } - else - display?.supportedModes?.maxByOrNull { it.refreshRate } - ?.let { window.attributes.preferredDisplayModeId = it.modeId } - } - - private fun setFullScreen(fullscreen: Boolean) { - requestedOrientation = - if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER - - val insets = WindowCompat.getInsetsController(window, window.decorView) - - insets.apply { - if (fullscreen) { - insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) - insets.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } else { - insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) - insets.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_DEFAULT - } - } - } - - @Composable - fun GameView(mainViewModel: MainViewModel) { - Box(modifier = Modifier.fillMaxSize()) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - GameHost(context, mainViewModel) - } - ) - GameOverlay(mainViewModel) - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun GameOverlay(mainViewModel: MainViewModel) { - Box(modifier = Modifier.fillMaxSize()) { - GameStats(mainViewModel) - - val ryujinxNative = RyujinxNative() - - val showController = remember { - mutableStateOf(QuickSettings(this@GameActivity).useVirtualController) - } - val enableVsync = remember { - mutableStateOf(QuickSettings(this@GameActivity).enableVsync) - } - val showMore = remember { - mutableStateOf(false) - } - - val showLoading = remember { - mutableStateOf(true) - } - - val progressValue = remember { - mutableStateOf(0.0f) - } - - val progress = remember { - mutableStateOf("Loading") - } - - mainViewModel.setProgressStates(showLoading, progressValue, progress) - - // touch surface - Surface(color = Color.Transparent, modifier = Modifier - .fillMaxSize() - .padding(0.dp) - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - if (showController.value) - continue - - val change = event - .component1() - .firstOrNull() - change?.apply { - val position = this.position - - when (event.type) { - PointerEventType.Press -> { - ryujinxNative.inputSetTouchPoint( - position.x.roundToInt(), - position.y.roundToInt() - ) - } - - PointerEventType.Release -> { - ryujinxNative.inputReleaseTouchPoint() - - } - - PointerEventType.Move -> { - ryujinxNative.inputSetTouchPoint( - position.x.roundToInt(), - position.y.roundToInt() - ) - - } - } - } - } - } - }) { - } - if (!showLoading.value) { - GameController.Compose(mainViewModel) - - Row( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(8.dp) - ) { - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = true - }) { - Icon( - imageVector = CssGgIcons.ToolbarBottom, - contentDescription = "Open Panel" - ) - } - } - - if (showMore.value) { - Popup( - alignment = Alignment.BottomCenter, - onDismissRequest = { showMore.value = false }) { - Surface( - modifier = Modifier.padding(16.dp), - shape = MaterialTheme.shapes.medium - ) { - Row(modifier = Modifier.padding(8.dp)) { - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = false - showController.value = !showController.value - mainViewModel.controller?.setVisible(showController.value) - }) { - Icon( - imageVector = Icons.videoGame(), - contentDescription = "Toggle Virtual Pad" - ) - } - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = false - enableVsync.value = !enableVsync.value - RyujinxNative().graphicsRendererSetVsync(enableVsync.value) - }) { - Icon( - imageVector = Icons.vSync(), - tint = if (enableVsync.value) Color.Green else Color.Red, - contentDescription = "Toggle VSync" - ) - } - } - } - } - } - } - - val showBackNotice = remember { - mutableStateOf(false) - } - - BackHandler { - showBackNotice.value = true - } - - if (showLoading.value) { - Card( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(0.5f) - .align(Alignment.Center), - shape = MaterialTheme.shapes.medium - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - ) { - Text(text = progress.value) - - if (progressValue.value > -1) - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - progress = progressValue.value - ) - else - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) - } - - } - } - - if (showBackNotice.value) { - AlertDialog(onDismissRequest = { showBackNotice.value = false }) { - Column { - Surface( - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight(), - shape = MaterialTheme.shapes.large, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text(text = "Are you sure you want to exit the game?") - Text(text = "All unsaved data will be lost!") - } - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Button(onClick = { - showBackNotice.value = false - mainViewModel.closeGame() - setFullScreen(false) - finishActivity(0) - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Exit Game") - } - - Button(onClick = { - showBackNotice.value = false - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Dismiss") - } - } - } - } - } - } - } - } - } - - @Composable - fun GameStats(mainViewModel: MainViewModel) { - val fifo = remember { - mutableStateOf(0.0) - } - val gameFps = remember { - mutableStateOf(0.0) - } - val gameTime = remember { - mutableStateOf(0.0) - } - - Surface( - modifier = Modifier.padding(16.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) - } -} 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 index f8781021e..2e77ad21b 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -96,7 +96,7 @@ class GameController(var activity: Activity) { rightGamePad.gravityX = 1f rightGamePad.gravityY = 1f - ryujinxNative = RyujinxNative() + ryujinxNative = RyujinxNative.instance } fun setVisible(isVisible: Boolean){ @@ -110,7 +110,7 @@ class GameController(var activity: Activity) { fun connect(){ if(controllerId == -1) - controllerId = RyujinxNative().inputConnectGamepad(0) + controllerId = RyujinxNative.instance.inputConnectGamepad(0) } private fun handleEvent(ev: Event) { 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 index f19fc93da..48cfb4ff6 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -12,7 +12,8 @@ import org.ryujinx.android.viewmodels.QuickSettings import kotlin.concurrent.thread @SuppressLint("ViewConstructor") -class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { +class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), + SurfaceHolder.Callback { private var isProgressHidden: Boolean = false private var progress: MutableState? = null private var progressValue: MutableState? = null @@ -26,9 +27,9 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su private var _guestThread: Thread? = null private var _isInit: Boolean = false private var _isStarted: Boolean = false - private val nativeWindow : NativeWindow + private val nativeWindow: NativeWindow - private var _nativeRyujinx: RyujinxNative = RyujinxNative() + private var _nativeRyujinx: RyujinxNative = RyujinxNative.instance init { holder.addCallback(this) @@ -42,12 +43,11 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - if(_isClosed) + if (_isClosed) return start(holder) - if(_width != width || _height != height) - { + if (_width != width || _height != height) { val window = nativeWindow.requeryWindowHandle() _nativeRyujinx.graphicsSetSurface(window, nativeWindow.nativePointer) @@ -62,8 +62,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su height ) - if(_isStarted) - { + if (_isStarted) { _nativeRyujinx.inputSetClientSize(width, height) } } @@ -72,7 +71,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } - fun close(){ + fun close() { _isClosed = true _isInit = false _isStarted = false @@ -82,19 +81,18 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } private fun start(surfaceHolder: SurfaceHolder) { - if(_isStarted) + if (_isStarted) return - game = mainViewModel.gameModel + game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel; _nativeRyujinx.inputInitialize(width, height) val settings = QuickSettings(mainViewModel.activity) - if(!settings.useVirtualController){ + if (!settings.useVirtualController) { mainViewModel.controller?.setVisible(false) - } - else{ + } else { mainViewModel.controller?.connect() } @@ -112,19 +110,19 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su _updateThread = thread(start = true) { var c = 0 - val helper = NativeHelpers() + val helper = NativeHelpers.instance while (_isStarted) { _nativeRyujinx.inputUpdate() Thread.sleep(1) showLoading?.apply { - if(value){ + if (value) { var value = helper.getProgressValue() - if(value != -1f) - progress?.apply { - this.value = helper.getProgressInfo() - } + if (value != -1f) + progress?.apply { + this.value = helper.getProgressInfo() + } progressValue?.apply { this.value = value @@ -133,12 +131,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } c++ if (c >= 1000) { - if(helper.getProgressValue() == -1f) + if (helper.getProgressValue() == -1f) progress?.apply { - this.value = "Loading ${game!!.titleName}" + this.value = "Loading ${if(mainViewModel.isMiiEditorLaunched) "Mii Editor" else game!!.titleName}" } c = 0 - mainViewModel.updateStats(_nativeRyujinx.deviceGetGameFifo(), _nativeRyujinx.deviceGetGameFrameRate(), _nativeRyujinx.deviceGetGameFrameTime()) + mainViewModel.updateStats( + _nativeRyujinx.deviceGetGameFifo(), + _nativeRyujinx.deviceGetGameFrameRate(), + _nativeRyujinx.deviceGetGameFrameTime() + ) } } } 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 index aef544c7a..7cda8a3ac 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -216,7 +216,7 @@ class Helpers { } } finally { isImporting.value = false - RyujinxNative().deviceReloadFilesystem() + RyujinxNative.instance.deviceReloadFilesystem() } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt index 54a508298..b6e2cea6b 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -23,6 +23,121 @@ class Icons { companion object{ /// Icons exported from https://www.composables.com/icons @Composable + fun applets(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "apps", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(9.708f, 33.125f) + quadToRelative(-1.208f, 0f, -2.02f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.02f) + quadToRelative(0f, -1.167f, 0.813f, -2f) + quadToRelative(0.812f, -0.834f, 2.02f, -0.834f) + quadToRelative(1.167f, 0f, 2f, 0.813f) + quadToRelative(0.834f, 0.812f, 0.834f, 2.021f) + quadToRelative(0f, 1.208f, -0.813f, 2.02f) + quadToRelative(-0.812f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -1.979f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.02f) + quadToRelative(0f, -1.167f, 0.813f, -2f) + quadToRelative(0.812f, -0.834f, 1.979f, -0.834f) + reflectiveQuadToRelative(2f, 0.813f) + quadToRelative(0.833f, 0.812f, 0.833f, 2.021f) + quadToRelative(0f, 1.208f, -0.812f, 2.02f) + quadToRelative(-0.813f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -2f, -0.813f) + quadToRelative(-0.834f, -0.812f, -0.834f, -2.02f) + quadToRelative(0f, -1.167f, 0.813f, -2f) + quadToRelative(0.812f, -0.834f, 2.021f, -0.834f) + quadToRelative(1.208f, 0f, 2.02f, 0.813f) + quadToRelative(0.813f, 0.812f, 0.813f, 2.021f) + quadToRelative(0f, 1.208f, -0.813f, 2.02f) + quadToRelative(-0.812f, 0.813f, -2.02f, 0.813f) + close() + moveTo(9.708f, 22.792f) + quadToRelative(-1.208f, 0f, -2.02f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -1.979f) + reflectiveQuadToRelative(0.813f, -2f) + quadToRelative(0.812f, -0.833f, 2.02f, -0.833f) + quadToRelative(1.167f, 0f, 2f, 0.812f) + quadToRelative(0.834f, 0.813f, 0.834f, 2.021f) + quadToRelative(0f, 1.167f, -0.813f, 1.979f) + quadToRelative(-0.812f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -1.979f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -1.979f) + reflectiveQuadToRelative(0.813f, -2f) + quadToRelative(0.812f, -0.833f, 1.979f, -0.833f) + reflectiveQuadToRelative(2f, 0.812f) + quadToRelative(0.833f, 0.813f, 0.833f, 2.021f) + quadToRelative(0f, 1.167f, -0.812f, 1.979f) + quadToRelative(-0.813f, 0.813f, -2.021f, 0.813f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -2f, -0.813f) + quadToRelative(-0.834f, -0.812f, -0.834f, -1.979f) + reflectiveQuadToRelative(0.813f, -2f) + quadToRelative(0.812f, -0.833f, 2.021f, -0.833f) + quadToRelative(1.208f, 0f, 2.02f, 0.812f) + quadToRelative(0.813f, 0.813f, 0.813f, 2.021f) + quadToRelative(0f, 1.167f, -0.813f, 1.979f) + quadToRelative(-0.812f, 0.813f, -2.02f, 0.813f) + close() + moveTo(9.708f, 12.542f) + quadToRelative(-1.208f, 0f, -2.02f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.021f) + quadToRelative(0f, -1.208f, 0.813f, -2.02f) + quadToRelative(0.812f, -0.813f, 2.02f, -0.813f) + quadToRelative(1.167f, 0f, 2f, 0.813f) + quadToRelative(0.834f, 0.812f, 0.834f, 2.02f) + quadToRelative(0f, 1.167f, -0.813f, 2f) + quadToRelative(-0.812f, 0.834f, -2.021f, 0.834f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -1.979f, -0.813f) + quadToRelative(-0.813f, -0.812f, -0.813f, -2.021f) + quadToRelative(0f, -1.208f, 0.813f, -2.02f) + quadToRelative(0.812f, -0.813f, 1.979f, -0.813f) + reflectiveQuadToRelative(2f, 0.813f) + quadToRelative(0.833f, 0.812f, 0.833f, 2.02f) + quadToRelative(0f, 1.167f, -0.812f, 2f) + quadToRelative(-0.813f, 0.834f, -2.021f, 0.834f) + close() + moveToRelative(10.292f, 0f) + quadToRelative(-1.167f, 0f, -2f, -0.813f) + quadToRelative(-0.834f, -0.812f, -0.834f, -2.021f) + quadToRelative(0f, -1.208f, 0.813f, -2.02f) + quadToRelative(0.812f, -0.813f, 2.021f, -0.813f) + quadToRelative(1.208f, 0f, 2.02f, 0.813f) + quadToRelative(0.813f, 0.812f, 0.813f, 2.02f) + quadToRelative(0f, 1.167f, -0.813f, 2f) + quadToRelative(-0.812f, 0.834f, -2.02f, 0.834f) + close() + } + }.build() + } + } + @Composable fun playArrow(color: Color): ImageVector { return remember { ImageVector.Builder( 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 index 29fa53dbf..1c851484d 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -1,8 +1,13 @@ package org.ryujinx.android +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo import android.os.Build import android.os.Bundle import android.os.Environment +import android.view.KeyEvent +import android.view.MotionEvent import android.view.WindowManager import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize @@ -10,14 +15,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import com.anggrayudi.storage.SimpleStorageHelper import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.views.MainView +import kotlin.math.abs class MainActivity : BaseActivity() { + private var physicalControllerManager: PhysicalControllerManager = + PhysicalControllerManager(this) private var _isInit: Boolean = false + var isGameRunning = false var storageHelper: SimpleStorageHelper? = null companion object { var mainViewModel: MainViewModel? = null @@ -53,7 +64,7 @@ class MainActivity : BaseActivity() { return val appPath: String = AppPath - val success = RyujinxNative().initialize(appPath, false) + val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath), false) _isInit = success } override fun onCreate(savedInstanceState: Bundle?) { @@ -67,12 +78,14 @@ class MainActivity : BaseActivity() { AppPath = this.getExternalFilesDir(null)!!.absolutePath + initialize() window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowCompat.setDecorFitsSystemWindows(window,false) mainViewModel = MainViewModel(this) + mainViewModel!!.physicalControllerManager = physicalControllerManager mainViewModel?.apply { setContent { @@ -98,4 +111,87 @@ class MainActivity : BaseActivity() { super.onRestoreInstanceState(savedInstanceState) storageHelper?.onRestoreInstanceState(savedInstanceState) } + + // Game Stuff + private fun force60HzRefreshRate(enable: Boolean) { + // Hack for MIUI devices since they don't support the standard Android APIs + try { + val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS") + setFpsIntent.putExtra("package_name", "org.ryujinx.android") + setFpsIntent.putExtra("isEnter", enable) + sendBroadcast(setFpsIntent) + } catch (_: Exception) { + } + + if (enable) + display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) } + ?.let { window.attributes.preferredDisplayModeId = it.modeId } + else + display?.supportedModes?.maxByOrNull { it.refreshRate } + ?.let { window.attributes.preferredDisplayModeId = it.modeId } + } + + fun setFullScreen(fullscreen: Boolean) { + requestedOrientation = + if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER + + val insets = WindowCompat.getInsetsController(window, window.decorView) + + insets.apply { + if (fullscreen) { + insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } + } + } + + + @SuppressLint("RestrictedApi") + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + event?.apply { + if (physicalControllerManager.onKeyEvent(this)) + return true + } + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + ev?.apply { + physicalControllerManager.onMotionEvent(this) + } + return super.dispatchGenericMotionEvent(ev) + } + + override fun onStop() { + super.onStop() + + if(isGameRunning) { + NativeHelpers.instance.setTurboMode(false) + force60HzRefreshRate(false) + } + } + + override fun onResume() { + super.onResume() + + if(isGameRunning) { + setFullScreen(true) + NativeHelpers.instance.setTurboMode(true) + force60HzRefreshRate(true) + } + } + + override fun onPause() { + super.onPause() + + if(isGameRunning) { + NativeHelpers.instance.setTurboMode(false) + force60HzRefreshRate(false) + } + } } 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 index 229b61a90..dceab1e1e 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -5,6 +5,7 @@ import android.view.Surface class NativeHelpers { companion object { + val instance = NativeHelpers() init { System.loadLibrary("ryujinxjni") } @@ -28,6 +29,6 @@ class NativeHelpers { external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int external fun getProgressInfo() : String external fun getProgressValue() : Float - external fun pushStringJava(string: String) - external fun popStringJava() : String + external fun storeStringJava(string: String) : Long + external fun getStringJava(id: Long) : String } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt index 74f96c058..dee3b4d61 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeWindow.kt @@ -4,7 +4,7 @@ import android.view.SurfaceView class NativeWindow(val surface: SurfaceView) { var nativePointer: Long - var nativeHelpers: NativeHelpers = NativeHelpers() + var nativeHelpers: NativeHelpers = NativeHelpers.instance private var _swapInterval : Int = 0 var maxSwapInterval : Int = 0 diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt index 9913956bd..c61ac456e 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt @@ -3,9 +3,9 @@ package org.ryujinx.android import android.view.KeyEvent import android.view.MotionEvent -class PhysicalControllerManager(val activity: GameActivity) { +class PhysicalControllerManager(val activity: MainActivity) { private var controllerId: Int = -1 - private var ryujinxNative: RyujinxNative = RyujinxNative() + private var ryujinxNative: RyujinxNative = RyujinxNative.instance fun onKeyEvent(event: KeyEvent) : Boolean{ if(controllerId != -1) { 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 index 4c990f094..989226de6 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -4,10 +4,10 @@ import org.ryujinx.android.viewmodels.GameInfo @Suppress("KotlinJniMissingFunction") class RyujinxNative { - - external fun initialize(appPath: String, enableDebugLogs : Boolean): Boolean + external fun initialize(appPath: Long, enableDebugLogs : Boolean): Boolean companion object { + val instance: RyujinxNative = RyujinxNative() init { System.loadLibrary("ryujinx") } @@ -20,7 +20,7 @@ class RyujinxNative { enableDockedMode : Boolean, enablePtc : Boolean, enableInternetAccess : Boolean, - timeZone : String, + timeZone : Long, ignoreMissingServices : Boolean): Boolean external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitializeRenderer( @@ -29,6 +29,7 @@ class RyujinxNative { ): Boolean external fun deviceLoad(game: String): Boolean + external fun deviceLaunchMiiEditor(): Boolean external fun deviceGetGameFrameRate(): Double external fun deviceGetGameFrameTime(): Double external fun deviceGetGameFifo(): Double @@ -51,16 +52,16 @@ class RyujinxNative { external fun graphicsSetSurface(surface: Long, window: Long) external fun deviceCloseEmulation() external fun deviceSignalEmulationClose() - external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String - external fun deviceGetDlcContentList(path: String, titleId: Long) : Array - external fun userGetOpenedUser() - external fun userGetUserPicture(userId: String) : String + external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long + external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array + external fun userGetOpenedUser() : Long + external fun userGetUserPicture(userId: Long) : Long external fun userSetUserPicture(userId: String, picture: String) - external fun userGetUserName(userId: String) : String + external fun userGetUserName(userId: Long) : Long external fun userSetUserName(userId: String, userName: String) external fun userGetAllUsers() : Array external fun userAddUser(username: String, picture: String) external fun userDeleteUser(userId: String) - external fun userOpenUser(userId: String) + external fun userOpenUser(userId: Long) external fun userCloseUser(userId: String) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt index 1d440c11c..a61eadf49 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt @@ -11,6 +11,7 @@ import com.anggrayudi.storage.file.getAbsolutePath import com.google.gson.Gson import com.google.gson.reflect.TypeToken import org.ryujinx.android.MainActivity +import org.ryujinx.android.NativeHelpers import org.ryujinx.android.RyujinxNative import java.io.File @@ -39,8 +40,8 @@ class DlcViewModel(val titleId: String) { val path = file.getAbsolutePath(storageHelper.storage.context) if (path.isNotEmpty()) { data?.apply { - var contents = RyujinxNative().deviceGetDlcContentList( - path, + var contents = RyujinxNative.instance.deviceGetDlcContentList( + NativeHelpers.instance.storeStringJava(path), titleId.toLong(16) ) @@ -101,7 +102,7 @@ class DlcViewModel(val titleId: String) { enabled, containerPath, dlc.fullPath, - RyujinxNative().deviceGetDlcTitleId(containerPath, dlc.fullPath) + NativeHelpers.instance.getStringJava(RyujinxNative.instance.deviceGetDlcTitleId(NativeHelpers.instance.storeStringJava(containerPath), NativeHelpers.instance.storeStringJava(dlc.fullPath))) ) ) } @@ -149,4 +150,4 @@ data class DlcItem( var isEnabled: MutableState = mutableStateOf(false), var containerPath: String = "", var fullPath: String = "", - var titleId: String = "") \ No newline at end of file + var titleId: String = "") 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 index bf166b1c9..376ae33a8 100644 --- 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 @@ -15,12 +15,12 @@ class GameModel(var file: DocumentFile, val context: Context) { var titleId: String? = null var developer: String? = null var version: String? = null - var iconCache: String? = null + var icon: String? = null init { fileName = file.name var pid = open() - val gameInfo = RyujinxNative().deviceGetGameInfo(pid, file.extension.contains("xci")) + val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci")) close() fileSize = gameInfo.FileSize @@ -28,7 +28,7 @@ class GameModel(var file: DocumentFile, val context: Context) { titleName = gameInfo.TitleName developer = gameInfo.Developer version = gameInfo.Version - iconCache = gameInfo.IconCache + icon = gameInfo.Icon } fun open() : Int { @@ -53,5 +53,5 @@ class GameInfo { var TitleId: String? = null var Developer: String? = null var Version: String? = null - var IconCache: String? = null + var Icon: String? = null } 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 index bbfeb9e8a..2419dbb37 100644 --- 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 @@ -18,10 +18,10 @@ class HomeViewModel( val mainViewModel: MainViewModel? = null ) { private var isLoading: Boolean = false - private var gameList: SnapshotStateList? = null private var loadedCache: List = listOf() private var gameFolderPath: DocumentFile? = null private var sharedPref: SharedPreferences? = null + val gameList: SnapshotStateList = SnapshotStateList() init { if (activity != null) { @@ -68,53 +68,35 @@ class HomeViewModel( ) } - fun reloadGameList(ignoreCache: Boolean = false) { + fun reloadGameList() { var storage = activity?.storageHelper ?: return if(isLoading) return val folder = gameFolderPath ?: return + + gameList.clear() isLoading = true - - if(!ignoreCache) { - val files = mutableListOf() - - thread { - try { - 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() - - isLoading = false - - applyFilter() - } finally { - isLoading = false + thread { + try { + val files = mutableListOf() + for (file in folder.search(false, DocumentFileType.FILE)) { + if (file.extension == "xci" || file.extension == "nsp") + activity.let { + val item = GameModel(file, it) + files.add(item) + gameList.add(item) + } } + + loadedCache = files.toList() + + isLoading = false + } finally { + isLoading = false } } - else{ - isLoading = false - applyFilter() - } - } - - private fun applyFilter() { - if(isLoading) - return - gameList?.clear() - gameList?.addAll(loadedCache) - } - - fun setViewList(list: SnapshotStateList) { - gameList = list - reloadGameList(loadedCache.isNotEmpty()) } fun clearLoadedCache(){ 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 index 0d3850425..7f451449d 100644 --- 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 @@ -2,7 +2,6 @@ package org.ryujinx.android.viewmodels import android.annotation.SuppressLint import android.content.Context -import android.content.Intent import android.os.Build import android.os.PerformanceHintManager import androidx.compose.runtime.MutableState @@ -10,7 +9,6 @@ import androidx.navigation.NavHostController import com.anggrayudi.storage.extension.launchOnUiThread import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore -import org.ryujinx.android.GameActivity import org.ryujinx.android.GameController import org.ryujinx.android.GameHost import org.ryujinx.android.GraphicsConfiguration @@ -31,6 +29,8 @@ class MainViewModel(val activity: MainActivity) { var controller: GameController? = null var performanceManager: PerformanceManager? = null var selected: GameModel? = null + var isMiiEditorLaunched = false + val userViewModel = UserViewModel() private var gameTimeState: MutableState? = null private var gameFpsState: MutableState? = null private var fifoState: MutableState? = null @@ -56,13 +56,13 @@ class MainViewModel(val activity: MainActivity) { } fun closeGame() { - RyujinxNative().deviceSignalEmulationClose() + RyujinxNative.instance.deviceSignalEmulationClose() gameHost?.close() - RyujinxNative().deviceCloseEmulation() + RyujinxNative.instance.deviceCloseEmulation() } fun loadGame(game:GameModel) : Boolean { - val nativeRyujinx = RyujinxNative() + val nativeRyujinx = RyujinxNative.instance val descriptor = game.open() @@ -70,6 +70,7 @@ class MainViewModel(val activity: MainActivity) { return false gameModel = game + isMiiEditorLaunched = false val settings = QuickSettings(activity) @@ -83,7 +84,7 @@ class MainViewModel(val activity: MainActivity) { if (!success) return false - val nativeHelpers = NativeHelpers() + val nativeHelpers = NativeHelpers.instance val nativeInterop = NativeGraphicsInterop() nativeInterop.VkRequiredExtensions = arrayOf( "VK_KHR_surface", "VK_KHR_android_surface" @@ -118,7 +119,7 @@ class MainViewModel(val activity: MainActivity) { } } - driverHandle = NativeHelpers().loadDriver( + driverHandle = NativeHelpers.instance.loadDriver( activity.applicationInfo.nativeLibraryDir!! + "/", privateDriverPath, this.libraryName @@ -148,7 +149,7 @@ class MainViewModel(val activity: MainActivity) { settings.enableDocked, settings.enablePtc, false, - "UTC", + NativeHelpers.instance.storeStringJava("UTC"), settings.ignoreMissingServices ) @@ -169,6 +170,112 @@ class MainViewModel(val activity: MainActivity) { return true } + + + fun loadMiiEditor() : Boolean { + val nativeRyujinx = RyujinxNative.instance + + gameModel = null + isMiiEditorLaunched = true + + val settings = QuickSettings(activity) + + var success = nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply { + EnableShaderCache = settings.enableShaderCache + EnableTextureRecompression = settings.enableTextureRecompression + ResScale = settings.resScale + BackendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal + }) + + if (!success) + return false + + val nativeHelpers = NativeHelpers.instance + val nativeInterop = NativeGraphicsInterop() + nativeInterop.VkRequiredExtensions = arrayOf( + "VK_KHR_surface", "VK_KHR_android_surface" + ) + nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() + nativeInterop.SurfaceHandle = 0 + + val driverViewModel = VulkanDriverViewModel(activity) + val drivers = driverViewModel.getAvailableDrivers() + + var driverHandle = 0L + + if (driverViewModel.selected.isNotEmpty()) { + val metaData = drivers.find { it.driverPath == driverViewModel.selected } + + metaData?.apply { + val privatePath = activity.filesDir + val privateDriverPath = privatePath.canonicalPath + "/driver/" + val pD = File(privateDriverPath) + if (pD.exists()) + pD.deleteRecursively() + + pD.mkdirs() + + val driver = File(driverViewModel.selected) + val parent = driver.parentFile + if (parent != null) { + for (file in parent.walkTopDown()) { + if (file.absolutePath == parent.absolutePath) + continue + file.copyTo(File(privateDriverPath + file.name), true) + } + } + + driverHandle = NativeHelpers.instance.loadDriver( + activity.applicationInfo.nativeLibraryDir!! + "/", + privateDriverPath, + this.libraryName + ) + } + + } + + success = nativeRyujinx.graphicsInitializeRenderer( + nativeInterop.VkRequiredExtensions!!, + driverHandle + ) + if (!success) + return false + + val semaphore = Semaphore(1, 0) + runBlocking { + semaphore.acquire() + launchOnUiThread { + // We are only able to initialize the emulation context on the main thread + success = nativeRyujinx.deviceInitialize( + settings.isHostMapped, + settings.useNce, + SystemLanguage.AmericanEnglish.ordinal, + RegionCode.USA.ordinal, + settings.enableVsync, + settings.enableDocked, + settings.enablePtc, + false, + NativeHelpers.instance.storeStringJava("UTC"), + settings.ignoreMissingServices + ) + + semaphore.release() + } + semaphore.acquire() + semaphore.release() + } + + if (!success) + return false + + success = nativeRyujinx.deviceLaunchMiiEditor() + + if (!success) + return false + + return true + } + fun clearPptcCache(titleId :String){ if(titleId.isNotEmpty()){ val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu" @@ -241,8 +348,9 @@ class MainViewModel(val activity: MainActivity) { } fun navigateToGame() { - val intent = Intent(activity, GameActivity::class.java) - activity.startActivity(intent) + activity.setFullScreen(true) + navController?.navigate("game") + activity.isGameRunning = true } fun setProgressStates( @@ -255,15 +363,4 @@ class MainViewModel(val activity: MainActivity) { this.progress = progress gameHost?.setProgressStates(showLoading, progressValue, progress) } - - fun setRefreshUserState(refreshUser: MutableState) - { - this.refreshUser = refreshUser - } - - fun requestUserRefresh(){ - refreshUser?.apply { - value = true - } - } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt new file mode 100644 index 000000000..6cb5cdcb7 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/UserViewModel.kt @@ -0,0 +1,85 @@ +package org.ryujinx.android.viewmodels + +import org.ryujinx.android.NativeHelpers +import org.ryujinx.android.RyujinxNative +import java.util.Base64 + +class UserViewModel { + var openedUser = UserModel() + val userList = mutableListOf() + + init { + refreshUsers() + } + + fun refreshUsers() { + userList.clear() + val native = RyujinxNative.instance + val helper = NativeHelpers.instance + val decoder = Base64.getDecoder() + openedUser = UserModel() + openedUser.id = helper.getStringJava(native.userGetOpenedUser()) + if (openedUser.id.isNotEmpty()) { + openedUser.username = + helper.getStringJava(native.userGetUserName(helper.storeStringJava(openedUser.id))) + openedUser.userPicture = decoder.decode( + helper.getStringJava( + native.userGetUserPicture( + helper.storeStringJava(openedUser.id) + ) + ) + ) + } + + val users = native.userGetAllUsers() + for (user in users) { + userList.add( + UserModel( + user, + helper.getStringJava(native.userGetUserName(helper.storeStringJava(user))), + decoder.decode( + helper.getStringJava( + native.userGetUserPicture( + helper.storeStringJava(user) + ) + ) + ) + ) + ) + } + } + + fun openUser(userModel: UserModel){ + val native = RyujinxNative.instance + val helper = NativeHelpers.instance + native.userOpenUser(helper.storeStringJava(userModel.id)) + + refreshUsers() + } +} + + +data class UserModel(var id : String = "", var username: String = "", var userPicture: ByteArray? = null) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserModel + + if (id != other.id) return false + if (username != other.username) return false + if (userPicture != null) { + if (other.userPicture == null) return false + if (!userPicture.contentEquals(other.userPicture)) return false + } else if (other.userPicture != null) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + (userPicture?.contentHashCode() ?: 0) + return result + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt new file mode 100644 index 000000000..63bd49209 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt @@ -0,0 +1,319 @@ +package org.ryujinx.android.views + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup +import compose.icons.CssGgIcons +import compose.icons.cssggicons.ToolbarBottom +import org.ryujinx.android.GameController +import org.ryujinx.android.GameHost +import org.ryujinx.android.Icons +import org.ryujinx.android.MainActivity +import org.ryujinx.android.RyujinxNative +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings +import kotlin.math.roundToInt + +class GameViews { + companion object { + @Composable + fun Main() { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + GameView(mainViewModel = MainActivity.mainViewModel!!) + } + } + + @Composable + fun GameView(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + GameHost(context, mainViewModel) + } + ) + GameOverlay(mainViewModel) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun GameOverlay(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + GameStats(mainViewModel) + + val ryujinxNative = RyujinxNative.instance + + val showController = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController) + } + val enableVsync = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) + } + val showMore = remember { + mutableStateOf(false) + } + + val showLoading = remember { + mutableStateOf(true) + } + + val progressValue = remember { + mutableStateOf(0.0f) + } + + val progress = remember { + mutableStateOf("Loading") + } + + mainViewModel.setProgressStates(showLoading, progressValue, progress) + + // touch surface + Surface(color = Color.Transparent, modifier = Modifier + .fillMaxSize() + .padding(0.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (showController.value) + continue + + val change = event + .component1() + .firstOrNull() + change?.apply { + val position = this.position + + when (event.type) { + PointerEventType.Press -> { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + } + + PointerEventType.Release -> { + ryujinxNative.inputReleaseTouchPoint() + + } + + PointerEventType.Move -> { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + + } + } + } + } + } + }) { + } + if (!showLoading.value) { + GameController.Compose(mainViewModel) + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp) + ) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = true + }) { + Icon( + imageVector = CssGgIcons.ToolbarBottom, + contentDescription = "Open Panel" + ) + } + } + + if (showMore.value) { + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = { showMore.value = false }) { + Surface( + modifier = Modifier.padding(16.dp), + shape = MaterialTheme.shapes.medium + ) { + Row(modifier = Modifier.padding(8.dp)) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + showController.value = !showController.value + mainViewModel.controller?.setVisible(showController.value) + }) { + Icon( + imageVector = Icons.videoGame(), + contentDescription = "Toggle Virtual Pad" + ) + } + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + enableVsync.value = !enableVsync.value + RyujinxNative.instance.graphicsRendererSetVsync(enableVsync.value) + }) { + Icon( + imageVector = Icons.vSync(), + tint = if (enableVsync.value) Color.Green else Color.Red, + contentDescription = "Toggle VSync" + ) + } + } + } + } + } + } + + val showBackNotice = remember { + mutableStateOf(false) + } + + BackHandler { + showBackNotice.value = true + } + + if (showLoading.value) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(0.5f) + .align(Alignment.Center), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = progress.value) + + if (progressValue.value > -1) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + progress = progressValue.value + ) + else + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } + + } + } + + if (showBackNotice.value) { + AlertDialog(onDismissRequest = { showBackNotice.value = false }) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Are you sure you want to exit the game?") + Text(text = "All unsaved data will be lost!") + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Button(onClick = { + showBackNotice.value = false + mainViewModel.closeGame() + mainViewModel.activity.setFullScreen(false) + mainViewModel.navController?.popBackStack() + mainViewModel.activity.isGameRunning = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Exit Game") + } + + Button(onClick = { + showBackNotice.value = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Dismiss") + } + } + } + } + } + } + } + } + } + + @Composable + fun GameStats(mainViewModel: MainViewModel) { + val fifo = remember { + mutableStateOf(0.0) + } + val gameFps = remember { + mutableStateOf(0.0) + } + val gameTime = remember { + mutableStateOf(0.0) + } + + Surface( + modifier = Modifier.padding(16.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) + } + } +} 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 index f567410f1..877aa8cb7 100644 --- 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 @@ -48,7 +48,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar 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.Modifier @@ -58,14 +57,9 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import coil.compose.AsyncImage import com.anggrayudi.storage.extension.launchOnUiThread -import org.ryujinx.android.MainActivity -import org.ryujinx.android.NativeHelpers -import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.HomeViewModel -import java.io.File import java.util.Base64 import java.util.Locale import kotlin.concurrent.thread @@ -81,7 +75,6 @@ class HomeViews { viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null ) { - val native = RyujinxNative() val showAppActions = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) } val openTitleUpdateDialog = remember { mutableStateOf(false) } @@ -90,31 +83,7 @@ class HomeViews { val query = remember { mutableStateOf("") } - val refresh = remember { - mutableStateOf(true) - } - val refreshUser = remember { - mutableStateOf(true) - } - viewModel.mainViewModel?.setRefreshUserState(refreshUser) - val user = remember { - mutableStateOf("") - } - val pic = remember { - mutableStateOf(ByteArray(0)) - } - - if (refreshUser.value) { - native.userGetOpenedUser() - user.value = NativeHelpers().popStringJava() - if (user.value.isNotEmpty()) { - val decoder = Base64.getDecoder() - pic.value = decoder.decode(native.userGetUserPicture(user.value)) - } - - refreshUser.value = false; - } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -148,12 +117,14 @@ class HomeViews { IconButton(onClick = { navController?.navigate("user") }) { - if (pic.value.isNotEmpty()) { + if (viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { + val pic = + viewModel.mainViewModel.userViewModel.openedUser.userPicture Image( bitmap = BitmapFactory.decodeByteArray( - pic.value, + pic, 0, - pic.value.size + pic?.size ?: 0 ) .asImageBitmap(), contentDescription = "user image", @@ -184,9 +155,26 @@ class HomeViews { ) }, bottomBar = { - BottomAppBar(actions = { + BottomAppBar( + actions = { if (showAppActions.value) { IconButton(onClick = { + if(viewModel.mainViewModel?.selected != null) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(viewModel.mainViewModel.selected!!) + ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + viewModel.mainViewModel?.selected!!.close() + } + showLoading.value = false + } + } }) { Icon( org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), @@ -210,13 +198,17 @@ class HomeViews { Text(text = "Clear PPTC Cache") }, onClick = { showAppMenu.value = false - viewModel.mainViewModel?.clearPptcCache(viewModel.mainViewModel?.selected?.titleId ?: "") + viewModel.mainViewModel?.clearPptcCache( + viewModel.mainViewModel?.selected?.titleId ?: "" + ) }) DropdownMenuItem(text = { Text(text = "Purge Shader Cache") }, onClick = { showAppMenu.value = false - viewModel.mainViewModel?.purgeShaderCache(viewModel.mainViewModel?.selected?.titleId ?: "") + viewModel.mainViewModel?.purgeShaderCache( + viewModel.mainViewModel?.selected?.titleId ?: "" + ) }) DropdownMenuItem(text = { Text(text = "Manage Updates") @@ -233,6 +225,39 @@ class HomeViews { } } } + + /*\val showAppletMenu = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { + showAppletMenu.value = true + }) { + Icon( + org.ryujinx.android.Icons.applets(MaterialTheme.colorScheme.onSurface), + contentDescription = "Applets" + ) + } + DropdownMenu( + expanded = showAppletMenu.value, + onDismissRequest = { showAppletMenu.value = false }) { + DropdownMenuItem(text = { + Text(text = "Launch Mii Editor") + }, onClick = { + showAppletMenu.value = false + showLoading.value = true + thread { + val success = + viewModel.mainViewModel?.loadMiiEditor() ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else + viewModel.mainViewModel!!.isMiiEditorLaunched = false + showLoading.value = false + } + }) + } + }*/ }, floatingActionButton = { FloatingActionButton( @@ -255,12 +280,7 @@ class HomeViews { ) { contentPadding -> Box(modifier = Modifier.padding(contentPadding)) { val list = remember { - mutableStateListOf() - } - if (refresh.value) { - viewModel.setViewList(list) - refresh.value = false - showAppActions.value = false + viewModel.gameList } val selectedModel = remember { mutableStateOf(viewModel.mainViewModel?.selected) @@ -365,6 +385,7 @@ class HomeViews { val color = if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + val decoder = Base64.getDecoder() Surface( shape = MaterialTheme.shapes.medium, color = color, @@ -409,13 +430,12 @@ class HomeViews { ) { Row { if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { - val iconSource = - MainActivity.AppPath + "/iconCache/" + gameModel.iconCache - val imageFile = File(iconSource) - if (imageFile.exists()) { + if (gameModel.icon?.isNotEmpty() == true) { + val pic = decoder.decode(gameModel.icon) val size = ImageSize / Resources.getSystem().displayMetrics.density - AsyncImage( - model = imageFile, + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), contentDescription = gameModel.titleName + " icon", modifier = Modifier .padding(end = 8.dp) 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 index b613cc51d..e4243a67c 100644 --- 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 @@ -17,6 +17,7 @@ class MainView { NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("user") { UserViews.Main(mainViewModel, navController) } + composable("game") { GameViews.Main() } composable("settings") { SettingViews.Main( SettingsViewModel( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index d1d00251c..d1c17d4b6 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -306,7 +306,7 @@ class SettingViews { thread { Helpers.importAppData(this, isImporting) showImportCompletion.value = true - mainViewModel.requestUserRefresh() + mainViewModel.userViewModel.refreshUsers() } } }, modifier = Modifier.padding(horizontal = 8.dp)) { @@ -329,7 +329,7 @@ class SettingViews { AlertDialog(onDismissRequest = { showImportCompletion.value = false importFile.value = null - mainViewModel.requestUserRefresh() + mainViewModel.userViewModel.refreshUsers() mainViewModel.homeViewModel.clearLoadedCache() }) { Card( @@ -461,7 +461,8 @@ class SettingViews { Column( modifier = Modifier .fillMaxWidth() - .height(300.dp) + .height(350.dp) + .verticalScroll(rememberScrollState()) ) { Row( modifier = Modifier @@ -494,7 +495,7 @@ class SettingViews { Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(4.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( 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 index 96347f259..719911db1 100644 --- 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 @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete @@ -62,6 +64,7 @@ class TitleUpdateViews { modifier = Modifier .height(250.dp) .fillMaxWidth() + .verticalScroll(rememberScrollState()) ) { Row(modifier = Modifier.padding(8.dp)) { RadioButton( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt index 63f0f2379..3cd8591d9 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -36,40 +37,23 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import org.ryujinx.android.NativeHelpers -import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.MainViewModel -import java.util.Base64 class UserViews { companion object { @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) { - val ryujinxNative = RyujinxNative() - val decoder = Base64.getDecoder() - ryujinxNative.userGetOpenedUser() - val openedUser = remember { - mutableStateOf(NativeHelpers().popStringJava()) + val reload = remember { + mutableStateOf(true) } - - val openedUserPic = remember { - mutableStateOf(decoder.decode(ryujinxNative.userGetUserPicture(openedUser.value))) - } - val openedUserName = remember { - mutableStateOf(ryujinxNative.userGetUserName(openedUser.value)) - } - - val userList = remember { - mutableListOf("") - } - fun refresh() { - userList.clear() - userList.addAll(ryujinxNative.userGetAllUsers()) + viewModel?.userViewModel?.refreshUsers() + reload.value = true + } + LaunchedEffect(reload.value) { + reload.value = false } - - refresh() Scaffold(modifier = Modifier.fillMaxSize(), topBar = { @@ -102,25 +86,28 @@ class UserViews { .padding(4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Image( - bitmap = BitmapFactory.decodeByteArray( - openedUserPic.value, - 0, - openedUserPic.value.size - ).asImageBitmap(), - contentDescription = "selected image", - contentScale = ContentScale.Crop, - modifier = Modifier - .padding(4.dp) - .size(96.dp) - .clip(CircleShape) - ) - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text(text = openedUserName.value) - Text(text = openedUser.value) + if (viewModel?.userViewModel?.openedUser?.id?.isNotEmpty() == true) { + val openUser = viewModel.userViewModel.openedUser + Image( + bitmap = BitmapFactory.decodeByteArray( + openUser.userPicture, + 0, + openUser.userPicture?.size ?: 0 + ).asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(96.dp) + .clip(CircleShape) + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = openUser.username) + Text(text = openUser.id) + } } } @@ -139,34 +126,32 @@ class UserViews { ) } } + LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 96.dp), modifier = Modifier .fillMaxSize() .padding(4.dp) ) { - items(userList) { user -> - val pic = decoder.decode(ryujinxNative.userGetUserPicture(user)) - val name = ryujinxNative.userGetUserName(user) - Image( - bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) - .asImageBitmap(), - contentDescription = "selected image", - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .padding(4.dp) - .clip(CircleShape) - .align(Alignment.CenterHorizontally) - .combinedClickable( - onClick = { - ryujinxNative.userOpenUser(user) - openedUser.value = user - openedUserPic.value = pic - openedUserName.value = name - viewModel?.requestUserRefresh() - }) - ) + if(viewModel?.userViewModel?.userList?.isNotEmpty() == true) { + items(viewModel.userViewModel.userList) { user -> + Image( + bitmap = BitmapFactory.decodeByteArray(user.userPicture, 0, user.userPicture?.size ?: 0) + .asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally) + .combinedClickable( + onClick = { + viewModel.userViewModel.openUser(user) + reload.value = true + }) + ) + } } } }