From 83c9e4fcb23a835bbd6887c5ea4b9beb50c52268 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 6 Aug 2023 12:53:01 +0000 Subject: [PATCH] move game view to new activity --- src/RyujinxAndroid/app/build.gradle | 6 + .../app/src/main/AndroidManifest.xml | 17 +- .../app/src/main/cpp/ryujinx.cpp | 6 + .../java/org/ryujinx/android/GameActivity.kt | 346 ++++++++++++++++++ .../main/java/org/ryujinx/android/GameHost.kt | 17 +- .../main/java/org/ryujinx/android/Helpers.kt | 14 +- .../main/java/org/ryujinx/android/Icons.kt | 2 - .../java/org/ryujinx/android/MainActivity.kt | 93 +---- .../ryujinx/android/NativeGraphicsInterop.kt | 4 +- .../java/org/ryujinx/android/NativeHelpers.kt | 10 +- .../org/ryujinx/android/PerformanceManager.kt | 6 +- .../android/PhysicalControllerManager.kt | 8 +- .../java/org/ryujinx/android/RyujinxNative.kt | 24 +- .../android/viewmodels/MainViewModel.kt | 55 +-- .../org/ryujinx/android/views/HomeViews.kt | 5 +- .../org/ryujinx/android/views/MainView.kt | 240 ------------ 16 files changed, 444 insertions(+), 409 deletions(-) create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index 581fe1aa1..c1e90b005 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -75,6 +75,12 @@ tasks.named("preBuild") { } dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation platform('androidx.compose:compose-bom:2023.03.00') + implementation platform('androidx.compose:compose-bom:2023.03.00') + androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00') + androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00') runtimeOnly project(":libryujinx") implementation 'androidx.core:core-ktx:1.10.1' implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index ad43a05f7..96c7072de 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ - + + @@ -12,23 +15,29 @@ tools:ignore="ScopedStorage" /> + + diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index a93948d42..12a331b9d 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -225,3 +225,9 @@ void debug_break(int code){ if(code >= 3) int r = 0; } + +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_setTurboMode(JNIEnv *env, jobject thiz, jboolean enable) { + adrenotools_set_turbo(enable); +} 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 new file mode 100644 index 000000000..3f9c4fe86 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt @@ -0,0 +1,346 @@ +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.ComponentActivity +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.ExperimentalMaterial3Api +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.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 : ComponentActivity() { + 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(mainViewModel.activity).useVirtualController) + } + val enableVsync = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) + } + val showMore = remember { + mutableStateOf(false) + } + + // 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() + ) + + } + } + } + } + } + }) { + } + 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 (showBackNotice.value) { + AlertDialog(onDismissRequest = { showBackNotice.value = false }) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Are you sure you want to exit the game?") + Text(text = "All unsaved data will be lost!") + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Button(onClick = { + mainViewModel.closeGame() + 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 a85b6852c..76bc2c9f2 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 @@ -1,5 +1,6 @@ package org.ryujinx.android +import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.view.SurfaceHolder @@ -7,22 +8,19 @@ import android.view.SurfaceView import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.QuickSettings -import org.ryujinx.android.viewmodels.VulkanDriverViewModel -import java.io.File import kotlin.concurrent.thread -class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { +@SuppressLint("ViewConstructor") +class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { private var game: GameModel? = null private var _isClosed: Boolean = false private var _renderingThreadWatcher: Thread? = null private var _height: Int = 0 private var _width: Int = 0 private var _updateThread: Thread? = null - private var nativeInterop: NativeGraphicsInterop? = null private var _guestThread: Thread? = null private var _isInit: Boolean = false private var _isStarted: Boolean = false - private var _nativeWindow: Long = 0 private var _nativeRyujinx: RyujinxNative = RyujinxNative() @@ -48,6 +46,11 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie _width = width _height = height + _nativeRyujinx.graphicsRendererSetSize( + width, + height + ) + if(_isStarted) { _nativeRyujinx.inputSetClientSize(width, height) @@ -70,7 +73,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie private fun start(surfaceHolder: SurfaceHolder) { mainViewModel.gameHost = this if(_isStarted) - return; + return game = mainViewModel.gameModel @@ -85,7 +88,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie mainViewModel.controller?.connect() } - mainViewModel.activity.physicalControllerManager.connect() + mainViewModel.physicalControllerManager?.connect() _nativeRyujinx.graphicsRendererSetSize( surfaceHolder.surfaceFrame.width(), diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt index e833b82d4..780a8fa6d 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -4,7 +4,6 @@ import android.content.ContentUris import android.content.Context import android.database.Cursor import android.net.Uri -import android.os.Build import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore @@ -12,10 +11,9 @@ import android.provider.MediaStore class Helpers { companion object{ fun getPath(context: Context, uri: Uri): String? { - val isKitKatorAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT // DocumentProvider - if (isKitKatorAbove && DocumentsContract.isDocumentUri(context, uri)) { + if (DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { val docId = DocumentsContract.getDocumentId(uri) @@ -57,7 +55,7 @@ class Helpers { return null } - fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { + private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { var cursor: Cursor? = null val column = "_data" val projection = arrayOf(column) @@ -73,16 +71,16 @@ class Helpers { return null } - fun isExternalStorageDocument(uri: Uri): Boolean { + private fun isExternalStorageDocument(uri: Uri): Boolean { return "com.android.externalstorage.documents" == uri.authority } - fun isDownloadsDocument(uri: Uri): Boolean { + private fun isDownloadsDocument(uri: Uri): Boolean { return "com.android.providers.downloads.documents" == uri.authority } - fun isMediaDocument(uri: Uri): Boolean { + private fun isMediaDocument(uri: Uri): Boolean { return "com.android.providers.media.documents" == uri.authority } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt index 23e8d6fef..13820e150 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -1,8 +1,6 @@ package org.ryujinx.android import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme 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 178ee43b6..d9817f69a 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,39 +1,23 @@ package org.ryujinx.android -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.ActivityInfo -import android.media.AudioDeviceInfo -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.addCallback import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview 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.viewmodels.VulkanDriverViewModel -import org.ryujinx.android.views.HomeViews import org.ryujinx.android.views.MainView -import java.io.File class MainActivity : ComponentActivity() { - var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this) private var _isInit: Boolean = false var storageHelper: SimpleStorageHelper? = null companion object { @@ -61,60 +45,7 @@ class MainActivity : ComponentActivity() { } external fun getRenderingThreadId() : Long - external fun initVm() - - 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 - } - } - } - - private fun getAudioDevice () : Int { - val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - - return if (devices.isEmpty()) - 0 - else { - val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET } - if(earPiece != null) - return earPiece.id - if(speaker != null) - return speaker.id - devices.first().id - } - } - - @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) - } + private external fun initVm() private fun initialize() { if (_isInit) @@ -150,15 +81,6 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - /*Box { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - GameHost(context) - } - ) - controller.Compose(lifecycleScope, lifecycle) - }*/ MainView.Main(mainViewModel = this) } } @@ -176,16 +98,3 @@ class MainActivity : ComponentActivity() { storageHelper?.onRestoreInstanceState(savedInstanceState) } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - RyujinxAndroidTheme { - HomeViews.Home() - } -} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt index 262b551c3..e4dddff01 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt @@ -1,9 +1,7 @@ package org.ryujinx.android -import android.view.Surface - class NativeGraphicsInterop { var VkCreateSurface: Long = 0 var SurfaceHandle: Long = 0 var VkRequiredExtensions: Array? = null -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt index 4b52c2862..f37114ad7 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -9,12 +9,14 @@ class NativeHelpers { System.loadLibrary("ryujinxjni") } } - external fun releaseNativeWindow(window:Long) : Unit + external fun releaseNativeWindow(window:Long) external fun createSurface(vkInstance:Long, window:Long) : Long external fun getCreateSurfacePtr() : Long external fun getNativeWindow(surface:Surface) : Long - external fun attachCurrentThread() : Unit - external fun detachCurrentThread() : Unit + external fun attachCurrentThread() + external fun detachCurrentThread() external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long -} \ No newline at end of file + + external fun setTurboMode(enable: Boolean) +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt index b675854e5..0b1cdbfdd 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt @@ -4,10 +4,10 @@ import android.os.Build import android.os.PerformanceHintManager import androidx.annotation.RequiresApi -class PerformanceManager(val performanceHintManager: PerformanceHintManager) { +class PerformanceManager(private val performanceHintManager: PerformanceHintManager) { private var _isEnabled: Boolean = false private var renderingSession: PerformanceHintManager.Session? = null - val DEFAULT_TARGET_NS = 16666666L + private val DEFAULT_TARGET_NS = 16666666L @RequiresApi(Build.VERSION_CODES.S) fun initializeRenderingSession(threadId : Long){ @@ -46,4 +46,4 @@ class PerformanceManager(val performanceHintManager: PerformanceHintManager) { this.reportActualWorkDuration(effectiveTime) } } -} \ No newline at end of file +} 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 7191c883e..9913956bd 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,13 +3,13 @@ package org.ryujinx.android import android.view.KeyEvent import android.view.MotionEvent -class PhysicalControllerManager(val activity: MainActivity) { +class PhysicalControllerManager(val activity: GameActivity) { private var controllerId: Int = -1 private var ryujinxNative: RyujinxNative = RyujinxNative() fun onKeyEvent(event: KeyEvent) : Boolean{ if(controllerId != -1) { - val id = GetGamePadButtonInputId(event.keyCode) + val id = getGamePadButtonInputId(event.keyCode) if(id != GamePadButtonInputId.None) { when (event.action) { @@ -45,7 +45,7 @@ class PhysicalControllerManager(val activity: MainActivity) { controllerId = ryujinxNative.inputConnectGamepad(0) } - fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId { + private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId { return when (keycode) { KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A @@ -66,4 +66,4 @@ class PhysicalControllerManager(val activity: MainActivity) { else -> GamePadButtonInputId.None } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index c89259147..682050602 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -35,21 +35,21 @@ class RyujinxNative { external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo external fun deviceGetGameInfoFromPath(path: String): GameInfo external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean - external fun graphicsRendererSetSize(width: Int, height: Int): Unit - external fun graphicsRendererSetVsync(enabled: Boolean): Unit - external fun graphicsRendererRunLoop(): Unit - external fun inputInitialize(width: Int, height: Int): Unit - external fun inputSetClientSize(width: Int, height: Int): Unit - external fun inputSetTouchPoint(x: Int, y: Int): Unit - external fun inputReleaseTouchPoint(): Unit - external fun inputUpdate(): Unit - external fun inputSetButtonPressed(button: Int, id: Int): Unit - external fun inputSetButtonReleased(button: Int, id: Int): Unit + external fun graphicsRendererSetSize(width: Int, height: Int) + external fun graphicsRendererSetVsync(enabled: Boolean) + external fun graphicsRendererRunLoop() + external fun inputInitialize(width: Int, height: Int) + external fun inputSetClientSize(width: Int, height: Int) + external fun inputSetTouchPoint(x: Int, y: Int) + external fun inputReleaseTouchPoint() + external fun inputUpdate() + external fun inputSetButtonPressed(button: Int, id: Int) + external fun inputSetButtonReleased(button: Int, id: Int) external fun inputConnectGamepad(index: Int): Int - external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int): Unit + external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int) external fun graphicsSetSurface(surface: Long) external fun deviceCloseEmulation() external fun deviceSignalEmulationClose() external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcContentList(path: String, titleId: Long) : Array -} \ No newline at end of file +} 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 b86d7d799..c6c157f8d 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,10 +2,12 @@ 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 import androidx.navigation.NavHostController +import org.ryujinx.android.GameActivity import org.ryujinx.android.GameController import org.ryujinx.android.GameHost import org.ryujinx.android.GraphicsConfiguration @@ -13,6 +15,7 @@ import org.ryujinx.android.MainActivity import org.ryujinx.android.NativeGraphicsInterop import org.ryujinx.android.NativeHelpers import org.ryujinx.android.PerformanceManager +import org.ryujinx.android.PhysicalControllerManager import org.ryujinx.android.RegionCode import org.ryujinx.android.RyujinxNative import org.ryujinx.android.SystemLanguage @@ -20,6 +23,7 @@ import java.io.File @SuppressLint("WrongConstant") class MainViewModel(val activity: MainActivity) { + var physicalControllerManager: PhysicalControllerManager? = null var gameModel: GameModel? = null var gameHost: GameHost? = null var controller: GameController? = null @@ -44,23 +48,17 @@ class MainViewModel(val activity: MainActivity) { RyujinxNative().deviceSignalEmulationClose() gameHost?.close() RyujinxNative().deviceCloseEmulation() - goBack() - activity.setFullScreen(false) - } - - fun goBack(){ - navController?.popBackStack() } fun loadGame(game:GameModel) : Boolean { - var nativeRyujinx = RyujinxNative() + val nativeRyujinx = RyujinxNative() val descriptor = game.open() if(descriptor == 0) return false - gameModel = game; + gameModel = game val settings = QuickSettings(activity) @@ -68,42 +66,45 @@ class MainViewModel(val activity: MainActivity) { EnableShaderCache = settings.enableShaderCache EnableTextureRecompression = settings.enableTextureRecompression ResScale = settings.resScale + BackendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal }) if(!success) return false val nativeHelpers = NativeHelpers() - var nativeInterop = NativeGraphicsInterop() - nativeInterop!!.VkRequiredExtensions = arrayOf( + val nativeInterop = NativeGraphicsInterop() + nativeInterop.VkRequiredExtensions = arrayOf( "VK_KHR_surface", "VK_KHR_android_surface" ) - nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() - nativeInterop!!.SurfaceHandle = 0 + nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() + nativeInterop.SurfaceHandle = 0 - var driverViewModel = VulkanDriverViewModel(activity); - var drivers = driverViewModel.getAvailableDrivers() + val driverViewModel = VulkanDriverViewModel(activity) + val drivers = driverViewModel.getAvailableDrivers() - var driverHandle = 0L; + var driverHandle = 0L if (driverViewModel.selected.isNotEmpty()) { - var metaData = drivers.find { it.driverPath == driverViewModel.selected } + val metaData = drivers.find { it.driverPath == driverViewModel.selected } metaData?.apply { - var privatePath = activity.filesDir; - var privateDriverPath = privatePath.canonicalPath + "/driver/" + val privatePath = activity.filesDir + val privateDriverPath = privatePath.canonicalPath + "/driver/" val pD = File(privateDriverPath) if (pD.exists()) pD.deleteRecursively() pD.mkdirs() - var driver = File(driverViewModel.selected) - var parent = driver.parentFile - for (file in parent.walkTopDown()) { - if (file.absolutePath == parent.absolutePath) - continue - file.copyTo(File(privateDriverPath + file.name), true) + val driver = File(driverViewModel.selected) + val parent = driver.parentFile + if (parent != null) { + for (file in parent.walkTopDown()) { + if (file.absolutePath == parent.absolutePath) + continue + file.copyTo(File(privateDriverPath + file.name), true) + } } driverHandle = NativeHelpers().loadDriver( @@ -116,7 +117,7 @@ class MainViewModel(val activity: MainActivity) { } success = nativeRyujinx.graphicsInitializeRenderer( - nativeInterop!!.VkRequiredExtensions!!, + nativeInterop.VkRequiredExtensions!!, driverHandle ) if(!success) @@ -175,6 +176,8 @@ class MainViewModel(val activity: MainActivity) { this.controller = controller } - fun backCalled() { + fun navigateToGame() { + val intent = Intent(activity, GameActivity::class.java) + activity.startActivity(intent) } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index 3c7e7d9e3..5a8e44167 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -387,10 +387,7 @@ class HomeViews { viewModel.mainViewModel?.loadGame(gameModel) ?: false if (success) { launchOnUiThread { - viewModel.mainViewModel?.activity?.setFullScreen( - true - ) - viewModel.mainViewModel?.navController?.navigate("game") + viewModel.mainViewModel?.navigateToGame() } } else { gameModel.close() 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 872c19119..7c5c988e1 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 @@ -1,48 +1,11 @@ package org.ryujinx.android.views -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -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.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -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.RyujinxNative import org.ryujinx.android.viewmodels.MainViewModel -import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.viewmodels.SettingsViewModel -import kotlin.math.roundToInt class MainView { companion object { @@ -53,7 +16,6 @@ class MainView { NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } - composable("game") { GameView(mainViewModel) } composable("settings") { SettingViews.Main( SettingsViewModel( @@ -64,207 +26,5 @@ class MainView { } } } - - @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() - - var showController = remember { - mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController) - } - var enableVsync = remember { - mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) - } - var showMore = remember { - mutableStateOf(false) - } - - // 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() - ) - - } - } - } - } - } - }) { - } - 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" - ) - } - } - } - } - } - - var showBackNotice = remember { - mutableStateOf(false) - } - - BackHandler { - showBackNotice.value = true - } - - if (showBackNotice.value) { - AlertDialog(onDismissRequest = { showBackNotice.value = false }) { - Column { - Surface( - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight(), - shape = MaterialTheme.shapes.large, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text(text = "Are you sure you want to exit the game?") - Text(text = "All unsaved data will be lost!") - } - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Button(onClick = { - mainViewModel.closeGame() - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Exit Game") - } - - Button(onClick = { - showBackNotice.value = false - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Dismiss") - } - } - } - } - } - } - } - } - } - @Composable - fun 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) - } } }