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 LibRyujinx.Shared.Audio.Oboe;
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
using Rxmxnx.PInvoke; using Rxmxnx.PInvoke;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Targets; using Ryujinx.Common.Logging.Targets;
@ -21,7 +22,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -42,7 +42,23 @@ namespace LibRyujinx
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
[DllImport("libryujinxjni")] [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")] [DllImport("libryujinxjni")]
internal extern static void setRenderingThread(); internal extern static void setRenderingThread();
@ -68,8 +84,9 @@ namespace LibRyujinx
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")] [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; SystemInfo.IsBionic = true;
Logger.AddTarget( Logger.AddTarget(
@ -79,7 +96,7 @@ namespace LibRyujinx
AsyncLogTargetOverflowAction.Block AsyncLogTargetOverflowAction.Block
)); ));
var path = GetString(jEnv, jpath); var path = GetStoredString(jpathId);
var init = Initialize(path, enableDebugLogs); var init = Initialize(path, enableDebugLogs);
@ -102,6 +119,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")]
public static void JniReloadFileSystem() public static void JniReloadFileSystem()
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SwitchDevice?.ReloadFileSystem(); SwitchDevice?.ReloadFileSystem();
} }
@ -116,10 +134,11 @@ namespace LibRyujinx
JBoolean enableDockedMode, JBoolean enableDockedMode,
JBoolean enablePtc, JBoolean enablePtc,
JBoolean enableInternetAccess, JBoolean enableInternetAccess,
JStringLocalRef timeZone, JLong timeZoneId,
JBoolean ignoreMissingServices) JBoolean ignoreMissingServices)
{ {
AudioDriver = new OboeHardwareDeviceDriver(); Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
AudioDriver = new OpenALHardwareDeviceDriver();//new OboeHardwareDeviceDriver();
return InitializeDevice(isHostMapped, return InitializeDevice(isHostMapped,
useNce, useNce,
(SystemLanguage)(int)systemLanguage, (SystemLanguage)(int)systemLanguage,
@ -128,13 +147,14 @@ namespace LibRyujinx
enableDockedMode, enableDockedMode,
enablePtc, enablePtc,
enableInternetAccess, enableInternetAccess,
GetString(jEnv, timeZone), GetStoredString(timeZoneId),
ignoreMissingServices); ignoreMissingServices);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFifo")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFifo")]
public static JDouble JniGetGameFifo(JEnvRef jEnv, JObjectLocalRef jObj) public static JDouble JniGetGameFifo(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stats = SwitchDevice.EmulationContext?.Statistics.GetFifoPercent() ?? 0; var stats = SwitchDevice.EmulationContext?.Statistics.GetFifoPercent() ?? 0;
return stats; return stats;
@ -143,6 +163,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFrameTime")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFrameTime")]
public static JDouble JniGetGameFrameTime(JEnvRef jEnv, JObjectLocalRef jObj) public static JDouble JniGetGameFrameTime(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stats = SwitchDevice.EmulationContext?.Statistics.GetGameFrameTime() ?? 0; var stats = SwitchDevice.EmulationContext?.Statistics.GetGameFrameTime() ?? 0;
return stats; return stats;
@ -151,6 +172,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFrameRate")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameFrameRate")]
public static JDouble JniGetGameFrameRate(JEnvRef jEnv, JObjectLocalRef jObj) public static JDouble JniGetGameFrameRate(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var stats = SwitchDevice.EmulationContext?.Statistics.GetGameFrameRate() ?? 0; var stats = SwitchDevice.EmulationContext?.Statistics.GetGameFrameRate() ?? 0;
return stats; return stats;
@ -159,6 +181,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoad")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoad")]
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr) public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (SwitchDevice?.EmulationContext == null) if (SwitchDevice?.EmulationContext == null)
{ {
return false; return false;
@ -169,10 +192,23 @@ namespace LibRyujinx
return LoadApplication(path); return LoadApplication(path);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcContentList")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLaunchMiiEditor")]
public static JArrayLocalRef JniGetDlcContentListNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JLong titleId) 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); debug_break(4);
@ -180,26 +216,30 @@ namespace LibRyujinx
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcTitleId")] [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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceSignalEmulationClose")]
public static void JniSignalEmulationCloseNative(JEnvRef jEnv, JObjectLocalRef jObj) public static void JniSignalEmulationCloseNative(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SignalEmulationClose(); SignalEmulationClose();
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceCloseEmulation")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceCloseEmulation")]
public static void JniCloseEmulationNative(JEnvRef jEnv, JObjectLocalRef jObj) public static void JniCloseEmulationNative(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
CloseEmulation(); CloseEmulation();
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci) public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (SwitchDevice?.EmulationContext == null) if (SwitchDevice?.EmulationContext == null)
{ {
return false; return false;
@ -213,6 +253,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject) public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
JEnvValue value = jEnv.Environment; JEnvValue value = jEnv.Environment;
ref JNativeInterface jInterface = ref value.Functions; ref JNativeInterface jInterface = ref value.Functions;
IntPtr getObjectClassPtr = jInterface.GetObjectClassPointer; IntPtr getObjectClassPtr = jInterface.GetObjectClassPointer;
@ -255,6 +296,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsSetSurface")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsSetSurface")]
public static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr, JLong window) public static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr, JLong window)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
_surfacePtr = surfacePtr; _surfacePtr = surfacePtr;
_window = window; _window = window;
@ -267,6 +309,7 @@ namespace LibRyujinx
JArrayLocalRef extensionsArray, JArrayLocalRef extensionsArray,
JLong driverHandle) JLong driverHandle)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (Renderer != null) if (Renderer != null)
{ {
return false; return false;
@ -354,12 +397,14 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSize")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSize")]
public static void JniSetRendererSizeNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) 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); Renderer?.Window?.SetSize(width, height);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")]
public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj) public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetSwapBuffersCallback(() => SetSwapBuffersCallback(() =>
{ {
var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime(); var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime();
@ -371,21 +416,24 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")]
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path) public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var info = GetGameInfo(GetString(jEnv, path)); var info = GetGameInfo(GetString(jEnv, path));
return GetInfo(jEnv, info, out SHA256 _); return GetInfo(jEnv, info);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci) 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); using var stream = OpenFile(fileDescriptor);
var info = GetGameInfo(stream, isXci); 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"); var javaClassName = GetCCharSequence("org/ryujinx/android/viewmodels/GameInfo");
JEnvValue value = jEnv.Environment; JEnvValue value = jEnv.Environment;
@ -411,24 +459,12 @@ namespace LibRyujinx
var newGlobal = newGlobalRef(jEnv, javaClass._value); var newGlobal = newGlobalRef(jEnv, javaClass._value);
var constructor = getMethod(jEnv, javaClass, GetCCharSequence("<init>"), GetCCharSequence("()V")); var constructor = getMethod(jEnv, javaClass, GetCCharSequence("<init>"), GetCCharSequence("()V"));
var newObj = newObject(jEnv, javaClass, constructor, 0); 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("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("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("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("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); setDoubleField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("FileSize"), GetCCharSequence("D")), info?.FileSize ?? 0d);
return newObj; return newObj;
@ -450,88 +486,102 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetVsync")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetVsync")]
public static void JniSetVsyncStateNative(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean enabled) public static void JniSetVsyncStateNative(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean enabled)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetVsyncState(enabled); SetVsyncState(enabled);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSwapBufferCallback")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSwapBufferCallback")]
public static void JniSetSwapBuffersCallbackNative(JEnvRef jEnv, JObjectLocalRef jObj, IntPtr swapBuffersCallback) public static void JniSetSwapBuffersCallbackNative(JEnvRef jEnv, JObjectLocalRef jObj, IntPtr swapBuffersCallback)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
_swapBuffersCallback = Marshal.GetDelegateForFunctionPointer<SwapBuffersCallback>(swapBuffersCallback); _swapBuffersCallback = Marshal.GetDelegateForFunctionPointer<SwapBuffersCallback>(swapBuffersCallback);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputInitialize")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputInitialize")]
public static void JniInitializeInput(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) public static void JniInitializeInput(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
InitializeInput(width, height); InitializeInput(width, height);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetClientSize")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetClientSize")]
public static void JniSetClientSize(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) public static void JniSetClientSize(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetClientSize(width, height); SetClientSize(width, height);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetTouchPoint")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetTouchPoint")]
public static void JniSetTouchPoint(JEnvRef jEnv, JObjectLocalRef jObj, JInt x, JInt y) public static void JniSetTouchPoint(JEnvRef jEnv, JObjectLocalRef jObj, JInt x, JInt y)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
SetTouchPoint(x, y); SetTouchPoint(x, y);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputReleaseTouchPoint")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputReleaseTouchPoint")]
public static void JniReleaseTouchPoint(JEnvRef jEnv, JObjectLocalRef jObj) public static void JniReleaseTouchPoint(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
ReleaseTouchPoint(); ReleaseTouchPoint();
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputUpdate")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputUpdate")]
public static void JniUpdateInput(JEnvRef jEnv, JObjectLocalRef jObj) public static void JniUpdateInput(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
UpdateInput(); UpdateInput();
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonPressed")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonPressed")]
public static void JniSetButtonPressed(JEnvRef jEnv, JObjectLocalRef jObj, JInt button, JInt id) 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); SetButtonPressed((GamepadButtonInputId)(int)button, id);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonReleased")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonReleased")]
public static void JniSetButtonReleased(JEnvRef jEnv, JObjectLocalRef jObj, JInt button, JInt id) 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); SetButtonReleased((GamepadButtonInputId)(int)button, id);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")]
public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id) 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); 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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputConnectGamepad")]
public static JInt JniConnectGamepad(JEnvRef jEnv, JObjectLocalRef jObj, JInt index) public static JInt JniConnectGamepad(JEnvRef jEnv, JObjectLocalRef jObj, JInt index)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
return ConnectGamepad(index); return ConnectGamepad(index);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")] [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(); var userId = GetOpenedUser();
pushString(userId); return storeString(userId);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")] [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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserPicture")]
public static void JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef picturePtr) 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 userId = GetString(jEnv, userIdPtr) ?? "";
var picture = GetString(jEnv, picturePtr) ?? ""; var picture = GetString(jEnv, picturePtr) ?? "";
@ -539,16 +589,18 @@ namespace LibRyujinx
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserName")] [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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserName")]
public static void JniSetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef userNamePtr) 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 userId = GetString(jEnv, userIdPtr) ?? "";
var userName = GetString(jEnv, userNamePtr) ?? ""; var userName = GetString(jEnv, userNamePtr) ?? "";
@ -558,6 +610,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetAllUsers")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetAllUsers")]
public static JArrayLocalRef JniGetAllUsers(JEnvRef jEnv, JObjectLocalRef jObj) public static JArrayLocalRef JniGetAllUsers(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var users = GetAllUsers(); var users = GetAllUsers();
return CreateStringArray(jEnv, users.ToList()); return CreateStringArray(jEnv, users.ToList());
@ -566,6 +619,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userAddUser")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userAddUser")]
public static void JniAddUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userNamePtr, JStringLocalRef picturePtr) 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 userName = GetString(jEnv, userNamePtr) ?? "";
var picture = GetString(jEnv, picturePtr) ?? ""; var picture = GetString(jEnv, picturePtr) ?? "";
@ -575,15 +629,17 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userDeleteUser")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userDeleteUser")]
public static void JniDeleteUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) public static void JniDeleteUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = GetString(jEnv, userIdPtr) ?? ""; var userId = GetString(jEnv, userIdPtr) ?? "";
DeleteUser(userId); DeleteUser(userId);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")] [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); OpenUser(userId);
} }
@ -591,6 +647,7 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userCloseUser")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userCloseUser")]
public static void JniCloseUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) public static void JniCloseUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{ {
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
var userId = GetString(jEnv, userIdPtr) ?? ""; var userId = GetString(jEnv, userIdPtr) ?? "";
CloseUser(userId); CloseUser(userId);

View File

@ -26,6 +26,7 @@ using LibHac.FsSystem;
using LibHac.Fs; using LibHac.Fs;
using Path = System.IO.Path; using Path = System.IO.Path;
using LibHac; using LibHac;
using OpenTK.Audio.OpenAL;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using System.Globalization; using System.Globalization;
@ -76,6 +77,8 @@ namespace LibRyujinx
Console.WriteLine(ex); Console.WriteLine(ex);
return false; return false;
} }
OpenALLibraryNameContainer.OverridePath = "libopenal.so";
return true; return true;
} }

View File

@ -11,8 +11,8 @@ android {
applicationId "org.ryujinx.android" applicationId "org.ryujinx.android"
minSdk 30 minSdk 30
targetSdk 33 targetSdk 33
versionCode 10001 versionCode 10004
versionName '1.0.1' versionName '1.0.4'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -94,7 +94,6 @@ dependencies {
implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material3:material3'
implementation 'com.github.swordfish90:radialgamepad:2.0.0' implementation 'com.github.swordfish90:radialgamepad:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.oboe:oboe:1.7.0'
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'

View File

@ -27,14 +27,10 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.RyujinxAndroid" android:theme="@style/Theme.RyujinxAndroid"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".GameActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:theme="@style/Theme.RyujinxAndroid" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:theme="@style/Theme.RyujinxAndroid"> android:theme="@style/Theme.RyujinxAndroid">
<intent-filter> <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). # Provides a relative path to your source file(s).
vulkan_wrapper.cpp vulkan_wrapper.cpp
oboe.cpp string_helper.cpp
ryujinx.cpp) ryujinx.cpp)
# Searches for a specified prebuilt library and stores the path as a # 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. # you want CMake to locate.
log ) log )
find_package (oboe REQUIRED CONFIG)
# Specifies libraries CMake should link to your target library. You # Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this # can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries. # build script, prebuilt third-party libraries, or system libraries.
@ -54,7 +52,6 @@ target_link_libraries( # Specifies the target library.
ryujinxjni ryujinxjni
# Links the target library to the log library # Links the target library to the log library
# included in the NDK. # included in the NDK.
oboe::oboe
${log-lib} ${log-lib}
-lvulkan -lvulkan
-landroid -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 <fcntl.h>
#include "libraries/adrenotools/include/adrenotools/driver.h" #include "libraries/adrenotools/include/adrenotools/driver.h"
#include "native_window.h" #include "native_window.h"
#include "string_helper.h"
// A macro to pass call to Vulkan and check for return value for success // A macro to pass call to Vulkan and check for return value for success
#define CALL_VK(func) \ #define CALL_VK(func) \
@ -46,5 +47,6 @@ JavaVM* _vm = nullptr;
jobject _mainActivity = nullptr; jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr; jclass _mainActivityClass = nullptr;
std::string _currentString = ""; std::string _currentString = "";
string_helper str_helper = string_helper();
#endif //RYUJINXNATIVE_RYUIJNX_H #endif //RYUJINXNATIVE_RYUIJNX_H

View File

@ -312,24 +312,28 @@ Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz
return createStringFromStdString(env, progressInfo); 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" extern "C"
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_org_ryujinx_android_NativeHelpers_popStringJava(JNIEnv *env, jobject thiz) { Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) {
return createStringFromStdString(env, _currentString); return createStringFromStdString(env, str_helper.get_stored(id));
}
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();
} }

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.gravityX = 1f
rightGamePad.gravityY = 1f rightGamePad.gravityY = 1f
ryujinxNative = RyujinxNative() ryujinxNative = RyujinxNative.instance
} }
fun setVisible(isVisible: Boolean){ fun setVisible(isVisible: Boolean){
@ -110,7 +110,7 @@ class GameController(var activity: Activity) {
fun connect(){ fun connect(){
if(controllerId == -1) if(controllerId == -1)
controllerId = RyujinxNative().inputConnectGamepad(0) controllerId = RyujinxNative.instance.inputConnectGamepad(0)
} }
private fun handleEvent(ev: Event) { private fun handleEvent(ev: Event) {

View File

@ -12,7 +12,8 @@ import org.ryujinx.android.viewmodels.QuickSettings
import kotlin.concurrent.thread import kotlin.concurrent.thread
@SuppressLint("ViewConstructor") @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 isProgressHidden: Boolean = false
private var progress: MutableState<String>? = null private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = 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 _guestThread: Thread? = null
private var _isInit: Boolean = false private var _isInit: Boolean = false
private var _isStarted: 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 { init {
holder.addCallback(this) 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) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if(_isClosed) if (_isClosed)
return return
start(holder) start(holder)
if(_width != width || _height != height) if (_width != width || _height != height) {
{
val window = nativeWindow.requeryWindowHandle() val window = nativeWindow.requeryWindowHandle()
_nativeRyujinx.graphicsSetSurface(window, nativeWindow.nativePointer) _nativeRyujinx.graphicsSetSurface(window, nativeWindow.nativePointer)
@ -62,8 +62,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
height height
) )
if(_isStarted) if (_isStarted) {
{
_nativeRyujinx.inputSetClientSize(width, height) _nativeRyujinx.inputSetClientSize(width, height)
} }
} }
@ -72,7 +71,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
} }
fun close(){ fun close() {
_isClosed = true _isClosed = true
_isInit = false _isInit = false
_isStarted = false _isStarted = false
@ -82,19 +81,18 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
} }
private fun start(surfaceHolder: SurfaceHolder) { private fun start(surfaceHolder: SurfaceHolder) {
if(_isStarted) if (_isStarted)
return return
game = mainViewModel.gameModel game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel;
_nativeRyujinx.inputInitialize(width, height) _nativeRyujinx.inputInitialize(width, height)
val settings = QuickSettings(mainViewModel.activity) val settings = QuickSettings(mainViewModel.activity)
if(!settings.useVirtualController){ if (!settings.useVirtualController) {
mainViewModel.controller?.setVisible(false) mainViewModel.controller?.setVisible(false)
} } else {
else{
mainViewModel.controller?.connect() mainViewModel.controller?.connect()
} }
@ -112,19 +110,19 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
_updateThread = thread(start = true) { _updateThread = thread(start = true) {
var c = 0 var c = 0
val helper = NativeHelpers() val helper = NativeHelpers.instance
while (_isStarted) { while (_isStarted) {
_nativeRyujinx.inputUpdate() _nativeRyujinx.inputUpdate()
Thread.sleep(1) Thread.sleep(1)
showLoading?.apply { showLoading?.apply {
if(value){ if (value) {
var value = helper.getProgressValue() var value = helper.getProgressValue()
if(value != -1f) if (value != -1f)
progress?.apply { progress?.apply {
this.value = helper.getProgressInfo() this.value = helper.getProgressInfo()
} }
progressValue?.apply { progressValue?.apply {
this.value = value this.value = value
@ -133,12 +131,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
} }
c++ c++
if (c >= 1000) { if (c >= 1000) {
if(helper.getProgressValue() == -1f) if (helper.getProgressValue() == -1f)
progress?.apply { progress?.apply {
this.value = "Loading ${game!!.titleName}" this.value = "Loading ${if(mainViewModel.isMiiEditorLaunched) "Mii Editor" else game!!.titleName}"
} }
c = 0 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 { } finally {
isImporting.value = false isImporting.value = false
RyujinxNative().deviceReloadFilesystem() RyujinxNative.instance.deviceReloadFilesystem()
} }
} }
} }

View File

@ -23,6 +23,121 @@ class Icons {
companion object{ companion object{
/// Icons exported from https://www.composables.com/icons /// Icons exported from https://www.composables.com/icons
@Composable @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 { fun playArrow(color: Color): ImageVector {
return remember { return remember {
ImageVector.Builder( ImageVector.Builder(

View File

@ -1,8 +1,13 @@
package org.ryujinx.android package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -10,14 +15,20 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.views.MainView import org.ryujinx.android.views.MainView
import kotlin.math.abs
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager =
PhysicalControllerManager(this)
private var _isInit: Boolean = false private var _isInit: Boolean = false
var isGameRunning = false
var storageHelper: SimpleStorageHelper? = null var storageHelper: SimpleStorageHelper? = null
companion object { companion object {
var mainViewModel: MainViewModel? = null var mainViewModel: MainViewModel? = null
@ -53,7 +64,7 @@ class MainActivity : BaseActivity() {
return return
val appPath: String = AppPath val appPath: String = AppPath
val success = RyujinxNative().initialize(appPath, false) val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath), false)
_isInit = success _isInit = success
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -67,12 +78,14 @@ class MainActivity : BaseActivity() {
AppPath = this.getExternalFilesDir(null)!!.absolutePath AppPath = this.getExternalFilesDir(null)!!.absolutePath
initialize() initialize()
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
WindowCompat.setDecorFitsSystemWindows(window,false) WindowCompat.setDecorFitsSystemWindows(window,false)
mainViewModel = MainViewModel(this) mainViewModel = MainViewModel(this)
mainViewModel!!.physicalControllerManager = physicalControllerManager
mainViewModel?.apply { mainViewModel?.apply {
setContent { setContent {
@ -98,4 +111,87 @@ class MainActivity : BaseActivity() {
super.onRestoreInstanceState(savedInstanceState) super.onRestoreInstanceState(savedInstanceState)
storageHelper?.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 { class NativeHelpers {
companion object { companion object {
val instance = NativeHelpers()
init { init {
System.loadLibrary("ryujinxjni") System.loadLibrary("ryujinxjni")
} }
@ -28,6 +29,6 @@ class NativeHelpers {
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
external fun getProgressInfo() : String external fun getProgressInfo() : String
external fun getProgressValue() : Float external fun getProgressValue() : Float
external fun pushStringJava(string: String) external fun storeStringJava(string: String) : Long
external fun popStringJava() : String external fun getStringJava(id: Long) : String
} }

View File

@ -4,7 +4,7 @@ import android.view.SurfaceView
class NativeWindow(val surface: SurfaceView) { class NativeWindow(val surface: SurfaceView) {
var nativePointer: Long var nativePointer: Long
var nativeHelpers: NativeHelpers = NativeHelpers() var nativeHelpers: NativeHelpers = NativeHelpers.instance
private var _swapInterval : Int = 0 private var _swapInterval : Int = 0
var maxSwapInterval : Int = 0 var maxSwapInterval : Int = 0

View File

@ -3,9 +3,9 @@ package org.ryujinx.android
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
class PhysicalControllerManager(val activity: GameActivity) { class PhysicalControllerManager(val activity: MainActivity) {
private var controllerId: Int = -1 private var controllerId: Int = -1
private var ryujinxNative: RyujinxNative = RyujinxNative() private var ryujinxNative: RyujinxNative = RyujinxNative.instance
fun onKeyEvent(event: KeyEvent) : Boolean{ fun onKeyEvent(event: KeyEvent) : Boolean{
if(controllerId != -1) { if(controllerId != -1) {

View File

@ -4,10 +4,10 @@ import org.ryujinx.android.viewmodels.GameInfo
@Suppress("KotlinJniMissingFunction") @Suppress("KotlinJniMissingFunction")
class RyujinxNative { class RyujinxNative {
external fun initialize(appPath: Long, enableDebugLogs : Boolean): Boolean
external fun initialize(appPath: String, enableDebugLogs : Boolean): Boolean
companion object { companion object {
val instance: RyujinxNative = RyujinxNative()
init { init {
System.loadLibrary("ryujinx") System.loadLibrary("ryujinx")
} }
@ -20,7 +20,7 @@ class RyujinxNative {
enableDockedMode : Boolean, enableDockedMode : Boolean,
enablePtc : Boolean, enablePtc : Boolean,
enableInternetAccess : Boolean, enableInternetAccess : Boolean,
timeZone : String, timeZone : Long,
ignoreMissingServices : Boolean): Boolean ignoreMissingServices : Boolean): Boolean
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
external fun graphicsInitializeRenderer( external fun graphicsInitializeRenderer(
@ -29,6 +29,7 @@ class RyujinxNative {
): Boolean ): Boolean
external fun deviceLoad(game: String): Boolean external fun deviceLoad(game: String): Boolean
external fun deviceLaunchMiiEditor(): Boolean
external fun deviceGetGameFrameRate(): Double external fun deviceGetGameFrameRate(): Double
external fun deviceGetGameFrameTime(): Double external fun deviceGetGameFrameTime(): Double
external fun deviceGetGameFifo(): Double external fun deviceGetGameFifo(): Double
@ -51,16 +52,16 @@ class RyujinxNative {
external fun graphicsSetSurface(surface: Long, window: Long) external fun graphicsSetSurface(surface: Long, window: Long)
external fun deviceCloseEmulation() external fun deviceCloseEmulation()
external fun deviceSignalEmulationClose() external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String> external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array<String>
external fun userGetOpenedUser() external fun userGetOpenedUser() : Long
external fun userGetUserPicture(userId: String) : String external fun userGetUserPicture(userId: Long) : Long
external fun userSetUserPicture(userId: String, picture: String) 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 userSetUserName(userId: String, userName: String)
external fun userGetAllUsers() : Array<String> external fun userGetAllUsers() : Array<String>
external fun userAddUser(username: String, picture: String) external fun userAddUser(username: String, picture: String)
external fun userDeleteUser(userId: String) external fun userDeleteUser(userId: String)
external fun userOpenUser(userId: String) external fun userOpenUser(userId: Long)
external fun userCloseUser(userId: String) 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.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative import org.ryujinx.android.RyujinxNative
import java.io.File import java.io.File
@ -39,8 +40,8 @@ class DlcViewModel(val titleId: String) {
val path = file.getAbsolutePath(storageHelper.storage.context) val path = file.getAbsolutePath(storageHelper.storage.context)
if (path.isNotEmpty()) { if (path.isNotEmpty()) {
data?.apply { data?.apply {
var contents = RyujinxNative().deviceGetDlcContentList( var contents = RyujinxNative.instance.deviceGetDlcContentList(
path, NativeHelpers.instance.storeStringJava(path),
titleId.toLong(16) titleId.toLong(16)
) )
@ -101,7 +102,7 @@ class DlcViewModel(val titleId: String) {
enabled, enabled,
containerPath, containerPath,
dlc.fullPath, 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<Boolean> = mutableStateOf(false), var isEnabled: MutableState<Boolean> = mutableStateOf(false),
var containerPath: String = "", var containerPath: String = "",
var fullPath: String = "", var fullPath: String = "",
var titleId: String = "") var titleId: String = "")

View File

@ -15,12 +15,12 @@ class GameModel(var file: DocumentFile, val context: Context) {
var titleId: String? = null var titleId: String? = null
var developer: String? = null var developer: String? = null
var version: String? = null var version: String? = null
var iconCache: String? = null var icon: String? = null
init { init {
fileName = file.name fileName = file.name
var pid = open() var pid = open()
val gameInfo = RyujinxNative().deviceGetGameInfo(pid, file.extension.contains("xci")) val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci"))
close() close()
fileSize = gameInfo.FileSize fileSize = gameInfo.FileSize
@ -28,7 +28,7 @@ class GameModel(var file: DocumentFile, val context: Context) {
titleName = gameInfo.TitleName titleName = gameInfo.TitleName
developer = gameInfo.Developer developer = gameInfo.Developer
version = gameInfo.Version version = gameInfo.Version
iconCache = gameInfo.IconCache icon = gameInfo.Icon
} }
fun open() : Int { fun open() : Int {
@ -53,5 +53,5 @@ class GameInfo {
var TitleId: String? = null var TitleId: String? = null
var Developer: String? = null var Developer: String? = null
var Version: 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 val mainViewModel: MainViewModel? = null
) { ) {
private var isLoading: Boolean = false private var isLoading: Boolean = false
private var gameList: SnapshotStateList<GameModel>? = null
private var loadedCache: List<GameModel> = listOf() private var loadedCache: List<GameModel> = listOf()
private var gameFolderPath: DocumentFile? = null private var gameFolderPath: DocumentFile? = null
private var sharedPref: SharedPreferences? = null private var sharedPref: SharedPreferences? = null
val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
init { init {
if (activity != null) { if (activity != null) {
@ -68,53 +68,35 @@ class HomeViewModel(
) )
} }
fun reloadGameList(ignoreCache: Boolean = false) { fun reloadGameList() {
var storage = activity?.storageHelper ?: return var storage = activity?.storageHelper ?: return
if(isLoading) if(isLoading)
return return
val folder = gameFolderPath ?: return val folder = gameFolderPath ?: return
gameList.clear()
isLoading = true isLoading = true
thread {
if(!ignoreCache) { try {
val files = mutableListOf<GameModel>() val files = mutableListOf<GameModel>()
for (file in folder.search(false, DocumentFileType.FILE)) {
thread { if (file.extension == "xci" || file.extension == "nsp")
try { activity.let {
for (file in folder.search(false, DocumentFileType.FILE)) { val item = GameModel(file, it)
if (file.extension == "xci" || file.extension == "nsp") files.add(item)
activity.let { gameList.add(item)
files.add(GameModel(file, it)) }
}
}
loadedCache = files.toList()
isLoading = false
applyFilter()
} finally {
isLoading = false
} }
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<GameModel>) {
gameList = list
reloadGameList(loadedCache.isNotEmpty())
} }
fun clearLoadedCache(){ fun clearLoadedCache(){

View File

@ -2,7 +2,6 @@ package org.ryujinx.android.viewmodels
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.PerformanceHintManager import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -10,7 +9,6 @@ import androidx.navigation.NavHostController
import com.anggrayudi.storage.extension.launchOnUiThread import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import org.ryujinx.android.GameActivity
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.GraphicsConfiguration import org.ryujinx.android.GraphicsConfiguration
@ -31,6 +29,8 @@ class MainViewModel(val activity: MainActivity) {
var controller: GameController? = null var controller: GameController? = null
var performanceManager: PerformanceManager? = null var performanceManager: PerformanceManager? = null
var selected: GameModel? = null var selected: GameModel? = null
var isMiiEditorLaunched = false
val userViewModel = UserViewModel()
private var gameTimeState: MutableState<Double>? = null private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null private var fifoState: MutableState<Double>? = null
@ -56,13 +56,13 @@ class MainViewModel(val activity: MainActivity) {
} }
fun closeGame() { fun closeGame() {
RyujinxNative().deviceSignalEmulationClose() RyujinxNative.instance.deviceSignalEmulationClose()
gameHost?.close() gameHost?.close()
RyujinxNative().deviceCloseEmulation() RyujinxNative.instance.deviceCloseEmulation()
} }
fun loadGame(game:GameModel) : Boolean { fun loadGame(game:GameModel) : Boolean {
val nativeRyujinx = RyujinxNative() val nativeRyujinx = RyujinxNative.instance
val descriptor = game.open() val descriptor = game.open()
@ -70,6 +70,7 @@ class MainViewModel(val activity: MainActivity) {
return false return false
gameModel = game gameModel = game
isMiiEditorLaunched = false
val settings = QuickSettings(activity) val settings = QuickSettings(activity)
@ -83,7 +84,7 @@ class MainViewModel(val activity: MainActivity) {
if (!success) if (!success)
return false return false
val nativeHelpers = NativeHelpers() val nativeHelpers = NativeHelpers.instance
val nativeInterop = NativeGraphicsInterop() val nativeInterop = NativeGraphicsInterop()
nativeInterop.VkRequiredExtensions = arrayOf( nativeInterop.VkRequiredExtensions = arrayOf(
"VK_KHR_surface", "VK_KHR_android_surface" "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!! + "/", activity.applicationInfo.nativeLibraryDir!! + "/",
privateDriverPath, privateDriverPath,
this.libraryName this.libraryName
@ -148,7 +149,7 @@ class MainViewModel(val activity: MainActivity) {
settings.enableDocked, settings.enableDocked,
settings.enablePtc, settings.enablePtc,
false, false,
"UTC", NativeHelpers.instance.storeStringJava("UTC"),
settings.ignoreMissingServices settings.ignoreMissingServices
) )
@ -169,6 +170,112 @@ class MainViewModel(val activity: MainActivity) {
return true 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){ fun clearPptcCache(titleId :String){
if(titleId.isNotEmpty()){ if(titleId.isNotEmpty()){
val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu" val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu"
@ -241,8 +348,9 @@ class MainViewModel(val activity: MainActivity) {
} }
fun navigateToGame() { fun navigateToGame() {
val intent = Intent(activity, GameActivity::class.java) activity.setFullScreen(true)
activity.startActivity(intent) navController?.navigate("game")
activity.isGameRunning = true
} }
fun setProgressStates( fun setProgressStates(
@ -255,15 +363,4 @@ class MainViewModel(val activity: MainActivity) {
this.progress = progress this.progress = progress
gameHost?.setProgressStates(showLoading, progressValue, 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.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread 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.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel import org.ryujinx.android.viewmodels.HomeViewModel
import java.io.File
import java.util.Base64 import java.util.Base64
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -81,7 +75,6 @@ class HomeViews {
viewModel: HomeViewModel = HomeViewModel(), viewModel: HomeViewModel = HomeViewModel(),
navController: NavHostController? = null navController: NavHostController? = null
) { ) {
val native = RyujinxNative()
val showAppActions = remember { mutableStateOf(false) } val showAppActions = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) }
val openTitleUpdateDialog = remember { mutableStateOf(false) } val openTitleUpdateDialog = remember { mutableStateOf(false) }
@ -90,31 +83,7 @@ class HomeViews {
val query = remember { val query = remember {
mutableStateOf("") 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( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
@ -148,12 +117,14 @@ class HomeViews {
IconButton(onClick = { IconButton(onClick = {
navController?.navigate("user") navController?.navigate("user")
}) { }) {
if (pic.value.isNotEmpty()) { if (viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) {
val pic =
viewModel.mainViewModel.userViewModel.openedUser.userPicture
Image( Image(
bitmap = BitmapFactory.decodeByteArray( bitmap = BitmapFactory.decodeByteArray(
pic.value, pic,
0, 0,
pic.value.size pic?.size ?: 0
) )
.asImageBitmap(), .asImageBitmap(),
contentDescription = "user image", contentDescription = "user image",
@ -184,9 +155,26 @@ class HomeViews {
) )
}, },
bottomBar = { bottomBar = {
BottomAppBar(actions = { BottomAppBar(
actions = {
if (showAppActions.value) { if (showAppActions.value) {
IconButton(onClick = { 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( Icon(
org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface),
@ -210,13 +198,17 @@ class HomeViews {
Text(text = "Clear PPTC Cache") Text(text = "Clear PPTC Cache")
}, onClick = { }, onClick = {
showAppMenu.value = false showAppMenu.value = false
viewModel.mainViewModel?.clearPptcCache(viewModel.mainViewModel?.selected?.titleId ?: "") viewModel.mainViewModel?.clearPptcCache(
viewModel.mainViewModel?.selected?.titleId ?: ""
)
}) })
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text(text = "Purge Shader Cache") Text(text = "Purge Shader Cache")
}, onClick = { }, onClick = {
showAppMenu.value = false showAppMenu.value = false
viewModel.mainViewModel?.purgeShaderCache(viewModel.mainViewModel?.selected?.titleId ?: "") viewModel.mainViewModel?.purgeShaderCache(
viewModel.mainViewModel?.selected?.titleId ?: ""
)
}) })
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text(text = "Manage Updates") 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 = {
FloatingActionButton( FloatingActionButton(
@ -255,12 +280,7 @@ class HomeViews {
) { contentPadding -> ) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) { Box(modifier = Modifier.padding(contentPadding)) {
val list = remember { val list = remember {
mutableStateListOf<GameModel>() viewModel.gameList
}
if (refresh.value) {
viewModel.setViewList(list)
refresh.value = false
showAppActions.value = false
} }
val selectedModel = remember { val selectedModel = remember {
mutableStateOf(viewModel.mainViewModel?.selected) mutableStateOf(viewModel.mainViewModel?.selected)
@ -365,6 +385,7 @@ class HomeViews {
val color = val color =
if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
val decoder = Base64.getDecoder()
Surface( Surface(
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
color = color, color = color,
@ -409,13 +430,12 @@ class HomeViews {
) { ) {
Row { Row {
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
val iconSource = if (gameModel.icon?.isNotEmpty() == true) {
MainActivity.AppPath + "/iconCache/" + gameModel.iconCache val pic = decoder.decode(gameModel.icon)
val imageFile = File(iconSource)
if (imageFile.exists()) {
val size = ImageSize / Resources.getSystem().displayMetrics.density val size = ImageSize / Resources.getSystem().displayMetrics.density
AsyncImage( Image(
model = imageFile, bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
.asImageBitmap(),
contentDescription = gameModel.titleName + " icon", contentDescription = gameModel.titleName + " icon",
modifier = Modifier modifier = Modifier
.padding(end = 8.dp) .padding(end = 8.dp)

View File

@ -17,6 +17,7 @@ class MainView {
NavHost(navController = navController, startDestination = "home") { NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
composable("user") { UserViews.Main(mainViewModel, navController) } composable("user") { UserViews.Main(mainViewModel, navController) }
composable("game") { GameViews.Main() }
composable("settings") { composable("settings") {
SettingViews.Main( SettingViews.Main(
SettingsViewModel( SettingsViewModel(

View File

@ -306,7 +306,7 @@ class SettingViews {
thread { thread {
Helpers.importAppData(this, isImporting) Helpers.importAppData(this, isImporting)
showImportCompletion.value = true showImportCompletion.value = true
mainViewModel.requestUserRefresh() mainViewModel.userViewModel.refreshUsers()
} }
} }
}, modifier = Modifier.padding(horizontal = 8.dp)) { }, modifier = Modifier.padding(horizontal = 8.dp)) {
@ -329,7 +329,7 @@ class SettingViews {
AlertDialog(onDismissRequest = { AlertDialog(onDismissRequest = {
showImportCompletion.value = false showImportCompletion.value = false
importFile.value = null importFile.value = null
mainViewModel.requestUserRefresh() mainViewModel.userViewModel.refreshUsers()
mainViewModel.homeViewModel.clearLoadedCache() mainViewModel.homeViewModel.clearLoadedCache()
}) { }) {
Card( Card(
@ -461,7 +461,8 @@ class SettingViews {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(300.dp) .height(350.dp)
.verticalScroll(rememberScrollState())
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -494,7 +495,7 @@ class SettingViews {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .padding(4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(

View File

@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@ -62,6 +64,7 @@ class TitleUpdateViews {
modifier = Modifier modifier = Modifier
.height(250.dp) .height(250.dp)
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState())
) { ) {
Row(modifier = Modifier.padding(8.dp)) { Row(modifier = Modifier.padding(8.dp)) {
RadioButton( RadioButton(

View File

@ -26,6 +26,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import java.util.Base64
class UserViews { class UserViews {
companion object { companion object {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) { fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) {
val ryujinxNative = RyujinxNative() val reload = remember {
val decoder = Base64.getDecoder() mutableStateOf(true)
ryujinxNative.userGetOpenedUser()
val openedUser = remember {
mutableStateOf(NativeHelpers().popStringJava())
} }
val openedUserPic = remember {
mutableStateOf(decoder.decode(ryujinxNative.userGetUserPicture(openedUser.value)))
}
val openedUserName = remember {
mutableStateOf(ryujinxNative.userGetUserName(openedUser.value))
}
val userList = remember {
mutableListOf("")
}
fun refresh() { fun refresh() {
userList.clear() viewModel?.userViewModel?.refreshUsers()
userList.addAll(ryujinxNative.userGetAllUsers()) reload.value = true
}
LaunchedEffect(reload.value) {
reload.value = false
} }
refresh()
Scaffold(modifier = Modifier.fillMaxSize(), Scaffold(modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
@ -102,25 +86,28 @@ class UserViews {
.padding(4.dp), .padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Image( if (viewModel?.userViewModel?.openedUser?.id?.isNotEmpty() == true) {
bitmap = BitmapFactory.decodeByteArray( val openUser = viewModel.userViewModel.openedUser
openedUserPic.value, Image(
0, bitmap = BitmapFactory.decodeByteArray(
openedUserPic.value.size openUser.userPicture,
).asImageBitmap(), 0,
contentDescription = "selected image", openUser.userPicture?.size ?: 0
contentScale = ContentScale.Crop, ).asImageBitmap(),
modifier = Modifier contentDescription = "selected image",
.padding(4.dp) contentScale = ContentScale.Crop,
.size(96.dp) modifier = Modifier
.clip(CircleShape) .padding(4.dp)
) .size(96.dp)
Column( .clip(CircleShape)
modifier = Modifier.fillMaxWidth(), )
verticalArrangement = Arrangement.spacedBy(4.dp) Column(
) { modifier = Modifier.fillMaxWidth(),
Text(text = openedUserName.value) verticalArrangement = Arrangement.spacedBy(4.dp)
Text(text = openedUser.value) ) {
Text(text = openUser.username)
Text(text = openUser.id)
}
} }
} }
@ -139,34 +126,32 @@ class UserViews {
) )
} }
} }
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 96.dp), columns = GridCells.Adaptive(minSize = 96.dp),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(4.dp) .padding(4.dp)
) { ) {
items(userList) { user -> if(viewModel?.userViewModel?.userList?.isNotEmpty() == true) {
val pic = decoder.decode(ryujinxNative.userGetUserPicture(user)) items(viewModel.userViewModel.userList) { user ->
val name = ryujinxNative.userGetUserName(user) Image(
Image( bitmap = BitmapFactory.decodeByteArray(user.userPicture, 0, user.userPicture?.size ?: 0)
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) .asImageBitmap(),
.asImageBitmap(), contentDescription = "selected image",
contentDescription = "selected image", contentScale = ContentScale.Crop,
contentScale = ContentScale.Crop, modifier = Modifier
modifier = Modifier .fillMaxSize()
.fillMaxSize() .padding(4.dp)
.padding(4.dp) .clip(CircleShape)
.clip(CircleShape) .align(Alignment.CenterHorizontally)
.align(Alignment.CenterHorizontally) .combinedClickable(
.combinedClickable( onClick = {
onClick = { viewModel.userViewModel.openUser(user)
ryujinxNative.userOpenUser(user) reload.value = true
openedUser.value = user })
openedUserPic.value = pic )
openedUserName.value = name }
viewModel?.requestUserRefresh()
})
)
} }
} }
} }