move game view to new activity

This commit is contained in:
Emmanuel Hansen 2023-08-06 12:53:01 +00:00
parent 19d796d6b1
commit efcf29c435
16 changed files with 444 additions and 409 deletions

View File

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

View File

@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -12,23 +15,29 @@
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:isGame="true"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:largeHeap="true"
android:appCategory="game"
android:theme="@style/Theme.RyujinxAndroid"
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
android:name=".MainActivity"
android:exported="true"
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">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

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

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
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(),

View File

@ -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>?): String? {
private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): 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
}
}
}
}

View File

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

View File

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

View File

@ -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<String>? = null
}
}

View File

@ -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
}
external fun setTurboMode(enable: Boolean)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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