diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..3b9fa893b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libadrenotools"] + path = src/RyujinxAndroid/app/src/main/cpp/libraries/adrenotools + url = https://github.com/bylaws/libadrenotools.git diff --git a/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceDriver.cs b/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceDriver.cs index c56d0fe38..7377b171e 100644 --- a/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceDriver.cs +++ b/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceDriver.cs @@ -62,11 +62,6 @@ namespace LibRyujinx.Shared.Audio.Oboe return session; } - internal bool Unregister(OboeHardwareDeviceSession session) - { - return _sessions.TryRemove(session, out _); - } - public void Dispose() { Dispose(true); @@ -82,6 +77,8 @@ namespace LibRyujinx.Shared.Audio.Oboe } _pauseEvent.Dispose(); + + _sessions.Clear(); } } diff --git a/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceSession.cs b/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceSession.cs index b23426d30..76b41d802 100644 --- a/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceSession.cs +++ b/src/LibRyujinx/Android/Audio/Oboe/OboeHardwareDeviceSession.cs @@ -10,6 +10,7 @@ namespace LibRyujinx.Shared.Audio.Oboe internal class OboeHardwareDeviceSession : HardwareDeviceSessionOutputBase { private OboeHardwareDeviceDriver _driver; + private bool _isClosed; private bool _isWorkerActive; private Queue _queuedBuffers; private bool _isActive; @@ -59,6 +60,9 @@ namespace LibRyujinx.Shared.Audio.Oboe { StartIfNotPlaying(); + if (_isClosed) + break; + fixed(byte* ptr = buffer.Data) OboeInterop.WriteToSession(_session, (ulong)ptr, buffer.SampleCount); @@ -90,18 +94,31 @@ namespace LibRyujinx.Shared.Audio.Oboe public override void Dispose() { + if (_session == 0) + return; + + PrepareToClose(); + OboeInterop.CloseSession(_session); + + _session = 0; } + public override void PrepareToClose() { + _isClosed = true; _isWorkerActive = false; - _workerThread.Join(); + _workerThread?.Join(); + Stop(); } private void StartIfNotPlaying() { lock (_trackLock) { + if (_isClosed) + return; + if (OboeInterop.IsPlaying(_session) == 0) { Start(); @@ -145,6 +162,9 @@ namespace LibRyujinx.Shared.Audio.Oboe public override void Start() { + if (_isClosed) + return; + OboeInterop.StartSession(_session); } diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index 77f5518f9..7e6351421 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -31,6 +31,8 @@ namespace LibRyujinx private static ManualResetEvent _surfaceEvent; private static long _surfacePtr; + public static VulkanLoader? VulkanLoader { get; private set; } + [DllImport("libryujinxjni")] private extern static IntPtr getStringPointer(JEnvRef jEnv, JStringLocalRef s); @@ -40,6 +42,9 @@ namespace LibRyujinx [DllImport("libryujinxjni")] internal extern static void setRenderingThread(); + [DllImport("libryujinxjni")] + internal extern static void debug_break(int code); + [DllImport("libryujinxjni")] internal extern static void onFrameEnd(double time); @@ -67,7 +72,7 @@ namespace LibRyujinx var init = Initialize(path, enableDebugLogs); - AudioDriver = new OboeHardwareDeviceDriver(); + _surfaceEvent?.Set(); _surfaceEvent = new ManualResetEvent(false); @@ -97,6 +102,7 @@ namespace LibRyujinx JStringLocalRef timeZone, JBoolean ignoreMissingServices) { + AudioDriver = new OboeHardwareDeviceDriver(); return InitializeDevice(isHostMapped, useNce, (SystemLanguage)(int)systemLanguage, @@ -146,6 +152,34 @@ namespace LibRyujinx 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")] 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, JObjectLocalRef jObj, JArrayLocalRef extensionsArray, - JLong surfacePtr) + JLong driverHandle) { if (Renderer != null) { @@ -248,16 +282,17 @@ namespace LibRyujinx extensions.Add(GetString(jEnv, ext)); } - _surfaceEvent.Set(); - - _surfacePtr = surfacePtr; + if((long)driverHandle != 0) + { + VulkanLoader = new VulkanLoader((IntPtr)(long)driverHandle); + } CreateSurface createSurfaceFunc = instance => { _surfaceEvent.WaitOne(); _surfaceEvent.Reset(); - var api = Vk.GetApi(); + var api = VulkanLoader?.GetApi() ?? Vk.GetApi(); if (api.TryGetInstanceExtension(new Instance(instance), out KhrAndroidSurface surfaceExtension)) { var createInfo = new AndroidSurfaceCreateInfoKHR @@ -277,6 +312,27 @@ namespace LibRyujinx return InitializeGraphicsRenderer(GraphicsBackend.Vulkan, createSurfaceFunc, extensions.ToArray()); } + private static JArrayLocalRef CreateStringArray(JEnvRef jEnv, List 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(); + var findClass = findClassPtr.GetUnsafeDelegate(); + var setObjectArrayElement = setObjectArrayElementPtr.GetUnsafeDelegate(); + 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")] public static void JniSetRendererSizeNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) { diff --git a/src/LibRyujinx/VulkanLoader.cs b/src/LibRyujinx/VulkanLoader.cs new file mode 100644 index 000000000..5b832b3b4 --- /dev/null +++ b/src/LibRyujinx/VulkanLoader.cs @@ -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(instanceGetProc); + _getDeviceProcAddr = Marshal.GetDelegateForFunctionPointer(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; + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index b33362363..4501665de 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -23,10 +23,12 @@ android:largeHeap="true" android:appCategory="game" android:theme="@style/Theme.RyujinxAndroid" + android:extractNativeLibs="true" tools:targetApi="31"> diff --git a/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt index 3060d2fb4..6ae2af030 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt +++ b/src/RyujinxAndroid/app/src/main/cpp/CMakeLists.txt @@ -10,6 +10,11 @@ cmake_minimum_required(VERSION 3.22.1) 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 # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. @@ -52,4 +57,6 @@ target_link_libraries( # Specifies the target library. oboe::oboe ${log-lib} -lvulkan - -landroid) + -landroid + adrenotools + ) diff --git a/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp b/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp index 61b605ab5..f643a4fbe 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/oboe.cpp @@ -10,8 +10,11 @@ void AudioSession::initialize() { } void AudioSession::destroy() { + if(stream == nullptr) + return; stream->close(); - delete stream; + + stream = nullptr; } void AudioSession::start() { @@ -37,101 +40,108 @@ extern "C" JNIEXPORT void JNICALL Java_org_ryujinx_android_NativeHelpers_setDeviceId( JNIEnv *env, -jobject instance, - jint device_id){ + jobject instance, + jint device_id) { s_device_id = device_id; } - AudioSession* create_session(int sample_format, - uint sample_rate, - uint channel_count) - { - using namespace oboe; +AudioSession *create_session(int sample_format, + uint sample_rate, + uint channel_count) { + using namespace oboe; - AudioStreamBuilder builder; + AudioStreamBuilder builder; - AudioFormat format; + AudioFormat format; - switch (sample_format) { - case 0: - format = AudioFormat::Invalid; - break; - case 1: - case 2: - format = AudioFormat::I16; - break; - case 3: - format = AudioFormat::I24; - break; - case 4: - format = AudioFormat::I32; - break; - case 5: - format = AudioFormat::Float; - break; - default: - std::ostringstream string; - string << "Invalid Format" << sample_format; + switch (sample_format) { + case 0: + format = AudioFormat::Invalid; + break; + case 1: + case 2: + format = AudioFormat::I16; + break; + case 3: + format = AudioFormat::I24; + break; + case 4: + format = AudioFormat::I32; + break; + case 5: + format = AudioFormat::Float; + break; + default: + std::ostringstream string; + string << "Invalid Format" << sample_format; - throw std::runtime_error(string.str()); - } + throw std::runtime_error(string.str()); + } - auto session = new AudioSession(); - session->initialize(); + auto session = new AudioSession(); + session->initialize(); - session->format = format; - session->channelCount = channel_count; + session->format = format; + session->channelCount = channel_count; - builder.setDirection(Direction::Output) + builder.setDirection(Direction::Output) ->setPerformanceMode(PerformanceMode::LowLatency) - ->setSharingMode(SharingMode::Shared) - ->setFormat(format) - ->setChannelCount(channel_count) - ->setSampleRate(sample_rate); - AudioStream* stream; - if(builder.openStream(&stream) != oboe::Result::OK) - { - delete session; - 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(); - + ->setSharingMode(SharingMode::Shared) + ->setFormat(format) + ->setChannelCount(channel_count) + ->setSampleRate(sample_rate); + AudioStream *stream; + if (builder.openStream(&stream) != oboe::Result::OK) { delete session; + session = nullptr; + return nullptr; } + session->stream = stream; - bool is_playing(AudioSession* session) { - return session->isStarted; - } + return session; +} - void write_to_session(AudioSession* session, uint64_t data, uint64_t samples) - { - session->read(data, samples); - } +void start_session(AudioSession *session) { + if (session == nullptr) + return; + session->start(); +} + +void stop_session(AudioSession *session) { + if (session == nullptr) + return; + session->stop(); +} + +void set_session_volume(AudioSession *session, float volume) { + if (session == nullptr) + return; + session->volume = volume; +} + +float get_session_volume(AudioSession *session) { + if (session == nullptr) + return 0; + return session->volume; +} + +void close_session(AudioSession *session) { + if (session == nullptr) + return; + session->destroy(); + + delete session; +} + +bool is_playing(AudioSession *session) { + if (session == nullptr) + return false; + return session->isStarted; +} + +void write_to_session(AudioSession *session, uint64_t data, uint64_t samples) { + if (session == nullptr) + return; + session->read(data, samples); +} } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h index 8770e3e87..b9573694c 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,8 @@ #include "vulkan_wrapper.h" #include #include +#include +#include "libraries/adrenotools/include/adrenotools/driver.h" // A macro to pass call to Vulkan and check for return value for success #define CALL_VK(func) \ diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index ad7e4b0b9..18d9fb67a 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -19,6 +19,7 @@ #include "ryuijnx.h" #include "pthread.h" #include +#include jmethodID _updateFrameTime; JNIEnv* _rendererEnv = nullptr; @@ -179,13 +180,48 @@ Java_org_ryujinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) { } extern "C" -void onFrameEnd(double time){ +void onFrameEnd(double time) { auto env = getEnv(true); 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 nano = std::chrono::duration_cast(now-_currentTimePoint).count(); + auto nano = std::chrono::duration_cast( + now - _currentTimePoint).count(); env->CallStaticVoidMethod(cl, _updateFrameTime, 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; } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt index 2266a2eb7..aaf97cd3e 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope import com.swordfish.radialgamepad.library.RadialGamePad import com.swordfish.radialgamepad.library.config.ButtonConfig 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.shareIn import kotlinx.coroutines.launch +import org.ryujinx.android.viewmodels.MainViewModel typealias GamePad = RadialGamePad 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(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(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 var leftGamePad: GamePad var rightGamePad: GamePad @@ -56,36 +93,8 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = leftGamePad.gravityY = 1f rightGamePad.gravityX = 1f rightGamePad.gravityY = 1f - } - @Composable - 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(R.id.leftcontainer)!!.addView(leftGamePad) - view.findViewById(R.id.rightcontainer)!!.addView(rightGamePad) - - controllerView = view - - return controllerView as View + ryujinxNative = RyujinxNative() } fun setVisible(isVisible: Boolean){ @@ -99,7 +108,7 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = fun connect(){ if(controllerId == -1) - controllerId = ryujinxNative.inputConnectGamepad(0) + controllerId = RyujinxNative().inputConnectGamepad(0) } private fun handleEvent(ev: Event) { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt index 1ac670f0c..38cd171b1 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -7,9 +7,12 @@ import android.view.SurfaceView import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.QuickSettings +import org.ryujinx.android.viewmodels.VulkanDriverViewModel +import java.io.File 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 _height: 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 _isStarted: Boolean = false private var _nativeWindow: Long = 0 - private var _nativeHelper: NativeHelpers = NativeHelpers() private var _nativeRyujinx: RyujinxNative = RyujinxNative() - companion object { - var gameModel: GameModel? = null - } - init { 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) { - val isStarted = _isStarted - + if(_isClosed) + return start(holder) - if(isStarted && (_width != width || _height != height)) + if(_width != width || _height != height) { val nativeHelpers = NativeHelpers() 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) { - val game = gameModel ?: return - val path = game.getPath() ?: return - if (_isStarted) - 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) + mainViewModel.gameHost = this + if(_isStarted) + return; _nativeRyujinx.inputInitialize(width, height) + val settings = QuickSettings(mainViewModel.activity) + if(!settings.useVirtualController){ - controller.setVisible(false) + mainViewModel.controller?.setVisible(false) } else{ - controller.connect() + mainViewModel.controller?.connect() } mainViewModel.activity.physicalControllerManager.connect() - // - _nativeRyujinx.graphicsRendererSetSize( surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.height() @@ -127,7 +92,7 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo _guestThread = thread(start = true) { runGame() } - _isStarted = success + _isStarted = true _updateThread = thread(start = true) { var c = 0 @@ -161,6 +126,7 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo mainViewModel.performanceManager?.initializeRenderingSession(threadId) } } + mainViewModel.performanceManager?.closeCurrentRenderingSession() } } _nativeRyujinx.graphicsRendererRunLoop() diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt new file mode 100644 index 000000000..96aa04bba --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 0a8ce1c68..178ee43b6 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -12,6 +12,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.WindowManager import androidx.activity.ComponentActivity +import androidx.activity.addCallback import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme @@ -25,8 +26,10 @@ import androidx.core.view.WindowInsetsControllerCompat import com.anggrayudi.storage.SimpleStorageHelper import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.VulkanDriverViewModel import org.ryujinx.android.views.HomeViews import org.ryujinx.android.views.MainView +import java.io.File class MainActivity : ComponentActivity() { @@ -60,15 +63,22 @@ class MainActivity : ComponentActivity() { external fun getRenderingThreadId() : Long external fun initVm() - fun setFullScreen() { + fun setFullScreen(fullscreen: Boolean) { requestedOrientation = - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER val insets = WindowCompat.getInsetsController(window, window.decorView) insets.apply { - insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) - insets.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + if (fullscreen) { + insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } } } @@ -93,7 +103,8 @@ class MainActivity : ComponentActivity() { @SuppressLint("RestrictedApi") override fun dispatchKeyEvent(event: KeyEvent?): Boolean { event?.apply { - return physicalControllerManager.onKeyEvent(this) + if(physicalControllerManager.onKeyEvent(this)) + return true; } return super.dispatchKeyEvent(event) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt index 32ff5f1d3..4b52c2862 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -15,4 +15,6 @@ class NativeHelpers { external fun getNativeWindow(surface:Surface) : Long external fun attachCurrentThread() : Unit external fun detachCurrentThread() : Unit + + external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index 7400d309a..c89259147 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -25,7 +25,7 @@ class RyujinxNative { external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitializeRenderer( extensions: Array, - surface: Long + driver: Long ): Boolean external fun deviceLoad(game: String): Boolean @@ -47,5 +47,9 @@ class RyujinxNative { external fun inputSetButtonReleased(button: Int, id: Int): Unit external fun inputConnectGamepad(index: Int): Int 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 } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt new file mode 100644 index 000000000..1d440c11c --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt @@ -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) { + 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) { + 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 { + var items = mutableListOf() + + 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? = 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>() {}.type + data = + gson.fromJson>(File(jsonPath).readText(), typeToken) + } + + } +} + +data class DlcContainerList( + var path: String = "", + var dlc_nca_list: MutableList = mutableListOf() +) + +data class DlcContainer( + var enabled: Boolean = false, + var titleId: String = "", + var fullPath: String = "") + +data class DlcItem( + var name:String = "", + var isEnabled: MutableState = mutableStateOf(false), + var containerPath: String = "", + var fullPath: String = "", + var titleId: String = "") \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index 1c1e34399..e8361e455 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -6,18 +6,28 @@ import android.os.Build import android.os.PerformanceHintManager import androidx.compose.runtime.MutableState import androidx.navigation.NavHostController +import org.ryujinx.android.GameController import org.ryujinx.android.GameHost +import org.ryujinx.android.GraphicsConfiguration import org.ryujinx.android.MainActivity +import org.ryujinx.android.NativeGraphicsInterop +import org.ryujinx.android.NativeHelpers 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") class MainViewModel(val activity: MainActivity) { + var gameHost: GameHost? = null + var controller: GameController? = null var performanceManager: PerformanceManager? = null var selected: GameModel? = null private var gameTimeState: MutableState? = null private var gameFpsState: MutableState? = null private var fifoState: MutableState? = null - private var navController : NavHostController? = null + var navController : NavHostController? = null var homeViewModel: HomeViewModel = HomeViewModel(activity, this) @@ -29,15 +39,104 @@ class MainViewModel(val activity: MainActivity) { } } - fun loadGame(game:GameModel) { - val controller = navController?: return - activity.setFullScreen() - GameHost.gameModel = game - controller.navigate("game") + fun closeGame() { + RyujinxNative().deviceSignalEmulationClose() + gameHost?.close() + RyujinxNative().deviceCloseEmulation() + goBack() + activity.setFullScreen(false) } - fun setNavController(controller: NavHostController) { - navController = controller + fun goBack(){ + 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( @@ -65,4 +164,11 @@ class MainViewModel(val activity: MainActivity) { this.value = gameTime } } + + fun setGameController(controller: GameController) { + this.controller = controller + } + + fun backCalled() { + } } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt new file mode 100644 index 000000000..ea14f654b --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt @@ -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) { + val viewModel = DlcViewModel(titleId) + + var dlcList = remember { + mutableListOf() + } + + 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") + } + } + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index 8a2c1c3dc..4d9b56fea 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -59,6 +60,9 @@ import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.zIndex import androidx.navigation.NavHostController 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.R import org.ryujinx.android.viewmodels.GameModel @@ -143,14 +147,16 @@ class HomeViews { Column { TextButton(onClick = { navController.navigate("settings") - }, modifier = Modifier.fillMaxWidth() + }, modifier = Modifier + .fillMaxWidth() .align(Alignment.Start), ) { Icon( Icons.Filled.Settings, contentDescription = "Settings" ) - Text(text = "Settings", modifier = Modifier.padding(16.dp) + Text(text = "Settings", modifier = Modifier + .padding(16.dp) .align(Alignment.CenterVertically)) } } @@ -167,6 +173,7 @@ class HomeViews { val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() val showBottomSheet = remember { mutableStateOf(false) } + val showLoading = remember { mutableStateOf(false) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -198,22 +205,42 @@ class HomeViews { items(list) { it.titleName?.apply { 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) { ModalBottomSheet(onDismissRequest = { showBottomSheet.value = false }, 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 = { - openDialog.value = false + openTitleUpdateDialog.value = false }) { Surface( modifier = Modifier @@ -224,24 +251,43 @@ class HomeViews { ) { val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" 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, modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { Card( + modifier = Modifier.padding(8.dp), onClick = { - openDialog.value = true + openTitleUpdateDialog.value = true } ) { Column(modifier = Modifier.padding(16.dp)) { Icon( painter = painterResource(R.drawable.app_update), - contentDescription = "More", + contentDescription = "Game Updates", tint = Color.Green, modifier = Modifier .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) @Composable - fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState) { - Card(shape = MaterialTheme.shapes.medium, + fun GameItem( + gameModel: GameModel, + viewModel: HomeViewModel, + showSheet: MutableState, + showLoading: MutableState + ) { + Surface(shape = MaterialTheme.shapes.medium, modifier = Modifier .fillMaxWidth() .padding(8.dp) .combinedClickable( onClick = { 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 = { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index 8ec1d5650..b37699768 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -1,10 +1,19 @@ package org.ryujinx.android.views +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton 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.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import org.ryujinx.android.GameController import org.ryujinx.android.GameHost +import org.ryujinx.android.Icons import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.SettingsViewModel @@ -40,35 +49,40 @@ import kotlin.math.roundToInt class MainView { companion object { @Composable - fun Main(mainViewModel: MainViewModel){ + fun Main(mainViewModel: MainViewModel) { val navController = rememberNavController() - mainViewModel.setNavController(navController) + mainViewModel.navController = navController NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("game") { GameView(mainViewModel) } - composable("settings") { SettingViews.Main(SettingsViewModel(navController, mainViewModel.activity)) } + composable("settings") { + SettingViews.Main( + SettingsViewModel( + navController, + mainViewModel.activity + ) + ) + } } } @Composable - fun GameView(mainViewModel: MainViewModel){ + fun GameView(mainViewModel: MainViewModel) { Box(modifier = Modifier.fillMaxSize()) { - val controller = remember { - GameController(mainViewModel.activity) - } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> - GameHost(context, controller, mainViewModel) + GameHost(context, mainViewModel) } ) - GameOverlay(mainViewModel, controller) + GameOverlay(mainViewModel) } } + @OptIn(ExperimentalMaterial3Api::class) @Composable - fun GameOverlay(mainViewModel: MainViewModel, controller: GameController){ + fun GameOverlay(mainViewModel: MainViewModel) { Box(modifier = Modifier.fillMaxSize()) { GameStats(mainViewModel) @@ -84,9 +98,6 @@ class MainView { Thread.sleep(2) val event = awaitPointerEvent() - if(controller.isVisible) - continue - val change = event .component1() .firstOrNull() @@ -100,10 +111,12 @@ class MainView { position.y.roundToInt() ) } + PointerEventType.Release -> { ryujinxNative.inputReleaseTouchPoint() } + PointerEventType.Move -> { ryujinxNative.inputSetTouchPoint( position.x.roundToInt(), @@ -117,116 +130,76 @@ class MainView { } }) { } - controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle) - Row(modifier = Modifier - .align(Alignment.BottomCenter) - .padding(8.dp)) { - IconButton(modifier = Modifier.padding(4.dp),onClick = { - controller.setVisible(!controller.isVisible) + GameController.Compose(mainViewModel) + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp) + ) { + 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 - fun rememberVideogameAsset(): 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() - } - } - - @Composable - fun GameStats(mainViewModel: MainViewModel){ + fun GameStats(mainViewModel: MainViewModel) { val fifo = remember { mutableStateOf(0.0) } @@ -237,8 +210,10 @@ class MainView { mutableStateOf(0.0) } - Surface(modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.surface.copy(0.4f)) { + Surface( + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.surface.copy(0.4f) + ) { Column { var gameTimeVal = 0.0 if (!gameTime.value.isInfinite()) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index ae7823747..f7cc010c0 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -13,24 +13,34 @@ 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.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 @@ -42,6 +52,7 @@ 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 { @@ -124,16 +135,6 @@ class SettingViews { }) }) { contentPadding -> Column(modifier = Modifier.padding(contentPadding)) { - BackHandler { - settingsViewModel.save( - isHostMapped, - useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices, - enableShaderCache, - enableTextureRecompression, - resScale, - useVirtualController - ) - } ExpandableView(onCardArrowClick = { }, title = "System") { Column(modifier = Modifier.fillMaxWidth()) { Row( @@ -279,7 +280,7 @@ class SettingViews { enableTextureRecompression.value = !enableTextureRecompression.value }) } - /*Row( + Row( modifier = Modifier .fillMaxWidth() .padding(8.dp), @@ -332,7 +333,7 @@ class SettingViews { .fillMaxWidth() .height(300.dp)) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( @@ -358,7 +359,7 @@ class SettingViews { for (driver in drivers) { var ind = driverIndex Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( @@ -369,18 +370,23 @@ class SettingViews { driverViewModel.selected = driver.driverPath }) - Column { + Column(modifier = Modifier.clickable { + selectedDriver.value = + ind + isChanged.value = + true + driverViewModel.selected = + driver.driverPath + }) { Text(text = driver.libraryName, modifier = Modifier - .fillMaxWidth() - .clickable { - selectedDriver.value = - ind - isChanged.value = - true - driverViewModel.selected = - driver.driverPath - }) + .fillMaxWidth()) + Text(text = driver.driverVersion, + modifier = Modifier + .fillMaxWidth()) + Text(text = driver.description, + modifier = Modifier + .fillMaxWidth()) } } @@ -423,7 +429,7 @@ class SettingViews { Text(text = "Drivers") } } - */ + } } 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() + } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt index ce832199a..ff0900268 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -107,7 +107,7 @@ class TitleUpdateViews { ) { Icon( Icons.Filled.Add, - contentDescription = "Remove" + contentDescription = "Add" ) } }