From fdb732003172f672302d855fd54e302e8cf1f725 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 30 Oct 2023 07:40:17 +0000 Subject: [PATCH] android - drop game activity, replace with compose view --- .../app/src/main/AndroidManifest.xml | 6 +- .../java/org/ryujinx/android/GameActivity.kt | 407 ------------------ .../main/java/org/ryujinx/android/GameHost.kt | 40 +- .../java/org/ryujinx/android/MainActivity.kt | 96 +++++ .../android/PhysicalControllerManager.kt | 2 +- .../android/viewmodels/MainViewModel.kt | 7 +- .../org/ryujinx/android/views/GameViews.kt | 319 ++++++++++++++ .../org/ryujinx/android/views/MainView.kt | 1 + .../org/ryujinx/android/views/SettingViews.kt | 5 +- .../ryujinx/android/views/TitleUpdateViews.kt | 3 + 10 files changed, 448 insertions(+), 438 deletions(-) delete mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index 73e7d70d3..549ed4ed2 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -27,14 +27,10 @@ android:supportsRtl="true" android:theme="@style/Theme.RyujinxAndroid" tools:targetApi="31"> - diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt deleted file mode 100644 index 1670e879a..000000000 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt +++ /dev/null @@ -1,407 +0,0 @@ -package org.ryujinx.android - -import android.annotation.SuppressLint -import android.content.Intent -import android.content.pm.ActivityInfo -import android.os.Bundle -import android.view.KeyEvent -import android.view.MotionEvent -import androidx.activity.compose.BackHandler -import androidx.activity.compose.setContent -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.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -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.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Popup -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat -import compose.icons.CssGgIcons -import compose.icons.cssggicons.ToolbarBottom -import org.ryujinx.android.ui.theme.RyujinxAndroidTheme -import org.ryujinx.android.viewmodels.MainViewModel -import org.ryujinx.android.viewmodels.QuickSettings -import kotlin.math.abs -import kotlin.math.roundToInt - -class GameActivity : BaseActivity() { - private var physicalControllerManager: PhysicalControllerManager = - PhysicalControllerManager(this) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - MainActivity.mainViewModel!!.physicalControllerManager = physicalControllerManager - setContent { - RyujinxAndroidTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - GameView(mainViewModel = MainActivity.mainViewModel!!) - } - } - } - } - - @SuppressLint("RestrictedApi") - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - event?.apply { - if (physicalControllerManager.onKeyEvent(this)) - return true - } - return super.dispatchKeyEvent(event) - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - ev?.apply { - physicalControllerManager.onMotionEvent(this) - } - return super.dispatchGenericMotionEvent(ev) - } - - override fun onStop() { - super.onStop() - - NativeHelpers().setTurboMode(false) - force60HzRefreshRate(false) - } - - override fun onResume() { - super.onResume() - - setFullScreen(true) - NativeHelpers().setTurboMode(true) - force60HzRefreshRate(true) - } - - override fun onPause() { - super.onPause() - - NativeHelpers().setTurboMode(false) - force60HzRefreshRate(false) - } - - private fun force60HzRefreshRate(enable: Boolean) { - // Hack for MIUI devices since they don't support the standard Android APIs - try { - val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS") - setFpsIntent.putExtra("package_name", "org.ryujinx.android") - setFpsIntent.putExtra("isEnter", enable) - sendBroadcast(setFpsIntent) - } catch (_: Exception) { - } - - if (enable) - display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) } - ?.let { window.attributes.preferredDisplayModeId = it.modeId } - else - display?.supportedModes?.maxByOrNull { it.refreshRate } - ?.let { window.attributes.preferredDisplayModeId = it.modeId } - } - - private fun setFullScreen(fullscreen: Boolean) { - requestedOrientation = - if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER - - val insets = WindowCompat.getInsetsController(window, window.decorView) - - insets.apply { - 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 - } - } - } - - @Composable - fun GameView(mainViewModel: MainViewModel) { - Box(modifier = Modifier.fillMaxSize()) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - GameHost(context, mainViewModel) - } - ) - GameOverlay(mainViewModel) - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun GameOverlay(mainViewModel: MainViewModel) { - Box(modifier = Modifier.fillMaxSize()) { - GameStats(mainViewModel) - - val ryujinxNative = RyujinxNative() - - val showController = remember { - mutableStateOf(QuickSettings(this@GameActivity).useVirtualController) - } - val enableVsync = remember { - mutableStateOf(QuickSettings(this@GameActivity).enableVsync) - } - val showMore = remember { - mutableStateOf(false) - } - - val showLoading = remember { - mutableStateOf(true) - } - - val progressValue = remember { - mutableStateOf(0.0f) - } - - val progress = remember { - mutableStateOf("Loading") - } - - mainViewModel.setProgressStates(showLoading, progressValue, progress) - - // touch surface - Surface(color = Color.Transparent, modifier = Modifier - .fillMaxSize() - .padding(0.dp) - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - if (showController.value) - continue - - val change = event - .component1() - .firstOrNull() - change?.apply { - val position = this.position - - when (event.type) { - PointerEventType.Press -> { - ryujinxNative.inputSetTouchPoint( - position.x.roundToInt(), - position.y.roundToInt() - ) - } - - PointerEventType.Release -> { - ryujinxNative.inputReleaseTouchPoint() - - } - - PointerEventType.Move -> { - ryujinxNative.inputSetTouchPoint( - position.x.roundToInt(), - position.y.roundToInt() - ) - - } - } - } - } - } - }) { - } - if (!showLoading.value) { - GameController.Compose(mainViewModel) - - Row( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(8.dp) - ) { - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = true - }) { - Icon( - imageVector = CssGgIcons.ToolbarBottom, - contentDescription = "Open Panel" - ) - } - } - - if (showMore.value) { - Popup( - alignment = Alignment.BottomCenter, - onDismissRequest = { showMore.value = false }) { - Surface( - modifier = Modifier.padding(16.dp), - shape = MaterialTheme.shapes.medium - ) { - Row(modifier = Modifier.padding(8.dp)) { - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = false - showController.value = !showController.value - mainViewModel.controller?.setVisible(showController.value) - }) { - Icon( - imageVector = Icons.videoGame(), - contentDescription = "Toggle Virtual Pad" - ) - } - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = false - enableVsync.value = !enableVsync.value - RyujinxNative().graphicsRendererSetVsync(enableVsync.value) - }) { - Icon( - imageVector = Icons.vSync(), - tint = if (enableVsync.value) Color.Green else Color.Red, - contentDescription = "Toggle VSync" - ) - } - } - } - } - } - } - - val showBackNotice = remember { - mutableStateOf(false) - } - - BackHandler { - showBackNotice.value = true - } - - if (showLoading.value) { - Card( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(0.5f) - .align(Alignment.Center), - shape = MaterialTheme.shapes.medium - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - ) { - Text(text = progress.value) - - if (progressValue.value > -1) - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - progress = progressValue.value - ) - else - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) - } - - } - } - - 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 = { - showBackNotice.value = false - mainViewModel.closeGame() - setFullScreen(false) - finishActivity(0) - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Exit Game") - } - - Button(onClick = { - showBackNotice.value = false - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Dismiss") - } - } - } - } - } - } - } - } - } - - @Composable - fun GameStats(mainViewModel: MainViewModel) { - val fifo = remember { - mutableStateOf(0.0) - } - val gameFps = remember { - mutableStateOf(0.0) - } - val gameTime = remember { - mutableStateOf(0.0) - } - - Surface( - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.surface.copy(0.4f) - ) { - Column { - var gameTimeVal = 0.0 - if (!gameTime.value.isInfinite()) - gameTimeVal = gameTime.value - Text(text = "${String.format("%.3f", fifo.value)} %") - Text(text = "${String.format("%.3f", gameFps.value)} FPS") - Text(text = "${String.format("%.3f", gameTimeVal)} ms") - } - } - - mainViewModel.setStatStates(fifo, gameFps, gameTime) - } -} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt index f19fc93da..d483c11ee 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -12,7 +12,8 @@ import org.ryujinx.android.viewmodels.QuickSettings import kotlin.concurrent.thread @SuppressLint("ViewConstructor") -class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { +class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), + SurfaceHolder.Callback { private var isProgressHidden: Boolean = false private var progress: MutableState? = null private var progressValue: MutableState? = null @@ -26,7 +27,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su private var _guestThread: Thread? = null private var _isInit: Boolean = false private var _isStarted: Boolean = false - private val nativeWindow : NativeWindow + private val nativeWindow: NativeWindow private var _nativeRyujinx: RyujinxNative = RyujinxNative() @@ -42,12 +43,11 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - if(_isClosed) + if (_isClosed) return start(holder) - if(_width != width || _height != height) - { + if (_width != width || _height != height) { val window = nativeWindow.requeryWindowHandle() _nativeRyujinx.graphicsSetSurface(window, nativeWindow.nativePointer) @@ -62,8 +62,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su height ) - if(_isStarted) - { + if (_isStarted) { _nativeRyujinx.inputSetClientSize(width, height) } } @@ -72,7 +71,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } - fun close(){ + fun close() { _isClosed = true _isInit = false _isStarted = false @@ -82,7 +81,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } private fun start(surfaceHolder: SurfaceHolder) { - if(_isStarted) + if (_isStarted) return game = mainViewModel.gameModel @@ -91,10 +90,9 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su val settings = QuickSettings(mainViewModel.activity) - if(!settings.useVirtualController){ + if (!settings.useVirtualController) { mainViewModel.controller?.setVisible(false) - } - else{ + } else { mainViewModel.controller?.connect() } @@ -118,13 +116,13 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su Thread.sleep(1) showLoading?.apply { - if(value){ + if (value) { var value = helper.getProgressValue() - if(value != -1f) - progress?.apply { - this.value = helper.getProgressInfo() - } + if (value != -1f) + progress?.apply { + this.value = helper.getProgressInfo() + } progressValue?.apply { this.value = value @@ -133,12 +131,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su } c++ if (c >= 1000) { - if(helper.getProgressValue() == -1f) + if (helper.getProgressValue() == -1f) progress?.apply { this.value = "Loading ${game!!.titleName}" } c = 0 - mainViewModel.updateStats(_nativeRyujinx.deviceGetGameFifo(), _nativeRyujinx.deviceGetGameFrameRate(), _nativeRyujinx.deviceGetGameFrameTime()) + mainViewModel.updateStats( + _nativeRyujinx.deviceGetGameFifo(), + _nativeRyujinx.deviceGetGameFrameRate(), + _nativeRyujinx.deviceGetGameFrameTime() + ) } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 29fa53dbf..499ac48c6 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -1,8 +1,13 @@ package org.ryujinx.android +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo 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.compose.setContent import androidx.compose.foundation.layout.fillMaxSize @@ -10,14 +15,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +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.views.MainView +import kotlin.math.abs class MainActivity : BaseActivity() { + private var physicalControllerManager: PhysicalControllerManager = + PhysicalControllerManager(this) private var _isInit: Boolean = false + var isGameRunning = false var storageHelper: SimpleStorageHelper? = null companion object { var mainViewModel: MainViewModel? = null @@ -67,12 +78,14 @@ class MainActivity : BaseActivity() { AppPath = this.getExternalFilesDir(null)!!.absolutePath + initialize() window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowCompat.setDecorFitsSystemWindows(window,false) mainViewModel = MainViewModel(this) + mainViewModel!!.physicalControllerManager = physicalControllerManager mainViewModel?.apply { setContent { @@ -98,4 +111,87 @@ class MainActivity : BaseActivity() { super.onRestoreInstanceState(savedInstanceState) storageHelper?.onRestoreInstanceState(savedInstanceState) } + + // Game Stuff + private fun force60HzRefreshRate(enable: Boolean) { + // Hack for MIUI devices since they don't support the standard Android APIs + try { + val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS") + setFpsIntent.putExtra("package_name", "org.ryujinx.android") + setFpsIntent.putExtra("isEnter", enable) + sendBroadcast(setFpsIntent) + } catch (_: Exception) { + } + + if (enable) + display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) } + ?.let { window.attributes.preferredDisplayModeId = it.modeId } + else + display?.supportedModes?.maxByOrNull { it.refreshRate } + ?.let { window.attributes.preferredDisplayModeId = it.modeId } + } + + fun setFullScreen(fullscreen: Boolean) { + requestedOrientation = + if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER + + val insets = WindowCompat.getInsetsController(window, window.decorView) + + insets.apply { + 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 + } + } + } + + + @SuppressLint("RestrictedApi") + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + event?.apply { + if (physicalControllerManager.onKeyEvent(this)) + return true + } + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + ev?.apply { + physicalControllerManager.onMotionEvent(this) + } + return super.dispatchGenericMotionEvent(ev) + } + + override fun onStop() { + super.onStop() + + if(isGameRunning) { + NativeHelpers().setTurboMode(false) + force60HzRefreshRate(false) + } + } + + override fun onResume() { + super.onResume() + + if(isGameRunning) { + setFullScreen(true) + NativeHelpers().setTurboMode(true) + force60HzRefreshRate(true) + } + } + + override fun onPause() { + super.onPause() + + if(isGameRunning) { + NativeHelpers().setTurboMode(false) + force60HzRefreshRate(false) + } + } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt index 9913956bd..0b7b16266 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt @@ -3,7 +3,7 @@ package org.ryujinx.android import android.view.KeyEvent import android.view.MotionEvent -class PhysicalControllerManager(val activity: GameActivity) { +class PhysicalControllerManager(val activity: MainActivity) { private var controllerId: Int = -1 private var ryujinxNative: RyujinxNative = RyujinxNative() diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index 0d3850425..ea859bb0f 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -2,7 +2,6 @@ package org.ryujinx.android.viewmodels import android.annotation.SuppressLint import android.content.Context -import android.content.Intent import android.os.Build import android.os.PerformanceHintManager import androidx.compose.runtime.MutableState @@ -10,7 +9,6 @@ import androidx.navigation.NavHostController import com.anggrayudi.storage.extension.launchOnUiThread import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore -import org.ryujinx.android.GameActivity import org.ryujinx.android.GameController import org.ryujinx.android.GameHost import org.ryujinx.android.GraphicsConfiguration @@ -241,8 +239,9 @@ class MainViewModel(val activity: MainActivity) { } fun navigateToGame() { - val intent = Intent(activity, GameActivity::class.java) - activity.startActivity(intent) + activity.setFullScreen(true) + navController?.navigate("game") + activity.isGameRunning = true } fun setProgressStates( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt new file mode 100644 index 000000000..d9402467b --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt @@ -0,0 +1,319 @@ +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.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +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.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup +import compose.icons.CssGgIcons +import compose.icons.cssggicons.ToolbarBottom +import org.ryujinx.android.GameController +import org.ryujinx.android.GameHost +import org.ryujinx.android.Icons +import org.ryujinx.android.MainActivity +import org.ryujinx.android.RyujinxNative +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings +import kotlin.math.roundToInt + +class GameViews { + companion object { + @Composable + fun Main() { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + GameView(mainViewModel = MainActivity.mainViewModel!!) + } + } + + @Composable + fun GameView(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + GameHost(context, mainViewModel) + } + ) + GameOverlay(mainViewModel) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun GameOverlay(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + GameStats(mainViewModel) + + val ryujinxNative = RyujinxNative() + + val showController = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController) + } + val enableVsync = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) + } + val showMore = remember { + mutableStateOf(false) + } + + val showLoading = remember { + mutableStateOf(true) + } + + val progressValue = remember { + mutableStateOf(0.0f) + } + + val progress = remember { + mutableStateOf("Loading") + } + + mainViewModel.setProgressStates(showLoading, progressValue, progress) + + // touch surface + Surface(color = Color.Transparent, modifier = Modifier + .fillMaxSize() + .padding(0.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (showController.value) + continue + + val change = event + .component1() + .firstOrNull() + change?.apply { + val position = this.position + + when (event.type) { + PointerEventType.Press -> { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + } + + PointerEventType.Release -> { + ryujinxNative.inputReleaseTouchPoint() + + } + + PointerEventType.Move -> { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + + } + } + } + } + } + }) { + } + if (!showLoading.value) { + GameController.Compose(mainViewModel) + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp) + ) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = true + }) { + Icon( + imageVector = CssGgIcons.ToolbarBottom, + contentDescription = "Open Panel" + ) + } + } + + if (showMore.value) { + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = { showMore.value = false }) { + Surface( + modifier = Modifier.padding(16.dp), + shape = MaterialTheme.shapes.medium + ) { + Row(modifier = Modifier.padding(8.dp)) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + showController.value = !showController.value + mainViewModel.controller?.setVisible(showController.value) + }) { + Icon( + imageVector = Icons.videoGame(), + contentDescription = "Toggle Virtual Pad" + ) + } + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + enableVsync.value = !enableVsync.value + RyujinxNative().graphicsRendererSetVsync(enableVsync.value) + }) { + Icon( + imageVector = Icons.vSync(), + tint = if (enableVsync.value) Color.Green else Color.Red, + contentDescription = "Toggle VSync" + ) + } + } + } + } + } + } + + val showBackNotice = remember { + mutableStateOf(false) + } + + BackHandler { + showBackNotice.value = true + } + + if (showLoading.value) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(0.5f) + .align(Alignment.Center), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = progress.value) + + if (progressValue.value > -1) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + progress = progressValue.value + ) + else + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } + + } + } + + 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 = { + showBackNotice.value = false + mainViewModel.closeGame() + mainViewModel.activity.setFullScreen(false) + mainViewModel.navController?.popBackStack() + mainViewModel.activity.isGameRunning = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Exit Game") + } + + Button(onClick = { + showBackNotice.value = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Dismiss") + } + } + } + } + } + } + } + } + } + + @Composable + fun GameStats(mainViewModel: MainViewModel) { + val fifo = remember { + mutableStateOf(0.0) + } + val gameFps = remember { + mutableStateOf(0.0) + } + val gameTime = remember { + mutableStateOf(0.0) + } + + Surface( + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.surface.copy(0.4f) + ) { + Column { + var gameTimeVal = 0.0 + if (!gameTime.value.isInfinite()) + gameTimeVal = gameTime.value + Text(text = "${String.format("%.3f", fifo.value)} %") + Text(text = "${String.format("%.3f", gameFps.value)} FPS") + Text(text = "${String.format("%.3f", gameTimeVal)} ms") + } + } + + mainViewModel.setStatStates(fifo, gameFps, gameTime) + } + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index b613cc51d..e4243a67c 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -17,6 +17,7 @@ class MainView { NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("user") { UserViews.Main(mainViewModel, navController) } + composable("game") { GameViews.Main() } composable("settings") { SettingViews.Main( SettingsViewModel( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index d1d00251c..c5898634c 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -461,7 +461,8 @@ class SettingViews { Column( modifier = Modifier .fillMaxWidth() - .height(300.dp) + .height(350.dp) + .verticalScroll(rememberScrollState()) ) { Row( modifier = Modifier @@ -494,7 +495,7 @@ class SettingViews { Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(4.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt index 96347f259..719911db1 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -6,6 +6,8 @@ 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.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete @@ -62,6 +64,7 @@ class TitleUpdateViews { modifier = Modifier .height(250.dp) .fillMaxWidth() + .verticalScroll(rememberScrollState()) ) { Row(modifier = Modifier.padding(8.dp)) { RadioButton(