forked from MeloNX/MeloNX
move game view to new activity
This commit is contained in:
parent
8c0bd460d9
commit
83c9e4fcb2
@ -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')
|
||||||
|
@ -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" />
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user