Archived
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.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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
/build

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@

View File

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

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"
include ':app'
include ':libryujinx'