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

View File

@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.audio.output" android:required="true" /> <uses-feature
android:name="android.hardware.audio.output"
android:required="true" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -12,23 +15,28 @@
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:theme="@style/Theme.RyujinxAndroid" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:hardwareAccelerated="false" android:hardwareAccelerated="true"
android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.RyujinxAndroid"> android:theme="@style/Theme.RyujinxAndroid">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

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

View File

@ -0,0 +1,346 @@
package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import compose.icons.CssGgIcons
import compose.icons.cssggicons.ToolbarBottom
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings
import kotlin.math.abs
import kotlin.math.roundToInt
class GameActivity : ComponentActivity() {
private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MainActivity.mainViewModel!!.physicalControllerManager = physicalControllerManager
setContent {
RyujinxAndroidTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
GameView(mainViewModel = MainActivity.mainViewModel!!)
}
}
}
}
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
event?.apply {
if(physicalControllerManager.onKeyEvent(this))
return true
}
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
ev?.apply {
physicalControllerManager.onMotionEvent(this)
}
return super.dispatchGenericMotionEvent(ev)
}
override fun onStop() {
super.onStop()
NativeHelpers().setTurboMode(false)
force60HzRefreshRate(false)
}
override fun onResume() {
super.onResume()
setFullScreen(true)
NativeHelpers().setTurboMode(true)
force60HzRefreshRate(true)
}
override fun onPause() {
super.onPause()
NativeHelpers().setTurboMode(false)
force60HzRefreshRate(false)
}
private fun force60HzRefreshRate(enable : Boolean) {
// Hack for MIUI devices since they don't support the standard Android APIs
try {
val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS")
setFpsIntent.putExtra("package_name", "org.ryujinx.android")
setFpsIntent.putExtra("isEnter", enable)
sendBroadcast(setFpsIntent)
} catch (_ : Exception) {
}
if (enable)
display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) }?.let { window.attributes.preferredDisplayModeId = it.modeId }
else
display?.supportedModes?.maxByOrNull { it.refreshRate }?.let { window.attributes.preferredDisplayModeId = it.modeId }
}
private fun setFullScreen(fullscreen: Boolean) {
requestedOrientation =
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
val insets = WindowCompat.getInsetsController(window, window.decorView)
insets.apply {
if (fullscreen) {
insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} else {
insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
}
}
@Composable
fun GameView(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
GameHost(context, mainViewModel)
}
)
GameOverlay(mainViewModel)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GameOverlay(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) {
GameStats(mainViewModel)
val ryujinxNative = RyujinxNative()
val showController = remember {
mutableStateOf(QuickSettings(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 return view
} }
@Composable @Composable
fun Compose(viewModel: MainViewModel) : Unit fun Compose(viewModel: MainViewModel) : Unit {
{
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context -> modifier = Modifier.fillMaxSize(), factory = { context ->
val controller = GameController(viewModel.activity) val controller = GameController(viewModel.activity)
val c = Create(context, viewModel.activity, controller) val c = Create(context, viewModel.activity, controller)
viewModel.activity.lifecycleScope.apply { viewModel.activity.lifecycleScope.apply {
viewModel.activity.lifecycleScope.launch { viewModel.activity.lifecycleScope.launch {
val events = merge(controller.leftGamePad.events(),controller.rightGamePad.events()) val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
.shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily) .shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily)
events.safeCollect { events.safeCollect {
controller.handleEvent(it) controller.handleEvent(it)

View File

@ -1,5 +1,6 @@
package org.ryujinx.android package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.view.SurfaceHolder import android.view.SurfaceHolder
@ -7,21 +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 _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()
@ -47,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)
@ -69,7 +73,9 @@ 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
_nativeRyujinx.inputInitialize(width, height) _nativeRyujinx.inputInitialize(width, height)
@ -82,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(),
@ -130,5 +136,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
} }
} }
_nativeRyujinx.graphicsRendererRunLoop() _nativeRyujinx.graphicsRendererRunLoop()
game?.close()
} }
} }

View File

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

View File

@ -1,8 +1,12 @@
package org.ryujinx.android 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.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor 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.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import compose.icons.CssGgIcons
import compose.icons.cssggicons.Games
class Icons { class Icons {
companion object{ companion object{
/// Icons exported from https://www.composables.com/icons /// Icons exported from https://www.composables.com/icons
@Composable @Composable
fun Download(): ImageVector { fun download(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember { return remember {
ImageVector.Builder( ImageVector.Builder(
name = "download", name = "download",
@ -26,9 +34,9 @@ class Icons {
viewportHeight = 40.0f viewportHeight = 40.0f
).apply { ).apply {
path( path(
fill = SolidColor(Color.Black), fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
stroke = SolidColor(primaryColor),
fillAlpha = 1f, fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f, strokeAlpha = 1f,
strokeLineWidth = 1.0f, strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt, strokeLineCap = StrokeCap.Butt,
@ -84,7 +92,77 @@ class Icons {
} }
} }
@Composable @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 val primaryColor = MaterialTheme.colorScheme.primary
return remember { return remember {
ImageVector.Builder( ImageVector.Builder(
@ -96,8 +174,8 @@ class Icons {
).apply { ).apply {
path( path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)), fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
fillAlpha = 1f,
stroke = SolidColor(primaryColor), stroke = SolidColor(primaryColor),
fillAlpha = 1f,
strokeAlpha = 1f, strokeAlpha = 1f,
strokeLineWidth = 1.0f, strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt, 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 package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.addCallback
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
import org.ryujinx.android.views.HomeViews
import org.ryujinx.android.views.MainView import org.ryujinx.android.views.MainView
import java.io.File
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
private var _isInit: Boolean = false private var _isInit: Boolean = false
var storageHelper: SimpleStorageHelper? = null var storageHelper: SimpleStorageHelper? = null
companion object { companion object {
@ -61,60 +45,7 @@ class MainActivity : ComponentActivity() {
} }
external fun getRenderingThreadId() : Long external fun getRenderingThreadId() : Long
external fun initVm() private external fun initVm()
fun setFullScreen(fullscreen: Boolean) {
requestedOrientation =
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
val insets = WindowCompat.getInsetsController(window, window.decorView)
insets.apply {
if (fullscreen) {
insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} else {
insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
insets.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
}
}
private fun getAudioDevice () : Int {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
return if (devices.isEmpty())
0
else {
val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
if(earPiece != null)
return earPiece.id
if(speaker != null)
return speaker.id
devices.first().id
}
}
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
event?.apply {
if(physicalControllerManager.onKeyEvent(this))
return true;
}
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
ev?.apply {
physicalControllerManager.onMotionEvent(this)
}
return super.dispatchGenericMotionEvent(ev)
}
private fun initialize() { private fun initialize() {
if (_isInit) if (_isInit)
@ -150,15 +81,6 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
/*Box {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
GameHost(context)
}
)
controller.Compose(lifecycleScope, lifecycle)
}*/
MainView.Main(mainViewModel = this) MainView.Main(mainViewModel = this)
} }
} }
@ -176,16 +98,3 @@ class MainActivity : ComponentActivity() {
storageHelper?.onRestoreInstanceState(savedInstanceState) storageHelper?.onRestoreInstanceState(savedInstanceState)
} }
} }
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
RyujinxAndroidTheme {
HomeViews.Home()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,12 @@ package org.ryujinx.android.viewmodels
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.PerformanceHintManager import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import org.ryujinx.android.GameActivity
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.GraphicsConfiguration import org.ryujinx.android.GraphicsConfiguration
@ -13,6 +15,7 @@ import org.ryujinx.android.MainActivity
import org.ryujinx.android.NativeGraphicsInterop import org.ryujinx.android.NativeGraphicsInterop
import org.ryujinx.android.NativeHelpers import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.PerformanceManager import org.ryujinx.android.PerformanceManager
import org.ryujinx.android.PhysicalControllerManager
import org.ryujinx.android.RegionCode import org.ryujinx.android.RegionCode
import org.ryujinx.android.RyujinxNative import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.SystemLanguage import org.ryujinx.android.SystemLanguage
@ -20,6 +23,8 @@ 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 gameHost: GameHost? = null var gameHost: GameHost? = null
var controller: GameController? = null var controller: GameController? = null
var performanceManager: PerformanceManager? = null var performanceManager: PerformanceManager? = null
@ -43,18 +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 path = game.getPath() ?: return false val descriptor = game.open()
if(descriptor == 0)
return false
gameModel = game
val settings = QuickSettings(activity) val settings = QuickSettings(activity)
@ -62,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(
@ -110,7 +117,7 @@ class MainViewModel(val activity: MainActivity) {
} }
success = nativeRyujinx.graphicsInitializeRenderer( success = nativeRyujinx.graphicsInitializeRenderer(
nativeInterop!!.VkRequiredExtensions!!, nativeInterop.VkRequiredExtensions!!,
driverHandle driverHandle
) )
if(!success) if(!success)
@ -131,7 +138,7 @@ class MainViewModel(val activity: MainActivity) {
if(!success) if(!success)
return false return false
success = nativeRyujinx.deviceLoad(path) success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
if(!success) if(!success)
return false return false
@ -169,6 +176,8 @@ class MainViewModel(val activity: MainActivity) {
this.controller = controller this.controller = controller
} }
fun backCalled() { fun navigateToGame() {
val intent = Intent(activity, GameActivity::class.java)
activity.startActivity(intent)
} }
} }

View File

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

View File

@ -1,50 +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.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.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController 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.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import kotlin.math.roundToInt
class MainView { class MainView {
companion object { companion object {
@ -55,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(
@ -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() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
@ -12,6 +13,7 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
} }
} }
rootProject.name = "RyujinxAndroid" rootProject.name = "RyujinxAndroid"