move game view to new activity

This commit is contained in:
Emmanuel Hansen 2023-08-06 12:53:01 +00:00
parent 8c0bd460d9
commit 83c9e4fcb2
16 changed files with 444 additions and 409 deletions

View File

@ -75,6 +75,12 @@ tasks.named("preBuild") {
} }
dependencies { 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") runtimeOnly project(":libryujinx")
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')

View File

@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.audio.output" android:required="true" /> <uses-feature
android:name="android.hardware.audio.output"
android:required="true" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -12,23 +15,29 @@
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:isGame="true"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true"
android:appCategory="game"
android:theme="@style/Theme.RyujinxAndroid" android:theme="@style/Theme.RyujinxAndroid"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".GameActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/title_activity_game"
android:theme="@style/Theme.RyujinxAndroid" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:hardwareAccelerated="false" android:hardwareAccelerated="false"
android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.RyujinxAndroid"> android:theme="@style/Theme.RyujinxAndroid">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -225,3 +225,9 @@ void debug_break(int code){
if(code >= 3) if(code >= 3)
int r = 0; int r = 0;
} }
extern "C"
JNIEXPORT void JNICALL
Java_org_ryujinx_android_NativeHelpers_setTurboMode(JNIEnv *env, jobject thiz, jboolean enable) {
adrenotools_set_turbo(enable);
}

View File

@ -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)
}
}

View File

@ -1,5 +1,6 @@
package org.ryujinx.android package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.view.SurfaceHolder import android.view.SurfaceHolder
@ -7,22 +8,19 @@ import android.view.SurfaceView
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.viewmodels.QuickSettings
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
import java.io.File
import kotlin.concurrent.thread 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 game: GameModel? = null
private var _isClosed: Boolean = false private var _isClosed: Boolean = false
private var _renderingThreadWatcher: Thread? = null private var _renderingThreadWatcher: Thread? = null
private var _height: Int = 0 private var _height: Int = 0
private var _width: Int = 0 private var _width: Int = 0
private var _updateThread: Thread? = null private var _updateThread: Thread? = null
private var nativeInterop: NativeGraphicsInterop? = null
private var _guestThread: Thread? = null private var _guestThread: Thread? = null
private var _isInit: Boolean = false private var _isInit: Boolean = false
private var _isStarted: Boolean = false private var _isStarted: Boolean = false
private var _nativeWindow: Long = 0
private var _nativeRyujinx: RyujinxNative = RyujinxNative() private var _nativeRyujinx: RyujinxNative = RyujinxNative()
@ -48,6 +46,11 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
_width = width _width = width
_height = height _height = height
_nativeRyujinx.graphicsRendererSetSize(
width,
height
)
if(_isStarted) if(_isStarted)
{ {
_nativeRyujinx.inputSetClientSize(width, height) _nativeRyujinx.inputSetClientSize(width, height)
@ -70,7 +73,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
private fun start(surfaceHolder: SurfaceHolder) { private fun start(surfaceHolder: SurfaceHolder) {
mainViewModel.gameHost = this mainViewModel.gameHost = this
if(_isStarted) if(_isStarted)
return; return
game = mainViewModel.gameModel game = mainViewModel.gameModel
@ -85,7 +88,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
mainViewModel.controller?.connect() mainViewModel.controller?.connect()
} }
mainViewModel.activity.physicalControllerManager.connect() mainViewModel.physicalControllerManager?.connect()
_nativeRyujinx.graphicsRendererSetSize( _nativeRyujinx.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.width(),

View File

@ -4,7 +4,6 @@ import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
@ -12,10 +11,9 @@ import android.provider.MediaStore
class Helpers { class Helpers {
companion object{ companion object{
fun getPath(context: Context, uri: Uri): String? { fun getPath(context: Context, uri: Uri): String? {
val isKitKatorAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
// DocumentProvider // DocumentProvider
if (isKitKatorAbove && DocumentsContract.isDocumentUri(context, uri)) { if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider // ExternalStorageProvider
if (isExternalStorageDocument(uri)) { if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri) val docId = DocumentsContract.getDocumentId(uri)
@ -57,7 +55,7 @@ class Helpers {
return null return null
} }
fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? { private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null var cursor: Cursor? = null
val column = "_data" val column = "_data"
val projection = arrayOf(column) val projection = arrayOf(column)
@ -73,16 +71,16 @@ class Helpers {
return null return null
} }
fun isExternalStorageDocument(uri: Uri): Boolean { private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority 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 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 return "com.android.providers.media.documents" == uri.authority
} }
} }
} }

View File

@ -1,8 +1,6 @@
package org.ryujinx.android package org.ryujinx.android
import androidx.compose.foundation.layout.padding 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme

View File

@ -1,39 +1,23 @@
package org.ryujinx.android 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.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
import org.ryujinx.android.viewmodels.MainViewModel 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 org.ryujinx.android.views.MainView
import java.io.File
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
private var _isInit: Boolean = false private var _isInit: Boolean = false
var storageHelper: SimpleStorageHelper? = null var storageHelper: SimpleStorageHelper? = null
companion object { companion object {
@ -61,60 +45,7 @@ class MainActivity : ComponentActivity() {
} }
external fun getRenderingThreadId() : Long external fun getRenderingThreadId() : Long
external fun initVm() private 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 fun initialize() { private fun initialize() {
if (_isInit) if (_isInit)
@ -150,15 +81,6 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
/*Box {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
GameHost(context)
}
)
controller.Compose(lifecycleScope, lifecycle)
}*/
MainView.Main(mainViewModel = this) MainView.Main(mainViewModel = this)
} }
} }
@ -176,16 +98,3 @@ class MainActivity : ComponentActivity() {
storageHelper?.onRestoreInstanceState(savedInstanceState) storageHelper?.onRestoreInstanceState(savedInstanceState)
} }
} }
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
RyujinxAndroidTheme {
HomeViews.Home()
}
}

View File

@ -1,9 +1,7 @@
package org.ryujinx.android package org.ryujinx.android
import android.view.Surface
class NativeGraphicsInterop { class NativeGraphicsInterop {
var VkCreateSurface: Long = 0 var VkCreateSurface: Long = 0
var SurfaceHandle: Long = 0 var SurfaceHandle: Long = 0
var VkRequiredExtensions: Array<String>? = null var VkRequiredExtensions: Array<String>? = null
} }

View File

@ -9,12 +9,14 @@ class NativeHelpers {
System.loadLibrary("ryujinxjni") System.loadLibrary("ryujinxjni")
} }
} }
external fun releaseNativeWindow(window:Long) : Unit external fun releaseNativeWindow(window:Long)
external fun createSurface(vkInstance:Long, window:Long) : Long external fun createSurface(vkInstance:Long, window:Long) : Long
external fun getCreateSurfacePtr() : Long external fun getCreateSurfacePtr() : Long
external fun getNativeWindow(surface:Surface) : Long external fun getNativeWindow(surface:Surface) : Long
external fun attachCurrentThread() : Unit external fun attachCurrentThread()
external fun detachCurrentThread() : Unit external fun detachCurrentThread()
external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long
}
external fun setTurboMode(enable: Boolean)
}

View File

@ -4,10 +4,10 @@ import android.os.Build
import android.os.PerformanceHintManager import android.os.PerformanceHintManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
class PerformanceManager(val performanceHintManager: PerformanceHintManager) { class PerformanceManager(private val performanceHintManager: PerformanceHintManager) {
private var _isEnabled: Boolean = false private var _isEnabled: Boolean = false
private var renderingSession: PerformanceHintManager.Session? = null private var renderingSession: PerformanceHintManager.Session? = null
val DEFAULT_TARGET_NS = 16666666L private val DEFAULT_TARGET_NS = 16666666L
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
fun initializeRenderingSession(threadId : Long){ fun initializeRenderingSession(threadId : Long){
@ -46,4 +46,4 @@ class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
this.reportActualWorkDuration(effectiveTime) this.reportActualWorkDuration(effectiveTime)
} }
} }
} }

View File

@ -3,13 +3,13 @@ package org.ryujinx.android
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
class PhysicalControllerManager(val activity: MainActivity) { class PhysicalControllerManager(val activity: GameActivity) {
private var controllerId: Int = -1 private var controllerId: Int = -1
private var ryujinxNative: RyujinxNative = RyujinxNative() private var ryujinxNative: RyujinxNative = RyujinxNative()
fun onKeyEvent(event: KeyEvent) : Boolean{ fun onKeyEvent(event: KeyEvent) : Boolean{
if(controllerId != -1) { if(controllerId != -1) {
val id = GetGamePadButtonInputId(event.keyCode) val id = getGamePadButtonInputId(event.keyCode)
if(id != GamePadButtonInputId.None) { if(id != GamePadButtonInputId.None) {
when (event.action) { when (event.action) {
@ -45,7 +45,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
controllerId = ryujinxNative.inputConnectGamepad(0) controllerId = ryujinxNative.inputConnectGamepad(0)
} }
fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId { private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
return when (keycode) { return when (keycode) {
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
@ -66,4 +66,4 @@ class PhysicalControllerManager(val activity: MainActivity) {
else -> GamePadButtonInputId.None else -> GamePadButtonInputId.None
} }
} }
} }

View File

@ -35,21 +35,21 @@ class RyujinxNative {
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo
external fun deviceGetGameInfoFromPath(path: String): GameInfo external fun deviceGetGameInfoFromPath(path: String): GameInfo
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
external fun graphicsRendererSetSize(width: Int, height: Int): Unit external fun graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean): Unit external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop(): Unit external fun graphicsRendererRunLoop()
external fun inputInitialize(width: Int, height: Int): Unit external fun inputInitialize(width: Int, height: Int)
external fun inputSetClientSize(width: Int, height: Int): Unit external fun inputSetClientSize(width: Int, height: Int)
external fun inputSetTouchPoint(x: Int, y: Int): Unit external fun inputSetTouchPoint(x: Int, y: Int)
external fun inputReleaseTouchPoint(): Unit external fun inputReleaseTouchPoint()
external fun inputUpdate(): Unit external fun inputUpdate()
external fun inputSetButtonPressed(button: Int, id: Int): Unit external fun inputSetButtonPressed(button: Int, id: Int)
external fun inputSetButtonReleased(button: Int, id: Int): Unit external fun inputSetButtonReleased(button: Int, id: Int)
external fun inputConnectGamepad(index: Int): 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 graphicsSetSurface(surface: Long)
external fun deviceCloseEmulation() external fun deviceCloseEmulation()
external fun deviceSignalEmulationClose() external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String> external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
} }

View File

@ -2,10 +2,12 @@ package org.ryujinx.android.viewmodels
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.PerformanceHintManager import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import org.ryujinx.android.GameActivity
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.GraphicsConfiguration import org.ryujinx.android.GraphicsConfiguration
@ -13,6 +15,7 @@ import org.ryujinx.android.MainActivity
import org.ryujinx.android.NativeGraphicsInterop import org.ryujinx.android.NativeGraphicsInterop
import org.ryujinx.android.NativeHelpers import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.PerformanceManager import org.ryujinx.android.PerformanceManager
import org.ryujinx.android.PhysicalControllerManager
import org.ryujinx.android.RegionCode import org.ryujinx.android.RegionCode
import org.ryujinx.android.RyujinxNative import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.SystemLanguage import org.ryujinx.android.SystemLanguage
@ -20,6 +23,7 @@ import java.io.File
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) { class MainViewModel(val activity: MainActivity) {
var physicalControllerManager: PhysicalControllerManager? = null
var gameModel: GameModel? = null var gameModel: GameModel? = null
var gameHost: GameHost? = null var gameHost: GameHost? = null
var controller: GameController? = null var controller: GameController? = null
@ -44,23 +48,17 @@ class MainViewModel(val activity: MainActivity) {
RyujinxNative().deviceSignalEmulationClose() RyujinxNative().deviceSignalEmulationClose()
gameHost?.close() gameHost?.close()
RyujinxNative().deviceCloseEmulation() RyujinxNative().deviceCloseEmulation()
goBack()
activity.setFullScreen(false)
}
fun goBack(){
navController?.popBackStack()
} }
fun loadGame(game:GameModel) : Boolean { fun loadGame(game:GameModel) : Boolean {
var nativeRyujinx = RyujinxNative() val nativeRyujinx = RyujinxNative()
val descriptor = game.open() val descriptor = game.open()
if(descriptor == 0) if(descriptor == 0)
return false return false
gameModel = game; gameModel = game
val settings = QuickSettings(activity) val settings = QuickSettings(activity)
@ -68,42 +66,45 @@ class MainViewModel(val activity: MainActivity) {
EnableShaderCache = settings.enableShaderCache EnableShaderCache = settings.enableShaderCache
EnableTextureRecompression = settings.enableTextureRecompression EnableTextureRecompression = settings.enableTextureRecompression
ResScale = settings.resScale ResScale = settings.resScale
BackendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal
}) })
if(!success) if(!success)
return false return false
val nativeHelpers = NativeHelpers() val nativeHelpers = NativeHelpers()
var nativeInterop = NativeGraphicsInterop() val nativeInterop = NativeGraphicsInterop()
nativeInterop!!.VkRequiredExtensions = arrayOf( nativeInterop.VkRequiredExtensions = arrayOf(
"VK_KHR_surface", "VK_KHR_android_surface" "VK_KHR_surface", "VK_KHR_android_surface"
) )
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
nativeInterop!!.SurfaceHandle = 0 nativeInterop.SurfaceHandle = 0
var driverViewModel = VulkanDriverViewModel(activity); val driverViewModel = VulkanDriverViewModel(activity)
var drivers = driverViewModel.getAvailableDrivers() val drivers = driverViewModel.getAvailableDrivers()
var driverHandle = 0L; var driverHandle = 0L
if (driverViewModel.selected.isNotEmpty()) { if (driverViewModel.selected.isNotEmpty()) {
var metaData = drivers.find { it.driverPath == driverViewModel.selected } val metaData = drivers.find { it.driverPath == driverViewModel.selected }
metaData?.apply { metaData?.apply {
var privatePath = activity.filesDir; val privatePath = activity.filesDir
var privateDriverPath = privatePath.canonicalPath + "/driver/" val privateDriverPath = privatePath.canonicalPath + "/driver/"
val pD = File(privateDriverPath) val pD = File(privateDriverPath)
if (pD.exists()) if (pD.exists())
pD.deleteRecursively() pD.deleteRecursively()
pD.mkdirs() pD.mkdirs()
var driver = File(driverViewModel.selected) val driver = File(driverViewModel.selected)
var parent = driver.parentFile val parent = driver.parentFile
for (file in parent.walkTopDown()) { if (parent != null) {
if (file.absolutePath == parent.absolutePath) for (file in parent.walkTopDown()) {
continue if (file.absolutePath == parent.absolutePath)
file.copyTo(File(privateDriverPath + file.name), true) continue
file.copyTo(File(privateDriverPath + file.name), true)
}
} }
driverHandle = NativeHelpers().loadDriver( driverHandle = NativeHelpers().loadDriver(
@ -116,7 +117,7 @@ class MainViewModel(val activity: MainActivity) {
} }
success = nativeRyujinx.graphicsInitializeRenderer( success = nativeRyujinx.graphicsInitializeRenderer(
nativeInterop!!.VkRequiredExtensions!!, nativeInterop.VkRequiredExtensions!!,
driverHandle driverHandle
) )
if(!success) if(!success)
@ -175,6 +176,8 @@ class MainViewModel(val activity: MainActivity) {
this.controller = controller this.controller = controller
} }
fun backCalled() { fun navigateToGame() {
val intent = Intent(activity, GameActivity::class.java)
activity.startActivity(intent)
} }
} }

View File

@ -387,10 +387,7 @@ class HomeViews {
viewModel.mainViewModel?.loadGame(gameModel) ?: false viewModel.mainViewModel?.loadGame(gameModel) ?: false
if (success) { if (success) {
launchOnUiThread { launchOnUiThread {
viewModel.mainViewModel?.activity?.setFullScreen( viewModel.mainViewModel?.navigateToGame()
true
)
viewModel.mainViewModel?.navController?.navigate("game")
} }
} else { } else {
gameModel.close() gameModel.close()

View File

@ -1,48 +1,11 @@
package org.ryujinx.android.views 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.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.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController 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.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import kotlin.math.roundToInt
class MainView { class MainView {
companion object { companion object {
@ -53,7 +16,6 @@ class MainView {
NavHost(navController = navController, startDestination = "home") { NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
composable("game") { GameView(mainViewModel) }
composable("settings") { composable("settings") {
SettingViews.Main( SettingViews.Main(
SettingsViewModel( 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)
}
} }
} }