Archived
1
0
forked from MeloNX/MeloNX

switch to using stream base game loading

add game searching

add bottom popup ingame

move game view to new activity

fix manifest error

reduced virtual controller deadzone

enable hardware accel for activity
This commit is contained in:
Emmanuel Hansen 2023-08-06 14:10:29 +00:00
parent 8ac307166a
commit eb8cdde8dd
20 changed files with 647 additions and 414 deletions

View File

@ -36,6 +36,7 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
}
compileOptions {
@ -74,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')
@ -92,6 +99,7 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.2.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
implementation 'com.google.code.gson:gson:2.10.1'
implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0")
implementation "io.coil-kt:coil-compose:2.4.0"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'

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,28 @@
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: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:hardwareAccelerated="true"
android:theme="@style/Theme.RyujinxAndroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -224,4 +224,10 @@ extern "C"
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(this@GameActivity).useVirtualController)
}
val enableVsync = remember {
mutableStateOf(QuickSettings(this@GameActivity).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

@ -46,15 +46,17 @@ class GameController(var activity: Activity) {
return view
}
@Composable
fun Compose(viewModel: MainViewModel) : Unit
{
fun Compose(viewModel: MainViewModel) : Unit {
AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context ->
val controller = GameController(viewModel.activity)
val c = Create(context, viewModel.activity, controller)
viewModel.activity.lifecycleScope.apply {
viewModel.activity.lifecycleScope.launch {
val events = merge(controller.leftGamePad.events(),controller.rightGamePad.events())
viewModel.activity.lifecycleScope.apply {
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
.shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily)
events.safeCollect {
controller.handleEvent(it)

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,21 +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()
@ -47,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)
@ -69,7 +73,9 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
private fun start(surfaceHolder: SurfaceHolder) {
mainViewModel.gameHost = this
if(_isStarted)
return;
return
game = mainViewModel.gameModel
_nativeRyujinx.inputInitialize(width, height)
@ -82,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(),
@ -130,5 +136,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
}
}
_nativeRyujinx.graphicsRendererRunLoop()
game?.close()
}
}
}

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,12 @@
package org.ryujinx.android
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
@ -10,13 +14,17 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import compose.icons.CssGgIcons
import compose.icons.cssggicons.Games
class Icons {
companion object{
/// Icons exported from https://www.composables.com/icons
@Composable
fun Download(): ImageVector {
fun download(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
name = "download",
@ -26,9 +34,9 @@ class Icons {
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black),
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
stroke = SolidColor(primaryColor),
fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
@ -84,7 +92,77 @@ class Icons {
}
}
@Composable
fun VideoGame(): ImageVector {
fun vSync(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
name = "60fps",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
stroke = SolidColor(primaryColor),
fillAlpha = 1f,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(7.292f, 31.458f)
quadToRelative(-1.542f, 0f, -2.625f, -1.041f)
quadToRelative(-1.084f, -1.042f, -1.084f, -2.625f)
verticalLineTo(12.208f)
quadToRelative(0f, -1.583f, 1.084f, -2.625f)
quadTo(5.75f, 8.542f, 7.292f, 8.542f)
horizontalLineTo(14f)
quadToRelative(0.75f, 0f, 1.292f, 0.541f)
quadToRelative(0.541f, 0.542f, 0.541f, 1.292f)
reflectiveQuadToRelative(-0.541f, 1.292f)
quadToRelative(-0.542f, 0.541f, -1.292f, 0.541f)
horizontalLineTo(7.208f)
verticalLineToRelative(5.084f)
horizontalLineToRelative(6.709f)
quadToRelative(1.541f, 0f, 2.583f, 1.041f)
quadToRelative(1.042f, 1.042f, 1.042f, 2.625f)
verticalLineToRelative(6.834f)
quadToRelative(0f, 1.583f, -1.042f, 2.625f)
quadToRelative(-1.042f, 1.041f, -2.583f, 1.041f)
close()
moveToRelative(-0.084f, -10.5f)
verticalLineToRelative(6.834f)
horizontalLineToRelative(6.709f)
verticalLineToRelative(-6.834f)
close()
moveToRelative(17.125f, 6.834f)
horizontalLineToRelative(8.459f)
verticalLineTo(12.208f)
horizontalLineToRelative(-8.459f)
verticalLineToRelative(15.584f)
close()
moveToRelative(0f, 3.666f)
quadToRelative(-1.541f, 0f, -2.583f, -1.041f)
quadToRelative(-1.042f, -1.042f, -1.042f, -2.625f)
verticalLineTo(12.208f)
quadToRelative(0f, -1.583f, 1.042f, -2.625f)
quadToRelative(1.042f, -1.041f, 2.583f, -1.041f)
horizontalLineToRelative(8.459f)
quadToRelative(1.541f, 0f, 2.583f, 1.041f)
quadToRelative(1.042f, 1.042f, 1.042f, 2.625f)
verticalLineToRelative(15.584f)
quadToRelative(0f, 1.583f, -1.042f, 2.625f)
quadToRelative(-1.042f, 1.041f, -2.583f, 1.041f)
close()
}
}.build()
}
}
@Composable
fun videoGame(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
@ -96,8 +174,8 @@ class Icons {
).apply {
path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
fillAlpha = 1f,
stroke = SolidColor(primaryColor),
fillAlpha = 1f,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
@ -179,4 +257,16 @@ class Icons {
}
}
}
}
}
@Preview
@Composable
fun Preview(){
IconButton(modifier = Modifier.padding(4.dp), onClick = {
}) {
Icon(
imageVector = CssGgIcons.Games,
contentDescription = "Open Panel"
)
}
}

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,6 +2,7 @@ package org.ryujinx.android.viewmodels
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers
@ -9,6 +10,7 @@ import org.ryujinx.android.RyujinxNative
class GameModel(var file: DocumentFile, val context: Context) {
private var descriptor: ParcelFileDescriptor? = null
var fileName: String?
var fileSize = 0.0
var titleName: String? = null
@ -37,7 +39,18 @@ class GameModel(var file: DocumentFile, val context: Context) {
return uri.path
}
fun getIsXci() : Boolean {
fun open() : Int {
descriptor = context.contentResolver.openFileDescriptor(file.uri, "rw")
return descriptor?.fd ?: 0
}
fun close() {
descriptor?.close()
descriptor = null
}
fun isXci() : Boolean {
return file.extension == "xci"
}
}
@ -49,4 +62,4 @@ class GameInfo {
var Developer: String? = null
var Version: String? = null
var IconCache: String? = null
}
}

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,8 @@ 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
var performanceManager: PerformanceManager? = null
@ -43,18 +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 path = game.getPath() ?: return false
val descriptor = game.open()
if(descriptor == 0)
return false
gameModel = game
val settings = QuickSettings(activity)
@ -62,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(
@ -110,7 +117,7 @@ class MainViewModel(val activity: MainActivity) {
}
success = nativeRyujinx.graphicsInitializeRenderer(
nativeInterop!!.VkRequiredExtensions!!,
nativeInterop.VkRequiredExtensions!!,
driverHandle
)
if(!success)
@ -131,7 +138,7 @@ class MainViewModel(val activity: MainActivity) {
if(!success)
return false
success = nativeRyujinx.deviceLoad(path)
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
if(!success)
return false
@ -169,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

@ -1,10 +1,10 @@
package org.ryujinx.android.viewmodels
import android.app.Activity
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import org.ryujinx.android.MainActivity
class QuickSettings(val activity: MainActivity) {
class QuickSettings(val activity: Activity) {
var ignoreMissingServices: Boolean
var enablePtc: Boolean
var enableDocked: Boolean
@ -30,4 +30,4 @@ class QuickSettings(val activity: MainActivity) {
resScale = sharedPref.getFloat("resScale", 1f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
}
}
}

View File

@ -21,6 +21,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
@ -47,7 +48,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -68,6 +68,7 @@ import org.ryujinx.android.R
import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel
import java.io.File
import java.util.Locale
import kotlin.math.roundToInt
class HomeViews {
@ -76,7 +77,11 @@ class HomeViews {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTopBar(navController: NavHostController) {
fun MainTopBar(
navController: NavHostController,
query: MutableState<String>,
refresh: MutableState<Boolean>
) {
val topBarSize = remember {
mutableStateOf(0)
}
@ -87,15 +92,17 @@ class HomeViews {
TopAppBar(
modifier = Modifier
.zIndex(1f)
.padding(top = 16.dp)
.padding(top = 8.dp)
.onSizeChanged {
topBarSize.value = it.height
},
title = {
DockedSearchBar(
shape = SearchBarDefaults.inputFieldShape,
query = "",
onQueryChange = {},
query = query.value,
onQueryChange = {
query.value = it
},
onSearch = {},
active = false,
onActiveChange = {},
@ -113,6 +120,16 @@ class HomeViews {
}
},
actions = {
IconButton(
onClick = {
refresh.value = true
}
) {
Icon(
Icons.Filled.Refresh,
contentDescription = "Refresh"
)
}
IconButton(
onClick = {
showOptionsPopup.value = true
@ -126,15 +143,17 @@ class HomeViews {
}
)
Box {
if(showOptionsPopup.value)
{
if (showOptionsPopup.value) {
AlertDialog(
modifier = Modifier.padding(top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp,
start = 16.dp, end = 16.dp),
modifier = Modifier.padding(
top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp,
start = 16.dp, end = 16.dp
),
onDismissRequest = {
showOptionsPopup.value = false
}) {
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
val dialogWindowProvider =
LocalView.current.parent as DialogWindowProvider
dialogWindowProvider.window.setGravity(Gravity.TOP)
Surface(
modifier = Modifier
@ -145,19 +164,23 @@ class HomeViews {
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column {
TextButton(onClick = {
navController.navigate("settings")
}, modifier = Modifier
.fillMaxWidth()
.align(Alignment.Start),
TextButton(
onClick = {
navController.navigate("settings")
},
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Start),
) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings"
)
Text(text = "Settings", modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterVertically))
Text(
text = "Settings", modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterVertically)
)
}
}
}
@ -171,15 +194,19 @@ class HomeViews {
@Composable
fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) {
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
val showBottomSheet = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) }
val query = remember {
mutableStateOf("")
}
val refresh = remember {
mutableStateOf(true)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
navController?.apply {
MainTopBar(navController)
MainTopBar(navController, query, refresh)
}
},
floatingActionButtonPosition = FabPosition.End,
@ -200,11 +227,19 @@ class HomeViews {
val list = remember {
mutableStateListOf<GameModel>()
}
viewModel.setViewList(list)
if(refresh.value) {
viewModel.setViewList(list)
refresh.value = false
}
LazyColumn(Modifier.fillMaxSize()) {
items(list) {
it.titleName?.apply {
if (this.isNotEmpty())
if (this.isNotEmpty() && (query.value.trim().isEmpty() || this.lowercase(
Locale.getDefault()
)
.contains(query.value)))
GameItem(it, viewModel, showBottomSheet, showLoading)
}
}
@ -308,7 +343,7 @@ class HomeViews {
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
imageVector = org.ryujinx.android.Icons.Download(),
imageVector = org.ryujinx.android.Icons.download(),
contentDescription = "Game Dlc",
tint = Color.Green,
modifier = Modifier
@ -352,11 +387,10 @@ 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()
}
showLoading.value = false
}
@ -421,4 +455,4 @@ class HomeViews {
fun HomePreview() {
Home()
}
}
}

View File

@ -1,50 +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.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
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.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
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.SettingsViewModel
import kotlin.math.roundToInt
class MainView {
companion object {
@ -55,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(
@ -66,165 +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()
// touch surface
Surface(color = Color.Transparent, modifier = Modifier
.fillMaxSize()
.padding(0.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
Thread.sleep(2)
val event = awaitPointerEvent()
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 = {
mainViewModel.controller?.setVisible(!mainViewModel.controller!!.isVisible)
}) {
Icon(
imageVector = Icons.VideoGame(),
contentDescription = "Toggle Virtual Pad"
)
}
}
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)
}
}
}
}

View File

@ -4,6 +4,7 @@ pluginManagement {
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
}
}
dependencyResolutionManagement {
@ -12,6 +13,7 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
}
}
rootProject.name = "RyujinxAndroid"