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(