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:
parent
c57f6a7fe3
commit
fcb511bbca
@ -36,6 +36,12 @@ namespace LibRyujinx
|
||||
[DllImport("libryujinxjni")]
|
||||
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);
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "JNI_OnLoad")]
|
||||
@ -279,6 +285,11 @@ namespace LibRyujinx
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")]
|
||||
public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||
{
|
||||
SetSwapBuffersCallback(() =>
|
||||
{
|
||||
var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime();
|
||||
onFrameEnd(time);
|
||||
});
|
||||
RunLoop();
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,11 @@ namespace LibRyujinx
|
||||
|
||||
_isActive = true;
|
||||
|
||||
if (Ryujinx.Common.SystemInfo.SystemInfo.IsBionic)
|
||||
{
|
||||
setRenderingThread();
|
||||
}
|
||||
|
||||
while (_isActive)
|
||||
{
|
||||
if (_isStopped)
|
||||
|
@ -20,6 +20,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:appCategory="game"
|
||||
android:theme="@style/Theme.RyujinxAndroid"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
|
@ -36,4 +36,10 @@ void* _ryujinxNative = NULL;
|
||||
// Ryujinx imported functions
|
||||
bool (*initialize)(char*) = NULL;
|
||||
|
||||
long _renderingThreadId = 0;
|
||||
long _currentRenderingThreadId = 0;
|
||||
JavaVM* _vm = nullptr;
|
||||
jobject _mainActivity = nullptr;
|
||||
jclass _mainActivityClass = nullptr;
|
||||
|
||||
#endif //RYUJINXNATIVE_RYUIJNX_H
|
||||
|
@ -17,6 +17,31 @@
|
||||
// }
|
||||
|
||||
#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"
|
||||
{
|
||||
@ -128,4 +153,39 @@ 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);
|
||||
}
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.GraphicsLayerScope
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
@ -45,9 +46,18 @@ typealias GamePad = RadialGamePad
|
||||
typealias GamePadConfig = RadialGamePadConfig
|
||||
|
||||
class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) {
|
||||
private var controllerView: View? = null
|
||||
var leftGamePad: GamePad
|
||||
var rightGamePad: GamePad
|
||||
var controllerId: Int = -1
|
||||
val isVisible : Boolean
|
||||
get() {
|
||||
controllerView?.apply {
|
||||
return this.isVisible
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
init {
|
||||
leftGamePad = GamePad(generateConfig(true), 16f, activity)
|
||||
@ -65,7 +75,6 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
|
||||
@Composable
|
||||
fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit
|
||||
{
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)})
|
||||
|
||||
@ -81,14 +90,25 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
|
||||
}
|
||||
}
|
||||
|
||||
private fun Create(context: Context) : View
|
||||
private fun Create(context: Context) : View
|
||||
{
|
||||
var inflator = LayoutInflater.from(context);
|
||||
var view = inflator.inflate(R.layout.game_layout, null)
|
||||
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad);
|
||||
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(){
|
||||
|
@ -1,13 +1,10 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Build
|
||||
import android.view.MotionEvent
|
||||
import android.view.SurfaceHolder
|
||||
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.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
@ -15,6 +12,7 @@ import kotlin.concurrent.thread
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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 _width: Int = 0
|
||||
private var _updateThread: Thread? = null
|
||||
@ -35,26 +33,6 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
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) {
|
||||
}
|
||||
|
||||
@ -132,7 +110,16 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
|
||||
_nativeRyujinx.inputInitialize(width, height)
|
||||
|
||||
controller.connect()
|
||||
if(!settings.useVirtualController){
|
||||
controller.setVisible(false)
|
||||
}
|
||||
else{
|
||||
controller.connect()
|
||||
}
|
||||
|
||||
mainViewModel.activity.physicalControllerManager.connect()
|
||||
|
||||
//
|
||||
|
||||
_nativeRyujinx.graphicsRendererSetSize(
|
||||
surfaceHolder.surfaceFrame.width(),
|
||||
@ -159,7 +146,25 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
@ -8,6 +9,8 @@ import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@ -29,28 +32,43 @@ import org.ryujinx.android.views.MainView
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var mainViewModel: MainViewModel? = null
|
||||
var physicalControllerManager: PhysicalControllerManager
|
||||
private var _isInit: Boolean = false
|
||||
var storageHelper: SimpleStorageHelper? = null
|
||||
companion object {
|
||||
var mainViewModel: MainViewModel? = null
|
||||
var AppPath : String?
|
||||
var StorageHelper: SimpleStorageHelper? = null
|
||||
init {
|
||||
AppPath = ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun updateRenderSessionPerformance(gameTime : Long)
|
||||
{
|
||||
if(gameTime <= 0)
|
||||
return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
mainViewModel?.performanceManager?.updateRenderingSessionTime(gameTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
physicalControllerManager = PhysicalControllerManager(this)
|
||||
storageHelper = SimpleStorageHelper(this)
|
||||
StorageHelper = storageHelper
|
||||
System.loadLibrary("ryujinxjni")
|
||||
initVm()
|
||||
}
|
||||
|
||||
external fun getRenderingThreadId() : Long
|
||||
external fun initVm()
|
||||
|
||||
fun setFullScreen() :Unit {
|
||||
requestedOrientation =
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window,false)
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
|
||||
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
|
||||
{
|
||||
if(_isInit)
|
||||
@ -94,6 +127,9 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
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) {
|
||||
!Environment.isExternalStorageManager()
|
||||
} else {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,33 @@
|
||||
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.navigation.NavHostController
|
||||
import org.ryujinx.android.GameHost
|
||||
import org.ryujinx.android.MainActivity
|
||||
import org.ryujinx.android.PerformanceManager
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
class MainViewModel(val activity: MainActivity) {
|
||||
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 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) {
|
||||
var controller = navController?: return;
|
||||
|
@ -10,6 +10,7 @@ class QuickSettings(val activity: MainActivity) {
|
||||
var enableDocked: Boolean
|
||||
var enableVsync: Boolean
|
||||
var useNce: Boolean
|
||||
var useVirtualController: Boolean
|
||||
var isHostMapped: Boolean
|
||||
var enableShaderCache: Boolean
|
||||
var enableTextureRecompression: Boolean
|
||||
@ -27,5 +28,6 @@ class QuickSettings(val activity: MainActivity) {
|
||||
enableShaderCache = sharedPref.getBoolean("enableShaderCache", true)
|
||||
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
|
||||
resScale = sharedPref.getFloat("resScale", 1f)
|
||||
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
|
||||
}
|
||||
}
|
@ -26,7 +26,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
ignoreMissingServices: MutableState<Boolean>,
|
||||
enableShaderCache: 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)
|
||||
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
|
||||
resScale.value = sharedPref.getFloat("resScale", 1f)
|
||||
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
|
||||
}
|
||||
|
||||
fun save(
|
||||
@ -50,7 +52,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
ignoreMissingServices: MutableState<Boolean>,
|
||||
enableShaderCache: MutableState<Boolean>,
|
||||
enableTextureRecompression: MutableState<Boolean>,
|
||||
resScale: MutableState<Float>
|
||||
resScale: MutableState<Float>,
|
||||
useVirtualController: MutableState<Boolean>
|
||||
){
|
||||
var editor = sharedPref.edit()
|
||||
|
||||
@ -63,6 +66,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true)
|
||||
editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false)
|
||||
editor.putFloat("resScale", resScale?.value ?: 1f)
|
||||
editor.putBoolean("useVirtualController", useVirtualController?.value ?: true)
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ 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.safeContentPadding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
|
@ -1,16 +1,30 @@
|
||||
package org.ryujinx.android.views
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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.padding
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
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.viewinterop.AndroidView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@ -19,8 +33,10 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.ryujinx.android.GameController
|
||||
import org.ryujinx.android.GameHost
|
||||
import org.ryujinx.android.RyujinxNative
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.SettingsViewModel
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MainView {
|
||||
companion object {
|
||||
@ -38,16 +54,171 @@ class MainView {
|
||||
|
||||
@Composable
|
||||
fun GameView(mainViewModel: MainViewModel){
|
||||
Box {
|
||||
var controller = GameController(mainViewModel.activity)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
val controller = remember {
|
||||
GameController(mainViewModel.activity)
|
||||
}
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
GameHost(context, controller, mainViewModel)
|
||||
}
|
||||
)
|
||||
GameOverlay(mainViewModel, controller)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GameOverlay(mainViewModel: MainViewModel, controller: GameController){
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
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)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,8 @@ 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.safeContentPadding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -92,6 +94,9 @@ class SettingViews {
|
||||
var resScale = remember {
|
||||
mutableStateOf(1f)
|
||||
}
|
||||
var useVirtualController = remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
|
||||
if (!loaded.value) {
|
||||
settingsViewModel.initializeState(
|
||||
@ -100,7 +105,8 @@ class SettingViews {
|
||||
enableVsync, enableDocked, enablePtc, ignoreMissingServices,
|
||||
enableShaderCache,
|
||||
enableTextureRecompression,
|
||||
resScale
|
||||
resScale,
|
||||
useVirtualController
|
||||
)
|
||||
loaded.value = true
|
||||
}
|
||||
@ -121,7 +127,8 @@ class SettingViews {
|
||||
ignoreMissingServices,
|
||||
enableShaderCache,
|
||||
enableTextureRecompression,
|
||||
resScale
|
||||
resScale,
|
||||
useVirtualController
|
||||
)
|
||||
settingsViewModel.navController.popBackStack()
|
||||
}) {
|
||||
@ -136,7 +143,8 @@ class SettingViews {
|
||||
useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
|
||||
enableShaderCache,
|
||||
enableTextureRecompression,
|
||||
resScale
|
||||
resScale,
|
||||
useVirtualController
|
||||
)
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user