1
0
forked from MeloNX/MeloNX

android - drop game activity, replace with compose view

android - add string map

android - fixes a few crashes in the user and home views

android - bumb version

android - bumb version, rebase over master

android - remove oboe
This commit is contained in:
Emmanuel Hansen 2023-11-11 18:33:14 +00:00
parent a583d6bf46
commit adda73f061
32 changed files with 1114 additions and 877 deletions

View File

@ -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("<init>"), GetCCharSequence("()V"));
var newObj = newObject(jEnv, javaClass, constructor, 0);
sha = SHA256.Create();
var iconCacheByte = sha.ComputeHash(info?.Icon ?? Array.Empty<byte>());
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<byte>());
}
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<byte>()))._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>(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);

View File

@ -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;
@ -77,6 +78,8 @@ namespace LibRyujinx
return false;
}
OpenALLibraryNameContainer.OverridePath = "libopenal.so";
return true;
}

View File

@ -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'

View File

@ -27,14 +27,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.RyujinxAndroid"
tools:targetApi="31">
<activity
android:name=".GameActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:theme="@style/Theme.RyujinxAndroid" />
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:hardwareAccelerated="true"
android:theme="@style/Theme.RyujinxAndroid">
<intent-filter>

View File

@ -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

View File

@ -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);
}
}

View File

@ -1,29 +0,0 @@
//
// Created by Emmanuel Hansen on 6/27/2023.
//
#ifndef RYUJINXNATIVE_OBOE_H
#define RYUJINXNATIVE_OBOE_H
#include <oboe/Oboe.h>
#include <stdlib.h>
#include <stdio.h>
#include <jni.h>
#include <queue>
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

View File

@ -19,6 +19,7 @@
#include <fcntl.h>
#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

View File

@ -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));
}

View File

@ -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;
}

View File

@ -0,0 +1,29 @@
//
// Created by Emmanuel Hansen on 10/30/2023.
//
#ifndef RYUJINXANDROID_STRING_HELPER_H
#define RYUJINXANDROID_STRING_HELPER_H
#include <string>
#include <unordered_map>
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<long,string>();
current_id = 0;
}
private:
unordered_map<long, string> _map;
long current_id;
};
#endif //RYUJINXANDROID_STRING_HELPER_H

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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<String>? = null
private var progressValue: MutableState<Float>? = 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,16 +110,16 @@ 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)
if (value != -1f)
progress?.apply {
this.value = helper.getProgressInfo()
}
@ -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()
)
}
}
}

View File

@ -216,7 +216,7 @@ class Helpers {
}
} finally {
isImporting.value = false
RyujinxNative().deviceReloadFilesystem()
RyujinxNative.instance.deviceReloadFilesystem()
}
}
}

View File

@ -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(

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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) {

View File

@ -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<String>
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<String>
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<String>
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)
}

View File

@ -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)))
)
)
}

View File

@ -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
}

View File

@ -18,10 +18,10 @@ class HomeViewModel(
val mainViewModel: MainViewModel? = null
) {
private var isLoading: Boolean = false
private var gameList: SnapshotStateList<GameModel>? = null
private var loadedCache: List<GameModel> = listOf()
private var gameFolderPath: DocumentFile? = null
private var sharedPref: SharedPreferences? = null
val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
init {
if (activity != null) {
@ -68,54 +68,36 @@ 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<GameModel>()
thread {
try {
val files = mutableListOf<GameModel>()
for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp")
activity.let {
files.add(GameModel(file, it))
val item = GameModel(file, it)
files.add(item)
gameList.add(item)
}
}
loadedCache = files.toList()
isLoading = false
applyFilter()
} finally {
isLoading = false
}
}
}
else{
isLoading = false
applyFilter()
}
}
private fun applyFilter() {
if(isLoading)
return
gameList?.clear()
gameList?.addAll(loadedCache)
}
fun setViewList(list: SnapshotStateList<GameModel>) {
gameList = list
reloadGameList(loadedCache.isNotEmpty())
}
fun clearLoadedCache(){
loadedCache = listOf()

View File

@ -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<Double>? = null
private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = 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<Boolean>)
{
this.refreshUser = refreshUser
}
fun requestUserRefresh(){
refreshUser?.apply {
value = true
}
}
}

View File

@ -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<UserModel>()
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
}
}

View File

@ -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)
}
}
}

View File

@ -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<GameModel>()
}
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)

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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,11 +86,13 @@ class UserViews {
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (viewModel?.userViewModel?.openedUser?.id?.isNotEmpty() == true) {
val openUser = viewModel.userViewModel.openedUser
Image(
bitmap = BitmapFactory.decodeByteArray(
openedUserPic.value,
openUser.userPicture,
0,
openedUserPic.value.size
openUser.userPicture?.size ?: 0
).asImageBitmap(),
contentDescription = "selected image",
contentScale = ContentScale.Crop,
@ -119,8 +105,9 @@ class UserViews {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(text = openedUserName.value)
Text(text = openedUser.value)
Text(text = openUser.username)
Text(text = openUser.id)
}
}
}
@ -139,17 +126,17 @@ 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)
if(viewModel?.userViewModel?.userList?.isNotEmpty() == true) {
items(viewModel.userViewModel.userList) { user ->
Image(
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
bitmap = BitmapFactory.decodeByteArray(user.userPicture, 0, user.userPicture?.size ?: 0)
.asImageBitmap(),
contentDescription = "selected image",
contentScale = ContentScale.Crop,
@ -160,16 +147,14 @@ class UserViews {
.align(Alignment.CenterHorizontally)
.combinedClickable(
onClick = {
ryujinxNative.userOpenUser(user)
openedUser.value = user
openedUserPic.value = pic
openedUserName.value = name
viewModel?.requestUserRefresh()
viewModel.userViewModel.openUser(user)
reload.value = true
})
)
}
}
}
}
}
}