1
0
forked from MeloNX/MeloNX

Cleanup gitignore and project file structure

Switch to Java 17 LTS

Keep libryujinx symbols

Add gradle module for libryujinx

Update dependencies

Raise minSdk to 30 to fix linter errors

Make stripSymbols a gradle property

Preserve other jni libraries

Fix file trees

Fix AndroidManifest.xml warnings

Suppress Google Play permission warning

Add preBuild dependency on libryujinx for app

Add toolchain path to all operating systems correctly

Make dotnet executable path configurable

Fix OS detection

Only build LibRyujinx if source or project files changed

Add toolchain path to output

Fix PATH variable on Windows

I spent ~7 hours debugging this.
I searched for a bug in the Exec task and found nothing.
I tried different ways to invoke the dotnet command
to make sure PATH is always set before.
I also modified Microsoft.NETCore.Native.Unix.targets to echo PATH via Exec and via Text.
But in the end I was confused about seeing two PATH variables
when checking the dotnet.exe subprocess with ProcessHacker.
This made me try setting the Path variable instead of PATH
and to my surprise this just worked.
Turns out Windows environment variables are case-sensitive and
Windows uses Path instead of PATH like Unix.

God, I love Microsoft Windows. :)

Cleanup LibRyujinx and add more verbose logging

Cleanup RyujinxAndroid
This commit is contained in:
TSR Berry 2023-07-22 07:37:11 +02:00 committed by Emmanuel Hansen
parent fcb511bbca
commit 8b7beb6f22
29 changed files with 448 additions and 340 deletions

View File

@ -1,10 +1,7 @@
using Ryujinx.Common; using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Formatters; using Ryujinx.Common.Logging.Formatters;
using Ryujinx.Common.Logging.Targets; using Ryujinx.Common.Logging.Targets;
using System; using System;
using System.IO;
using System.Linq;
namespace LibRyujinx namespace LibRyujinx
{ {
@ -26,7 +23,7 @@ namespace LibRyujinx
Logcat.AndroidLogPrint(GetLogLevel(args.Level), _name, _formatter.Format(args)); Logcat.AndroidLogPrint(GetLogLevel(args.Level), _name, _formatter.Format(args));
} }
private Logcat.LogLevel GetLogLevel(LogLevel logLevel) private static Logcat.LogLevel GetLogLevel(LogLevel logLevel)
{ {
return logLevel switch return logLevel switch
{ {
@ -39,13 +36,14 @@ namespace LibRyujinx
LogLevel.AccessLog => Logcat.LogLevel.Info, LogLevel.AccessLog => Logcat.LogLevel.Info,
LogLevel.Notice => Logcat.LogLevel.Info, LogLevel.Notice => Logcat.LogLevel.Info,
LogLevel.Trace => Logcat.LogLevel.Verbose, LogLevel.Trace => Logcat.LogLevel.Verbose,
_ => throw new NotImplementedException() _ => throw new NotImplementedException(),
}; };
} }
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -1,27 +1,28 @@
using System; using LibRyujinx.Jni;
using System.Runtime.InteropServices;
using Ryujinx.Common.Configuration;
using System.Collections.Generic;
using LibRyujinx.Jni.Pointers; using LibRyujinx.Jni.Pointers;
using LibRyujinx.Jni.Primitives;
using LibRyujinx.Jni.References; using LibRyujinx.Jni.References;
using LibRyujinx.Jni.Values; using LibRyujinx.Jni.Values;
using LibRyujinx.Jni.Primitives; using LibRyujinx.Shared.Audio.Oboe;
using LibRyujinx.Jni; using Microsoft.Win32.SafeHandles;
using Rxmxnx.PInvoke; using Rxmxnx.PInvoke;
using System.Text; using Ryujinx.Common.Configuration;
using LibRyujinx.Jni.Internal.Pointers;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Targets; using Ryujinx.Common.Logging.Targets;
using Ryujinx.Common.SystemInfo;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using Silk.NET.Core.Loader;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR; using Silk.NET.Vulkan.Extensions.KHR;
using LibRyujinx.Shared.Audio.Oboe; using System;
using System.Threading; using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.Win32.SafeHandles; using System.Numerics;
using Newtonsoft.Json.Linq; using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using LibHac.Tools.Fs; using System.Text;
using Ryujinx.HLE.HOS.SystemState; using System.Threading;
namespace LibRyujinx namespace LibRyujinx
{ {
@ -53,15 +54,7 @@ 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, JStringLocalRef jpath, JBoolean enableDebugLogs)
{ {
var path = GetString(jEnv, jpath); SystemInfo.IsBionic = true;
Ryujinx.Common.SystemInfo.SystemInfo.IsBionic = true;
var init = Initialize(path, enableDebugLogs);
AudioDriver = new OboeHardwareDeviceDriver();
_surfaceEvent = new ManualResetEvent(false);
Logger.AddTarget( Logger.AddTarget(
new AsyncLogTargetWrapper( new AsyncLogTargetWrapper(
@ -70,10 +63,18 @@ namespace LibRyujinx
AsyncLogTargetOverflowAction.Block AsyncLogTargetOverflowAction.Block
)); ));
var path = GetString(jEnv, jpath);
var init = Initialize(path, enableDebugLogs);
AudioDriver = new OboeHardwareDeviceDriver();
_surfaceEvent = new ManualResetEvent(false);
return init; return init;
} }
private static string GetString(JEnvRef jEnv, JStringLocalRef jString) private static string? GetString(JEnvRef jEnv, JStringLocalRef jString)
{ {
var stringPtr = getStringPointer(jEnv, jString); var stringPtr = getStringPointer(jEnv, jString);
@ -179,7 +180,7 @@ namespace LibRyujinx
var jobject = getObjectClass(jEnv, graphicObject); var jobject = getObjectClass(jEnv, graphicObject);
GraphicsConfiguration graphicsConfiguration = new GraphicsConfiguration() GraphicsConfiguration graphicsConfiguration = new()
{ {
EnableShaderCache = getBooleanField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("EnableShaderCache"), GetCCharSequence("Z"))), EnableShaderCache = getBooleanField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("EnableShaderCache"), GetCCharSequence("Z"))),
EnableMacroHLE = getBooleanField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("EnableMacroHLE"), GetCCharSequence("Z"))), EnableMacroHLE = getBooleanField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("EnableMacroHLE"), GetCCharSequence("Z"))),
@ -191,17 +192,17 @@ namespace LibRyujinx
MaxAnisotropy = getFloatField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("MaxAnisotropy"), GetCCharSequence("F"))), MaxAnisotropy = getFloatField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("MaxAnisotropy"), GetCCharSequence("F"))),
BackendThreading = (BackendThreading)(int)getIntField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("BackendThreading"), GetCCharSequence("I"))) BackendThreading = (BackendThreading)(int)getIntField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("BackendThreading"), GetCCharSequence("I")))
}; };
Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.Android; SearchPathContainer.Platform = UnderlyingPlatform.Android;
return InitializeGraphics(graphicsConfiguration); return InitializeGraphics(graphicsConfiguration);
} }
private static CCharSequence GetCCharSequence(string s) private static CCharSequence GetCCharSequence(string s)
{ {
return (CCharSequence)Encoding.UTF8.GetBytes(s).AsSpan(); return Encoding.UTF8.GetBytes(s).AsSpan();
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsSetSurface")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsSetSurface")]
public unsafe static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr) public static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr)
{ {
_surfacePtr = surfacePtr; _surfacePtr = surfacePtr;
@ -235,7 +236,7 @@ namespace LibRyujinx
var getLongField = getLongFieldPtr.GetUnsafeDelegate<GetLongFieldDelegate>(); var getLongField = getLongFieldPtr.GetUnsafeDelegate<GetLongFieldDelegate>();
var getObjectField = getObjectFieldPtr.GetUnsafeDelegate<GetObjectFieldDelegate>(); var getObjectField = getObjectFieldPtr.GetUnsafeDelegate<GetObjectFieldDelegate>();
List<string> extensions = new List<string>(); List<string?> extensions = new();
var count = getArrayLength(jEnv, extensionsArray); var count = getArrayLength(jEnv, extensionsArray);
@ -249,9 +250,9 @@ namespace LibRyujinx
_surfaceEvent.Set(); _surfaceEvent.Set();
_surfacePtr = (long)surfacePtr; _surfacePtr = surfacePtr;
CreateSurface createSurfaceFunc = (IntPtr instance) => CreateSurface createSurfaceFunc = instance =>
{ {
_surfaceEvent.WaitOne(); _surfaceEvent.WaitOne();
_surfaceEvent.Reset(); _surfaceEvent.Reset();
@ -259,10 +260,10 @@ namespace LibRyujinx
var api = Vk.GetApi(); var api = Vk.GetApi();
if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension)) if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension))
{ {
var createInfo = new AndroidSurfaceCreateInfoKHR() var createInfo = new AndroidSurfaceCreateInfoKHR
{ {
SType = StructureType.AndroidSurfaceCreateInfoKhr, SType = StructureType.AndroidSurfaceCreateInfoKhr,
Window = (nint*)_surfacePtr Window = (nint*)_surfacePtr,
}; };
var result = surfaceExtension.CreateAndroidSurface(new Instance(instance), createInfo, null, out var surface); var result = surfaceExtension.CreateAndroidSurface(new Instance(instance), createInfo, null, out var surface);
@ -296,9 +297,8 @@ 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)
{ {
var info = GetGameInfo(GetString(jEnv, path)) ?? new GameInfo(); var info = GetGameInfo(GetString(jEnv, path));
SHA256 sha; return GetInfo(jEnv, info, out SHA256 _);
return GetInfo(jEnv, info, out sha);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
@ -306,12 +306,11 @@ namespace LibRyujinx
{ {
using var stream = OpenFile(fileDescriptor); using var stream = OpenFile(fileDescriptor);
var info = GetGameInfo(stream, isXci) ?? new GameInfo(); var info = GetGameInfo(stream, isXci);
SHA256 sha; return GetInfo(jEnv, info, out SHA256 _);
return GetInfo(jEnv, info, out sha);
} }
private static JObjectLocalRef GetInfo(JEnvRef jEnv, GameInfo info, out SHA256 sha) private static JObjectLocalRef GetInfo(JEnvRef jEnv, GameInfo? info, out SHA256 sha)
{ {
var javaClassName = GetCCharSequence("org/ryujinx/android/viewmodels/GameInfo"); var javaClassName = GetCCharSequence("org/ryujinx/android/viewmodels/GameInfo");
@ -339,7 +338,7 @@ namespace LibRyujinx
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(); sha = SHA256.Create();
var iconCacheByte = sha.ComputeHash(info.Icon ?? new byte[0]); var iconCacheByte = sha.ComputeHash(info?.Icon ?? Array.Empty<byte>());
var iconCache = BitConverter.ToString(iconCacheByte).Replace("-", ""); var iconCache = BitConverter.ToString(iconCacheByte).Replace("-", "");
var cacheDirectory = Path.Combine(AppDataManager.BaseDirPath, "iconCache"); var cacheDirectory = Path.Combine(AppDataManager.BaseDirPath, "iconCache");
@ -348,21 +347,23 @@ namespace LibRyujinx
var cachePath = Path.Combine(cacheDirectory, iconCache); var cachePath = Path.Combine(cacheDirectory, iconCache);
if (!File.Exists(cachePath)) if (!File.Exists(cachePath))
{ {
File.WriteAllBytes(cachePath, info.Icon ?? new byte[0]); 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("IconCache"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, iconCache)._value);
setDoubleField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("FileSize"), GetCCharSequence("D")), info.FileSize); setDoubleField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("FileSize"), GetCCharSequence("D")), info?.FileSize ?? 0d);
return newObj; return newObj;
} }
private static JStringLocalRef CreateString(JEnvRef jEnv, string s) private static JStringLocalRef CreateString(JEnvRef jEnv, string? s)
{ {
s ??= string.Empty;
var ptr = Marshal.StringToHGlobalAnsi(s); var ptr = Marshal.StringToHGlobalAnsi(s);
var str = createString(jEnv, ptr); var str = createString(jEnv, ptr);
@ -417,19 +418,19 @@ namespace LibRyujinx
[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)
{ {
SetButtonPressed((Ryujinx.Input.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)
{ {
SetButtonReleased((Ryujinx.Input.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)
{ {
SetStickAxis((Ryujinx.Input.StickInputId)(int)stick, new System.Numerics.Vector2(x, y), id); SetStickAxis((StickInputId)(int)stick, new Vector2(x, y), id);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputConnectGamepad")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputConnectGamepad")]
@ -438,7 +439,7 @@ namespace LibRyujinx
return ConnectGamepad(index); return ConnectGamepad(index);
} }
private static Stream OpenFile(int descriptor) private static FileStream OpenFile(int descriptor)
{ {
var safeHandle = new SafeFileHandle(descriptor, false); var safeHandle = new SafeFileHandle(descriptor, false);
@ -464,7 +465,7 @@ namespace LibRyujinx
Warn = 0x05, Warn = 0x05,
Error = 0x06, Error = 0x06,
Fatal = 0x07, Fatal = 0x07,
Silent = 0x08 Silent = 0x08,
} }
} }
} }

View File

@ -65,6 +65,10 @@ namespace LibRyujinx
1000, 1000,
AsyncLogTargetOverflowAction.Block AsyncLogTargetOverflowAction.Block
)); ));
Logger.Notice.Print(LogClass.Application, "Initializing...");
Logger.Notice.Print(LogClass.Application, $"Using base path: {AppDataManager.BaseDirPath}");
SwitchDevice = new SwitchDevice(); SwitchDevice = new SwitchDevice();
} }
catch (Exception ex) catch (Exception ex)
@ -417,6 +421,8 @@ namespace LibRyujinx
{ {
using var ncaFile = new UniqueRef<IFile>(); using var ncaFile = new UniqueRef<IFile>();
Logger.Info?.Print(LogClass.Application, $"Loading file from PFS: {fileEntry.FullPath}");
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage());

View File

@ -1,15 +1,12 @@
.idea/
*.iml *.iml
.gradle .gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties local.properties
.DS_Store
build/
captures
.externalNativeBuild
.cxx/
app/src/main/jniLibs/arm64-v8a/**
!app/src/main/jniLibs/arm64-v8a/.gitkeep

View File

@ -1 +0,0 @@
/build

View File

@ -9,7 +9,7 @@ android {
defaultConfig { defaultConfig {
applicationId "org.ryujinx.android" applicationId "org.ryujinx.android"
minSdk 28 minSdk 30
targetSdk 33 targetSdk 33
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@ -20,6 +20,7 @@ android {
} }
ndk { ndk {
//noinspection ChromeOsAbiSupport
abiFilters 'arm64-v8a' abiFilters 'arm64-v8a'
} }
@ -38,11 +39,11 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '17'
} }
buildFeatures { buildFeatures {
compose true compose true
@ -52,6 +53,9 @@ android {
kotlinCompilerExtensionVersion '1.3.2' kotlinCompilerExtensionVersion '1.3.2'
} }
packagingOptions { packagingOptions {
jniLibs {
keepDebugSymbols += '**/libryujinx.so'
}
resources { resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes += '/META-INF/{AL2.0,LGPL2.1}'
} }
@ -64,8 +68,12 @@ android {
} }
} }
dependencies { tasks.named("preBuild") {
dependsOn ':libryujinx:assemble'
}
dependencies {
runtimeOnly project(":libryujinx")
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
@ -81,7 +89,7 @@ dependencies {
implementation 'com.google.oboe:oboe:1.7.0' 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.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation "io.coil-kt:coil-compose:2.4.0" implementation "io.coil-kt:coil-compose:2.4.0"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
@ -89,7 +97,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.06.00') androidTestImplementation platform('androidx.compose:compose-bom:2023.06.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4' androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2'
debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest' debugImplementation 'androidx.compose.ui:ui-test-manifest'
} }

View File

@ -7,7 +7,9 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
@ -15,7 +17,6 @@
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
@ -27,7 +28,6 @@
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode" android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:label="@string/app_name"
android:theme="@style/Theme.RyujinxAndroid"> android:theme="@style/Theme.RyujinxAndroid">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -6,40 +6,26 @@ import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.flowWithLifecycle
import com.swordfish.radialgamepad.library.RadialGamePad import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadTheme
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.Event
import com.swordfish.radialgamepad.library.event.GestureType
import com.swordfish.radialgamepad.library.haptics.HapticConfig
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.subscribe
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
typealias GamePad = RadialGamePad typealias GamePad = RadialGamePad
@ -56,7 +42,7 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
return this.isVisible return this.isVisible
} }
return false; return false
} }
init { init {
@ -80,22 +66,22 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
lifecycleScope.apply { lifecycleScope.apply {
lifecycleScope.launch { lifecycleScope.launch {
var events = merge(leftGamePad.events(),rightGamePad.events()) val events = merge(leftGamePad.events(),rightGamePad.events())
.shareIn(lifecycleScope, SharingStarted.Lazily) .shareIn(lifecycleScope, SharingStarted.Lazily)
events.safeCollect { events.safeCollect {
handleEvent(it) handleEvent(it)
}; }
} }
} }
} }
private fun Create(context: Context) : View private fun Create(context: Context) : View
{ {
var inflator = LayoutInflater.from(context); val inflator = LayoutInflater.from(context)
var view = inflator.inflate(R.layout.game_layout, null) val view = inflator.inflate(R.layout.game_layout, null)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad); view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad); view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad)
controllerView = view controllerView = view
@ -120,51 +106,43 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
if(controllerId == -1) if(controllerId == -1)
controllerId = ryujinxNative.inputConnectGamepad(0) controllerId = ryujinxNative.inputConnectGamepad(0)
controllerId?.apply { controllerId.apply {
when (ev) { when (ev) {
is Event.Button -> { is Event.Button -> {
var action = ev.action val action = ev.action
when (action) { when (action) {
KeyEvent.ACTION_UP -> { KeyEvent.ACTION_UP -> {
ryujinxNative.inputSetButtonReleased(ev.id, this) ryujinxNative.inputSetButtonReleased(ev.id, this)
} }
KeyEvent.ACTION_DOWN -> { KeyEvent.ACTION_DOWN -> {
ryujinxNative.inputSetButtonPressed(ev.id, this) ryujinxNative.inputSetButtonPressed(ev.id, this)
} }
} }
} }
is Event.Direction -> { is Event.Direction -> {
var direction = ev.id val direction = ev.id
when(direction) { when(direction) {
GamePadButtonInputId.DpadUp.ordinal -> { GamePadButtonInputId.DpadUp.ordinal -> {
if (ev.xAxis > 0) if (ev.xAxis > 0) {
{
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this)
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
} } else if (ev.xAxis < 0) {
else if (ev.xAxis < 0)
{
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this)
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} } else {
else
{
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} }
if (ev.yAxis < 0) if (ev.yAxis < 0) {
{
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this)
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
} } else if (ev.yAxis > 0) {
else if (ev.yAxis > 0)
{
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this)
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} } else {
else
{
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} }
@ -173,6 +151,7 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
GamePadButtonInputId.LeftStick.ordinal -> { GamePadButtonInputId.LeftStick.ordinal -> {
ryujinxNative.inputSetStickAxis(1, ev.xAxis, -ev.yAxis ,this) ryujinxNative.inputSetStickAxis(1, ev.xAxis, -ev.yAxis ,this)
} }
GamePadButtonInputId.RightStick.ordinal -> { GamePadButtonInputId.RightStick.ordinal -> {
ryujinxNative.inputSetStickAxis(2, ev.xAxis, -ev.yAxis ,this) ryujinxNative.inputSetStickAxis(2, ev.xAxis, -ev.yAxis ,this)
} }
@ -193,7 +172,7 @@ suspend fun <T> Flow<T>.safeCollect(
} }
private fun generateConfig(isLeft: Boolean): GamePadConfig { private fun generateConfig(isLeft: Boolean): GamePadConfig {
var distance = 0.05f val distance = 0.05f
if (isLeft) { if (isLeft) {
return GamePadConfig( return GamePadConfig(

View File

@ -2,14 +2,12 @@ package org.ryujinx.android
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.view.MotionEvent
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.viewmodels.QuickSettings
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.roundToInt
class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
private var _renderingThreadWatcher: Thread? = null private var _renderingThreadWatcher: Thread? = null
@ -37,19 +35,19 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
var isStarted = _isStarted; val isStarted = _isStarted
start(holder) start(holder)
if(isStarted && (_width != width || _height != height)) if(isStarted && (_width != width || _height != height))
{ {
var nativeHelpers = NativeHelpers() val nativeHelpers = NativeHelpers()
var window = nativeHelpers.getNativeWindow(holder.surface); val window = nativeHelpers.getNativeWindow(holder.surface)
_nativeRyujinx.graphicsSetSurface(window); _nativeRyujinx.graphicsSetSurface(window)
} }
_width = width; _width = width
_height = height; _height = height
if(_isStarted) if(_isStarted)
{ {
@ -61,15 +59,15 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
} }
private fun start(surfaceHolder: SurfaceHolder) : Unit { private fun start(surfaceHolder: SurfaceHolder) {
var game = gameModel ?: return val game = gameModel ?: return
var path = game.getPath() ?: return val path = game.getPath() ?: return
if (_isStarted) if (_isStarted)
return return
var surface = surfaceHolder.surface; var surface = surfaceHolder.surface
var settings = QuickSettings(mainViewModel.activity) val settings = QuickSettings(mainViewModel.activity)
var success = _nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply { var success = _nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply {
EnableShaderCache = settings.enableShaderCache EnableShaderCache = settings.enableShaderCache
@ -78,14 +76,14 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
}) })
var nativeHelpers = NativeHelpers() val nativeHelpers = NativeHelpers()
var window = nativeHelpers.getNativeWindow(surfaceHolder.surface); val window = nativeHelpers.getNativeWindow(surfaceHolder.surface)
nativeInterop = NativeGraphicsInterop() nativeInterop = NativeGraphicsInterop()
nativeInterop!!.VkRequiredExtensions = arrayOf( nativeInterop!!.VkRequiredExtensions = arrayOf(
"VK_KHR_surface", "VK_KHR_android_surface" "VK_KHR_surface", "VK_KHR_android_surface"
); )
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
nativeInterop!!.SurfaceHandle = window; nativeInterop!!.SurfaceHandle = window
success = _nativeRyujinx.graphicsInitializeRenderer( success = _nativeRyujinx.graphicsInitializeRenderer(
nativeInterop!!.VkRequiredExtensions!!, nativeInterop!!.VkRequiredExtensions!!,
@ -104,7 +102,7 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
false, false,
"UTC", "UTC",
settings.ignoreMissingServices settings.ignoreMissingServices
); )
success = _nativeRyujinx.deviceLoad(path) success = _nativeRyujinx.deviceLoad(path)
@ -124,12 +122,12 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
_nativeRyujinx.graphicsRendererSetSize( _nativeRyujinx.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.width(),
surfaceHolder.surfaceFrame.height() surfaceHolder.surfaceFrame.height()
); )
_guestThread = thread(start = true) { _guestThread = thread(start = true) {
runGame() runGame()
} }
_isStarted = success; _isStarted = success
_updateThread = thread(start = true) { _updateThread = thread(start = true) {
var c = 0 var c = 0
@ -145,20 +143,20 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
} }
} }
private fun runGame() : Unit{ private fun runGame() {
// RenderingThreadWatcher // RenderingThreadWatcher
_renderingThreadWatcher = thread(start = true) { _renderingThreadWatcher = thread(start = true) {
var threadId = 0L; var threadId = 0L
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mainViewModel.performanceManager?.enable() mainViewModel.performanceManager?.enable()
while (_isStarted) { while (_isStarted) {
Thread.sleep(1000) Thread.sleep(1000)
var newthreadId = mainViewModel.activity.getRenderingThreadId() val newthreadId = mainViewModel.activity.getRenderingThreadId()
if (threadId != newthreadId) { if (threadId != newthreadId) {
mainViewModel.performanceManager?.closeCurrentRenderingSession() mainViewModel.performanceManager?.closeCurrentRenderingSession()
} }
threadId = newthreadId; threadId = newthreadId
if (threadId != 0L) { if (threadId != 0L) {
mainViewModel.performanceManager?.initializeRenderingSession(threadId) mainViewModel.performanceManager?.initializeRenderingSession(threadId)
} }

View File

@ -34,13 +34,17 @@ class Helpers {
val split = docId.split(":".toRegex()).toTypedArray() val split = docId.split(":".toRegex()).toTypedArray()
val type = split[0] val type = split[0]
var contentUri: Uri? = null var contentUri: Uri? = null
if ("image" == type) { when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} else if ("video" == type) { }
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
} else if ("audio" == type) { }
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
} }
}
val selection = "_id=?" val selection = "_id=?"
val selectionArgs = arrayOf(split[1]) val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs) return getDataColumn(context, contentUri, selection, selectionArgs)
@ -58,13 +62,13 @@ class Helpers {
val column = "_data" val column = "_data"
val projection = arrayOf(column) val projection = arrayOf(column)
try { try {
cursor = uri?.let { context.getContentResolver().query(it, projection, selection, selectionArgs,null) } cursor = uri?.let { context.contentResolver.query(it, projection, selection, selectionArgs,null) }
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
val column_index: Int = cursor.getColumnIndexOrThrow(column) val column_index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index) return cursor.getString(column_index)
} }
} finally { } finally {
if (cursor != null) cursor.close() cursor?.close()
} }
return null return null
} }

View File

@ -2,7 +2,6 @@ package org.ryujinx.android
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.media.AudioDeviceInfo import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
@ -25,23 +24,19 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat 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.HomeViewModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.views.HomeViews import org.ryujinx.android.views.HomeViews
import org.ryujinx.android.views.MainView import org.ryujinx.android.views.MainView
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
var physicalControllerManager: PhysicalControllerManager var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
private var _isInit: Boolean = false private var _isInit: Boolean = false
var storageHelper: SimpleStorageHelper? = null var storageHelper: SimpleStorageHelper? = null
companion object { companion object {
var mainViewModel: MainViewModel? = null var mainViewModel: MainViewModel? = null
var AppPath : String? var AppPath : String = ""
var StorageHelper: SimpleStorageHelper? = null var StorageHelper: SimpleStorageHelper? = null
init {
AppPath = ""
}
@JvmStatic @JvmStatic
fun updateRenderSessionPerformance(gameTime : Long) fun updateRenderSessionPerformance(gameTime : Long)
@ -56,7 +51,6 @@ class MainActivity : ComponentActivity() {
} }
init { init {
physicalControllerManager = PhysicalControllerManager(this)
storageHelper = SimpleStorageHelper(this) storageHelper = SimpleStorageHelper(this)
StorageHelper = storageHelper StorageHelper = storageHelper
System.loadLibrary("ryujinxjni") System.loadLibrary("ryujinxjni")
@ -66,29 +60,28 @@ class MainActivity : ComponentActivity() {
external fun getRenderingThreadId() : Long external fun getRenderingThreadId() : Long
external fun initVm() external fun initVm()
fun setFullScreen() :Unit { fun setFullScreen() {
requestedOrientation = requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
var insets = WindowCompat.getInsetsController(window, window.decorView) val insets = WindowCompat.getInsetsController(window, window.decorView)
insets?.apply { insets.apply {
insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE insets.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} }
} }
private fun getAudioDevice () : Int { private fun getAudioDevice () : Int {
var audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
var devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
return if (devices.isEmpty()) return if (devices.isEmpty())
0 0
else { else {
var speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
var earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET } val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
if(earPiece != null) if(earPiece != null)
return earPiece.id return earPiece.id
if(speaker != null) if(speaker != null)
@ -112,17 +105,23 @@ class MainActivity : ComponentActivity() {
return super.dispatchGenericMotionEvent(ev) return super.dispatchGenericMotionEvent(ev)
} }
private fun initialize() : Unit private fun initialize() {
{
if (_isInit) if (_isInit)
return return
var appPath: String = AppPath ?: return val appPath: String = AppPath
var success = RyujinxNative().initialize(appPath, false) val success = RyujinxNative().initialize(appPath, false)
_isInit = success _isInit = success
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if(
!Environment.isExternalStorageManager()
) {
storageHelper?.storage?.requestFullStorageAccess()
}
AppPath = this.getExternalFilesDir(null)!!.absolutePath AppPath = this.getExternalFilesDir(null)!!.absolutePath
initialize() initialize()
@ -130,14 +129,6 @@ class MainActivity : ComponentActivity() {
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)
if(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager()
} else {
false
}
) {
storageHelper?.storage?.requestFullStorageAccess()
}
mainViewModel = MainViewModel(this) mainViewModel = MainViewModel(this)
mainViewModel?.apply { mainViewModel?.apply {

View File

@ -14,7 +14,7 @@ class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
if(!_isEnabled || renderingSession != null) if(!_isEnabled || renderingSession != null)
return return
var threads = IntArray(1) val threads = IntArray(1)
threads[0] = threadId.toInt() threads[0] = threadId.toInt()
renderingSession = performanceHintManager.createHintSession(threads, DEFAULT_TARGET_NS) renderingSession = performanceHintManager.createHintSession(threads, DEFAULT_TARGET_NS)
} }

View File

@ -9,7 +9,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
fun onKeyEvent(event: KeyEvent) : Boolean{ fun onKeyEvent(event: KeyEvent) : Boolean{
if(controllerId != -1) { if(controllerId != -1) {
var id = GetGamePadButtonInputId(event.keyCode) val id = GetGamePadButtonInputId(event.keyCode)
if(id != GamePadButtonInputId.None) { if(id != GamePadButtonInputId.None) {
when (event.action) { when (event.action) {
@ -21,7 +21,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId) ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId)
} }
} }
return true; return true
} }
} }
@ -31,10 +31,10 @@ class PhysicalControllerManager(val activity: MainActivity) {
fun onMotionEvent(ev: MotionEvent) { fun onMotionEvent(ev: MotionEvent) {
if(controllerId != -1) { if(controllerId != -1) {
if(ev.action == MotionEvent.ACTION_MOVE) { if(ev.action == MotionEvent.ACTION_MOVE) {
var leftStickX = ev.getAxisValue(MotionEvent.AXIS_X); val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
var leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y); val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
var rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z); val rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z)
var rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ); val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId) ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId) ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
} }

View File

@ -1,9 +1,8 @@
package org.ryujinx.android package org.ryujinx.android
import android.view.Surface
import org.ryujinx.android.viewmodels.GameInfo import org.ryujinx.android.viewmodels.GameInfo
import java.io.FileDescriptor
@Suppress("KotlinJniMissingFunction")
class RyujinxNative { class RyujinxNative {
external fun initialize(appPath: String, enableDebugLogs : Boolean): Boolean external fun initialize(appPath: String, enableDebugLogs : Boolean): Boolean

View File

@ -1,10 +1,7 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import android.R.string
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers import org.ryujinx.android.Helpers
@ -22,8 +19,8 @@ class GameModel(var file: DocumentFile, val context: Context) {
init { init {
fileName = file.name fileName = file.name
var absPath = getPath() val absPath = getPath()
var gameInfo = RyujinxNative().deviceGetGameInfoFromPath(absPath ?: "") val gameInfo = RyujinxNative().deviceGetGameInfoFromPath(absPath ?: "")
fileSize = gameInfo.FileSize fileSize = gameInfo.FileSize
titleId = gameInfo.TitleId titleId = gameInfo.TitleId
@ -37,7 +34,7 @@ class GameModel(var file: DocumentFile, val context: Context) {
var uri = file.uri var uri = file.uri
if (uri.scheme != "file") if (uri.scheme != "file")
uri = Uri.parse("file://" + Helpers.getPath(context, file.uri)) uri = Uri.parse("file://" + Helpers.getPath(context, file.uri))
return uri.path; return uri.path
} }
fun getIsXci() : Boolean { fun getIsXci() : Boolean {

View File

@ -8,7 +8,6 @@ import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileType import com.anggrayudi.storage.file.DocumentFileType
import com.anggrayudi.storage.file.FileFullPath import com.anggrayudi.storage.file.FileFullPath
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.fullName
import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.search import com.anggrayudi.storage.file.search
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
@ -20,7 +19,7 @@ class HomeViewModel(
private var gameList: SnapshotStateList<GameModel>? = null 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
init { init {
if (activity != null) { if (activity != null) {
@ -28,15 +27,15 @@ class HomeViewModel(
activity.storageHelper!!.onFolderSelected = { requestCode, folder -> activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
run { run {
gameFolderPath = folder gameFolderPath = folder
var p = folder.getAbsolutePath(activity!!) val p = folder.getAbsolutePath(activity!!)
var editor = sharedPref?.edit() val editor = sharedPref?.edit()
editor?.putString("gameFolder", p); editor?.putString("gameFolder", p)
editor?.apply() editor?.apply()
reloadGameList() reloadGameList()
} }
} }
var savedFolder = sharedPref?.getString("gameFolder", "") ?: "" val savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
if (savedFolder.isNotEmpty()) { if (savedFolder.isNotEmpty()) {
try { try {
@ -56,26 +55,26 @@ class HomeViewModel(
} }
fun openGameFolder() { fun openGameFolder() {
var path = sharedPref?.getString("gameFolder", "") ?: "" val path = sharedPref?.getString("gameFolder", "") ?: ""
if (path.isNullOrEmpty()) if (path.isEmpty())
activity?.storageHelper?.storage?.openFolderPicker(); activity?.storageHelper?.storage?.openFolderPicker()
else else
activity?.storageHelper?.storage?.openFolderPicker( activity?.storageHelper?.storage?.openFolderPicker(
activity!!.storageHelper!!.storage.requestCodeFolderPicker, activity.storageHelper!!.storage.requestCodeFolderPicker,
FileFullPath(activity, path) FileFullPath(activity, path)
) )
} }
fun reloadGameList() { fun reloadGameList() {
var storage = activity?.storageHelper ?: return var storage = activity?.storageHelper ?: return
var folder = gameFolderPath ?: return val folder = gameFolderPath ?: return
var files = mutableListOf<GameModel>() val files = mutableListOf<GameModel>()
for (file in folder.search(false, DocumentFileType.FILE)) { for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp") if (file.extension == "xci" || file.extension == "nsp")
activity?.let { activity.let {
files.add(GameModel(file, it)) files.add(GameModel(file, it))
} }
} }
@ -91,7 +90,7 @@ class HomeViewModel(
} }
fun setViewList(list: SnapshotStateList<GameModel>) { fun setViewList(list: SnapshotStateList<GameModel>) {
gameList = list; gameList = list
applyFilter() applyFilter()
} }
} }

View File

@ -23,14 +23,14 @@ class MainViewModel(val activity: MainActivity) {
init { init {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
var hintService = val hintService =
activity.getSystemService(Context.PERFORMANCE_HINT_SERVICE) as PerformanceHintManager activity.getSystemService(Context.PERFORMANCE_HINT_SERVICE) as PerformanceHintManager
performanceManager = PerformanceManager(hintService) performanceManager = PerformanceManager(hintService)
} }
} }
fun loadGame(game:GameModel) { fun loadGame(game:GameModel) {
var controller = navController?: return; val controller = navController?: return
activity.setFullScreen() activity.setFullScreen()
GameHost.gameModel = game GameHost.gameModel = game
controller.navigate("game") controller.navigate("game")

View File

@ -55,18 +55,18 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
resScale: MutableState<Float>, resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean> useVirtualController: MutableState<Boolean>
){ ){
var editor = sharedPref.edit() val editor = sharedPref.edit()
editor.putBoolean("isHostMapped", isHostMapped?.value ?: true) editor.putBoolean("isHostMapped", isHostMapped.value)
editor.putBoolean("useNce", useNce?.value ?: true) editor.putBoolean("useNce", useNce.value)
editor.putBoolean("enableVsync", enableVsync?.value ?: true) editor.putBoolean("enableVsync", enableVsync.value)
editor.putBoolean("enableDocked", enableDocked?.value ?: true) editor.putBoolean("enableDocked", enableDocked.value)
editor.putBoolean("enablePtc", enablePtc?.value ?: true) editor.putBoolean("enablePtc", enablePtc.value)
editor.putBoolean("ignoreMissingServices", ignoreMissingServices?.value ?: false) editor.putBoolean("ignoreMissingServices", ignoreMissingServices.value)
editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true) editor.putBoolean("enableShaderCache", enableShaderCache.value)
editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false) editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
editor.putFloat("resScale", resScale?.value ?: 1f) editor.putFloat("resScale", resScale.value)
editor.putBoolean("useVirtualController", useVirtualController?.value ?: true) editor.putBoolean("useVirtualController", useVirtualController.value)
editor.apply() editor.apply()
} }

View File

@ -1,6 +1,5 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import androidx.appcompat.widget.ThemedSpinnerAdapter.Helper
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
@ -24,23 +23,23 @@ class TitleUpdateViewModel(val titleId: String) {
return return
data?.paths?.apply { data?.paths?.apply {
removeAt(index - 1); removeAt(index - 1)
pathsState?.clear() pathsState?.clear()
pathsState?.addAll(this) pathsState?.addAll(this)
} }
} }
fun Add() { fun Add() {
var callBack = storageHelper.onFileSelected val callBack = storageHelper.onFileSelected
storageHelper.onFileSelected = { requestCode, files -> storageHelper.onFileSelected = { requestCode, files ->
run { run {
storageHelper.onFileSelected = callBack storageHelper.onFileSelected = callBack
if(requestCode == UpdateRequestCode) if(requestCode == UpdateRequestCode)
{ {
var file = files.firstOrNull() val file = files.firstOrNull()
file?.apply { file?.apply {
var path = Helpers.getPath(storageHelper.storage.context, file.uri) val path = Helpers.getPath(storageHelper.storage.context, file.uri)
if(!path.isNullOrEmpty()){ if(!path.isNullOrEmpty()){
data?.apply { data?.apply {
if(!paths.contains(path)) { if(!paths.contains(path)) {
@ -62,20 +61,19 @@ class TitleUpdateViewModel(val titleId: String) {
this.selected = "" this.selected = ""
if(paths.isNotEmpty() && index > 0) if(paths.isNotEmpty() && index > 0)
{ {
var ind = max(index - 1, paths.count() - 1) val ind = max(index - 1, paths.count() - 1)
this.selected = paths[ind] this.selected = paths[ind]
} }
var gson = Gson() val gson = Gson()
var json = gson.toJson(this) val json = gson.toJson(this)
jsonPath = (MainActivity.AppPath jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
?: "") + "/games/" + titleId.toLowerCase(Locale.current)
File(jsonPath).mkdirs() File(jsonPath).mkdirs()
File(jsonPath + "/updates.json").writeText(json) File("$jsonPath/updates.json").writeText(json)
} }
} }
fun setPaths(paths: SnapshotStateList<String>) { fun setPaths(paths: SnapshotStateList<String>) {
pathsState = paths; pathsState = paths
data?.apply { data?.apply {
pathsState?.clear() pathsState?.clear()
pathsState?.addAll(this.paths) pathsState?.addAll(this.paths)
@ -86,16 +84,15 @@ class TitleUpdateViewModel(val titleId: String) {
private var jsonPath: String private var jsonPath: String
init { init {
jsonPath = (MainActivity.AppPath jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json"
?: "") + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json"
data = TitleUpdateMetadata() data = TitleUpdateMetadata()
if (File(jsonPath).exists()) { if (File(jsonPath).exists()) {
var gson = Gson() val gson = Gson()
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
data?.apply { data?.apply {
var existingPaths = mutableListOf<String>() val existingPaths = mutableListOf<String>()
for (path in paths) { for (path in paths) {
if (File(path).exists()) { if (File(path).exists()) {
existingPaths.add(path) existingPaths.add(path)

View File

@ -12,22 +12,22 @@ class VulkanDriverViewModel(val activity: MainActivity) {
var selected: String = "" var selected: String = ""
companion object { companion object {
val DriverRequestCode: Int = 1003 const val DriverRequestCode: Int = 1003
const val DriverFolder: String = "drivers" const val DriverFolder: String = "drivers"
} }
private fun getAppPath() : String { private fun getAppPath() : String {
var appPath = var appPath =
(MainActivity.AppPath ?: activity.getExternalFilesDir(null)?.absolutePath ?: ""); MainActivity.AppPath
appPath += "/" appPath += "/"
return appPath return appPath
} }
fun ensureDriverPath() : File { fun ensureDriverPath() : File {
var driverPath = getAppPath() + DriverFolder val driverPath = getAppPath() + DriverFolder
var driverFolder = File(driverPath) val driverFolder = File(driverPath)
if(!driverFolder.exists()) if(!driverFolder.exists())
driverFolder.mkdirs() driverFolder.mkdirs()
@ -36,13 +36,13 @@ class VulkanDriverViewModel(val activity: MainActivity) {
} }
fun getAvailableDrivers() : MutableList<DriverMetadata> { fun getAvailableDrivers() : MutableList<DriverMetadata> {
var driverFolder = ensureDriverPath() val driverFolder = ensureDriverPath()
var folders = driverFolder.walkTopDown() val folders = driverFolder.walkTopDown()
var drivers = mutableListOf<DriverMetadata>() val drivers = mutableListOf<DriverMetadata>()
var selectedDriverFile = File(driverFolder.absolutePath + "/selected"); val selectedDriverFile = File(driverFolder.absolutePath + "/selected")
if(selectedDriverFile.exists()){ if(selectedDriverFile.exists()){
selected = selectedDriverFile.readText() selected = selectedDriverFile.readText()
@ -52,16 +52,16 @@ class VulkanDriverViewModel(val activity: MainActivity) {
} }
} }
var gson = Gson() val gson = Gson()
for (folder in folders){ for (folder in folders){
if(folder.isDirectory() && folder.parent == driverFolder.absolutePath){ if(folder.isDirectory() && folder.parent == driverFolder.absolutePath){
var meta = File(folder.absolutePath + "/meta.json") val meta = File(folder.absolutePath + "/meta.json")
if(meta.exists()){ if(meta.exists()){
var metadata = gson.fromJson(meta.readText(), DriverMetadata::class.java) val metadata = gson.fromJson(meta.readText(), DriverMetadata::class.java)
if(metadata.name.isNotEmpty()) { if(metadata.name.isNotEmpty()) {
var driver = folder.absolutePath + "/${metadata.libraryName}" val driver = folder.absolutePath + "/${metadata.libraryName}"
metadata.driverPath = driver metadata.driverPath = driver
if (File(driver).exists()) if (File(driver).exists())
drivers.add(metadata) drivers.add(metadata)
@ -74,18 +74,17 @@ class VulkanDriverViewModel(val activity: MainActivity) {
} }
fun saveSelected() { fun saveSelected() {
var driverFolder = ensureDriverPath() val driverFolder = ensureDriverPath()
var selectedDriverFile = File(driverFolder.absolutePath + "/selected") val selectedDriverFile = File(driverFolder.absolutePath + "/selected")
selectedDriverFile.writeText(selected) selectedDriverFile.writeText(selected)
} }
fun removeSelected(){ fun removeSelected(){
if(selected.isNotEmpty()){ if(selected.isNotEmpty()){
var sel = File(selected) val sel = File(selected)
if(sel.exists()) { if(sel.exists()) {
var parent = sel.parentFile sel.parentFile?.deleteRecursively()
parent.deleteRecursively()
} }
selected = "" selected = ""
@ -96,20 +95,20 @@ class VulkanDriverViewModel(val activity: MainActivity) {
fun add(refresh: MutableState<Boolean>) { fun add(refresh: MutableState<Boolean>) {
activity.storageHelper?.apply { activity.storageHelper?.apply {
var callBack = this.onFileSelected val callBack = this.onFileSelected
onFileSelected = { requestCode, files -> onFileSelected = { requestCode, files ->
run { run {
onFileSelected = callBack onFileSelected = callBack
if(requestCode == DriverRequestCode) if(requestCode == DriverRequestCode)
{ {
var file = files.firstOrNull() val file = files.firstOrNull()
file?.apply { file?.apply {
var path = Helpers.getPath(storage.context, file.uri) val path = Helpers.getPath(storage.context, file.uri)
if(!path.isNullOrEmpty()){ if(!path.isNullOrEmpty()){
var name = file.name?.removeSuffix("." + file.extension) ?: "" val name = file.name?.removeSuffix("." + file.extension) ?: ""
var driverFolder = ensureDriverPath() val driverFolder = ensureDriverPath()
var extractionFolder = File(driverFolder.absolutePath + "/${name}") val extractionFolder = File(driverFolder.absolutePath + "/${name}")
extractionFolder.mkdirs() extractionFolder.mkdirs()
ZipFile(path)?.use { zip -> ZipFile(path)?.use { zip ->
zip.entries().asSequence().forEach { entry -> zip.entries().asSequence().forEach { entry ->
@ -118,7 +117,7 @@ class VulkanDriverViewModel(val activity: MainActivity) {
val filePath = extractionFolder.absolutePath + File.separator + entry.name val filePath = extractionFolder.absolutePath + File.separator + entry.name
if (!entry.isDirectory) { if (!entry.isDirectory) {
var length = input.available() val length = input.available()
val bytesIn = ByteArray(length) val bytesIn = ByteArray(length)
input.read(bytesIn) input.read(bytesIn)
File(filePath).writeBytes(bytesIn) File(filePath).writeBytes(bytesIn)

View File

@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
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.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
@ -56,12 +54,9 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.Popup
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
@ -78,11 +73,11 @@ class HomeViews {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainTopBar(navController: NavHostController) { fun MainTopBar(navController: NavHostController) {
var topBarSize = remember { val topBarSize = remember {
mutableStateOf(0) mutableStateOf(0)
} }
Column { Column {
var showOptionsPopup = remember { val showOptionsPopup = remember {
mutableStateOf(false) mutableStateOf(false)
} }
TopAppBar( TopAppBar(
@ -116,7 +111,7 @@ class HomeViews {
actions = { actions = {
IconButton( IconButton(
onClick = { onClick = {
showOptionsPopup.value = true; showOptionsPopup.value = true
} }
) { ) {
Icon( Icon(
@ -171,7 +166,7 @@ class HomeViews {
fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) { fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showBottomSheet = remember { mutableStateOf(false) } val showBottomSheet = remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -195,7 +190,7 @@ class HomeViews {
) { contentPadding -> ) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) { Box(modifier = Modifier.padding(contentPadding)) {
var list = remember { val list = remember {
mutableStateListOf<GameModel>() mutableStateListOf<GameModel>()
} }
viewModel.setViewList(list) viewModel.setViewList(list)
@ -227,8 +222,8 @@ class HomeViews {
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation tonalElevation = AlertDialogDefaults.TonalElevation
) { ) {
var titleId = viewModel.mainViewModel?.selected?.titleId ?: "" val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
var name = viewModel.mainViewModel?.selected?.titleName ?: "" val name = viewModel.mainViewModel?.selected?.titleName ?: ""
TitleUpdateViews.Main(titleId, name, openDialog) TitleUpdateViews.Main(titleId, name, openDialog)
} }
@ -267,7 +262,7 @@ class HomeViews {
} }
} }
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState<Boolean>) { fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState<Boolean>) {
Card(shape = MaterialTheme.shapes.medium, Card(shape = MaterialTheme.shapes.medium,
@ -291,8 +286,8 @@ class HomeViews {
Row { Row {
if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000")
{ {
var iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
var imageFile = File(iconSource) val imageFile = File(iconSource)
if(imageFile.exists()) { if(imageFile.exists()) {
val size = ImageSize / Resources.getSystem().displayMetrics.density val size = ImageSize / Resources.getSystem().displayMetrics.density
AsyncImage(model = imageFile, AsyncImage(model = imageFile,
@ -320,7 +315,7 @@ class HomeViews {
@Composable @Composable
fun NotAvailableIcon() { fun NotAvailableIcon() {
var size = ImageSize / Resources.getSystem().displayMetrics.density val size = ImageSize / Resources.getSystem().displayMetrics.density
Icon( Icon(
Icons.Filled.Add, Icons.Filled.Add,
contentDescription = "Options", contentDescription = "Options",

View File

@ -1,6 +1,5 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -73,7 +72,7 @@ class MainView {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
GameStats(mainViewModel) GameStats(mainViewModel)
var ryujinxNative = RyujinxNative() val ryujinxNative = RyujinxNative()
// touch surface // touch surface
Surface(color = Color.Transparent, modifier = Modifier Surface(color = Color.Transparent, modifier = Modifier
@ -82,27 +81,30 @@ class MainView {
.pointerInput(Unit) { .pointerInput(Unit) {
awaitPointerEventScope { awaitPointerEventScope {
while (true) { while (true) {
Thread.sleep(2); Thread.sleep(2)
val event = awaitPointerEvent() val event = awaitPointerEvent()
if(controller.isVisible) if(controller.isVisible)
continue continue
var change = event val change = event
.component1() .component1()
.firstOrNull() .firstOrNull()
change?.apply { change?.apply {
var position = this.position val position = this.position
if (event.type == PointerEventType.Press) { when (event.type) {
PointerEventType.Press -> {
ryujinxNative.inputSetTouchPoint( ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(), position.x.roundToInt(),
position.y.roundToInt() position.y.roundToInt()
) )
} else if (event.type == PointerEventType.Release) { }
PointerEventType.Release -> {
ryujinxNative.inputReleaseTouchPoint() ryujinxNative.inputReleaseTouchPoint()
} else if (event.type == PointerEventType.Move) { }
PointerEventType.Move -> {
ryujinxNative.inputSetTouchPoint( ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(), position.x.roundToInt(),
position.y.roundToInt() position.y.roundToInt()
@ -112,6 +114,7 @@ class MainView {
} }
} }
} }
}
}) { }) {
} }
controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle) controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle)
@ -128,7 +131,7 @@ class MainView {
} }
@Composable @Composable
fun rememberVideogameAsset(): ImageVector { fun rememberVideogameAsset(): ImageVector {
var primaryColor = MaterialTheme.colorScheme.primary val primaryColor = MaterialTheme.colorScheme.primary
return remember { return remember {
ImageVector.Builder( ImageVector.Builder(
name = "videogame_asset", name = "videogame_asset",
@ -224,20 +227,20 @@ class MainView {
@Composable @Composable
fun GameStats(mainViewModel: MainViewModel){ fun GameStats(mainViewModel: MainViewModel){
var fifo = remember { val fifo = remember {
mutableStateOf(0.0) mutableStateOf(0.0)
} }
var gameFps = remember { val gameFps = remember {
mutableStateOf(0.0) mutableStateOf(0.0)
} }
var gameTime = remember { val gameTime = remember {
mutableStateOf(0.0) mutableStateOf(0.0)
} }
Surface(modifier = Modifier.padding(16.dp), Surface(modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.surface.copy(0.4f)) { color = MaterialTheme.colorScheme.surface.copy(0.4f)) {
Column { Column {
var gameTimeVal = 0.0; var gameTimeVal = 0.0
if (!gameTime.value.isInfinite()) if (!gameTime.value.isInfinite())
gameTimeVal = gameTime.value gameTimeVal = gameTime.value
Text(text = "${String.format("%.3f", fifo.value)} %") Text(text = "${String.format("%.3f", fifo.value)} %")

View File

@ -13,36 +13,24 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -54,7 +42,6 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
class SettingViews { class SettingViews {
companion object { companion object {
@ -63,38 +50,38 @@ class SettingViews {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Main(settingsViewModel: SettingsViewModel) { fun Main(settingsViewModel: SettingsViewModel) {
var loaded = remember { val loaded = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var isHostMapped = remember { val isHostMapped = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var useNce = remember { val useNce = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var enableVsync = remember { val enableVsync = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var enableDocked = remember { val enableDocked = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var enablePtc = remember { val enablePtc = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var ignoreMissingServices = remember { val ignoreMissingServices = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var enableShaderCache = remember { val enableShaderCache = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var enableTextureRecompression = remember { val enableTextureRecompression = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var resScale = remember { val resScale = remember {
mutableStateOf(1f) mutableStateOf(1f)
} }
var useVirtualController = remember { val useVirtualController = remember {
mutableStateOf(true) mutableStateOf(true)
} }
@ -470,8 +457,8 @@ class SettingViews {
title: String, title: String,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
var expanded = false val expanded = false
var mutableExpanded = remember { val mutableExpanded = remember {
mutableStateOf(expanded) mutableStateOf(expanded)
} }
val transitionState = remember { val transitionState = remember {

View File

@ -3,11 +3,9 @@ package org.ryujinx.android.views
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.layout.wrapContentHeight
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
@ -35,7 +33,7 @@ class TitleUpdateViews {
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) { fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
val viewModel = TitleUpdateViewModel(titleId) val viewModel = TitleUpdateViewModel(titleId)
var selected = remember { mutableStateOf(0) } val selected = remember { mutableStateOf(0) }
viewModel.data?.apply { viewModel.data?.apply {
selected.value = paths.indexOf(this.selected) + 1 selected.value = paths.indexOf(this.selected) + 1
} }
@ -67,14 +65,14 @@ class TitleUpdateViews {
) )
} }
var paths = remember { val paths = remember {
mutableStateListOf<String>() mutableStateListOf<String>()
} }
viewModel.setPaths(paths) viewModel.setPaths(paths)
var index = 1 var index = 1
for (path in paths) { for (path in paths) {
var i = index val i = index
Row(modifier = Modifier.padding(8.dp)) { Row(modifier = Modifier.padding(8.dp)) {
RadioButton( RadioButton(
selected = (selected.value == i), selected = (selected.value == i),

View File

@ -0,0 +1 @@

View File

@ -21,3 +21,15 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Build configuration
# It needs to be set to either "debug" or "release" and can also be overriden on a per build basis
# by adding -Dorg.ryujinx.config=NAME to the command line.
org.ryujinx.config=debug
# Controls stripping of symbols from libryujinx
# Setting this property to auto causes symbols to be stripped for release builds,
# but not for debug builds.
# Valid values are: ["auto", "-1", "true", "1", "false", "0"]
# Default: auto
org.ryujinx.symbols.strip=auto
# Output path of libryujinx.so
org.ryujinx.publish.path=app/src/main/jniLibs/arm64-v8a

View File

@ -0,0 +1,139 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
plugins {
id 'base'
}
// Configurable properties
// Path to the LLVM toolchain to use. This should be configured in your global gradle.properties
// See: https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home
def toolchainPath = providers.gradleProperty("org.ryujinx.llvm.toolchain.path").getOrNull()
// Path to the dotnet executable This should be configured in your global gradle.properties
// See: https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home
def dotnetExecutable = providers.gradleProperty("org.ryujinx.dotnet.bin").getOrElse("dotnet")
// Build configuration
def configuration = providers.gradleProperty("org.ryujinx.config").getOrElse("debug").toLowerCase()
// Publish directory
def publishDirectory = providers.gradleProperty("org.ryujinx.publish.path").getOrNull()
// Should the symbols be stripped from the published library?
// Per default the symbols will be stripped for release builds, but not for debug builds.
// This can be overridden using this property.
// Valid values are: ["auto", "-1", "true", "1", "false", "0"]
def stripSymbols = providers.gradleProperty("org.ryujinx.symbols.strip").getOrElse("")
//noinspection GroovyFallthrough
switch (stripSymbols) {
case "true":
case "1":
stripSymbols = true
break
case "false":
case "0":
stripSymbols = false
break
default:
stripSymbols = configuration == "release"
break
}
// Additional arguments for the dotnet publish command.
def additionalArgs = project.hasProperty("org.ryujinx.args") ? project.property("org.ryujinx.args") : ""
configuration = configuration.substring(0, 1).toUpperCase() + configuration.substring(1)
if (publishDirectory != null) {
publishDirectory = "${rootProject.projectDir}/${publishDirectory}"
}
else {
publishDirectory = libsDirectory.get().toString()
}
// Trees
ext.outputTree = fileTree("${buildDir}/publish") {
include '**/*'
builtBy 'compileLibRyujinx'
}
ext.publishTree = fileTree(publishDirectory) {
include ext.outputTree.getFiles().collect { it.getName().toLowerCase() }.findAll { it.endsWith(".so") }
builtBy 'compileLibRyujinx'
}
// Tasks
tasks.register('compileLibRyujinx', Exec) {
def projectName = "LibRyujinx"
workingDir "../../${projectName}"
def solutionFiles = fileTree("../../") {
include '**/*.cs'
include '**/*.csproj'
exclude '**/bin/**'
exclude '**/obj/**'
exclude '**/RyujinxAndroid/**'
}
inputs.files(solutionFiles)
.withPropertyName('sourceFiles')
.withPathSensitivity(PathSensitivity.RELATIVE)
.ignoreEmptyDirectories()
outputs.file("${publishDirectory}/${projectName.toLowerCase()}.so")
OperatingSystem os = DefaultNativePlatform.currentOperatingSystem
if (toolchainPath != null) {
if (os.isWindows()) {
// NOTE: This is not a typo. dotnet.exe actually uses Path instead of PATH.
environment "Path", "${toolchainPath};${providers.environmentVariable("PATH").get()}"
}
else {
environment "PATH", "${toolchainPath}:${providers.environmentVariable("PATH").get()}"
}
}
doFirst {
println "Building ${projectName} in ${configuration} mode."
println "Configuration:"
println "\tusing: ${dotnetExecutable}"
println "\tStripSymbols: ${stripSymbols}"
println "\tadditional args: ${additionalArgs.split(" ")}"
println "\tcustom LLVM toolchain path: ${toolchainPath}"
}
executable dotnetExecutable
args 'publish',
'-r', 'linux-bionic-arm64',
'-c', configuration,
"-p:DisableUnsupportedError=true",
"-p:PublishAotUsingRuntimePack=true",
"-p:StripSymbols=${stripSymbols}",
"--artifacts-path", buildDir
args additionalArgs.split(" ")
doLast {
project.sync {
from project.ext.outputTree.getFiles()
include '*.so'
into publishDirectory
rename (String originalName) -> originalName.toLowerCase()
duplicatesStrategy 'fail'
preserve {
include '.gitkeep'
include '*.so'
exclude {
project.ext.publishTree
}
}
}
}
}
tasks.register("cleanLibRyujinx", Delete) {
delete project.ext.publishTree.getFiles()
}
// Register tasks as standard lifecycle tasks
assemble.dependsOn("compileLibRyujinx")
clean.dependsOn("cleanLibRyujinx")

View File

@ -16,3 +16,4 @@ dependencyResolutionManagement {
} }
rootProject.name = "RyujinxAndroid" rootProject.name = "RyujinxAndroid"
include ':app' include ':app'
include ':libryujinx'