Archived
1
0
forked from MeloNX/MeloNX

android - add physical controller support

add performance hints

expand full screen to behind cutouts

fix touch, add toggle for virtual gamepad

remove safe area margins
This commit is contained in:
Emmanuel Hansen 2023-07-20 16:33:43 +00:00
parent c57f6a7fe3
commit fcb511bbca
16 changed files with 525 additions and 42 deletions

View File

@ -36,6 +36,12 @@ namespace LibRyujinx
[DllImport("libryujinxjni")]
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
[DllImport("libryujinxjni")]
internal extern static void setRenderingThread();
[DllImport("libryujinxjni")]
internal extern static void onFrameEnd(double time);
public delegate IntPtr JniCreateSurface(IntPtr native_surface, IntPtr instance);
[UnmanagedCallersOnly(EntryPoint = "JNI_OnLoad")]
@ -279,6 +285,11 @@ namespace LibRyujinx
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererRunLoop")]
public static void JniRunLoopNative(JEnvRef jEnv, JObjectLocalRef jObj)
{
SetSwapBuffersCallback(() =>
{
var time = SwitchDevice.EmulationContext.Statistics.GetGameFrameTime();
onFrameEnd(time);
});
RunLoop();
}

View File

@ -112,6 +112,11 @@ namespace LibRyujinx
_isActive = true;
if (Ryujinx.Common.SystemInfo.SystemInfo.IsBionic)
{
setRenderingThread();
}
while (_isActive)
{
if (_isStopped)

View File

@ -20,6 +20,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:largeHeap="true"
android:appCategory="game"
android:theme="@style/Theme.RyujinxAndroid"
tools:targetApi="31">
<activity

View File

@ -36,4 +36,10 @@ void* _ryujinxNative = NULL;
// Ryujinx imported functions
bool (*initialize)(char*) = NULL;
long _renderingThreadId = 0;
long _currentRenderingThreadId = 0;
JavaVM* _vm = nullptr;
jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr;
#endif //RYUJINXNATIVE_RYUIJNX_H

View File

@ -17,6 +17,31 @@
// }
#include "ryuijnx.h"
#include "pthread.h"
#include <chrono>
jmethodID _updateFrameTime;
JNIEnv* _rendererEnv = nullptr;
std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> _currentTimePoint;
JNIEnv* getEnv(bool isRenderer){
JNIEnv* env;
if(isRenderer){
env = _rendererEnv;
}
if(env != nullptr)
return env;
auto result = _vm->AttachCurrentThread(&env, NULL);
return env;
}
void detachEnv(){
auto result = _vm->DetachCurrentThread();
}
extern "C"
{
@ -128,4 +153,39 @@ jstring createString(
}
}
extern "C"
JNIEXPORT jlong JNICALL
Java_org_ryujinx_android_MainActivity_getRenderingThreadId(JNIEnv *env, jobject thiz) {
return _currentRenderingThreadId;
}
extern "C"
void setRenderingThread(){
auto currentId = pthread_self();
_currentRenderingThreadId = currentId;
_renderingThreadId = currentId;
_currentTimePoint = std::chrono::high_resolution_clock::now();
}
extern "C"
JNIEXPORT void JNICALL
Java_org_ryujinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) {
JavaVM* vm = nullptr;
auto success = env->GetJavaVM(&vm);
_vm = vm;
_mainActivity = thiz;
_mainActivityClass = env->GetObjectClass(thiz);
}
extern "C"
void onFrameEnd(double time){
auto env = getEnv(true);
auto cl = env->FindClass("org/ryujinx/android/MainActivity");
_updateFrameTime = env->GetStaticMethodID( cl , "updateRenderSessionPerformance", "(J)V");
auto now = std::chrono::high_resolution_clock::now();
auto nano = std::chrono::duration_cast<std::chrono::nanoseconds>(now-_currentTimePoint).count();
env->CallStaticVoidMethod(cl, _updateFrameTime,
nano);
}

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.flowWithLifecycle
@ -45,9 +46,18 @@ typealias GamePad = RadialGamePad
typealias GamePadConfig = RadialGamePadConfig
class GameController(var activity: Activity, var ryujinxNative: RyujinxNative = RyujinxNative()) {
private var controllerView: View? = null
var leftGamePad: GamePad
var rightGamePad: GamePad
var controllerId: Int = -1
val isVisible : Boolean
get() {
controllerView?.apply {
return this.isVisible
}
return false;
}
init {
leftGamePad = GamePad(generateConfig(true), 16f, activity)
@ -65,7 +75,6 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
@Composable
fun Compose(lifecycleScope: LifecycleCoroutineScope, lifecycle:Lifecycle) : Unit
{
AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context -> Create(context)})
@ -81,14 +90,25 @@ class GameController(var activity: Activity, var ryujinxNative: RyujinxNative =
}
}
private fun Create(context: Context) : View
private fun Create(context: Context) : View
{
var inflator = LayoutInflater.from(context);
var view = inflator.inflate(R.layout.game_layout, null)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(leftGamePad);
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(rightGamePad);
return view as View
controllerView = view
return controllerView as View
}
fun setVisible(isVisible: Boolean){
controllerView?.apply {
this.isVisible = isVisible
if(isVisible)
connect()
}
}
fun connect(){

View File

@ -1,13 +1,10 @@
package org.ryujinx.android
import android.content.Context
import android.os.ParcelFileDescriptor
import android.os.Build
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings
@ -15,6 +12,7 @@ import kotlin.concurrent.thread
import kotlin.math.roundToInt
class GameHost(context: Context?, val controller: GameController, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
private var _renderingThreadWatcher: Thread? = null
private var _height: Int = 0
private var _width: Int = 0
private var _updateThread: Thread? = null
@ -35,26 +33,6 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
holder.addCallback(this)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (_isStarted)
return when (event!!.actionMasked) {
MotionEvent.ACTION_MOVE -> {
_nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt())
true
}
MotionEvent.ACTION_DOWN -> {
_nativeRyujinx.inputSetTouchPoint(event.x.roundToInt(), event.y.roundToInt())
true
}
MotionEvent.ACTION_UP -> {
_nativeRyujinx.inputReleaseTouchPoint()
true
}
else -> super.onTouchEvent(event)
}
return super.onTouchEvent(event)
}
override fun surfaceCreated(holder: SurfaceHolder) {
}
@ -132,7 +110,16 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
_nativeRyujinx.inputInitialize(width, height)
controller.connect()
if(!settings.useVirtualController){
controller.setVisible(false)
}
else{
controller.connect()
}
mainViewModel.activity.physicalControllerManager.connect()
//
_nativeRyujinx.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(),
@ -159,7 +146,25 @@ class GameHost(context: Context?, val controller: GameController, val mainViewMo
}
private fun runGame() : Unit{
// RenderingThreadWatcher
_renderingThreadWatcher = thread(start = true) {
var threadId = 0L;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mainViewModel.performanceManager?.enable()
while (_isStarted) {
Thread.sleep(1000)
var newthreadId = mainViewModel.activity.getRenderingThreadId()
if (threadId != newthreadId) {
mainViewModel.performanceManager?.closeCurrentRenderingSession()
}
threadId = newthreadId;
if (threadId != 0L) {
mainViewModel.performanceManager?.initializeRenderingSession(threadId)
}
}
}
}
_nativeRyujinx.graphicsRendererRunLoop()
}
}

View File

@ -1,5 +1,6 @@
package org.ryujinx.android
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
@ -8,6 +9,8 @@ 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.compose.setContent
@ -29,28 +32,43 @@ import org.ryujinx.android.views.MainView
class MainActivity : ComponentActivity() {
private var mainViewModel: MainViewModel? = null
var physicalControllerManager: PhysicalControllerManager
private var _isInit: Boolean = false
var storageHelper: SimpleStorageHelper? = null
companion object {
var mainViewModel: MainViewModel? = null
var AppPath : String?
var StorageHelper: SimpleStorageHelper? = null
init {
AppPath = ""
}
@JvmStatic
fun updateRenderSessionPerformance(gameTime : Long)
{
if(gameTime <= 0)
return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mainViewModel?.performanceManager?.updateRenderingSessionTime(gameTime)
}
}
}
init {
physicalControllerManager = PhysicalControllerManager(this)
storageHelper = SimpleStorageHelper(this)
StorageHelper = storageHelper
System.loadLibrary("ryujinxjni")
initVm()
}
external fun getRenderingThreadId() : Long
external fun initVm()
fun setFullScreen() :Unit {
requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
WindowCompat.setDecorFitsSystemWindows(window,false)
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
var insets = WindowCompat.getInsetsController(window, window.decorView)
@ -79,6 +97,21 @@ class MainActivity : ComponentActivity() {
}
}
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
event?.apply {
return physicalControllerManager.onKeyEvent(this)
}
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
ev?.apply {
physicalControllerManager.onMotionEvent(this)
}
return super.dispatchGenericMotionEvent(ev)
}
private fun initialize() : Unit
{
if(_isInit)
@ -94,6 +127,9 @@ class MainActivity : ComponentActivity() {
initialize()
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
WindowCompat.setDecorFitsSystemWindows(window,false)
if(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager()
} else {

View File

@ -0,0 +1,49 @@
package org.ryujinx.android
import android.os.Build
import android.os.PerformanceHintManager
import androidx.annotation.RequiresApi
class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
private var _isEnabled: Boolean = false
private var renderingSession: PerformanceHintManager.Session? = null
val DEFAULT_TARGET_NS = 16666666L
@RequiresApi(Build.VERSION_CODES.S)
fun initializeRenderingSession(threadId : Long){
if(!_isEnabled || renderingSession != null)
return
var threads = IntArray(1)
threads[0] = threadId.toInt()
renderingSession = performanceHintManager.createHintSession(threads, DEFAULT_TARGET_NS)
}
@RequiresApi(Build.VERSION_CODES.S)
fun closeCurrentRenderingSession() {
if (_isEnabled)
renderingSession?.apply {
renderingSession = null
this.close()
}
}
fun enable(){
_isEnabled = true
}
@RequiresApi(Build.VERSION_CODES.S)
fun updateRenderingSessionTime(newTime : Long){
if(!_isEnabled)
return
var effectiveTime = newTime
if(newTime < DEFAULT_TARGET_NS)
effectiveTime = DEFAULT_TARGET_NS
renderingSession?.apply {
this.reportActualWorkDuration(effectiveTime)
}
}
}

View File

@ -0,0 +1,69 @@
package org.ryujinx.android
import android.view.KeyEvent
import android.view.MotionEvent
class PhysicalControllerManager(val activity: MainActivity) {
private var controllerId: Int = -1
private var ryujinxNative: RyujinxNative = RyujinxNative()
fun onKeyEvent(event: KeyEvent) : Boolean{
if(controllerId != -1) {
var id = GetGamePadButtonInputId(event.keyCode)
if(id != GamePadButtonInputId.None) {
when (event.action) {
KeyEvent.ACTION_UP -> {
ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
}
KeyEvent.ACTION_DOWN -> {
ryujinxNative.inputSetButtonPressed(id.ordinal, controllerId)
}
}
return true;
}
}
return false
}
fun onMotionEvent(ev: MotionEvent) {
if(controllerId != -1) {
if(ev.action == MotionEvent.ACTION_MOVE) {
var leftStickX = ev.getAxisValue(MotionEvent.AXIS_X);
var leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y);
var rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z);
var rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ);
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
}
}
}
fun connect(){
controllerId = ryujinxNative.inputConnectGamepad(0)
}
fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
return when (keycode) {
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X
KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder
KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger
KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStick
KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStick
KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp
KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown
KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft
KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight
KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus
KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus
else -> GamePadButtonInputId.None
}
}
}

View File

@ -1,18 +1,33 @@
package org.ryujinx.android.viewmodels
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState
import androidx.navigation.NavHostController
import org.ryujinx.android.GameHost
import org.ryujinx.android.MainActivity
import org.ryujinx.android.PerformanceManager
@SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) {
var performanceManager: PerformanceManager? = null
var selected: GameModel? = null
private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null
private var navController : NavHostController? = null
var homeViewModel: HomeViewModel = HomeViewModel(activity, this,)
var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
init {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
var hintService =
activity.getSystemService(Context.PERFORMANCE_HINT_SERVICE) as PerformanceHintManager
performanceManager = PerformanceManager(hintService)
}
}
fun loadGame(game:GameModel) {
var controller = navController?: return;

View File

@ -10,6 +10,7 @@ class QuickSettings(val activity: MainActivity) {
var enableDocked: Boolean
var enableVsync: Boolean
var useNce: Boolean
var useVirtualController: Boolean
var isHostMapped: Boolean
var enableShaderCache: Boolean
var enableTextureRecompression: Boolean
@ -27,5 +28,6 @@ class QuickSettings(val activity: MainActivity) {
enableShaderCache = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
resScale = sharedPref.getFloat("resScale", 1f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
}
}

View File

@ -26,7 +26,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
ignoreMissingServices: MutableState<Boolean>,
enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float>
resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean>
)
{
@ -39,6 +40,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
resScale.value = sharedPref.getFloat("resScale", 1f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
}
fun save(
@ -50,7 +52,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
ignoreMissingServices: MutableState<Boolean>,
enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float>
resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean>
){
var editor = sharedPref.edit()
@ -63,6 +66,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("enableShaderCache", enableShaderCache?.value ?: true)
editor.putBoolean("enableTextureRecompression", enableTextureRecompression?.value ?: false)
editor.putFloat("resScale", resScale?.value ?: 1f)
editor.putBoolean("useVirtualController", useVirtualController?.value ?: true)
editor.apply()
}

View File

@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth

View File

@ -1,16 +1,30 @@
package org.ryujinx.android.views
import androidx.compose.foundation.Image
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.padding
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.lifecycle.lifecycleScope
@ -19,8 +33,10 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost
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 {
@ -38,16 +54,171 @@ class MainView {
@Composable
fun GameView(mainViewModel: MainViewModel){
Box {
var controller = GameController(mainViewModel.activity)
Box(modifier = Modifier.fillMaxSize()) {
val controller = remember {
GameController(mainViewModel.activity)
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
GameHost(context, controller, mainViewModel)
}
)
GameOverlay(mainViewModel, controller)
}
}
@Composable
fun GameOverlay(mainViewModel: MainViewModel, controller: GameController){
Box(modifier = Modifier.fillMaxSize()) {
GameStats(mainViewModel)
var 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()
if(controller.isVisible)
continue
var change = event
.component1()
.firstOrNull()
change?.apply {
var position = this.position
if (event.type == PointerEventType.Press) {
ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(),
position.y.roundToInt()
)
} else if (event.type == PointerEventType.Release) {
ryujinxNative.inputReleaseTouchPoint()
} else if (event.type == PointerEventType.Move) {
ryujinxNative.inputSetTouchPoint(
position.x.roundToInt(),
position.y.roundToInt()
)
}
}
}
}
}) {
}
controller.Compose(mainViewModel.activity.lifecycleScope, mainViewModel.activity.lifecycle)
Row(modifier = Modifier
.align(Alignment.BottomCenter)
.padding(8.dp)) {
IconButton(modifier = Modifier.padding(4.dp),onClick = {
controller.setVisible(!controller.isVisible)
}) {
Icon(imageVector = rememberVideogameAsset(), contentDescription = "Toggle Virtual Pad")
}
}
}
}
@Composable
fun rememberVideogameAsset(): ImageVector {
var primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
name = "videogame_asset",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
fillAlpha = 1f,
stroke = SolidColor(primaryColor),
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(6.25f, 29.792f)
quadToRelative(-1.083f, 0f, -1.854f, -0.792f)
quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f)
verticalLineTo(12.833f)
quadToRelative(0f, -1.083f, 0.771f, -1.854f)
quadToRelative(0.771f, -0.771f, 1.854f, -0.771f)
horizontalLineToRelative(27.5f)
quadToRelative(1.083f, 0f, 1.854f, 0.771f)
quadToRelative(0.771f, 0.771f, 0.771f, 1.854f)
verticalLineToRelative(14.334f)
quadToRelative(0f, 1.041f, -0.771f, 1.833f)
reflectiveQuadToRelative(-1.854f, 0.792f)
close()
moveToRelative(0f, -2.625f)
horizontalLineToRelative(27.5f)
verticalLineTo(12.833f)
horizontalLineTo(6.25f)
verticalLineToRelative(14.334f)
close()
moveToRelative(7.167f, -1.792f)
quadToRelative(0.541f, 0f, 0.916f, -0.375f)
reflectiveQuadToRelative(0.375f, -0.917f)
verticalLineToRelative(-2.791f)
horizontalLineToRelative(2.75f)
quadToRelative(0.584f, 0f, 0.959f, -0.375f)
reflectiveQuadToRelative(0.375f, -0.917f)
quadToRelative(0f, -0.542f, -0.375f, -0.938f)
quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f)
horizontalLineToRelative(-2.75f)
verticalLineToRelative(-2.75f)
quadToRelative(0f, -0.542f, -0.375f, -0.938f)
quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f)
quadToRelative(-0.584f, 0f, -0.959f, 0.396f)
reflectiveQuadToRelative(-0.375f, 0.938f)
verticalLineToRelative(2.75f)
horizontalLineToRelative(-2.75f)
quadToRelative(-0.541f, 0f, -0.937f, 0.395f)
quadTo(8f, 19.458f, 8f, 20f)
quadToRelative(0f, 0.542f, 0.396f, 0.917f)
reflectiveQuadToRelative(0.937f, 0.375f)
horizontalLineToRelative(2.75f)
verticalLineToRelative(2.791f)
quadToRelative(0f, 0.542f, 0.396f, 0.917f)
reflectiveQuadToRelative(0.938f, 0.375f)
close()
moveToRelative(11.125f, -0.5f)
quadToRelative(0.791f, 0f, 1.396f, -0.583f)
quadToRelative(0.604f, -0.584f, 0.604f, -1.375f)
quadToRelative(0f, -0.834f, -0.604f, -1.417f)
quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f)
quadToRelative(-0.834f, 0f, -1.417f, 0.583f)
quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f)
quadToRelative(0f, 0.833f, 0.583f, 1.417f)
quadToRelative(0.583f, 0.583f, 1.417f, 0.583f)
close()
moveToRelative(3.916f, -5.833f)
quadToRelative(0.834f, 0f, 1.417f, -0.584f)
quadToRelative(0.583f, -0.583f, 0.583f, -1.416f)
quadToRelative(0f, -0.792f, -0.583f, -1.375f)
quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f)
quadToRelative(-0.791f, 0f, -1.375f, 0.584f)
quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f)
quadToRelative(0f, 0.833f, 0.583f, 1.416f)
quadToRelative(0.584f, 0.584f, 1.375f, 0.584f)
close()
moveTo(6.25f, 27.167f)
verticalLineTo(12.833f)
verticalLineToRelative(14.334f)
close()
}
}.build()
}
}

View File

@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons
@ -92,6 +94,9 @@ class SettingViews {
var resScale = remember {
mutableStateOf(1f)
}
var useVirtualController = remember {
mutableStateOf(true)
}
if (!loaded.value) {
settingsViewModel.initializeState(
@ -100,7 +105,8 @@ class SettingViews {
enableVsync, enableDocked, enablePtc, ignoreMissingServices,
enableShaderCache,
enableTextureRecompression,
resScale
resScale,
useVirtualController
)
loaded.value = true
}
@ -121,7 +127,8 @@ class SettingViews {
ignoreMissingServices,
enableShaderCache,
enableTextureRecompression,
resScale
resScale,
useVirtualController
)
settingsViewModel.navController.popBackStack()
}) {
@ -136,7 +143,8 @@ class SettingViews {
useNce, enableVsync, enableDocked, enablePtc, ignoreMissingServices,
enableShaderCache,
enableTextureRecompression,
resScale
resScale,
useVirtualController
)
}
ExpandableView(onCardArrowClick = { }, title = "System") {
@ -431,6 +439,25 @@ class SettingViews {
*/
}
}
ExpandableView(onCardArrowClick = { }, title = "Input") {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Show virtual controller",
modifier = Modifier.align(Alignment.CenterVertically)
)
Switch(checked = useVirtualController.value, onCheckedChange = {
useVirtualController.value = !useVirtualController.value
})
}
}
}
}
}
}