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

View File

@ -10,6 +10,7 @@ namespace LibRyujinx.Shared.Audio.Oboe
internal class OboeHardwareDeviceSession : HardwareDeviceSessionOutputBase
{
private OboeHardwareDeviceDriver _driver;
private bool _isClosed;
private bool _isWorkerActive;
private Queue<OboeAudioBuffer> _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);
}

View File

@ -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<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")]
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:appCategory="game"
android:theme="@style/Theme.RyujinxAndroid"
android:extractNativeLibs="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:hardwareAccelerated="false"
android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.RyujinxAndroid">
<intent-filter>

View File

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

View File

@ -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;
}
void write_to_session(AudioSession* session, uint64_t data, uint64_t samples)
{
session->read(data, samples);
}
return session;
}
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);
}
}

View File

@ -7,6 +7,7 @@
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include <string>
#include <jni.h>
#include <exception>
#include <android/log.h>
@ -15,6 +16,8 @@
#include "vulkan_wrapper.h"
#include <vulkan/vulkan_android.h>
#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
#define CALL_VK(func) \

View File

@ -19,6 +19,7 @@
#include "ryuijnx.h"
#include "pthread.h"
#include <chrono>
#include <csignal>
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<std::chrono::nanoseconds>(now-_currentTimePoint).count();
auto nano = std::chrono::duration_cast<std::chrono::nanoseconds>(
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;
}

View File

@ -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<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
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<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad)
view.findViewById<FrameLayout>(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) {

View File

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

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

View File

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

View File

@ -25,7 +25,7 @@ class RyujinxNative {
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
external fun graphicsInitializeRenderer(
extensions: Array<String>,
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<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 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<Double>? = null
private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = 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() {
}
}

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.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<Boolean>) {
Card(shape = MaterialTheme.shapes.medium,
fun GameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showSheet: MutableState<Boolean>,
showLoading: MutableState<Boolean>
) {
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 = {

View File

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

View File

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

View File

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