1
0
forked from MeloNX/MeloNX

android - add physical controller support

add performance hints

expand full screen to behind cutouts

fix touch, add toggle for virtual gamepad

remove safe area margins
This commit is contained in:
Emmanuel Hansen 2023-07-20 16:33:43 +00:00
parent c57f6a7fe3
commit fcb511bbca
16 changed files with 525 additions and 42 deletions

View File

@ -36,6 +36,12 @@ namespace LibRyujinx
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
[DllImport("libryujinxjni")]
internal extern static void setRenderingThread();
[DllImport("libryujinxjni")]
internal extern static void onFrameEnd(double time);
public delegate IntPtr JniCreateSurface(IntPtr native_surface, IntPtr instance); public delegate IntPtr JniCreateSurface(IntPtr native_surface, IntPtr instance);
[UnmanagedCallersOnly(EntryPoint = "JNI_OnLoad")] [UnmanagedCallersOnly(EntryPoint = "JNI_OnLoad")]
@ -279,6 +285,11 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")]
public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj) public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj)
{ {
SetSwapBuffersCallback(() =>
{
var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime();
onFrameEnd(time);
});
RunLoop(); RunLoop();
} }

View File

@ -112,6 +112,11 @@ namespace LibRyujinx
_isActive = true; _isActive = true;
if (Ryujinx.Common.SystemInfo.SystemInfo.IsBionic)
{
setRenderingThread();
}
while (_isActive) while (_isActive)
{ {
if (_isStopped) if (_isStopped)

View File

@ -20,6 +20,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true" android:largeHeap="true"
android:appCategory="game"
android:theme="@style/Theme.RyujinxAndroid" android:theme="@style/Theme.RyujinxAndroid"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity

View File

@ -36,4 +36,10 @@ void* _ryujinxNative = NULL;
// Ryujinx imported functions // Ryujinx imported functions
bool (*initialize)(char*) = NULL; bool (*initialize)(char*) = NULL;
long _renderingThreadId = 0;
long _currentRenderingThreadId = 0;
JavaVM* _vm = nullptr;
jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr;
#endif //RYUJINXNATIVE_RYUIJNX_H #endif //RYUJINXNATIVE_RYUIJNX_H

View File

@ -17,6 +17,31 @@
// } // }
#include "ryuijnx.h" #include "ryuijnx.h"
#include "pthread.h"
#include <chrono>
jmethodID _updateFrameTime;
JNIEnv* _rendererEnv = nullptr;
std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> _currentTimePoint;
JNIEnv* getEnv(bool isRenderer){
JNIEnv* env;
if(isRenderer){
env = _rendererEnv;
}
if(env != nullptr)
return env;
auto result = _vm->AttachCurrentThread(&env, NULL);
return env;
}
void detachEnv(){
auto result = _vm->DetachCurrentThread();
}
extern "C" extern "C"
{ {
@ -129,3 +154,38 @@ jstring createString(
} }
extern "C"
JNIEXPORT jlong JNICALL
Java_org_ryujinx_android_MainActivity_getRenderingThreadId(JNIEnv *env, jobject thiz) {
return _currentRenderingThreadId;
}
extern "C"
void setRenderingThread(){
auto currentId = pthread_self();
_currentRenderingThreadId = currentId;
_renderingThreadId = currentId;
_currentTimePoint = std::chrono::high_resolution_clock::now();
}
extern "C"
JNIEXPORT void JNICALL
Java_org_ryujinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) {
JavaVM* vm = nullptr;
auto success = env->GetJavaVM(&vm);
_vm = vm;
_mainActivity = thiz;
_mainActivityClass = env->GetObjectClass(thiz);
}
extern "C"
void onFrameEnd(double time){
auto env = getEnv(true);
auto cl = env->FindClass("org/ryujinx/android/MainActivity");
_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();
env->CallStaticVoidMethod(cl, _updateFrameTime,
nano);
}

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
@ -45,9 +46,18 @@ typealias GamePad = RadialGamePad
typealias GamePadConfig = RadialGamePadConfig typealias GamePadConfig = RadialGamePadConfig
class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) { class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) {
private var controllerView: View? = null
var leftGamePad: GamePad var leftGamePad: GamePad
var rightGamePad: GamePad var rightGamePad: GamePad
var controllerId: Int = -1 var controllerId: Int = -1
val isVisible : Boolean
get() {
controllerView?.apply {
return this.isVisible
}
return false;
}
init { init {
leftGamePad = GamePad(generateConfig(true), 16f, activity) leftGamePad = GamePad(generateConfig(true), 16f, activity)
@ -65,7 +75,6 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
@Composable @Composable
fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit
{ {
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)}) modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)})
@ -88,7 +97,18 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad); view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad);
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad); view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad);
return view as View controllerView = view
return controllerView as View
}
fun setVisible(isVisible: Boolean){
controllerView?.apply {
this.isVisible = isVisible
if(isVisible)
connect()
}
} }
fun connect(){ fun connect(){

View File

@ -1,13 +1,10 @@
package org.ryujinx.android package org.ryujinx.android
import android.content.Context import android.content.Context
import android.os.ParcelFileDescriptor import android.os.Build
import android.view.MotionEvent import android.view.MotionEvent
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.viewmodels.QuickSettings
@ -15,6 +12,7 @@ import kotlin.concurrent.thread
import kotlin.math.roundToInt import kotlin.math.roundToInt
class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
private var _renderingThreadWatcher: Thread? = null
private var _height: Int = 0 private var _height: Int = 0
private var _width: Int = 0 private var _width: Int = 0
private var _updateThread: Thread? = null private var _updateThread: Thread? = null
@ -35,26 +33,6 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
holder.addCallback(this) holder.addCallback(this)
} }
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (_isStarted)
return when (event!!.actionMasked) {
MotionEvent.ACTION_MOVE -> {
_nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt())
true
}
MotionEvent.ACTION_DOWN -> {
_nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt())
true
}
MotionEvent.ACTION_UP -> {
_nativeRyujinx.inputReleaseTouchPoint()
true
}
else -> super.onTouchEvent(event)
}
return super.onTouchEvent(event)
}
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
} }
@ -132,7 +110,16 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
_nativeRyujinx.inputInitialize(width, height) _nativeRyujinx.inputInitialize(width, height)
if(!settings.useVirtualController){
controller.setVisible(false)
}
else{
controller.connect() controller.connect()
}
mainViewModel.activity.physicalControllerManager.connect()
//
_nativeRyujinx.graphicsRendererSetSize( _nativeRyujinx.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.width(),
@ -159,7 +146,25 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
} }
private fun runGame() : Unit{ private fun runGame() : Unit{
// RenderingThreadWatcher
_renderingThreadWatcher = thread(start = true) {
var threadId = 0L;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mainViewModel.performanceManager?.enable()
while (_isStarted) {
Thread.sleep(1000)
var newthreadId = mainViewModel.activity.getRenderingThreadId()
if (threadId != newthreadId) {
mainViewModel.performanceManager?.closeCurrentRenderingSession()
}
threadId = newthreadId;
if (threadId != 0L) {
mainViewModel.performanceManager?.initializeRenderingSession(threadId)
}
}
}
}
_nativeRyujinx.graphicsRendererRunLoop() _nativeRyujinx.graphicsRendererRunLoop()
} }
} }

View File

@ -1,5 +1,6 @@
package org.ryujinx.android package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
@ -8,6 +9,8 @@ import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -29,28 +32,43 @@ import org.ryujinx.android.views.MainView
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private var mainViewModel: MainViewModel? = null var physicalControllerManager: PhysicalControllerManager
private var _isInit: Boolean = false private var _isInit: Boolean = false
var storageHelper: SimpleStorageHelper? = null var storageHelper: SimpleStorageHelper? = null
companion object { companion object {
var mainViewModel: MainViewModel? = null
var AppPath : String? var AppPath : String?
var StorageHelper: SimpleStorageHelper? = null var StorageHelper: SimpleStorageHelper? = null
init { init {
AppPath = "" AppPath = ""
} }
@JvmStatic
fun updateRenderSessionPerformance(gameTime : Long)
{
if(gameTime <= 0)
return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mainViewModel?.performanceManager?.updateRenderingSessionTime(gameTime)
}
}
} }
init { init {
physicalControllerManager = PhysicalControllerManager(this)
storageHelper = SimpleStorageHelper(this) storageHelper = SimpleStorageHelper(this)
StorageHelper = storageHelper StorageHelper = storageHelper
System.loadLibrary("ryujinxjni")
initVm()
} }
external fun getRenderingThreadId() : Long
external fun initVm()
fun setFullScreen() :Unit { fun setFullScreen() :Unit {
requestedOrientation = requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
WindowCompat.setDecorFitsSystemWindows(window,false)
var insets = WindowCompat.getInsetsController(window, window.decorView) var insets = WindowCompat.getInsetsController(window, window.decorView)
@ -79,6 +97,21 @@ class MainActivity : ComponentActivity() {
} }
} }
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
event?.apply {
return physicalControllerManager.onKeyEvent(this)
}
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
ev?.apply {
physicalControllerManager.onMotionEvent(this)
}
return super.dispatchGenericMotionEvent(ev)
}
private fun initialize() : Unit private fun initialize() : Unit
{ {
if(_isInit) if(_isInit)
@ -94,6 +127,9 @@ class MainActivity : ComponentActivity() {
initialize() initialize()
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
WindowCompat.setDecorFitsSystemWindows(window,false)
if(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager() !Environment.isExternalStorageManager()
} else { } else {

View File

@ -0,0 +1,49 @@
package org.ryujinx.android
import android.os.Build
import android.os.PerformanceHintManager
import androidx.annotation.RequiresApi
class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
private var _isEnabled: Boolean = false
private var renderingSession: PerformanceHintManager.Session? = null
val DEFAULT_TARGET_NS = 16666666L
@RequiresApi(Build.VERSION_CODES.S)
fun initializeRenderingSession(threadId : Long){
if(!_isEnabled || renderingSession != null)
return
var threads = IntArray(1)
threads[0] = threadId.toInt()
renderingSession = performanceHintManager.createHintSession(threads, DEFAULT_TARGET_NS)
}
@RequiresApi(Build.VERSION_CODES.S)
fun closeCurrentRenderingSession() {
if (_isEnabled)
renderingSession?.apply {
renderingSession = null
this.close()
}
}
fun enable(){
_isEnabled = true
}
@RequiresApi(Build.VERSION_CODES.S)
fun updateRenderingSessionTime(newTime : Long){
if(!_isEnabled)
return
var effectiveTime = newTime
if(newTime < DEFAULT_TARGET_NS)
effectiveTime = DEFAULT_TARGET_NS
renderingSession?.apply {
this.reportActualWorkDuration(effectiveTime)
}
}
}

View File

@ -0,0 +1,69 @@
package org.ryujinx.android
import android.view.KeyEvent
import android.view.MotionEvent
class PhysicalControllerManager(val activity: MainActivity) {
private var controllerId: Int = -1
private var ryujinxNative: RyujinxNative = RyujinxNative()
fun onKeyEvent(event: KeyEvent) : Boolean{
if(controllerId != -1) {
var id = GetGamePadButtonInputId(event.keyCode)
if(id != GamePadButtonInputId.None) {
when (event.action) {
KeyEvent.ACTION_UP -> {
ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
}
KeyEvent.ACTION_DOWN -> {
ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId)
}
}
return true;
}
}
return false
}
fun onMotionEvent(ev: MotionEvent) {
if(controllerId != -1) {
if(ev.action == MotionEvent.ACTION_MOVE) {
var leftStickX = ev.getAxisValue(MotionEvent.AXIS_X);
var leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y);
var rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z);
var rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ);
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
}
}
}
fun connect(){
controllerId = ryujinxNative.inputConnectGamepad(0)
}
fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
return when (keycode) {
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X
KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder
KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger
KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStick
KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStick
KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp
KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown
KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft
KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight
KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus
KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus
else -> GamePadButtonInputId.None
}
}
}

View File

@ -1,18 +1,33 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.PerformanceManager
@SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) { class MainViewModel(val activity: MainActivity) {
var performanceManager: PerformanceManager? = null
var selected: GameModel? = null var selected: GameModel? = null
private var gameTimeState: MutableState<Double>? = null private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null private var fifoState: MutableState<Double>? = null
private var navController : NavHostController? = null private var navController : NavHostController? = null
var homeViewModel: HomeViewModel = HomeViewModel(activity, this,) var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
init {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
var hintService =
activity.getSystemService(Context.PERFORMANCE_HINT_SERVICE) as PerformanceHintManager
performanceManager = PerformanceManager(hintService)
}
}
fun loadGame(game:GameModel) { fun loadGame(game:GameModel) {
var controller = navController?: return; var controller = navController?: return;

View File

@ -10,6 +10,7 @@ class QuickSettings(val activity: MainActivity) {
var enableDocked: Boolean var enableDocked: Boolean
var enableVsync: Boolean var enableVsync: Boolean
var useNce: Boolean var useNce: Boolean
var useVirtualController: Boolean
var isHostMapped: Boolean var isHostMapped: Boolean
var enableShaderCache: Boolean var enableShaderCache: Boolean
var enableTextureRecompression: Boolean var enableTextureRecompression: Boolean
@ -27,5 +28,6 @@ class QuickSettings(val activity: MainActivity) {
enableShaderCache = sharedPref.getBoolean("enableShaderCache", true) enableShaderCache = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
resScale = sharedPref.getFloat("resScale", 1f) resScale = sharedPref.getFloat("resScale", 1f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
} }
} }

View File

@ -26,7 +26,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
ignoreMissingServices: MutableState<Boolean>, ignoreMissingServices: MutableState<Boolean>,
enableShaderCache: MutableState<Boolean>, enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>, enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float> resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean>
) )
{ {
@ -39,6 +40,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true) enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
resScale.value = sharedPref.getFloat("resScale", 1f) resScale.value = sharedPref.getFloat("resScale", 1f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
} }
fun save( fun save(
@ -50,7 +52,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
ignoreMissingServices: MutableState<Boolean>, ignoreMissingServices: MutableState<Boolean>,
enableShaderCache: MutableState<Boolean>, enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>, enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float> resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean>
){ ){
var editor = sharedPref.edit() var editor = sharedPref.edit()
@ -63,6 +66,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true) editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true)
editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false) editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false)
editor.putFloat("resScale", resScale?.value ?: 1f) editor.putFloat("resScale", resScale?.value ?: 1f)
editor.putBoolean("useVirtualController", useVirtualController?.value ?: true)
editor.apply() editor.apply()
} }

View File

@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth

View File

@ -1,16 +1,30 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -19,8 +33,10 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import kotlin.math.roundToInt
class MainView { class MainView {
companion object { companion object {
@ -38,16 +54,171 @@ class MainView {
@Composable @Composable
fun GameView(mainViewModel: MainViewModel){ fun GameView(mainViewModel: MainViewModel){
Box { Box(modifier = Modifier.fillMaxSize()) {
var controller = GameController(mainViewModel.activity) val controller = remember {
GameController(mainViewModel.activity)
}
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
factory = { context -> factory = { context ->
GameHost(context, controller, mainViewModel) GameHost(context, controller, mainViewModel)
} }
) )
GameOverlay(mainViewModel, controller)
}
}
@Composable
fun GameOverlay(mainViewModel: MainViewModel, controller: GameController){
Box(modifier = Modifier.fillMaxSize()) {
GameStats(mainViewModel) GameStats(mainViewModel)
var ryujinxNative = RyujinxNative()
// touch surface
Surface(color = Color.Transparent, modifier = Modifier
.fillMaxSize()
.padding(0.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
Thread.sleep(2);
val event = awaitPointerEvent()
if(controller.isVisible)
continue
var change = event
.component1()
.firstOrNull()
change?.apply {
var position = this.position
if (event.type == PointerEventType.Press) {
ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(),
position.y.roundToInt()
)
} else if (event.type == PointerEventType.Release) {
ryujinxNative.inputReleaseTouchPoint()
} else if (event.type == PointerEventType.Move) {
ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(),
position.y.roundToInt()
)
}
}
}
}
}) {
}
controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle) 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)
}) {
Icon(imageVector = rememberVideogameAsset(), contentDescription = "Toggle Virtual Pad")
}
}
}
}
@Composable
fun rememberVideogameAsset(): ImageVector {
var 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

@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -92,6 +94,9 @@ class SettingViews {
var resScale = remember { var resScale = remember {
mutableStateOf(1f) mutableStateOf(1f)
} }
var useVirtualController = remember {
mutableStateOf(true)
}
if (!loaded.value) { if (!loaded.value) {
settingsViewModel.initializeState( settingsViewModel.initializeState(
@ -100,7 +105,8 @@ class SettingViews {
enableVsync, enableDocked, enablePtc, ignoreMissingServices, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale resScale,
useVirtualController
) )
loaded.value = true loaded.value = true
} }
@ -121,7 +127,8 @@ class SettingViews {
ignoreMissingServices, ignoreMissingServices,
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale resScale,
useVirtualController
) )
settingsViewModel.navController.popBackStack() settingsViewModel.navController.popBackStack()
}) { }) {
@ -136,7 +143,8 @@ class SettingViews {
useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices, useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale resScale,
useVirtualController
) )
} }
ExpandableView(onCardArrowClick = { }, title = "System") { ExpandableView(onCardArrowClick = { }, title = "System") {
@ -431,6 +439,25 @@ class SettingViews {
*/ */
} }
} }
ExpandableView(onCardArrowClick = { }, title = "Input") {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Show virtual controller",
modifier = Modifier.align(Alignment.CenterVertically)
)
Switch(checked = useVirtualController.value, onCheckedChange = {
useVirtualController.value = !useVirtualController.value
})
}
}
}
} }
} }
} }