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 {
|
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'
|
||||||
|
@ -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" />
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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
|
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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import android.content.ContentUris
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
@ -12,10 +11,9 @@ import android.provider.MediaStore
|
|||||||
class Helpers {
|
class Helpers {
|
||||||
companion object{
|
companion object{
|
||||||
fun getPath(context: Context, uri: Uri): String? {
|
fun getPath(context: Context, uri: Uri): String? {
|
||||||
val isKitKatorAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
|
|
||||||
// DocumentProvider
|
// DocumentProvider
|
||||||
if (isKitKatorAbove && DocumentsContract.isDocumentUri(context, uri)) {
|
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||||
// ExternalStorageProvider
|
// ExternalStorageProvider
|
||||||
if (isExternalStorageDocument(uri)) {
|
if (isExternalStorageDocument(uri)) {
|
||||||
val docId = DocumentsContract.getDocumentId(uri)
|
val docId = DocumentsContract.getDocumentId(uri)
|
||||||
@ -57,7 +55,7 @@ class Helpers {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
|
private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
|
||||||
var cursor: Cursor? = null
|
var cursor: Cursor? = null
|
||||||
val column = "_data"
|
val column = "_data"
|
||||||
val projection = arrayOf(column)
|
val projection = arrayOf(column)
|
||||||
@ -73,16 +71,16 @@ class Helpers {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isExternalStorageDocument(uri: Uri): Boolean {
|
private fun isExternalStorageDocument(uri: Uri): Boolean {
|
||||||
return "com.android.externalstorage.documents" == uri.authority
|
return "com.android.externalstorage.documents" == uri.authority
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDownloadsDocument(uri: Uri): Boolean {
|
private fun isDownloadsDocument(uri: Uri): Boolean {
|
||||||
return "com.android.providers.downloads.documents" == uri.authority
|
return "com.android.providers.downloads.documents" == uri.authority
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMediaDocument(uri: Uri): Boolean {
|
private fun isMediaDocument(uri: Uri): Boolean {
|
||||||
return "com.android.providers.media.documents" == uri.authority
|
return "com.android.providers.media.documents" == uri.authority
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,39 +1,23 @@
|
|||||||
package org.ryujinx.android
|
package org.ryujinx.android
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.media.AudioDeviceInfo
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.addCallback
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
|
||||||
import org.ryujinx.android.views.HomeViews
|
|
||||||
import org.ryujinx.android.views.MainView
|
import org.ryujinx.android.views.MainView
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
|
|
||||||
private var _isInit: Boolean = false
|
private var _isInit: Boolean = false
|
||||||
var storageHelper: SimpleStorageHelper? = null
|
var storageHelper: SimpleStorageHelper? = null
|
||||||
companion object {
|
companion object {
|
||||||
@ -61,60 +45,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
external fun getRenderingThreadId() : Long
|
external fun getRenderingThreadId() : Long
|
||||||
external fun initVm()
|
private external fun initVm()
|
||||||
|
|
||||||
fun setFullScreen(fullscreen: Boolean) {
|
|
||||||
requestedOrientation =
|
|
||||||
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
|
||||||
|
|
||||||
val insets = WindowCompat.getInsetsController(window, window.decorView)
|
|
||||||
|
|
||||||
insets.apply {
|
|
||||||
if (fullscreen) {
|
|
||||||
insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
|
||||||
insets.systemBarsBehavior =
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
} else {
|
|
||||||
insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
|
||||||
insets.systemBarsBehavior =
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAudioDevice () : Int {
|
|
||||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
||||||
|
|
||||||
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
|
||||||
|
|
||||||
return if (devices.isEmpty())
|
|
||||||
0
|
|
||||||
else {
|
|
||||||
val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
|
|
||||||
val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
|
|
||||||
if(earPiece != null)
|
|
||||||
return earPiece.id
|
|
||||||
if(speaker != null)
|
|
||||||
return speaker.id
|
|
||||||
devices.first().id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
|
||||||
event?.apply {
|
|
||||||
if(physicalControllerManager.onKeyEvent(this))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.dispatchKeyEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
|
|
||||||
ev?.apply {
|
|
||||||
physicalControllerManager.onMotionEvent(this)
|
|
||||||
}
|
|
||||||
return super.dispatchGenericMotionEvent(ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
if (_isInit)
|
if (_isInit)
|
||||||
@ -150,15 +81,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
/*Box {
|
|
||||||
AndroidView(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
factory = { context ->
|
|
||||||
GameHost(context)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
controller.Compose(lifecycleScope, lifecycle)
|
|
||||||
}*/
|
|
||||||
MainView.Main(mainViewModel = this)
|
MainView.Main(mainViewModel = this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,16 +98,3 @@ class MainActivity : ComponentActivity() {
|
|||||||
storageHelper?.onRestoreInstanceState(savedInstanceState)
|
storageHelper?.onRestoreInstanceState(savedInstanceState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun GreetingPreview() {
|
|
||||||
RyujinxAndroidTheme {
|
|
||||||
HomeViews.Home()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,7 @@
|
|||||||
package org.ryujinx.android
|
package org.ryujinx.android
|
||||||
|
|
||||||
import android.view.Surface
|
|
||||||
|
|
||||||
class NativeGraphicsInterop {
|
class NativeGraphicsInterop {
|
||||||
var VkCreateSurface: Long = 0
|
var VkCreateSurface: Long = 0
|
||||||
var SurfaceHandle: Long = 0
|
var SurfaceHandle: Long = 0
|
||||||
var VkRequiredExtensions: Array<String>? = null
|
var VkRequiredExtensions: Array<String>? = null
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,14 @@ class NativeHelpers {
|
|||||||
System.loadLibrary("ryujinxjni")
|
System.loadLibrary("ryujinxjni")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
external fun releaseNativeWindow(window:Long) : Unit
|
external fun releaseNativeWindow(window:Long)
|
||||||
external fun createSurface(vkInstance:Long, window:Long) : Long
|
external fun createSurface(vkInstance:Long, window:Long) : Long
|
||||||
external fun getCreateSurfacePtr() : Long
|
external fun getCreateSurfacePtr() : Long
|
||||||
external fun getNativeWindow(surface:Surface) : Long
|
external fun getNativeWindow(surface:Surface) : Long
|
||||||
external fun attachCurrentThread() : Unit
|
external fun attachCurrentThread()
|
||||||
external fun detachCurrentThread() : Unit
|
external fun detachCurrentThread()
|
||||||
|
|
||||||
external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long
|
external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long
|
||||||
}
|
|
||||||
|
external fun setTurboMode(enable: Boolean)
|
||||||
|
}
|
||||||
|
@ -4,10 +4,10 @@ import android.os.Build
|
|||||||
import android.os.PerformanceHintManager
|
import android.os.PerformanceHintManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
|
class PerformanceManager(private val performanceHintManager: PerformanceHintManager) {
|
||||||
private var _isEnabled: Boolean = false
|
private var _isEnabled: Boolean = false
|
||||||
private var renderingSession: PerformanceHintManager.Session? = null
|
private var renderingSession: PerformanceHintManager.Session? = null
|
||||||
val DEFAULT_TARGET_NS = 16666666L
|
private val DEFAULT_TARGET_NS = 16666666L
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.S)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
fun initializeRenderingSession(threadId : Long){
|
fun initializeRenderingSession(threadId : Long){
|
||||||
@ -46,4 +46,4 @@ class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
|
|||||||
this.reportActualWorkDuration(effectiveTime)
|
this.reportActualWorkDuration(effectiveTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@ package org.ryujinx.android
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
|
||||||
class PhysicalControllerManager(val activity: MainActivity) {
|
class PhysicalControllerManager(val activity: GameActivity) {
|
||||||
private var controllerId: Int = -1
|
private var controllerId: Int = -1
|
||||||
private var ryujinxNative: RyujinxNative = RyujinxNative()
|
private var ryujinxNative: RyujinxNative = RyujinxNative()
|
||||||
|
|
||||||
fun onKeyEvent(event: KeyEvent) : Boolean{
|
fun onKeyEvent(event: KeyEvent) : Boolean{
|
||||||
if(controllerId != -1) {
|
if(controllerId != -1) {
|
||||||
val id = GetGamePadButtonInputId(event.keyCode)
|
val id = getGamePadButtonInputId(event.keyCode)
|
||||||
|
|
||||||
if(id != GamePadButtonInputId.None) {
|
if(id != GamePadButtonInputId.None) {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
@ -45,7 +45,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
|||||||
controllerId = ryujinxNative.inputConnectGamepad(0)
|
controllerId = ryujinxNative.inputConnectGamepad(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
||||||
return when (keycode) {
|
return when (keycode) {
|
||||||
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
|
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
|
||||||
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
|
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
|
||||||
@ -66,4 +66,4 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
|||||||
else -> GamePadButtonInputId.None
|
else -> GamePadButtonInputId.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,21 +35,21 @@ class RyujinxNative {
|
|||||||
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo
|
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo
|
||||||
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
||||||
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
|
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
|
||||||
external fun graphicsRendererSetSize(width: Int, height: Int): Unit
|
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||||
external fun graphicsRendererSetVsync(enabled: Boolean): Unit
|
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||||
external fun graphicsRendererRunLoop(): Unit
|
external fun graphicsRendererRunLoop()
|
||||||
external fun inputInitialize(width: Int, height: Int): Unit
|
external fun inputInitialize(width: Int, height: Int)
|
||||||
external fun inputSetClientSize(width: Int, height: Int): Unit
|
external fun inputSetClientSize(width: Int, height: Int)
|
||||||
external fun inputSetTouchPoint(x: Int, y: Int): Unit
|
external fun inputSetTouchPoint(x: Int, y: Int)
|
||||||
external fun inputReleaseTouchPoint(): Unit
|
external fun inputReleaseTouchPoint()
|
||||||
external fun inputUpdate(): Unit
|
external fun inputUpdate()
|
||||||
external fun inputSetButtonPressed(button: Int, id: Int): Unit
|
external fun inputSetButtonPressed(button: Int, id: Int)
|
||||||
external fun inputSetButtonReleased(button: Int, id: Int): Unit
|
external fun inputSetButtonReleased(button: Int, id: Int)
|
||||||
external fun inputConnectGamepad(index: Int): Int
|
external fun inputConnectGamepad(index: Int): Int
|
||||||
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int): Unit
|
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int)
|
||||||
external fun graphicsSetSurface(surface: Long)
|
external fun graphicsSetSurface(surface: Long)
|
||||||
external fun deviceCloseEmulation()
|
external fun deviceCloseEmulation()
|
||||||
external fun deviceSignalEmulationClose()
|
external fun deviceSignalEmulationClose()
|
||||||
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
|
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
|
||||||
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
|
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
|
||||||
}
|
}
|
||||||
|
@ -2,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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user