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:
parent
8ac307166a
commit
eb8cdde8dd
@ -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'
|
||||
|
@ -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" />
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user