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:
parent
fcb511bbca
commit
8b7beb6f22
@ -1,10 +1,7 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Logging.Formatters;
|
||||
using Ryujinx.Common.Logging.Targets;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibRyujinx
|
||||
{
|
||||
@ -26,7 +23,7 @@ namespace LibRyujinx
|
||||
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
|
||||
{
|
||||
@ -39,13 +36,14 @@ namespace LibRyujinx
|
||||
LogLevel.AccessLog => Logcat.LogLevel.Info,
|
||||
LogLevel.Notice => Logcat.LogLevel.Info,
|
||||
LogLevel.Trace => Logcat.LogLevel.Verbose,
|
||||
_ => throw new NotImplementedException()
|
||||
_ => throw new NotImplementedException(),
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,28 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using System.Collections.Generic;
|
||||
using LibRyujinx.Jni;
|
||||
using LibRyujinx.Jni.Pointers;
|
||||
using LibRyujinx.Jni.Primitives;
|
||||
using LibRyujinx.Jni.References;
|
||||
using LibRyujinx.Jni.Values;
|
||||
using LibRyujinx.Jni.Primitives;
|
||||
using LibRyujinx.Jni;
|
||||
using LibRyujinx.Shared.Audio.Oboe;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Rxmxnx.PInvoke;
|
||||
using System.Text;
|
||||
using LibRyujinx.Jni.Internal.Pointers;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
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.Extensions.KHR;
|
||||
using LibRyujinx.Shared.Audio.Oboe;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using LibHac.Tools.Fs;
|
||||
using Ryujinx.HLE.HOS.SystemState;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace LibRyujinx
|
||||
{
|
||||
@ -53,9 +54,16 @@ namespace LibRyujinx
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")]
|
||||
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;
|
||||
Logger.AddTarget(
|
||||
new AsyncLogTargetWrapper(
|
||||
new AndroidLogTarget("Ryujinx"),
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
|
||||
var path = GetString(jEnv, jpath);
|
||||
|
||||
var init = Initialize(path, enableDebugLogs);
|
||||
|
||||
@ -63,17 +71,10 @@ namespace LibRyujinx
|
||||
|
||||
_surfaceEvent = new ManualResetEvent(false);
|
||||
|
||||
Logger.AddTarget(
|
||||
new AsyncLogTargetWrapper(
|
||||
new AndroidLogTarget("Ryujinx"),
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
private static string GetString(JEnvRef jEnv, JStringLocalRef jString)
|
||||
private static string? GetString(JEnvRef jEnv, JStringLocalRef jString)
|
||||
{
|
||||
var stringPtr = getStringPointer(jEnv, jString);
|
||||
|
||||
@ -179,7 +180,7 @@ namespace LibRyujinx
|
||||
|
||||
var jobject = getObjectClass(jEnv, graphicObject);
|
||||
|
||||
GraphicsConfiguration graphicsConfiguration = new GraphicsConfiguration()
|
||||
GraphicsConfiguration graphicsConfiguration = new()
|
||||
{
|
||||
EnableShaderCache = getBooleanField(jEnv, graphicObject, getFieldId(jEnv, jobject, GetCCharSequence("EnableShaderCache"), 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"))),
|
||||
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);
|
||||
}
|
||||
|
||||
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")]
|
||||
public unsafe static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr)
|
||||
public static void JniSetSurface(JEnvRef jEnv, JObjectLocalRef jObj, JLong surfacePtr)
|
||||
{
|
||||
_surfacePtr = surfacePtr;
|
||||
|
||||
@ -235,7 +236,7 @@ namespace LibRyujinx
|
||||
var getLongField = getLongFieldPtr.GetUnsafeDelegate<GetLongFieldDelegate>();
|
||||
var getObjectField = getObjectFieldPtr.GetUnsafeDelegate<GetObjectFieldDelegate>();
|
||||
|
||||
List<string> extensions = new List<string>();
|
||||
List<string?> extensions = new();
|
||||
|
||||
var count = getArrayLength(jEnv, extensionsArray);
|
||||
|
||||
@ -249,9 +250,9 @@ namespace LibRyujinx
|
||||
|
||||
_surfaceEvent.Set();
|
||||
|
||||
_surfacePtr = (long)surfacePtr;
|
||||
_surfacePtr = surfacePtr;
|
||||
|
||||
CreateSurface createSurfaceFunc = (IntPtr instance) =>
|
||||
CreateSurface createSurfaceFunc = instance =>
|
||||
{
|
||||
_surfaceEvent.WaitOne();
|
||||
_surfaceEvent.Reset();
|
||||
@ -259,10 +260,10 @@ namespace LibRyujinx
|
||||
var api = Vk.GetApi();
|
||||
if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension))
|
||||
{
|
||||
var createInfo = new AndroidSurfaceCreateInfoKHR()
|
||||
var createInfo = new AndroidSurfaceCreateInfoKHR
|
||||
{
|
||||
SType = StructureType.AndroidSurfaceCreateInfoKhr,
|
||||
Window = (nint*)_surfacePtr
|
||||
Window = (nint*)_surfacePtr,
|
||||
};
|
||||
|
||||
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")]
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path)
|
||||
{
|
||||
var info = GetGameInfo(GetString(jEnv, path)) ?? new GameInfo();
|
||||
SHA256 sha;
|
||||
return GetInfo(jEnv, info, out sha);
|
||||
var info = GetGameInfo(GetString(jEnv, path));
|
||||
return GetInfo(jEnv, info, out SHA256 _);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
||||
@ -306,12 +306,11 @@ namespace LibRyujinx
|
||||
{
|
||||
using var stream = OpenFile(fileDescriptor);
|
||||
|
||||
var info = GetGameInfo(stream, isXci) ?? new GameInfo();
|
||||
SHA256 sha;
|
||||
return GetInfo(jEnv, info, out sha);
|
||||
var info = GetGameInfo(stream, isXci);
|
||||
return GetInfo(jEnv, info, out SHA256 _);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@ -339,7 +338,7 @@ namespace LibRyujinx
|
||||
var constructor = getMethod(jEnv, javaClass, GetCCharSequence("<init>"), GetCCharSequence("()V"));
|
||||
var newObj = newObject(jEnv, javaClass, constructor, 0);
|
||||
sha = SHA256.Create();
|
||||
var iconCacheByte = sha.ComputeHash(info.Icon ?? new byte[0]);
|
||||
var iconCacheByte = sha.ComputeHash(info?.Icon ?? Array.Empty<byte>());
|
||||
var iconCache = BitConverter.ToString(iconCacheByte).Replace("-", "");
|
||||
|
||||
var cacheDirectory = Path.Combine(AppDataManager.BaseDirPath, "iconCache");
|
||||
@ -348,21 +347,23 @@ namespace LibRyujinx
|
||||
var cachePath = Path.Combine(cacheDirectory, iconCache);
|
||||
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("TitleId"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info.TitleId)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Developer"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info.Developer)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Version"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info.Version)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("TitleName"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.TitleName)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("TitleId"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.TitleId)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Developer"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.Developer)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("Version"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, info?.Version)._value);
|
||||
setObjectField(jEnv, newObj, getFieldId(jEnv, javaClass, GetCCharSequence("IconCache"), GetCCharSequence("Ljava/lang/String;")), CreateString(jEnv, iconCache)._value);
|
||||
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;
|
||||
}
|
||||
|
||||
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 str = createString(jEnv, ptr);
|
||||
@ -417,19 +418,19 @@ namespace LibRyujinx
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetButtonPressed")]
|
||||
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")]
|
||||
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")]
|
||||
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")]
|
||||
@ -438,7 +439,7 @@ namespace LibRyujinx
|
||||
return ConnectGamepad(index);
|
||||
}
|
||||
|
||||
private static Stream OpenFile(int descriptor)
|
||||
private static FileStream OpenFile(int descriptor)
|
||||
{
|
||||
var safeHandle = new SafeFileHandle(descriptor, false);
|
||||
|
||||
@ -464,7 +465,7 @@ namespace LibRyujinx
|
||||
Warn = 0x05,
|
||||
Error = 0x06,
|
||||
Fatal = 0x07,
|
||||
Silent = 0x08
|
||||
Silent = 0x08,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,10 @@ namespace LibRyujinx
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
|
||||
Logger.Notice.Print(LogClass.Application, "Initializing...");
|
||||
Logger.Notice.Print(LogClass.Application, $"Using base path: {AppDataManager.BaseDirPath}");
|
||||
|
||||
SwitchDevice = new SwitchDevice();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -417,6 +421,8 @@ namespace LibRyujinx
|
||||
{
|
||||
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();
|
||||
|
||||
Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage());
|
||||
|
@ -2,8 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LinkerFlavor Condition="'$(RuntimeIdentifier)'=='linux-bionic-arm64'">lld</LinkerFlavor>
|
||||
<DefineConstants>$(DefineConstants);FORCE_EXTERNAL_BASE_DIR</DefineConstants>
|
||||
<LinkerFlavor Condition="'$(RuntimeIdentifier)'=='linux-bionic-arm64'">lld</LinkerFlavor>
|
||||
<DefineConstants>$(DefineConstants);FORCE_EXTERNAL_BASE_DIR</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PublishAot>true</PublishAot>
|
||||
@ -39,4 +39,4 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Jni\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
21
src/RyujinxAndroid/.gitignore
vendored
21
src/RyujinxAndroid/.gitignore
vendored
@ -1,15 +1,12 @@
|
||||
.idea/
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
.DS_Store
|
||||
build/
|
||||
captures
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
app/src/main/jniLibs/arm64-v8a/**
|
||||
!app/src/main/jniLibs/arm64-v8a/.gitkeep
|
||||
|
1
src/RyujinxAndroid/app/.gitignore
vendored
1
src/RyujinxAndroid/app/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/build
|
@ -9,7 +9,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.ryujinx.android"
|
||||
minSdk 28
|
||||
minSdk 30
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
@ -20,6 +20,7 @@ android {
|
||||
}
|
||||
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
|
||||
@ -38,11 +39,11 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
@ -52,6 +53,9 @@ android {
|
||||
kotlinCompilerExtensionVersion '1.3.2'
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
keepDebugSymbols += '**/libryujinx.so'
|
||||
}
|
||||
resources {
|
||||
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 platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
||||
@ -81,7 +89,7 @@ dependencies {
|
||||
implementation 'com.google.oboe:oboe:1.7.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation "io.coil-kt:coil-compose:2.4.0"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
@ -89,7 +97,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation platform('androidx.compose:compose-bom:2023.06.00')
|
||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2'
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
||||
}
|
@ -7,7 +7,9 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_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.READ_EXTERNAL_STORAGE" />
|
||||
<application
|
||||
@ -15,7 +17,6 @@
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="true"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
@ -27,7 +28,6 @@
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@ -37,4 +37,4 @@
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
@ -6,40 +6,26 @@ import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.GraphicsLayerScope
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import com.swordfish.radialgamepad.library.RadialGamePad
|
||||
import com.swordfish.radialgamepad.library.config.ButtonConfig
|
||||
import com.swordfish.radialgamepad.library.config.CrossConfig
|
||||
import com.swordfish.radialgamepad.library.config.CrossContentDescription
|
||||
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
|
||||
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
|
||||
import com.swordfish.radialgamepad.library.config.RadialGamePadTheme
|
||||
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
|
||||
import com.swordfish.radialgamepad.library.event.Event
|
||||
import com.swordfish.radialgamepad.library.event.GestureType
|
||||
import com.swordfish.radialgamepad.library.haptics.HapticConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.subscribe
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
typealias GamePad = RadialGamePad
|
||||
@ -56,7 +42,7 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
|
||||
return this.isVisible
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
init {
|
||||
@ -80,22 +66,22 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
|
||||
|
||||
lifecycleScope.apply {
|
||||
lifecycleScope.launch {
|
||||
var events = merge(leftGamePad.events(),rightGamePad.events())
|
||||
val events = merge(leftGamePad.events(),rightGamePad.events())
|
||||
.shareIn(lifecycleScope, SharingStarted.Lazily)
|
||||
|
||||
events.safeCollect {
|
||||
handleEvent(it)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Create(context: Context) : View
|
||||
{
|
||||
var inflator = LayoutInflater.from(context);
|
||||
var view = inflator.inflate(R.layout.game_layout, null)
|
||||
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad);
|
||||
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad);
|
||||
val inflator = LayoutInflater.from(context)
|
||||
val view = inflator.inflate(R.layout.game_layout, null)
|
||||
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad)
|
||||
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad)
|
||||
|
||||
controllerView = view
|
||||
|
||||
@ -120,51 +106,43 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
|
||||
if(controllerId == -1)
|
||||
controllerId = ryujinxNative.inputConnectGamepad(0)
|
||||
|
||||
controllerId?.apply {
|
||||
controllerId.apply {
|
||||
when (ev) {
|
||||
is Event.Button -> {
|
||||
var action = ev.action
|
||||
val action = ev.action
|
||||
when (action) {
|
||||
KeyEvent.ACTION_UP -> {
|
||||
ryujinxNative.inputSetButtonReleased(ev.id, this)
|
||||
}
|
||||
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
ryujinxNative.inputSetButtonPressed(ev.id, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Event.Direction -> {
|
||||
var direction = ev.id
|
||||
val direction = ev.id
|
||||
|
||||
when(direction) {
|
||||
GamePadButtonInputId.DpadUp.ordinal -> {
|
||||
if (ev.xAxis > 0)
|
||||
{
|
||||
if (ev.xAxis > 0) {
|
||||
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.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.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
|
||||
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
|
||||
}
|
||||
if (ev.yAxis < 0)
|
||||
{
|
||||
if (ev.yAxis < 0) {
|
||||
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.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.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
|
||||
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
|
||||
}
|
||||
@ -173,6 +151,7 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
|
||||
GamePadButtonInputId.LeftStick.ordinal -> {
|
||||
ryujinxNative.inputSetStickAxis(1, ev.xAxis, -ev.yAxis ,this)
|
||||
}
|
||||
|
||||
GamePadButtonInputId.RightStick.ordinal -> {
|
||||
ryujinxNative.inputSetStickAxis(2, ev.xAxis, -ev.yAxis ,this)
|
||||
}
|
||||
@ -193,7 +172,7 @@ suspend fun <T> Flow<T>.safeCollect(
|
||||
}
|
||||
|
||||
private fun generateConfig(isLeft: Boolean): GamePadConfig {
|
||||
var distance = 0.05f
|
||||
val distance = 0.05f
|
||||
|
||||
if (isLeft) {
|
||||
return GamePadConfig(
|
||||
|
@ -2,14 +2,12 @@ package org.ryujinx.android
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.MotionEvent
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import org.ryujinx.android.viewmodels.GameModel
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
|
||||
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) {
|
||||
var isStarted = _isStarted;
|
||||
val isStarted = _isStarted
|
||||
|
||||
start(holder)
|
||||
|
||||
if(isStarted && (_width != width || _height != height))
|
||||
{
|
||||
var nativeHelpers = NativeHelpers()
|
||||
var window = nativeHelpers.getNativeWindow(holder.surface);
|
||||
_nativeRyujinx.graphicsSetSurface(window);
|
||||
val nativeHelpers = NativeHelpers()
|
||||
val window = nativeHelpers.getNativeWindow(holder.surface)
|
||||
_nativeRyujinx.graphicsSetSurface(window)
|
||||
}
|
||||
|
||||
_width = width;
|
||||
_height = height;
|
||||
_width = width
|
||||
_height = height
|
||||
|
||||
if(_isStarted)
|
||||
{
|
||||
@ -61,15 +59,15 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
|
||||
}
|
||||
|
||||
private fun start(surfaceHolder: SurfaceHolder) : Unit {
|
||||
var game = gameModel ?: return
|
||||
var path = game.getPath() ?: return
|
||||
private fun start(surfaceHolder: SurfaceHolder) {
|
||||
val game = gameModel ?: return
|
||||
val path = game.getPath() ?: return
|
||||
if (_isStarted)
|
||||
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 {
|
||||
EnableShaderCache = settings.enableShaderCache
|
||||
@ -78,14 +76,14 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
})
|
||||
|
||||
|
||||
var nativeHelpers = NativeHelpers()
|
||||
var window = nativeHelpers.getNativeWindow(surfaceHolder.surface);
|
||||
val nativeHelpers = NativeHelpers()
|
||||
val window = nativeHelpers.getNativeWindow(surfaceHolder.surface)
|
||||
nativeInterop = NativeGraphicsInterop()
|
||||
nativeInterop!!.VkRequiredExtensions = arrayOf(
|
||||
"VK_KHR_surface", "VK_KHR_android_surface"
|
||||
);
|
||||
)
|
||||
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
|
||||
nativeInterop!!.SurfaceHandle = window;
|
||||
nativeInterop!!.SurfaceHandle = window
|
||||
|
||||
success = _nativeRyujinx.graphicsInitializeRenderer(
|
||||
nativeInterop!!.VkRequiredExtensions!!,
|
||||
@ -104,7 +102,7 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
false,
|
||||
"UTC",
|
||||
settings.ignoreMissingServices
|
||||
);
|
||||
)
|
||||
|
||||
success = _nativeRyujinx.deviceLoad(path)
|
||||
|
||||
@ -124,12 +122,12 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
_nativeRyujinx.graphicsRendererSetSize(
|
||||
surfaceHolder.surfaceFrame.width(),
|
||||
surfaceHolder.surfaceFrame.height()
|
||||
);
|
||||
)
|
||||
|
||||
_guestThread = thread(start = true) {
|
||||
runGame()
|
||||
}
|
||||
_isStarted = success;
|
||||
_isStarted = success
|
||||
|
||||
_updateThread = thread(start = true) {
|
||||
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 = thread(start = true) {
|
||||
var threadId = 0L;
|
||||
var threadId = 0L
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
mainViewModel.performanceManager?.enable()
|
||||
while (_isStarted) {
|
||||
Thread.sleep(1000)
|
||||
var newthreadId = mainViewModel.activity.getRenderingThreadId()
|
||||
val newthreadId = mainViewModel.activity.getRenderingThreadId()
|
||||
|
||||
if (threadId != newthreadId) {
|
||||
mainViewModel.performanceManager?.closeCurrentRenderingSession()
|
||||
}
|
||||
threadId = newthreadId;
|
||||
threadId = newthreadId
|
||||
if (threadId != 0L) {
|
||||
mainViewModel.performanceManager?.initializeRenderingSession(threadId)
|
||||
}
|
||||
|
@ -34,12 +34,16 @@ class Helpers {
|
||||
val split = docId.split(":".toRegex()).toTypedArray()
|
||||
val type = split[0]
|
||||
var contentUri: Uri? = null
|
||||
if ("image" == type) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
} else if ("video" == type) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
} else if ("audio" == type) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
when (type) {
|
||||
"image" -> {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
"video" -> {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
"audio" -> {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
}
|
||||
val selection = "_id=?"
|
||||
val selectionArgs = arrayOf(split[1])
|
||||
@ -58,13 +62,13 @@ class Helpers {
|
||||
val column = "_data"
|
||||
val projection = arrayOf(column)
|
||||
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()) {
|
||||
val column_index: Int = cursor.getColumnIndexOrThrow(column)
|
||||
return cursor.getString(column_index)
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close()
|
||||
cursor?.close()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package org.ryujinx.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
@ -25,23 +24,19 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.views.HomeViews
|
||||
import org.ryujinx.android.views.MainView
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
var physicalControllerManager: PhysicalControllerManager
|
||||
var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
|
||||
private var _isInit: Boolean = false
|
||||
var storageHelper: SimpleStorageHelper? = null
|
||||
companion object {
|
||||
var mainViewModel: MainViewModel? = null
|
||||
var AppPath : String?
|
||||
var AppPath : String = ""
|
||||
var StorageHelper: SimpleStorageHelper? = null
|
||||
init {
|
||||
AppPath = ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun updateRenderSessionPerformance(gameTime : Long)
|
||||
@ -56,7 +51,6 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
init {
|
||||
physicalControllerManager = PhysicalControllerManager(this)
|
||||
storageHelper = SimpleStorageHelper(this)
|
||||
StorageHelper = storageHelper
|
||||
System.loadLibrary("ryujinxjni")
|
||||
@ -66,29 +60,28 @@ class MainActivity : ComponentActivity() {
|
||||
external fun getRenderingThreadId() : Long
|
||||
external fun initVm()
|
||||
|
||||
fun setFullScreen() :Unit {
|
||||
fun setFullScreen() {
|
||||
requestedOrientation =
|
||||
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.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAudioDevice () : Int {
|
||||
var audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
var devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
|
||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||
|
||||
return if (devices.isEmpty())
|
||||
0
|
||||
else {
|
||||
var speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
|
||||
var earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
|
||||
val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
|
||||
val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
|
||||
if(earPiece != null)
|
||||
return earPiece.id
|
||||
if(speaker != null)
|
||||
@ -112,17 +105,23 @@ class MainActivity : ComponentActivity() {
|
||||
return super.dispatchGenericMotionEvent(ev)
|
||||
}
|
||||
|
||||
private fun initialize() : Unit
|
||||
{
|
||||
if(_isInit)
|
||||
private fun initialize() {
|
||||
if (_isInit)
|
||||
return
|
||||
|
||||
var appPath: String = AppPath ?: return
|
||||
var success = RyujinxNative().initialize(appPath, false)
|
||||
val appPath: String = AppPath
|
||||
val success = RyujinxNative().initialize(appPath, false)
|
||||
_isInit = success
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if(
|
||||
!Environment.isExternalStorageManager()
|
||||
) {
|
||||
storageHelper?.storage?.requestFullStorageAccess()
|
||||
}
|
||||
|
||||
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
||||
|
||||
initialize()
|
||||
@ -130,14 +129,6 @@ class MainActivity : ComponentActivity() {
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
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?.apply {
|
||||
|
@ -14,7 +14,7 @@ class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
|
||||
if(!_isEnabled || renderingSession != null)
|
||||
return
|
||||
|
||||
var threads = IntArray(1)
|
||||
val threads = IntArray(1)
|
||||
threads[0] = threadId.toInt()
|
||||
renderingSession = performanceHintManager.createHintSession(threads, DEFAULT_TARGET_NS)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
||||
|
||||
fun onKeyEvent(event: KeyEvent) : Boolean{
|
||||
if(controllerId != -1) {
|
||||
var id = GetGamePadButtonInputId(event.keyCode)
|
||||
val id = GetGamePadButtonInputId(event.keyCode)
|
||||
|
||||
if(id != GamePadButtonInputId.None) {
|
||||
when (event.action) {
|
||||
@ -21,7 +21,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
||||
ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId)
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,10 +31,10 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
||||
fun onMotionEvent(ev: MotionEvent) {
|
||||
if(controllerId != -1) {
|
||||
if(ev.action == MotionEvent.ACTION_MOVE) {
|
||||
var leftStickX = ev.getAxisValue(MotionEvent.AXIS_X);
|
||||
var leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y);
|
||||
var rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z);
|
||||
var rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ);
|
||||
val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
|
||||
val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
|
||||
val rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z)
|
||||
val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
|
||||
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
|
||||
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.view.Surface
|
||||
import org.ryujinx.android.viewmodels.GameInfo
|
||||
import java.io.FileDescriptor
|
||||
|
||||
@Suppress("KotlinJniMissingFunction")
|
||||
class RyujinxNative {
|
||||
|
||||
external fun initialize(appPath: String, enableDebugLogs : Boolean): Boolean
|
||||
|
@ -1,10 +1,7 @@
|
||||
package org.ryujinx.android.viewmodels
|
||||
|
||||
import android.R.string
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import org.ryujinx.android.Helpers
|
||||
@ -22,8 +19,8 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
|
||||
init {
|
||||
fileName = file.name
|
||||
var absPath = getPath()
|
||||
var gameInfo = RyujinxNative().deviceGetGameInfoFromPath(absPath ?: "")
|
||||
val absPath = getPath()
|
||||
val gameInfo = RyujinxNative().deviceGetGameInfoFromPath(absPath ?: "")
|
||||
|
||||
fileSize = gameInfo.FileSize
|
||||
titleId = gameInfo.TitleId
|
||||
@ -37,7 +34,7 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
var uri = file.uri
|
||||
if (uri.scheme != "file")
|
||||
uri = Uri.parse("file://" + Helpers.getPath(context, file.uri))
|
||||
return uri.path;
|
||||
return uri.path
|
||||
}
|
||||
|
||||
fun getIsXci() : Boolean {
|
||||
|
@ -8,7 +8,6 @@ import com.anggrayudi.storage.file.DocumentFileCompat
|
||||
import com.anggrayudi.storage.file.DocumentFileType
|
||||
import com.anggrayudi.storage.file.FileFullPath
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import com.anggrayudi.storage.file.fullName
|
||||
import com.anggrayudi.storage.file.getAbsolutePath
|
||||
import com.anggrayudi.storage.file.search
|
||||
import org.ryujinx.android.MainActivity
|
||||
@ -20,7 +19,7 @@ class HomeViewModel(
|
||||
private var gameList: SnapshotStateList<GameModel>? = null
|
||||
private var loadedCache: List<GameModel> = listOf()
|
||||
private var gameFolderPath: DocumentFile? = null
|
||||
private var sharedPref: SharedPreferences? = null;
|
||||
private var sharedPref: SharedPreferences? = null
|
||||
|
||||
init {
|
||||
if (activity != null) {
|
||||
@ -28,15 +27,15 @@ class HomeViewModel(
|
||||
activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
|
||||
run {
|
||||
gameFolderPath = folder
|
||||
var p = folder.getAbsolutePath(activity!!)
|
||||
var editor = sharedPref?.edit()
|
||||
editor?.putString("gameFolder", p);
|
||||
val p = folder.getAbsolutePath(activity!!)
|
||||
val editor = sharedPref?.edit()
|
||||
editor?.putString("gameFolder", p)
|
||||
editor?.apply()
|
||||
reloadGameList()
|
||||
}
|
||||
}
|
||||
|
||||
var savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
|
||||
val savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
|
||||
|
||||
if (savedFolder.isNotEmpty()) {
|
||||
try {
|
||||
@ -56,26 +55,26 @@ class HomeViewModel(
|
||||
}
|
||||
|
||||
fun openGameFolder() {
|
||||
var path = sharedPref?.getString("gameFolder", "") ?: ""
|
||||
val path = sharedPref?.getString("gameFolder", "") ?: ""
|
||||
|
||||
if (path.isNullOrEmpty())
|
||||
activity?.storageHelper?.storage?.openFolderPicker();
|
||||
if (path.isEmpty())
|
||||
activity?.storageHelper?.storage?.openFolderPicker()
|
||||
else
|
||||
activity?.storageHelper?.storage?.openFolderPicker(
|
||||
activity!!.storageHelper!!.storage.requestCodeFolderPicker,
|
||||
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
||||
FileFullPath(activity, path)
|
||||
)
|
||||
}
|
||||
|
||||
fun reloadGameList() {
|
||||
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)) {
|
||||
if (file.extension == "xci" || file.extension == "nsp")
|
||||
activity?.let {
|
||||
activity.let {
|
||||
files.add(GameModel(file, it))
|
||||
}
|
||||
}
|
||||
@ -91,7 +90,7 @@ class HomeViewModel(
|
||||
}
|
||||
|
||||
fun setViewList(list: SnapshotStateList<GameModel>) {
|
||||
gameList = list;
|
||||
gameList = list
|
||||
applyFilter()
|
||||
}
|
||||
}
|
@ -23,14 +23,14 @@ class MainViewModel(val activity: MainActivity) {
|
||||
|
||||
init {
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
var hintService =
|
||||
val hintService =
|
||||
activity.getSystemService(Context.PERFORMANCE_HINT_SERVICE) as PerformanceHintManager
|
||||
performanceManager = PerformanceManager(hintService)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadGame(game:GameModel) {
|
||||
var controller = navController?: return;
|
||||
val controller = navController?: return
|
||||
activity.setFullScreen()
|
||||
GameHost.gameModel = game
|
||||
controller.navigate("game")
|
||||
|
@ -55,18 +55,18 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
resScale: MutableState<Float>,
|
||||
useVirtualController: MutableState<Boolean>
|
||||
){
|
||||
var editor = sharedPref.edit()
|
||||
val editor = sharedPref.edit()
|
||||
|
||||
editor.putBoolean("isHostMapped", isHostMapped?.value ?: true)
|
||||
editor.putBoolean("useNce", useNce?.value ?: true)
|
||||
editor.putBoolean("enableVsync", enableVsync?.value ?: true)
|
||||
editor.putBoolean("enableDocked", enableDocked?.value ?: true)
|
||||
editor.putBoolean("enablePtc", enablePtc?.value ?: true)
|
||||
editor.putBoolean("ignoreMissingServices", ignoreMissingServices?.value ?: false)
|
||||
editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true)
|
||||
editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false)
|
||||
editor.putFloat("resScale", resScale?.value ?: 1f)
|
||||
editor.putBoolean("useVirtualController", useVirtualController?.value ?: true)
|
||||
editor.putBoolean("isHostMapped", isHostMapped.value)
|
||||
editor.putBoolean("useNce", useNce.value)
|
||||
editor.putBoolean("enableVsync", enableVsync.value)
|
||||
editor.putBoolean("enableDocked", enableDocked.value)
|
||||
editor.putBoolean("enablePtc", enablePtc.value)
|
||||
editor.putBoolean("ignoreMissingServices", ignoreMissingServices.value)
|
||||
editor.putBoolean("enableShaderCache", enableShaderCache.value)
|
||||
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
|
||||
editor.putFloat("resScale", resScale.value)
|
||||
editor.putBoolean("useVirtualController", useVirtualController.value)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.ryujinx.android.viewmodels
|
||||
|
||||
import androidx.appcompat.widget.ThemedSpinnerAdapter.Helper
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
@ -24,23 +23,23 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
return
|
||||
|
||||
data?.paths?.apply {
|
||||
removeAt(index - 1);
|
||||
removeAt(index - 1)
|
||||
pathsState?.clear()
|
||||
pathsState?.addAll(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Add() {
|
||||
var callBack = storageHelper.onFileSelected
|
||||
val callBack = storageHelper.onFileSelected
|
||||
|
||||
storageHelper.onFileSelected = { requestCode, files ->
|
||||
run {
|
||||
storageHelper.onFileSelected = callBack
|
||||
if(requestCode == UpdateRequestCode)
|
||||
{
|
||||
var file = files.firstOrNull()
|
||||
val file = files.firstOrNull()
|
||||
file?.apply {
|
||||
var path = Helpers.getPath(storageHelper.storage.context, file.uri)
|
||||
val path = Helpers.getPath(storageHelper.storage.context, file.uri)
|
||||
if(!path.isNullOrEmpty()){
|
||||
data?.apply {
|
||||
if(!paths.contains(path)) {
|
||||
@ -62,20 +61,19 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
this.selected = ""
|
||||
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]
|
||||
}
|
||||
var gson = Gson()
|
||||
var json = gson.toJson(this)
|
||||
jsonPath = (MainActivity.AppPath
|
||||
?: "") + "/games/" + titleId.toLowerCase(Locale.current)
|
||||
val gson = Gson()
|
||||
val json = gson.toJson(this)
|
||||
jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
|
||||
File(jsonPath).mkdirs()
|
||||
File(jsonPath + "/updates.json").writeText(json)
|
||||
File("$jsonPath/updates.json").writeText(json)
|
||||
}
|
||||
}
|
||||
|
||||
fun setPaths(paths: SnapshotStateList<String>) {
|
||||
pathsState = paths;
|
||||
pathsState = paths
|
||||
data?.apply {
|
||||
pathsState?.clear()
|
||||
pathsState?.addAll(this.paths)
|
||||
@ -86,16 +84,15 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
private var jsonPath: String
|
||||
|
||||
init {
|
||||
jsonPath = (MainActivity.AppPath
|
||||
?: "") + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json"
|
||||
jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json"
|
||||
|
||||
data = TitleUpdateMetadata()
|
||||
if (File(jsonPath).exists()) {
|
||||
var gson = Gson()
|
||||
val gson = Gson()
|
||||
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
||||
|
||||
data?.apply {
|
||||
var existingPaths = mutableListOf<String>()
|
||||
val existingPaths = mutableListOf<String>()
|
||||
for (path in paths) {
|
||||
if (File(path).exists()) {
|
||||
existingPaths.add(path)
|
||||
|
@ -12,22 +12,22 @@ class VulkanDriverViewModel(val activity: MainActivity) {
|
||||
var selected: String = ""
|
||||
|
||||
companion object {
|
||||
val DriverRequestCode: Int = 1003
|
||||
const val DriverRequestCode: Int = 1003
|
||||
const val DriverFolder: String = "drivers"
|
||||
}
|
||||
|
||||
private fun getAppPath() : String {
|
||||
var appPath =
|
||||
(MainActivity.AppPath ?: activity.getExternalFilesDir(null)?.absolutePath ?: "");
|
||||
MainActivity.AppPath
|
||||
appPath += "/"
|
||||
|
||||
return appPath
|
||||
}
|
||||
|
||||
fun ensureDriverPath() : File {
|
||||
var driverPath = getAppPath() + DriverFolder
|
||||
val driverPath = getAppPath() + DriverFolder
|
||||
|
||||
var driverFolder = File(driverPath)
|
||||
val driverFolder = File(driverPath)
|
||||
|
||||
if(!driverFolder.exists())
|
||||
driverFolder.mkdirs()
|
||||
@ -36,13 +36,13 @@ class VulkanDriverViewModel(val activity: MainActivity) {
|
||||
}
|
||||
|
||||
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()){
|
||||
selected = selectedDriverFile.readText()
|
||||
|
||||
@ -52,16 +52,16 @@ class VulkanDriverViewModel(val activity: MainActivity) {
|
||||
}
|
||||
}
|
||||
|
||||
var gson = Gson()
|
||||
val gson = Gson()
|
||||
|
||||
for (folder in folders){
|
||||
if(folder.isDirectory() && folder.parent == driverFolder.absolutePath){
|
||||
var meta = File(folder.absolutePath + "/meta.json")
|
||||
val meta = File(folder.absolutePath + "/meta.json")
|
||||
|
||||
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()) {
|
||||
var driver = folder.absolutePath + "/${metadata.libraryName}"
|
||||
val driver = folder.absolutePath + "/${metadata.libraryName}"
|
||||
metadata.driverPath = driver
|
||||
if (File(driver).exists())
|
||||
drivers.add(metadata)
|
||||
@ -74,18 +74,17 @@ class VulkanDriverViewModel(val activity: MainActivity) {
|
||||
}
|
||||
|
||||
fun saveSelected() {
|
||||
var driverFolder = ensureDriverPath()
|
||||
val driverFolder = ensureDriverPath()
|
||||
|
||||
var selectedDriverFile = File(driverFolder.absolutePath + "/selected")
|
||||
val selectedDriverFile = File(driverFolder.absolutePath + "/selected")
|
||||
selectedDriverFile.writeText(selected)
|
||||
}
|
||||
|
||||
fun removeSelected(){
|
||||
if(selected.isNotEmpty()){
|
||||
var sel = File(selected)
|
||||
val sel = File(selected)
|
||||
if(sel.exists()) {
|
||||
var parent = sel.parentFile
|
||||
parent.deleteRecursively()
|
||||
sel.parentFile?.deleteRecursively()
|
||||
}
|
||||
selected = ""
|
||||
|
||||
@ -96,20 +95,20 @@ class VulkanDriverViewModel(val activity: MainActivity) {
|
||||
fun add(refresh: MutableState<Boolean>) {
|
||||
activity.storageHelper?.apply {
|
||||
|
||||
var callBack = this.onFileSelected
|
||||
val callBack = this.onFileSelected
|
||||
|
||||
onFileSelected = { requestCode, files ->
|
||||
run {
|
||||
onFileSelected = callBack
|
||||
if(requestCode == DriverRequestCode)
|
||||
{
|
||||
var file = files.firstOrNull()
|
||||
val file = files.firstOrNull()
|
||||
file?.apply {
|
||||
var path = Helpers.getPath(storage.context, file.uri)
|
||||
val path = Helpers.getPath(storage.context, file.uri)
|
||||
if(!path.isNullOrEmpty()){
|
||||
var name = file.name?.removeSuffix("." + file.extension) ?: ""
|
||||
var driverFolder = ensureDriverPath()
|
||||
var extractionFolder = File(driverFolder.absolutePath + "/${name}")
|
||||
val name = file.name?.removeSuffix("." + file.extension) ?: ""
|
||||
val driverFolder = ensureDriverPath()
|
||||
val extractionFolder = File(driverFolder.absolutePath + "/${name}")
|
||||
extractionFolder.mkdirs()
|
||||
ZipFile(path)?.use { zip ->
|
||||
zip.entries().asSequence().forEach { entry ->
|
||||
@ -118,7 +117,7 @@ class VulkanDriverViewModel(val activity: MainActivity) {
|
||||
val filePath = extractionFolder.absolutePath + File.separator + entry.name
|
||||
|
||||
if (!entry.isDirectory) {
|
||||
var length = input.available()
|
||||
val length = input.available()
|
||||
val bytesIn = ByteArray(length)
|
||||
input.read(bytesIn)
|
||||
File(filePath).writeBytes(bytesIn)
|
||||
|
@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContentPadding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import coil.compose.AsyncImage
|
||||
import org.ryujinx.android.MainActivity
|
||||
@ -78,11 +73,11 @@ class HomeViews {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainTopBar(navController: NavHostController) {
|
||||
var topBarSize = remember {
|
||||
val topBarSize = remember {
|
||||
mutableStateOf(0)
|
||||
}
|
||||
Column {
|
||||
var showOptionsPopup = remember {
|
||||
val showOptionsPopup = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
TopAppBar(
|
||||
@ -116,7 +111,7 @@ class HomeViews {
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
showOptionsPopup.value = true;
|
||||
showOptionsPopup.value = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@ -171,7 +166,7 @@ class HomeViews {
|
||||
fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val scope = rememberCoroutineScope()
|
||||
var showBottomSheet = remember { mutableStateOf(false) }
|
||||
val showBottomSheet = remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@ -195,7 +190,7 @@ class HomeViews {
|
||||
|
||||
) { contentPadding ->
|
||||
Box(modifier = Modifier.padding(contentPadding)) {
|
||||
var list = remember {
|
||||
val list = remember {
|
||||
mutableStateListOf<GameModel>()
|
||||
}
|
||||
viewModel.setViewList(list)
|
||||
@ -227,8 +222,8 @@ class HomeViews {
|
||||
shape = MaterialTheme.shapes.large,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||
) {
|
||||
var titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
|
||||
var name = viewModel.mainViewModel?.selected?.titleName ?: ""
|
||||
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
|
||||
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
|
||||
TitleUpdateViews.Main(titleId, name, openDialog)
|
||||
}
|
||||
|
||||
@ -267,7 +262,7 @@ class HomeViews {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState<Boolean>) {
|
||||
Card(shape = MaterialTheme.shapes.medium,
|
||||
@ -291,8 +286,8 @@ class HomeViews {
|
||||
Row {
|
||||
if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000")
|
||||
{
|
||||
var iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
|
||||
var imageFile = File(iconSource)
|
||||
val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
|
||||
val imageFile = File(iconSource)
|
||||
if(imageFile.exists()) {
|
||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
||||
AsyncImage(model = imageFile,
|
||||
@ -320,7 +315,7 @@ class HomeViews {
|
||||
|
||||
@Composable
|
||||
fun NotAvailableIcon() {
|
||||
var size = ImageSize / Resources.getSystem().displayMetrics.density
|
||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Options",
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.ryujinx.android.views
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -73,7 +72,7 @@ class MainView {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
GameStats(mainViewModel)
|
||||
|
||||
var ryujinxNative = RyujinxNative()
|
||||
val ryujinxNative = RyujinxNative()
|
||||
|
||||
// touch surface
|
||||
Surface(color = Color.Transparent, modifier = Modifier
|
||||
@ -82,32 +81,36 @@ class MainView {
|
||||
.pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
Thread.sleep(2);
|
||||
Thread.sleep(2)
|
||||
val event = awaitPointerEvent()
|
||||
|
||||
if(controller.isVisible)
|
||||
continue
|
||||
|
||||
var change = event
|
||||
val change = event
|
||||
.component1()
|
||||
.firstOrNull()
|
||||
change?.apply {
|
||||
var position = this.position
|
||||
val position = this.position
|
||||
|
||||
if (event.type == PointerEventType.Press) {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
} else if (event.type == PointerEventType.Release) {
|
||||
ryujinxNative.inputReleaseTouchPoint()
|
||||
when (event.type) {
|
||||
PointerEventType.Press -> {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
}
|
||||
PointerEventType.Release -> {
|
||||
ryujinxNative.inputReleaseTouchPoint()
|
||||
|
||||
} else if (event.type == PointerEventType.Move) {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
}
|
||||
PointerEventType.Move -> {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -128,7 +131,7 @@ class MainView {
|
||||
}
|
||||
@Composable
|
||||
fun rememberVideogameAsset(): ImageVector {
|
||||
var primaryColor = MaterialTheme.colorScheme.primary
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
return remember {
|
||||
ImageVector.Builder(
|
||||
name = "videogame_asset",
|
||||
@ -224,20 +227,20 @@ class MainView {
|
||||
|
||||
@Composable
|
||||
fun GameStats(mainViewModel: MainViewModel){
|
||||
var fifo = remember {
|
||||
val fifo = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
var gameFps = remember {
|
||||
val gameFps = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
var gameTime = remember {
|
||||
val gameTime = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
|
||||
Surface(modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(0.4f)) {
|
||||
Column {
|
||||
var gameTimeVal = 0.0;
|
||||
var gameTimeVal = 0.0
|
||||
if (!gameTime.value.isInfinite())
|
||||
gameTimeVal = gameTime.value
|
||||
Text(text = "${String.format("%.3f", fifo.value)} %")
|
||||
|
@ -13,36 +13,24 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.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.filled.ArrowBack
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
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.unit.dp
|
||||
import org.ryujinx.android.viewmodels.SettingsViewModel
|
||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||
|
||||
class SettingViews {
|
||||
companion object {
|
||||
@ -63,38 +50,38 @@ class SettingViews {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Main(settingsViewModel: SettingsViewModel) {
|
||||
var loaded = remember {
|
||||
val loaded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var isHostMapped = remember {
|
||||
val isHostMapped = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var useNce = remember {
|
||||
val useNce = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var enableVsync = remember {
|
||||
val enableVsync = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var enableDocked = remember {
|
||||
val enableDocked = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var enablePtc = remember {
|
||||
val enablePtc = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var ignoreMissingServices = remember {
|
||||
val ignoreMissingServices = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var enableShaderCache = remember {
|
||||
val enableShaderCache = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var enableTextureRecompression = remember {
|
||||
val enableTextureRecompression = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var resScale = remember {
|
||||
val resScale = remember {
|
||||
mutableStateOf(1f)
|
||||
}
|
||||
var useVirtualController = remember {
|
||||
val useVirtualController = remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
|
||||
@ -470,8 +457,8 @@ class SettingViews {
|
||||
title: String,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
var expanded = false
|
||||
var mutableExpanded = remember {
|
||||
val expanded = false
|
||||
val mutableExpanded = remember {
|
||||
mutableStateOf(expanded)
|
||||
}
|
||||
val transitionState = remember {
|
||||
|
@ -3,11 +3,9 @@ package org.ryujinx.android.views
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
@ -35,7 +33,7 @@ class TitleUpdateViews {
|
||||
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
|
||||
val viewModel = TitleUpdateViewModel(titleId)
|
||||
|
||||
var selected = remember { mutableStateOf(0) }
|
||||
val selected = remember { mutableStateOf(0) }
|
||||
viewModel.data?.apply {
|
||||
selected.value = paths.indexOf(this.selected) + 1
|
||||
}
|
||||
@ -67,14 +65,14 @@ class TitleUpdateViews {
|
||||
)
|
||||
}
|
||||
|
||||
var paths = remember {
|
||||
val paths = remember {
|
||||
mutableStateListOf<String>()
|
||||
}
|
||||
|
||||
viewModel.setPaths(paths)
|
||||
var index = 1
|
||||
for (path in paths) {
|
||||
var i = index
|
||||
val i = index
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
RadioButton(
|
||||
selected = (selected.value == i),
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -20,4 +20,16 @@ kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# 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
|
139
src/RyujinxAndroid/libryujinx/build.gradle
Normal file
139
src/RyujinxAndroid/libryujinx/build.gradle
Normal 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")
|
@ -16,3 +16,4 @@ dependencyResolutionManagement {
|
||||
}
|
||||
rootProject.name = "RyujinxAndroid"
|
||||
include ':app'
|
||||
include ':libryujinx'
|
||||
|
Reference in New Issue
Block a user