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:
parent
f28f2dfeeb
commit
b28f9a6331
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "libadrenotools"]
|
||||
path = src/RyujinxAndroid/app/src/main/cpp/libraries/adrenotools
|
||||
url = https://github.com/bylaws/libadrenotools.git
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
99
src/LibRyujinx/VulkanLoader.cs
Normal file
99
src/LibRyujinx/VulkanLoader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) \
|
||||
|
@ -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;
|
||||
}
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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>
|
||||
}
|
@ -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 = "")
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = {
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ class TitleUpdateViews {
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Remove"
|
||||
contentDescription = "Add"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user