1
0
forked from MeloNX/MeloNX

android - add adrenotools module

test

restore driver selection

fix adreno hooking

fix adreno hooking

fix unzip code

refactor virtual pad composition

separate game loading from surface creation

add closing emulation(starting a new one is still broken), disabled audio

safely close audio on game exit

add dlc manager

fix AsFlags rename conflict
This commit is contained in:
Emmanuel Hansen 2023-07-30 18:44:49 +00:00
parent f28f2dfeeb
commit b28f9a6331
23 changed files with 1241 additions and 361 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "libadrenotools"]
path = src/RyujinxAndroid/app/src/main/cpp/libraries/adrenotools
url = https://github.com/bylaws/libadrenotools.git

View File

@ -62,11 +62,6 @@ namespace LibRyujinx.Shared.Audio.Oboe
return session; return session;
} }
internal bool Unregister(OboeHardwareDeviceSession session)
{
return _sessions.TryRemove(session, out _);
}
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
@ -82,6 +77,8 @@ namespace LibRyujinx.Shared.Audio.Oboe
} }
_pauseEvent.Dispose(); _pauseEvent.Dispose();
_sessions.Clear();
} }
} }

View File

@ -10,6 +10,7 @@ namespace LibRyujinx.Shared.Audio.Oboe
internal class OboeHardwareDeviceSession : HardwareDeviceSessionOutputBase internal class OboeHardwareDeviceSession : HardwareDeviceSessionOutputBase
{ {
private OboeHardwareDeviceDriver _driver; private OboeHardwareDeviceDriver _driver;
private bool _isClosed;
private bool _isWorkerActive; private bool _isWorkerActive;
private Queue<OboeAudioBuffer> _queuedBuffers; private Queue<OboeAudioBuffer> _queuedBuffers;
private bool _isActive; private bool _isActive;
@ -59,6 +60,9 @@ namespace LibRyujinx.Shared.Audio.Oboe
{ {
StartIfNotPlaying(); StartIfNotPlaying();
if (_isClosed)
break;
fixed(byte* ptr = buffer.Data) fixed(byte* ptr = buffer.Data)
OboeInterop.WriteToSession(_session, (ulong)ptr, buffer.SampleCount); OboeInterop.WriteToSession(_session, (ulong)ptr, buffer.SampleCount);
@ -90,18 +94,31 @@ namespace LibRyujinx.Shared.Audio.Oboe
public override void Dispose() public override void Dispose()
{ {
if (_session == 0)
return;
PrepareToClose();
OboeInterop.CloseSession(_session); OboeInterop.CloseSession(_session);
_session = 0;
} }
public override void PrepareToClose() public override void PrepareToClose()
{ {
_isClosed = true;
_isWorkerActive = false; _isWorkerActive = false;
_workerThread.Join(); _workerThread?.Join();
Stop();
} }
private void StartIfNotPlaying() private void StartIfNotPlaying()
{ {
lock (_trackLock) lock (_trackLock)
{ {
if (_isClosed)
return;
if (OboeInterop.IsPlaying(_session) == 0) if (OboeInterop.IsPlaying(_session) == 0)
{ {
Start(); Start();
@ -145,6 +162,9 @@ namespace LibRyujinx.Shared.Audio.Oboe
public override void Start() public override void Start()
{ {
if (_isClosed)
return;
OboeInterop.StartSession(_session); OboeInterop.StartSession(_session);
} }

View File

@ -31,6 +31,8 @@ namespace LibRyujinx
private static ManualResetEvent _surfaceEvent; private static ManualResetEvent _surfaceEvent;
private static long _surfacePtr; private static long _surfacePtr;
public static VulkanLoader? VulkanLoader { get; private set; }
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
private extern static IntPtr getStringPointer(JEnvRef jEnv, JStringLocalRef s); private extern static IntPtr getStringPointer(JEnvRef jEnv, JStringLocalRef s);
@ -40,6 +42,9 @@ namespace LibRyujinx
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
internal extern static void setRenderingThread(); internal extern static void setRenderingThread();
[DllImport("libryujinxjni")]
internal extern static void debug_break(int code);
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
internal extern static void onFrameEnd(double time); internal extern static void onFrameEnd(double time);
@ -67,7 +72,7 @@ namespace LibRyujinx
var init = Initialize(path, enableDebugLogs); var init = Initialize(path, enableDebugLogs);
AudioDriver = new OboeHardwareDeviceDriver(); _surfaceEvent?.Set();
_surfaceEvent = new ManualResetEvent(false); _surfaceEvent = new ManualResetEvent(false);
@ -97,6 +102,7 @@ namespace LibRyujinx
JStringLocalRef timeZone, JStringLocalRef timeZone,
JBoolean ignoreMissingServices) JBoolean ignoreMissingServices)
{ {
AudioDriver = new OboeHardwareDeviceDriver();
return InitializeDevice(isHostMapped, return InitializeDevice(isHostMapped,
useNce, useNce,
(SystemLanguage)(int)systemLanguage, (SystemLanguage)(int)systemLanguage,
@ -146,6 +152,34 @@ namespace LibRyujinx
return LoadApplication(path); return LoadApplication(path);
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcContentList")]
public static JArrayLocalRef JniGetDlcContentListNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JLong titleId)
{
var list = GetDlcContentList(GetString(jEnv, pathPtr), (ulong)(long)titleId);
debug_break(4);
return CreateStringArray(jEnv, list);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcTitleId")]
public static JStringLocalRef JniGetDlcTitleIdNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JStringLocalRef ncaPath)
{
return CreateString(jEnv, GetDlcTitleId(GetString(jEnv, pathPtr), GetString(jEnv, ncaPath)));
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceSignalEmulationClose")]
public static void JniSignalEmulationCloseNative(JEnvRef jEnv, JObjectLocalRef jObj)
{
SignalEmulationClose();
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceCloseEmulation")]
public static void JniCloseEmulationNative(JEnvRef jEnv, JObjectLocalRef jObj)
{
CloseEmulation();
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci) public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
{ {
@ -213,7 +247,7 @@ namespace LibRyujinx
public unsafe static JBoolean JniInitializeGraphicsRendererNative(JEnvRef jEnv, public unsafe static JBoolean JniInitializeGraphicsRendererNative(JEnvRef jEnv,
JObjectLocalRef jObj, JObjectLocalRef jObj,
JArrayLocalRef extensionsArray, JArrayLocalRef extensionsArray,
JLong surfacePtr) JLong driverHandle)
{ {
if (Renderer != null) if (Renderer != null)
{ {
@ -248,16 +282,17 @@ namespace LibRyujinx
extensions.Add(GetString(jEnv, ext)); extensions.Add(GetString(jEnv, ext));
} }
_surfaceEvent.Set(); if((long)driverHandle != 0)
{
_surfacePtr = surfacePtr; VulkanLoader = new VulkanLoader((IntPtr)(long)driverHandle);
}
CreateSurface createSurfaceFunc = instance => CreateSurface createSurfaceFunc = instance =>
{ {
_surfaceEvent.WaitOne(); _surfaceEvent.WaitOne();
_surfaceEvent.Reset(); _surfaceEvent.Reset();
var api = Vk.GetApi(); var api = VulkanLoader?.GetApi() ?? Vk.GetApi();
if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension)) if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension))
{ {
var createInfo = new AndroidSurfaceCreateInfoKHR var createInfo = new AndroidSurfaceCreateInfoKHR
@ -277,6 +312,27 @@ namespace LibRyujinx
return InitializeGraphicsRenderer(GraphicsBackend.Vulkan, createSurfaceFunc, extensions.ToArray()); return InitializeGraphicsRenderer(GraphicsBackend.Vulkan, createSurfaceFunc, extensions.ToArray());
} }
private static JArrayLocalRef CreateStringArray(JEnvRef jEnv, List<string> strings)
{
JEnvValue value = jEnv.Environment;
ref JNativeInterface jInterface = ref value.Functions;
IntPtr newObjectArrayPtr = jInterface.NewObjectArrayPointer;
IntPtr findClassPtr = jInterface.FindClassPointer;
IntPtr setObjectArrayElementPtr = jInterface.SetObjectArrayElementPointer;
var newObjectArray = newObjectArrayPtr.GetUnsafeDelegate<NewObjectArrayDelegate>();
var findClass = findClassPtr.GetUnsafeDelegate<FindClassDelegate>();
var setObjectArrayElement = setObjectArrayElementPtr.GetUnsafeDelegate<SetObjectArrayElementDelegate>();
var array = newObjectArray(jEnv, strings.Count, findClass(jEnv, GetCCharSequence("java/lang/String")), CreateString(jEnv, "")._value);
for (int i = 0; i < strings.Count; i++)
{
setObjectArrayElement(jEnv, array, i, CreateString(jEnv, strings[i])._value);
}
return array;
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSize")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSize")]
public static void JniSetRendererSizeNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) public static void JniSetRendererSizeNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height)
{ {

View File

@ -0,0 +1,99 @@
using Ryujinx.Common.Logging;
using Silk.NET.Core.Contexts;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace LibRyujinx
{
public class VulkanLoader : IDisposable
{
private delegate IntPtr GetInstanceProcAddress(IntPtr instance, IntPtr name);
private delegate IntPtr GetDeviceProcAddress(IntPtr device, IntPtr name);
private IntPtr _loadedLibrary = IntPtr.Zero;
private GetInstanceProcAddress _getInstanceProcAddr;
private GetDeviceProcAddress _getDeviceProcAddr;
public void Dispose()
{
if (_loadedLibrary != IntPtr.Zero)
{
NativeLibrary.Free(_loadedLibrary);
_loadedLibrary = IntPtr.Zero;
}
}
public VulkanLoader(IntPtr driver)
{
_loadedLibrary = driver;
if (_loadedLibrary != IntPtr.Zero)
{
var instanceGetProc = NativeLibrary.GetExport(_loadedLibrary, "vkGetInstanceProcAddr");
var deviceProc = NativeLibrary.GetExport(_loadedLibrary, "vkGetDeviceProcAddr");
_getInstanceProcAddr = Marshal.GetDelegateForFunctionPointer<GetInstanceProcAddress>(instanceGetProc);
_getDeviceProcAddr = Marshal.GetDelegateForFunctionPointer<GetDeviceProcAddress>(deviceProc);
}
}
public unsafe Vk GetApi()
{
if (_loadedLibrary == IntPtr.Zero)
{
return Vk.GetApi();
}
var ctx = new MultiNativeContext(new INativeContext[1]);
var ret = new Vk(ctx);
ctx.Contexts[0] = new LamdaNativeContext
(
x =>
{
var xPtr = Marshal.StringToHGlobalAnsi(x);
byte* xp = (byte*)xPtr;
LibRyujinx.debug_break(0);
try
{
nint ptr = default;
ptr = _getInstanceProcAddr(ret.CurrentInstance.GetValueOrDefault().Handle, xPtr);
if (ptr == default)
{
ptr = _getInstanceProcAddr(IntPtr.Zero, xPtr);
if (ptr == default)
{
var currentDevice = ret.CurrentDevice.GetValueOrDefault().Handle;
if (currentDevice != IntPtr.Zero)
{
ptr = _getDeviceProcAddr(currentDevice, xPtr);
}
if (ptr == default)
{
Logger.Warning?.Print(LogClass.Gpu, $"Failed to get function pointer: {x}");
}
}
}
return ptr;
}
finally
{
Marshal.FreeHGlobal(xPtr);
}
}
);
return ret;
}
}
}

View File

@ -23,10 +23,12 @@
android:largeHeap="true" android:largeHeap="true"
android:appCategory="game" android:appCategory="game"
android:theme="@style/Theme.RyujinxAndroid" android:theme="@style/Theme.RyujinxAndroid"
android:extractNativeLibs="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:hardwareAccelerated="false"
android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode" android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.RyujinxAndroid"> android:theme="@style/Theme.RyujinxAndroid">
<intent-filter> <intent-filter>

View File

@ -10,6 +10,11 @@ cmake_minimum_required(VERSION 3.22.1)
project("ryujinxjni") project("ryujinxjni")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
add_subdirectory("libraries/adrenotools")
# Creates and names a library, sets it as either STATIC # Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code. # or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you. # You can define multiple libraries, and CMake builds them for you.
@ -52,4 +57,6 @@ target_link_libraries( # Specifies the target library.
oboe::oboe oboe::oboe
${log-lib} ${log-lib}
-lvulkan -lvulkan
-landroid) -landroid
adrenotools
)

View File

@ -10,8 +10,11 @@ void AudioSession::initialize() {
} }
void AudioSession::destroy() { void AudioSession::destroy() {
if(stream == nullptr)
return;
stream->close(); stream->close();
delete stream;
stream = nullptr;
} }
void AudioSession::start() { void AudioSession::start() {
@ -37,101 +40,108 @@ extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_org_ryujinx_android_NativeHelpers_setDeviceId( Java_org_ryujinx_android_NativeHelpers_setDeviceId(
JNIEnv *env, JNIEnv *env,
jobject instance, jobject instance,
jint device_id){ jint device_id) {
s_device_id = device_id; s_device_id = device_id;
} }
AudioSession* create_session(int sample_format, AudioSession *create_session(int sample_format,
uint sample_rate, uint sample_rate,
uint channel_count) uint channel_count) {
{ using namespace oboe;
using namespace oboe;
AudioStreamBuilder builder; AudioStreamBuilder builder;
AudioFormat format; AudioFormat format;
switch (sample_format) { switch (sample_format) {
case 0: case 0:
format = AudioFormat::Invalid; format = AudioFormat::Invalid;
break; break;
case 1: case 1:
case 2: case 2:
format = AudioFormat::I16; format = AudioFormat::I16;
break; break;
case 3: case 3:
format = AudioFormat::I24; format = AudioFormat::I24;
break; break;
case 4: case 4:
format = AudioFormat::I32; format = AudioFormat::I32;
break; break;
case 5: case 5:
format = AudioFormat::Float; format = AudioFormat::Float;
break; break;
default: default:
std::ostringstream string; std::ostringstream string;
string << "Invalid Format" << sample_format; string << "Invalid Format" << sample_format;
throw std::runtime_error(string.str()); throw std::runtime_error(string.str());
} }
auto session = new AudioSession(); auto session = new AudioSession();
session->initialize(); session->initialize();
session->format = format; session->format = format;
session->channelCount = channel_count; session->channelCount = channel_count;
builder.setDirection(Direction::Output) builder.setDirection(Direction::Output)
->setPerformanceMode(PerformanceMode::LowLatency) ->setPerformanceMode(PerformanceMode::LowLatency)
->setSharingMode(SharingMode::Shared) ->setSharingMode(SharingMode::Shared)
->setFormat(format) ->setFormat(format)
->setChannelCount(channel_count) ->setChannelCount(channel_count)
->setSampleRate(sample_rate); ->setSampleRate(sample_rate);
AudioStream* stream; AudioStream *stream;
if(builder.openStream(&stream) != oboe::Result::OK) if (builder.openStream(&stream) != oboe::Result::OK) {
{
delete session;
return nullptr;
}
session->stream = stream;
return session;
}
void start_session(AudioSession* session)
{
session->start();
}
void stop_session(AudioSession* session)
{
session->stop();
}
void set_session_volume(AudioSession* session, float volume)
{
session->volume = volume;
}
float get_session_volume(AudioSession* session)
{
return session->volume;
}
void close_session(AudioSession* session)
{
session->destroy();
delete session; delete session;
session = nullptr;
return nullptr;
} }
session->stream = stream;
bool is_playing(AudioSession* session) { return session;
return session->isStarted; }
}
void start_session(AudioSession *session) {
void write_to_session(AudioSession* session, uint64_t data, uint64_t samples) if (session == nullptr)
{ return;
session->read(data, samples); session->start();
} }
void stop_session(AudioSession *session) {
if (session == nullptr)
return;
session->stop();
}
void set_session_volume(AudioSession *session, float volume) {
if (session == nullptr)
return;
session->volume = volume;
}
float get_session_volume(AudioSession *session) {
if (session == nullptr)
return 0;
return session->volume;
}
void close_session(AudioSession *session) {
if (session == nullptr)
return;
session->destroy();
delete session;
}
bool is_playing(AudioSession *session) {
if (session == nullptr)
return false;
return session->isStarted;
}
void write_to_session(AudioSession *session, uint64_t data, uint64_t samples) {
if (session == nullptr)
return;
session->read(data, samples);
}
} }

View File

@ -7,6 +7,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <dlfcn.h> #include <dlfcn.h>
#include <string.h> #include <string.h>
#include <string>
#include <jni.h> #include <jni.h>
#include <exception> #include <exception>
#include <android/log.h> #include <android/log.h>
@ -15,6 +16,8 @@
#include "vulkan_wrapper.h" #include "vulkan_wrapper.h"
#include <vulkan/vulkan_android.h> #include <vulkan/vulkan_android.h>
#include <cassert> #include <cassert>
#include <fcntl.h>
#include "libraries/adrenotools/include/adrenotools/driver.h"
// A macro to pass call to Vulkan and check for return value for success // A macro to pass call to Vulkan and check for return value for success
#define CALL_VK(func) \ #define CALL_VK(func) \

View File

@ -19,6 +19,7 @@
#include "ryuijnx.h" #include "ryuijnx.h"
#include "pthread.h" #include "pthread.h"
#include <chrono> #include <chrono>
#include <csignal>
jmethodID _updateFrameTime; jmethodID _updateFrameTime;
JNIEnv* _rendererEnv = nullptr; JNIEnv* _rendererEnv = nullptr;
@ -179,13 +180,48 @@ Java_org_ryujinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) {
} }
extern "C" extern "C"
void onFrameEnd(double time){ void onFrameEnd(double time) {
auto env = getEnv(true); auto env = getEnv(true);
auto cl = env->FindClass("org/ryujinx/android/MainActivity"); auto cl = env->FindClass("org/ryujinx/android/MainActivity");
_updateFrameTime = env->GetStaticMethodID( cl , "updateRenderSessionPerformance", "(J)V"); _updateFrameTime = env->GetStaticMethodID(cl, "updateRenderSessionPerformance", "(J)V");
auto now = std::chrono::high_resolution_clock::now(); auto now = std::chrono::high_resolution_clock::now();
auto nano = std::chrono::duration_cast<std::chrono::nanoseconds>(now-_currentTimePoint).count(); auto nano = std::chrono::duration_cast<std::chrono::nanoseconds>(
now - _currentTimePoint).count();
env->CallStaticVoidMethod(cl, _updateFrameTime, env->CallStaticVoidMethod(cl, _updateFrameTime,
nano); nano);
} }
extern "C"
JNIEXPORT jlong JNICALL
Java_org_ryujinx_android_NativeHelpers_loadDriver(JNIEnv *env, jobject thiz,
jstring native_lib_path,
jstring private_apps_path,
jstring driver_name) {
auto libPath = getStringPointer(env, native_lib_path);
auto privateAppsPath = getStringPointer(env, private_apps_path);
auto driverName = getStringPointer(env, driver_name);
auto handle = adrenotools_open_libvulkan(
RTLD_NOW,
ADRENOTOOLS_DRIVER_CUSTOM,
nullptr,
libPath,
privateAppsPath,
driverName,
nullptr,
nullptr
);
delete libPath;
delete privateAppsPath;
delete driverName;
return (jlong)handle;
}
extern "C"
void debug_break(int code){
if(code >= 3)
int r = 0;
}

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
@ -27,11 +28,47 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.ryujinx.android.viewmodels.MainViewModel
typealias GamePad = RadialGamePad typealias GamePad = RadialGamePad
typealias GamePadConfig = RadialGamePadConfig typealias GamePadConfig = RadialGamePadConfig
class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) { class GameController(var activity: Activity) {
companion object{
private fun Create(context: Context, activity: Activity, controller: GameController) : View
{
val inflator = LayoutInflater.from(context)
val view = inflator.inflate(R.layout.game_layout, null)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel) : Unit
{
AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context ->
val controller = GameController(viewModel.activity)
val c = Create(context, viewModel.activity, controller)
viewModel.activity.lifecycleScope.apply {
viewModel.activity.lifecycleScope.launch {
val events = merge(controller.leftGamePad.events(),controller.rightGamePad.events())
.shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily)
events.safeCollect {
controller.handleEvent(it)
}
}
}
controller.controllerView = c
viewModel.setGameController(controller)
c
})
}
}
private var ryujinxNative: RyujinxNative
private var controllerView: View? = null private var controllerView: View? = null
var leftGamePad: GamePad var leftGamePad: GamePad
var rightGamePad: GamePad var rightGamePad: GamePad
@ -56,36 +93,8 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
leftGamePad.gravityY = 1f leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f rightGamePad.gravityY = 1f
}
@Composable ryujinxNative = RyujinxNative()
fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit
{
AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)})
lifecycleScope.apply {
lifecycleScope.launch {
val events = merge(leftGamePad.events(),rightGamePad.events())
.shareIn(lifecycleScope, SharingStarted.Lazily)
events.safeCollect {
handleEvent(it)
}
}
}
}
private fun Create(context: Context) : View
{
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
return controllerView as View
} }
fun setVisible(isVisible: Boolean){ fun setVisible(isVisible: Boolean){
@ -99,7 +108,7 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
fun connect(){ fun connect(){
if(controllerId == -1) if(controllerId == -1)
controllerId = ryujinxNative.inputConnectGamepad(0) controllerId = RyujinxNative().inputConnectGamepad(0)
} }
private fun handleEvent(ev: Event) { private fun handleEvent(ev: Event) {

View File

@ -7,9 +7,12 @@ import android.view.SurfaceView
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.viewmodels.QuickSettings
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
import java.io.File
import kotlin.concurrent.thread import kotlin.concurrent.thread
class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
private var _isClosed: Boolean = false
private var _renderingThreadWatcher: Thread? = null private var _renderingThreadWatcher: Thread? = null
private var _height: Int = 0 private var _height: Int = 0
private var _width: Int = 0 private var _width: Int = 0
@ -19,14 +22,9 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
private var _isInit: Boolean = false private var _isInit: Boolean = false
private var _isStarted: Boolean = false private var _isStarted: Boolean = false
private var _nativeWindow: Long = 0 private var _nativeWindow: Long = 0
private var _nativeHelper: NativeHelpers = NativeHelpers()
private var _nativeRyujinx: RyujinxNative = RyujinxNative() private var _nativeRyujinx: RyujinxNative = RyujinxNative()
companion object {
var gameModel: GameModel? = null
}
init { init {
holder.addCallback(this) holder.addCallback(this)
} }
@ -35,11 +33,11 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
val isStarted = _isStarted if(_isClosed)
return
start(holder) start(holder)
if(isStarted && (_width != width || _height != height)) if(_width != width || _height != height)
{ {
val nativeHelpers = NativeHelpers() val nativeHelpers = NativeHelpers()
val window = nativeHelpers.getNativeWindow(holder.surface) val window = nativeHelpers.getNativeWindow(holder.surface)
@ -59,66 +57,33 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
} }
fun close(){
_isClosed = true
_isInit = false
_isStarted = false
_updateThread?.join()
_renderingThreadWatcher?.join()
}
private fun start(surfaceHolder: SurfaceHolder) { private fun start(surfaceHolder: SurfaceHolder) {
val game = gameModel ?: return mainViewModel.gameHost = this
val path = game.getPath() ?: return if(_isStarted)
if (_isStarted) return;
return
var surface = surfaceHolder.surface
val settings = QuickSettings(mainViewModel.activity)
var success = _nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply {
EnableShaderCache = settings.enableShaderCache
EnableTextureRecompression = settings.enableTextureRecompression
ResScale = settings.resScale
})
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
success = _nativeRyujinx.graphicsInitializeRenderer(
nativeInterop!!.VkRequiredExtensions!!,
window
)
success = _nativeRyujinx.deviceInitialize(
settings.isHostMapped,
settings.useNce,
SystemLanguage.AmericanEnglish.ordinal,
RegionCode.USA.ordinal,
settings.enableVsync,
settings.enableDocked,
settings.enablePtc,
false,
"UTC",
settings.ignoreMissingServices
)
success = _nativeRyujinx.deviceLoad(path)
_nativeRyujinx.inputInitialize(width, height) _nativeRyujinx.inputInitialize(width, height)
val settings = QuickSettings(mainViewModel.activity)
if(!settings.useVirtualController){ if(!settings.useVirtualController){
controller.setVisible(false) mainViewModel.controller?.setVisible(false)
} }
else{ else{
controller.connect() mainViewModel.controller?.connect()
} }
mainViewModel.activity.physicalControllerManager.connect() mainViewModel.activity.physicalControllerManager.connect()
//
_nativeRyujinx.graphicsRendererSetSize( _nativeRyujinx.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.width(),
surfaceHolder.surfaceFrame.height() surfaceHolder.surfaceFrame.height()
@ -127,7 +92,7 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
_guestThread = thread(start = true) { _guestThread = thread(start = true) {
runGame() runGame()
} }
_isStarted = success _isStarted = true
_updateThread = thread(start = true) { _updateThread = thread(start = true) {
var c = 0 var c = 0
@ -161,6 +126,7 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
mainViewModel.performanceManager?.initializeRenderingSession(threadId) mainViewModel.performanceManager?.initializeRenderingSession(threadId)
} }
} }
mainViewModel.performanceManager?.closeCurrentRenderingSession()
} }
} }
_nativeRyujinx.graphicsRendererRunLoop() _nativeRyujinx.graphicsRendererRunLoop()

View File

@ -0,0 +1,182 @@
package org.ryujinx.android
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
class Icons {
companion object{
/// Icons exported from https://www.composables.com/icons
@Composable
fun Download(): ImageVector {
return remember {
ImageVector.Builder(
name = "download",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black),
fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(20f, 26.25f)
quadToRelative(-0.25f, 0f, -0.479f, -0.083f)
quadToRelative(-0.229f, -0.084f, -0.438f, -0.292f)
lineToRelative(-6.041f, -6.083f)
quadToRelative(-0.417f, -0.375f, -0.396f, -0.917f)
quadToRelative(0.021f, -0.542f, 0.396f, -0.917f)
reflectiveQuadToRelative(0.916f, -0.396f)
quadToRelative(0.542f, -0.02f, 0.959f, 0.396f)
lineToRelative(3.791f, 3.792f)
verticalLineTo(8.292f)
quadToRelative(0f, -0.584f, 0.375f, -0.959f)
reflectiveQuadTo(20f, 6.958f)
quadToRelative(0.542f, 0f, 0.938f, 0.375f)
quadToRelative(0.395f, 0.375f, 0.395f, 0.959f)
verticalLineTo(21.75f)
lineToRelative(3.792f, -3.792f)
quadToRelative(0.375f, -0.416f, 0.917f, -0.396f)
quadToRelative(0.541f, 0.021f, 0.958f, 0.396f)
quadToRelative(0.375f, 0.375f, 0.375f, 0.917f)
reflectiveQuadToRelative(-0.375f, 0.958f)
lineToRelative(-6.083f, 6.042f)
quadToRelative(-0.209f, 0.208f, -0.438f, 0.292f)
quadToRelative(-0.229f, 0.083f, -0.479f, 0.083f)
close()
moveTo(9.542f, 32.958f)
quadToRelative(-1.042f, 0f, -1.834f, -0.791f)
quadToRelative(-0.791f, -0.792f, -0.791f, -1.834f)
verticalLineToRelative(-4.291f)
quadToRelative(0f, -0.542f, 0.395f, -0.938f)
quadToRelative(0.396f, -0.396f, 0.938f, -0.396f)
quadToRelative(0.542f, 0f, 0.917f, 0.396f)
reflectiveQuadToRelative(0.375f, 0.938f)
verticalLineToRelative(4.291f)
horizontalLineToRelative(20.916f)
verticalLineToRelative(-4.291f)
quadToRelative(0f, -0.542f, 0.375f, -0.938f)
quadToRelative(0.375f, -0.396f, 0.917f, -0.396f)
quadToRelative(0.583f, 0f, 0.958f, 0.396f)
reflectiveQuadToRelative(0.375f, 0.938f)
verticalLineToRelative(4.291f)
quadToRelative(0f, 1.042f, -0.791f, 1.834f)
quadToRelative(-0.792f, 0.791f, -1.834f, 0.791f)
close()
}
}.build()
}
}
@Composable
fun VideoGame(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
name = "videogame_asset",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
fillAlpha = 1f,
stroke = SolidColor(primaryColor),
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(6.25f, 29.792f)
quadToRelative(-1.083f, 0f, -1.854f, -0.792f)
quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f)
verticalLineTo(12.833f)
quadToRelative(0f, -1.083f, 0.771f, -1.854f)
quadToRelative(0.771f, -0.771f, 1.854f, -0.771f)
horizontalLineToRelative(27.5f)
quadToRelative(1.083f, 0f, 1.854f, 0.771f)
quadToRelative(0.771f, 0.771f, 0.771f, 1.854f)
verticalLineToRelative(14.334f)
quadToRelative(0f, 1.041f, -0.771f, 1.833f)
reflectiveQuadToRelative(-1.854f, 0.792f)
close()
moveToRelative(0f, -2.625f)
horizontalLineToRelative(27.5f)
verticalLineTo(12.833f)
horizontalLineTo(6.25f)
verticalLineToRelative(14.334f)
close()
moveToRelative(7.167f, -1.792f)
quadToRelative(0.541f, 0f, 0.916f, -0.375f)
reflectiveQuadToRelative(0.375f, -0.917f)
verticalLineToRelative(-2.791f)
horizontalLineToRelative(2.75f)
quadToRelative(0.584f, 0f, 0.959f, -0.375f)
reflectiveQuadToRelative(0.375f, -0.917f)
quadToRelative(0f, -0.542f, -0.375f, -0.938f)
quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f)
horizontalLineToRelative(-2.75f)
verticalLineToRelative(-2.75f)
quadToRelative(0f, -0.542f, -0.375f, -0.938f)
quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f)
quadToRelative(-0.584f, 0f, -0.959f, 0.396f)
reflectiveQuadToRelative(-0.375f, 0.938f)
verticalLineToRelative(2.75f)
horizontalLineToRelative(-2.75f)
quadToRelative(-0.541f, 0f, -0.937f, 0.395f)
quadTo(8f, 19.458f, 8f, 20f)
quadToRelative(0f, 0.542f, 0.396f, 0.917f)
reflectiveQuadToRelative(0.937f, 0.375f)
horizontalLineToRelative(2.75f)
verticalLineToRelative(2.791f)
quadToRelative(0f, 0.542f, 0.396f, 0.917f)
reflectiveQuadToRelative(0.938f, 0.375f)
close()
moveToRelative(11.125f, -0.5f)
quadToRelative(0.791f, 0f, 1.396f, -0.583f)
quadToRelative(0.604f, -0.584f, 0.604f, -1.375f)
quadToRelative(0f, -0.834f, -0.604f, -1.417f)
quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f)
quadToRelative(-0.834f, 0f, -1.417f, 0.583f)
quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f)
quadToRelative(0f, 0.833f, 0.583f, 1.417f)
quadToRelative(0.583f, 0.583f, 1.417f, 0.583f)
close()
moveToRelative(3.916f, -5.833f)
quadToRelative(0.834f, 0f, 1.417f, -0.584f)
quadToRelative(0.583f, -0.583f, 0.583f, -1.416f)
quadToRelative(0f, -0.792f, -0.583f, -1.375f)
quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f)
quadToRelative(-0.791f, 0f, -1.375f, 0.584f)
quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f)
quadToRelative(0f, 0.833f, 0.583f, 1.416f)
quadToRelative(0.584f, 0.584f, 1.375f, 0.584f)
close()
moveTo(6.25f, 27.167f)
verticalLineTo(12.833f)
verticalLineToRelative(14.334f)
close()
}
}.build()
}
}
}
}

View File

@ -12,6 +12,7 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -25,8 +26,10 @@ import androidx.core.view.WindowInsetsControllerCompat
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
import org.ryujinx.android.views.HomeViews import org.ryujinx.android.views.HomeViews
import org.ryujinx.android.views.MainView import org.ryujinx.android.views.MainView
import java.io.File
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -60,15 +63,22 @@ class MainActivity : ComponentActivity() {
external fun getRenderingThreadId() : Long external fun getRenderingThreadId() : Long
external fun initVm() external fun initVm()
fun setFullScreen() { fun setFullScreen(fullscreen: Boolean) {
requestedOrientation = requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
val 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()) if (fullscreen) {
insets.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} else {
insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
} }
} }
@ -93,7 +103,8 @@ class MainActivity : ComponentActivity() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent?): Boolean { override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
event?.apply { event?.apply {
return physicalControllerManager.onKeyEvent(this) if(physicalControllerManager.onKeyEvent(this))
return true;
} }
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }

View File

@ -15,4 +15,6 @@ class NativeHelpers {
external fun getNativeWindow(surface:Surface) : Long external fun getNativeWindow(surface:Surface) : Long
external fun attachCurrentThread() : Unit external fun attachCurrentThread() : Unit
external fun detachCurrentThread() : Unit external fun detachCurrentThread() : Unit
external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long
} }

View File

@ -25,7 +25,7 @@ class RyujinxNative {
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
external fun graphicsInitializeRenderer( external fun graphicsInitializeRenderer(
extensions: Array<String>, extensions: Array<String>,
surface: Long driver: Long
): Boolean ): Boolean
external fun deviceLoad(game: String): Boolean external fun deviceLoad(game: String): Boolean
@ -47,5 +47,9 @@ class RyujinxNative {
external fun inputSetButtonReleased(button: Int, id: Int): Unit external fun inputSetButtonReleased(button: Int, id: Int): Unit
external fun inputConnectGamepad(index: Int): Int external fun inputConnectGamepad(index: Int): Int
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int): Unit external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int): Unit
external fun graphicsSetSurface(surface: Long): String external fun graphicsSetSurface(surface: Long)
external fun deviceCloseEmulation()
external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
} }

View File

@ -0,0 +1,152 @@
package org.ryujinx.android.viewmodels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.file.getAbsolutePath
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.ryujinx.android.MainActivity
import org.ryujinx.android.RyujinxNative
import java.io.File
class DlcViewModel(val titleId: String) {
private var storageHelper: SimpleStorageHelper
companion object {
const val UpdateRequestCode = 1002
}
fun remove(item: DlcItem) {
data?.apply {
this.removeAll { it.path == item.containerPath }
}
}
fun add(refresh: MutableState<Boolean>) {
val callBack = storageHelper.onFileSelected
storageHelper.onFileSelected = { requestCode, files ->
run {
storageHelper.onFileSelected = callBack
if (requestCode == UpdateRequestCode) {
val file = files.firstOrNull()
file?.apply {
val path = file.getAbsolutePath(storageHelper.storage.context)
if (path.isNotEmpty()) {
data?.apply {
var contents = RyujinxNative().deviceGetDlcContentList(
path,
titleId.toLong(16)
)
if (contents.isNotEmpty()) {
val contentPath = path
val container = DlcContainerList(contentPath);
for (content in contents)
container.dlc_nca_list.add(
DlcContainer(
true,
titleId,
content
)
)
this.add(container)
}
}
}
}
refresh.value = true
}
}
}
storageHelper.openFilePicker(UpdateRequestCode)
}
fun save(items: List<DlcItem>) {
data?.apply {
val gson = Gson()
val json = gson.toJson(this)
jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
File(jsonPath).mkdirs()
File("$jsonPath/dlc.json").writeText(json)
}
}
@Composable
fun getDlc(): List<DlcItem> {
var items = mutableListOf<DlcItem>()
data?.apply {
for (container in this) {
val containerPath = container.path
if (!File(containerPath).exists())
continue;
for (dlc in container.dlc_nca_list) {
val enabled = remember {
mutableStateOf(dlc.enabled)
}
items.add(
DlcItem(
File(containerPath).name,
enabled,
containerPath,
dlc.fullPath,
RyujinxNative().deviceGetDlcTitleId(containerPath, dlc.fullPath)
)
)
}
}
}
return items.toList()
}
var data: MutableList<DlcContainerList>? = null
private var jsonPath: String
init {
jsonPath =
MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json"
storageHelper = MainActivity.StorageHelper!!
reloadFromDisk()
}
private fun reloadFromDisk() {
data = mutableListOf()
if (File(jsonPath).exists()) {
val gson = Gson()
val typeToken = object : TypeToken<MutableList<DlcContainerList>>() {}.type
data =
gson.fromJson<MutableList<DlcContainerList>>(File(jsonPath).readText(), typeToken)
}
}
}
data class DlcContainerList(
var path: String = "",
var dlc_nca_list: MutableList<DlcContainer> = mutableListOf()
)
data class DlcContainer(
var enabled: Boolean = false,
var titleId: String = "",
var fullPath: String = "")
data class DlcItem(
var name:String = "",
var isEnabled: MutableState<Boolean> = mutableStateOf(false),
var containerPath: String = "",
var fullPath: String = "",
var titleId: String = "")

View File

@ -6,18 +6,28 @@ import android.os.Build
import android.os.PerformanceHintManager import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.GraphicsConfiguration
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.NativeGraphicsInterop
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.PerformanceManager import org.ryujinx.android.PerformanceManager
import org.ryujinx.android.RegionCode
import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.SystemLanguage
import java.io.File
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) { class MainViewModel(val activity: MainActivity) {
var gameHost: GameHost? = null
var controller: GameController? = null
var performanceManager: PerformanceManager? = null var performanceManager: PerformanceManager? = null
var selected: GameModel? = null var selected: GameModel? = null
private var gameTimeState: MutableState<Double>? = null private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null private var fifoState: MutableState<Double>? = null
private var navController : NavHostController? = null var navController : NavHostController? = null
var homeViewModel: HomeViewModel = HomeViewModel(activity, this) var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
@ -29,15 +39,104 @@ class MainViewModel(val activity: MainActivity) {
} }
} }
fun loadGame(game:GameModel) { fun closeGame() {
val controller = navController?: return RyujinxNative().deviceSignalEmulationClose()
activity.setFullScreen() gameHost?.close()
GameHost.gameModel = game RyujinxNative().deviceCloseEmulation()
controller.navigate("game") goBack()
activity.setFullScreen(false)
} }
fun setNavController(controller: NavHostController) { fun goBack(){
navController = controller navController?.popBackStack()
}
fun loadGame(game:GameModel) : Boolean {
var nativeRyujinx = RyujinxNative()
val path = game.getPath() ?: return false
val settings = QuickSettings(activity)
var success = nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply {
EnableShaderCache = settings.enableShaderCache
EnableTextureRecompression = settings.enableTextureRecompression
ResScale = settings.resScale
})
if(!success)
return false
val nativeHelpers = NativeHelpers()
var nativeInterop = NativeGraphicsInterop()
nativeInterop!!.VkRequiredExtensions = arrayOf(
"VK_KHR_surface", "VK_KHR_android_surface"
)
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
nativeInterop!!.SurfaceHandle = 0
var driverViewModel = VulkanDriverViewModel(activity);
var drivers = driverViewModel.getAvailableDrivers()
var driverHandle = 0L;
if (driverViewModel.selected.isNotEmpty()) {
var metaData = drivers.find { it.driverPath == driverViewModel.selected }
metaData?.apply {
var privatePath = activity.filesDir;
var privateDriverPath = privatePath.canonicalPath + "/driver/"
val pD = File(privateDriverPath)
if (pD.exists())
pD.deleteRecursively()
pD.mkdirs()
var driver = File(driverViewModel.selected)
var parent = driver.parentFile
for (file in parent.walkTopDown()) {
if (file.absolutePath == parent.absolutePath)
continue
file.copyTo(File(privateDriverPath + file.name), true)
}
driverHandle = NativeHelpers().loadDriver(
activity.applicationInfo.nativeLibraryDir!! + "/",
privateDriverPath,
this.libraryName
)
}
}
success = nativeRyujinx.graphicsInitializeRenderer(
nativeInterop!!.VkRequiredExtensions!!,
driverHandle
)
if(!success)
return false
success = nativeRyujinx.deviceInitialize(
settings.isHostMapped,
settings.useNce,
SystemLanguage.AmericanEnglish.ordinal,
RegionCode.USA.ordinal,
settings.enableVsync,
settings.enableDocked,
settings.enablePtc,
false,
"UTC",
settings.ignoreMissingServices
)
if(!success)
return false
success = nativeRyujinx.deviceLoad(path)
if(!success)
return false
return true
} }
fun setStatStates( fun setStatStates(
@ -65,4 +164,11 @@ class MainViewModel(val activity: MainActivity) {
this.value = gameTime this.value = gameTime
} }
} }
fun setGameController(controller: GameController) {
this.controller = controller
}
fun backCalled() {
}
} }

View File

@ -0,0 +1,134 @@
package org.ryujinx.android.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.ryujinx.android.viewmodels.DlcItem
import org.ryujinx.android.viewmodels.DlcViewModel
class DlcViews {
companion object {
@Composable
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
val viewModel = DlcViewModel(titleId)
var dlcList = remember {
mutableListOf<DlcItem>()
}
viewModel.data?.apply {
dlcList.clear()
}
var refresh = remember {
mutableStateOf(true)
}
Column(modifier = Modifier.padding(16.dp)) {
Column {
Row(modifier = Modifier.padding(8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align(
Alignment.CenterVertically
))
IconButton(
onClick = {
viewModel.add(refresh)
},
modifier = Modifier.align(
Alignment.CenterVertically
)
) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
)
}
}
Surface(
modifier = Modifier
.padding(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
if(refresh.value) {
dlcList.clear()
dlcList.addAll(viewModel.getDlc())
refresh.value = false
}
LazyColumn(modifier = Modifier
.fillMaxWidth()
.height(400.dp)){
items(dlcList) { dlcItem ->
dlcItem.apply {
Row(modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Checkbox(
checked = (dlcItem.isEnabled.value),
onCheckedChange = { dlcItem.isEnabled.value = it })
Text(
text = dlcItem.name,
modifier = Modifier
.align(Alignment.CenterVertically)
.wrapContentWidth(Alignment.Start)
.fillMaxWidth(0.9f)
)
IconButton(
onClick = {
viewModel.remove(dlcItem)
refresh.value = true
}) {
Icon(Icons.Filled.Delete,
contentDescription = "remove"
)
}
}
}
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
TextButton(
modifier = Modifier.align(Alignment.End),
onClick = {
openDialog.value = false
viewModel.save(dlcList)
},
) {
Text("Save")
}
}
}
}
}

View File

@ -32,6 +32,7 @@ import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -59,6 +60,9 @@ import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.R import org.ryujinx.android.R
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
@ -143,14 +147,16 @@ class HomeViews {
Column { Column {
TextButton(onClick = { TextButton(onClick = {
navController.navigate("settings") navController.navigate("settings")
}, modifier = Modifier.fillMaxWidth() }, modifier = Modifier
.fillMaxWidth()
.align(Alignment.Start), .align(Alignment.Start),
) { ) {
Icon( Icon(
Icons.Filled.Settings, Icons.Filled.Settings,
contentDescription = "Settings" contentDescription = "Settings"
) )
Text(text = "Settings", modifier = Modifier.padding(16.dp) Text(text = "Settings", modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterVertically)) .align(Alignment.CenterVertically))
} }
} }
@ -167,6 +173,7 @@ class HomeViews {
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val showBottomSheet = remember { mutableStateOf(false) } val showBottomSheet = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -198,22 +205,42 @@ class HomeViews {
items(list) { items(list) {
it.titleName?.apply { it.titleName?.apply {
if (this.isNotEmpty()) if (this.isNotEmpty())
GameItem(it, viewModel, showBottomSheet) GameItem(it, viewModel, showBottomSheet, showLoading)
} }
} }
} }
} }
if(showLoading.value){
AlertDialog(onDismissRequest = { }) {
Card(modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier
.padding(16.dp)
.fillMaxWidth()) {
Text(text = "Loading")
LinearProgressIndicator(modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp))
}
}
}
}
if(showBottomSheet.value) { if(showBottomSheet.value) {
ModalBottomSheet(onDismissRequest = { ModalBottomSheet(onDismissRequest = {
showBottomSheet.value = false showBottomSheet.value = false
}, },
sheetState = sheetState) { sheetState = sheetState) {
val openDialog = remember { mutableStateOf(false) } val openTitleUpdateDialog = remember { mutableStateOf(false) }
val openDlcDialog = remember { mutableStateOf(false) }
if(openDialog.value) { if(openTitleUpdateDialog.value) {
AlertDialog(onDismissRequest = { AlertDialog(onDismissRequest = {
openDialog.value = false openTitleUpdateDialog.value = false
}) { }) {
Surface( Surface(
modifier = Modifier modifier = Modifier
@ -224,24 +251,43 @@ class HomeViews {
) { ) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: "" val name = viewModel.mainViewModel?.selected?.titleName ?: ""
TitleUpdateViews.Main(titleId, name, openDialog) TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog)
} }
} }
} }
if(openDlcDialog.value) {
AlertDialog(onDismissRequest = {
openDlcDialog.value = false
}) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
DlcViews.Main(titleId, name, openDlcDialog)
}
}
}
Surface(color = MaterialTheme.colorScheme.surface, Surface(color = MaterialTheme.colorScheme.surface,
modifier = Modifier.padding(16.dp)) { modifier = Modifier.padding(16.dp)) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
Card( Card(
modifier = Modifier.padding(8.dp),
onClick = { onClick = {
openDialog.value = true openTitleUpdateDialog.value = true
} }
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Icon( Icon(
painter = painterResource(R.drawable.app_update), painter = painterResource(R.drawable.app_update),
contentDescription = "More", contentDescription = "Game Updates",
tint = Color.Green, tint = Color.Green,
modifier = Modifier modifier = Modifier
.width(48.dp) .width(48.dp)
@ -254,6 +300,28 @@ class HomeViews {
} }
} }
Card(
modifier = Modifier.padding(8.dp),
onClick = {
openDlcDialog.value = true
}
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
imageVector = org.ryujinx.android.Icons.Download(),
contentDescription = "Game Dlc",
tint = Color.Green,
modifier = Modifier
.width(48.dp)
.height(48.dp)
.align(Alignment.CenterHorizontally)
)
Text(text = "Game DLC",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface)
}
}
} }
} }
} }
@ -264,15 +332,35 @@ class HomeViews {
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState<Boolean>) { fun GameItem(
Card(shape = MaterialTheme.shapes.medium, gameModel: GameModel,
viewModel: HomeViewModel,
showSheet: MutableState<Boolean>,
showLoading: MutableState<Boolean>
) {
Surface(shape = MaterialTheme.shapes.medium,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
viewModel.mainViewModel?.loadGame(gameModel) runBlocking {
launch {
showLoading.value = true
val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false
if (success) {
launchOnUiThread {
viewModel.mainViewModel?.activity?.setFullScreen(
true
)
viewModel.mainViewModel?.navController?.navigate("game")
}
}
showLoading.value = false
}
}
} }
}, },
onLongClick = { onLongClick = {

View File

@ -1,10 +1,19 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -26,12 +35,12 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.Icons
import org.ryujinx.android.RyujinxNative import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
@ -40,35 +49,40 @@ import kotlin.math.roundToInt
class MainView { class MainView {
companion object { companion object {
@Composable @Composable
fun Main(mainViewModel: MainViewModel){ fun Main(mainViewModel: MainViewModel) {
val navController = rememberNavController() val navController = rememberNavController()
mainViewModel.setNavController(navController) mainViewModel.navController = navController
NavHost(navController = navController, startDestination = "home") { NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
composable("game") { GameView(mainViewModel) } composable("game") { GameView(mainViewModel) }
composable("settings") { SettingViews.Main(SettingsViewModel(navController, mainViewModel.activity)) } composable("settings") {
SettingViews.Main(
SettingsViewModel(
navController,
mainViewModel.activity
)
)
}
} }
} }
@Composable @Composable
fun GameView(mainViewModel: MainViewModel){ fun GameView(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
val controller = remember {
GameController(mainViewModel.activity)
}
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
factory = { context -> factory = { context ->
GameHost(context, controller, mainViewModel) GameHost(context, mainViewModel)
} }
) )
GameOverlay(mainViewModel, controller) GameOverlay(mainViewModel)
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GameOverlay(mainViewModel: MainViewModel, controller: GameController){ fun GameOverlay(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
GameStats(mainViewModel) GameStats(mainViewModel)
@ -84,9 +98,6 @@ class MainView {
Thread.sleep(2) Thread.sleep(2)
val event = awaitPointerEvent() val event = awaitPointerEvent()
if(controller.isVisible)
continue
val change = event val change = event
.component1() .component1()
.firstOrNull() .firstOrNull()
@ -100,10 +111,12 @@ class MainView {
position.y.roundToInt() position.y.roundToInt()
) )
} }
PointerEventType.Release -> { PointerEventType.Release -> {
ryujinxNative.inputReleaseTouchPoint() ryujinxNative.inputReleaseTouchPoint()
} }
PointerEventType.Move -> { PointerEventType.Move -> {
ryujinxNative.inputSetTouchPoint( ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(), position.x.roundToInt(),
@ -117,116 +130,76 @@ class MainView {
} }
}) { }) {
} }
controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle) GameController.Compose(mainViewModel)
Row(modifier = Modifier Row(
.align(Alignment.BottomCenter) modifier = Modifier
.padding(8.dp)) { .align(Alignment.BottomCenter)
IconButton(modifier = Modifier.padding(4.dp),onClick = { .padding(8.dp)
controller.setVisible(!controller.isVisible) ) {
IconButton(modifier = Modifier.padding(4.dp), onClick = {
mainViewModel.controller?.setVisible(!mainViewModel.controller!!.isVisible)
}) { }) {
Icon(imageVector = rememberVideogameAsset(), contentDescription = "Toggle Virtual Pad") Icon(
imageVector = Icons.VideoGame(),
contentDescription = "Toggle Virtual Pad"
)
}
}
var showBackNotice = remember {
mutableStateOf(false)
}
BackHandler {
showBackNotice.value = true
}
if (showBackNotice.value) {
AlertDialog(onDismissRequest = { showBackNotice.value = false }) {
Column {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Are you sure you want to exit the game?")
Text(text = "All unsaved data will be lost!")
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Button(onClick = {
mainViewModel.closeGame()
}, modifier = Modifier.padding(16.dp)) {
Text(text = "Exit Game")
}
Button(onClick = {
showBackNotice.value = false
}, modifier = Modifier.padding(16.dp)) {
Text(text = "Dismiss")
}
}
}
}
}
} }
} }
} }
} }
@Composable @Composable
fun rememberVideogameAsset(): ImageVector { fun GameStats(mainViewModel: MainViewModel) {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
name = "videogame_asset",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
fillAlpha = 1f,
stroke = SolidColor(primaryColor),
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(6.25f, 29.792f)
quadToRelative(-1.083f, 0f, -1.854f, -0.792f)
quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f)
verticalLineTo(12.833f)
quadToRelative(0f, -1.083f, 0.771f, -1.854f)
quadToRelative(0.771f, -0.771f, 1.854f, -0.771f)
horizontalLineToRelative(27.5f)
quadToRelative(1.083f, 0f, 1.854f, 0.771f)
quadToRelative(0.771f, 0.771f, 0.771f, 1.854f)
verticalLineToRelative(14.334f)
quadToRelative(0f, 1.041f, -0.771f, 1.833f)
reflectiveQuadToRelative(-1.854f, 0.792f)
close()
moveToRelative(0f, -2.625f)
horizontalLineToRelative(27.5f)
verticalLineTo(12.833f)
horizontalLineTo(6.25f)
verticalLineToRelative(14.334f)
close()
moveToRelative(7.167f, -1.792f)
quadToRelative(0.541f, 0f, 0.916f, -0.375f)
reflectiveQuadToRelative(0.375f, -0.917f)
verticalLineToRelative(-2.791f)
horizontalLineToRelative(2.75f)
quadToRelative(0.584f, 0f, 0.959f, -0.375f)
reflectiveQuadToRelative(0.375f, -0.917f)
quadToRelative(0f, -0.542f, -0.375f, -0.938f)
quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f)
horizontalLineToRelative(-2.75f)
verticalLineToRelative(-2.75f)
quadToRelative(0f, -0.542f, -0.375f, -0.938f)
quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f)
quadToRelative(-0.584f, 0f, -0.959f, 0.396f)
reflectiveQuadToRelative(-0.375f, 0.938f)
verticalLineToRelative(2.75f)
horizontalLineToRelative(-2.75f)
quadToRelative(-0.541f, 0f, -0.937f, 0.395f)
quadTo(8f, 19.458f, 8f, 20f)
quadToRelative(0f, 0.542f, 0.396f, 0.917f)
reflectiveQuadToRelative(0.937f, 0.375f)
horizontalLineToRelative(2.75f)
verticalLineToRelative(2.791f)
quadToRelative(0f, 0.542f, 0.396f, 0.917f)
reflectiveQuadToRelative(0.938f, 0.375f)
close()
moveToRelative(11.125f, -0.5f)
quadToRelative(0.791f, 0f, 1.396f, -0.583f)
quadToRelative(0.604f, -0.584f, 0.604f, -1.375f)
quadToRelative(0f, -0.834f, -0.604f, -1.417f)
quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f)
quadToRelative(-0.834f, 0f, -1.417f, 0.583f)
quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f)
quadToRelative(0f, 0.833f, 0.583f, 1.417f)
quadToRelative(0.583f, 0.583f, 1.417f, 0.583f)
close()
moveToRelative(3.916f, -5.833f)
quadToRelative(0.834f, 0f, 1.417f, -0.584f)
quadToRelative(0.583f, -0.583f, 0.583f, -1.416f)
quadToRelative(0f, -0.792f, -0.583f, -1.375f)
quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f)
quadToRelative(-0.791f, 0f, -1.375f, 0.584f)
quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f)
quadToRelative(0f, 0.833f, 0.583f, 1.416f)
quadToRelative(0.584f, 0.584f, 1.375f, 0.584f)
close()
moveTo(6.25f, 27.167f)
verticalLineTo(12.833f)
verticalLineToRelative(14.334f)
close()
}
}.build()
}
}
@Composable
fun GameStats(mainViewModel: MainViewModel){
val fifo = remember { val fifo = remember {
mutableStateOf(0.0) mutableStateOf(0.0)
} }
@ -237,8 +210,10 @@ class MainView {
mutableStateOf(0.0) mutableStateOf(0.0)
} }
Surface(modifier = Modifier.padding(16.dp), Surface(
color = MaterialTheme.colorScheme.surface.copy(0.4f)) { modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.surface.copy(0.4f)
) {
Column { Column {
var gameTimeVal = 0.0 var gameTimeVal = 0.0
if (!gameTime.value.isInfinite()) if (!gameTime.value.isInfinite())

View File

@ -13,24 +13,34 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -42,6 +52,7 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
class SettingViews { class SettingViews {
companion object { companion object {
@ -124,16 +135,6 @@ class SettingViews {
}) })
}) { contentPadding -> }) { contentPadding ->
Column(modifier = Modifier.padding(contentPadding)) { Column(modifier = Modifier.padding(contentPadding)) {
BackHandler {
settingsViewModel.save(
isHostMapped,
useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
enableShaderCache,
enableTextureRecompression,
resScale,
useVirtualController
)
}
ExpandableView(onCardArrowClick = { }, title = "System") { ExpandableView(onCardArrowClick = { }, title = "System") {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row( Row(
@ -279,7 +280,7 @@ class SettingViews {
enableTextureRecompression.value = !enableTextureRecompression.value enableTextureRecompression.value = !enableTextureRecompression.value
}) })
} }
/*Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .padding(8.dp),
@ -332,7 +333,7 @@ class SettingViews {
.fillMaxWidth() .fillMaxWidth()
.height(300.dp)) { .height(300.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(
@ -358,7 +359,7 @@ class SettingViews {
for (driver in drivers) { for (driver in drivers) {
var ind = driverIndex var ind = driverIndex
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(
@ -369,18 +370,23 @@ class SettingViews {
driverViewModel.selected = driverViewModel.selected =
driver.driverPath driver.driverPath
}) })
Column { Column(modifier = Modifier.clickable {
selectedDriver.value =
ind
isChanged.value =
true
driverViewModel.selected =
driver.driverPath
}) {
Text(text = driver.libraryName, Text(text = driver.libraryName,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth())
.clickable { Text(text = driver.driverVersion,
selectedDriver.value = modifier = Modifier
ind .fillMaxWidth())
isChanged.value = Text(text = driver.description,
true modifier = Modifier
driverViewModel.selected = .fillMaxWidth())
driver.driverPath
})
} }
} }
@ -423,7 +429,7 @@ class SettingViews {
Text(text = "Drivers") Text(text = "Drivers")
} }
} }
*/
} }
} }
ExpandableView(onCardArrowClick = { }, title = "Input") { ExpandableView(onCardArrowClick = { }, title = "Input") {
@ -446,6 +452,18 @@ class SettingViews {
} }
} }
} }
BackHandler() {
settingsViewModel.save(
isHostMapped,
useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
enableShaderCache,
enableTextureRecompression,
resScale,
useVirtualController
)
settingsViewModel.navController.popBackStack()
}
} }
} }

View File

@ -107,7 +107,7 @@ class TitleUpdateViews {
) { ) {
Icon( Icon(
Icons.Filled.Add, Icons.Filled.Add,
contentDescription = "Remove" contentDescription = "Add"
) )
} }
} }