summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/android/app/src/main/AndroidManifest.xml9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt21
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt45
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt52
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt103
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt)6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt20
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt60
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt32
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt154
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt37
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt316
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt27
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt247
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt37
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt55
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt46
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt53
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt213
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt90
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt57
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt285
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt151
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt538
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt58
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt255
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt159
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt62
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt32
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt235
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt184
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt59
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt96
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt206
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt77
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt33
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt48
-rw-r--r--src/android/app/src/main/jni/CMakeLists.txt2
-rw-r--r--src/android/app/src/main/jni/config.cpp31
-rw-r--r--src/android/app/src/main/jni/config.h24
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp14
-rw-r--r--src/android/app/src/main/jni/id_cache.h2
-rw-r--r--src/android/app/src/main/jni/native.cpp50
-rw-r--r--src/android/app/src/main/jni/native_config.cpp237
-rw-r--r--src/android/app/src/main/jni/uisettings.cpp10
-rw-r--r--src/android/app/src/main/jni/uisettings.h29
-rw-r--r--src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml16
-rw-r--r--src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml16
-rw-r--r--src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml16
-rw-r--r--src/android/app/src/main/res/anim/anim_settings_fragment_in.xml16
-rw-r--r--src/android/app/src/main/res/anim/anim_settings_fragment_out.xml10
-rw-r--r--src/android/app/src/main/res/animator/menu_slide_in_from_start.xml20
-rw-r--r--src/android/app/src/main/res/animator/menu_slide_out_to_start.xml21
-rw-r--r--src/android/app/src/main/res/layout/activity_settings.xml52
-rw-r--r--src/android/app/src/main/res/layout/fragment_emulation.xml83
-rw-r--r--src/android/app/src/main/res/layout/fragment_settings.xml39
-rw-r--r--src/android/app/src/main/res/layout/fragment_settings_search.xml120
-rw-r--r--src/android/app/src/main/res/menu/menu_settings.xml11
-rw-r--r--src/android/app/src/main/res/navigation/emulation_navigation.xml21
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml21
-rw-r--r--src/android/app/src/main/res/navigation/settings_navigation.xml32
-rw-r--r--src/android/app/src/main/res/values-de/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-es/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-fr/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-it/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-ja/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-ko/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-nb/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-pl/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-pt-rBR/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-pt-rPT/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-ru/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-uk/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-zh-rCN/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-zh-rTW/strings.xml1
-rw-r--r--src/android/app/src/main/res/values/arrays.xml10
-rw-r--r--src/android/app/src/main/res/values/strings.xml5
-rw-r--r--src/audio_core/renderer/system.cpp2
-rw-r--r--src/common/settings.cpp2
-rw-r--r--src/common/settings_common.cpp1
-rw-r--r--src/common/settings_common.h2
-rw-r--r--src/core/core.cpp25
-rw-r--r--src/core/core.h7
-rw-r--r--src/core/file_sys/content_archive.cpp17
-rw-r--r--src/core/hle/service/am/am.cpp8
-rw-r--r--src/core/hle/service/audio/hwopus.cpp6
-rw-r--r--src/core/hle/service/audio/hwopus.h1
-rw-r--r--src/core/hle/service/sockets/nsd.cpp11
-rw-r--r--src/core/hle/service/sockets/sfdnsres.cpp12
-rw-r--r--src/shader_recompiler/backend/glsl/emit_glsl_image.cpp2
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_image.cpp35
-rw-r--r--src/shader_recompiler/frontend/maxwell/translate/impl/move_special_register.cpp3
-rw-r--r--src/video_core/dma_pusher.cpp34
-rw-r--r--src/video_core/dma_pusher.h5
-rw-r--r--src/video_core/engines/engine_interface.h8
-rw-r--r--src/video_core/engines/engine_upload.h8
-rw-r--r--src/video_core/engines/kepler_compute.cpp20
-rw-r--r--src/video_core/engines/kepler_compute.h17
-rw-r--r--src/video_core/engines/maxwell_3d.cpp6
-rw-r--r--src/video_core/engines/puller.cpp15
-rw-r--r--src/video_core/host1x/codecs/codec.cpp3
-rw-r--r--src/video_core/renderer_opengl/gl_rasterizer.cpp11
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp13
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp14
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.cpp1
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h5
-rw-r--r--src/yuzu/main.cpp14
135 files changed, 3091 insertions, 2614 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6068c7a1f..a9f68a8f2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -105,6 +105,8 @@ if (MSVC)
105 set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE) 105 set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
106else() 106else()
107 add_compile_options( 107 add_compile_options(
108 -fwrapv
109
108 -Werror=all 110 -Werror=all
109 -Werror=extra 111 -Werror=extra
110 -Werror=missing-declarations 112 -Werror=missing-declarations
@@ -129,7 +131,6 @@ else()
129 131
130 if (ARCHITECTURE_x86_64) 132 if (ARCHITECTURE_x86_64)
131 add_compile_options("-mcx16") 133 add_compile_options("-mcx16")
132 add_compile_options("-fwrapv")
133 endif() 134 endif()
134 135
135 if (APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) 136 if (APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 36e2dac98..832c08e15 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -56,7 +56,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
56 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" 56 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
57 android:theme="@style/Theme.Yuzu.Main" 57 android:theme="@style/Theme.Yuzu.Main"
58 android:launchMode="singleTop" 58 android:launchMode="singleTop"
59 android:screenOrientation="userLandscape"
60 android:supportsPictureInPicture="true" 59 android:supportsPictureInPicture="true"
61 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode" 60 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
62 android:exported="true"> 61 android:exported="true">
@@ -67,6 +66,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
67 <data android:mimeType="application/octet-stream" /> 66 <data android:mimeType="application/octet-stream" />
68 </intent-filter> 67 </intent-filter>
69 68
69 <intent-filter>
70 <action android:name="android.intent.action.VIEW" />
71 <category android:name="android.intent.category.DEFAULT" />
72 <data
73 android:mimeType="application/octet-stream"
74 android:scheme="content"/>
75 </intent-filter>
76
70 <meta-data 77 <meta-data
71 android:name="android.nfc.action.TECH_DISCOVERED" 78 android:name="android.nfc.action.TECH_DISCOVERED"
72 android:resource="@xml/nfc_tech_filter" /> 79 android:resource="@xml/nfc_tech_filter" />
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 9c32e044c..c8706d7a6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -22,9 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil.exists
22import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize 22import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
23import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory 23import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
24import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri 24import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
25import org.yuzu.yuzu_emu.utils.Log.error 25import org.yuzu.yuzu_emu.utils.Log
26import org.yuzu.yuzu_emu.utils.Log.verbose
27import org.yuzu.yuzu_emu.utils.Log.warning
28import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable 26import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
29 27
30/** 28/**
@@ -219,10 +217,6 @@ object NativeLibrary {
219 217
220 external fun reloadSettings() 218 external fun reloadSettings()
221 219
222 external fun getUserSetting(gameID: String?, Section: String?, Key: String?): String?
223
224 external fun setUserSetting(gameID: String?, Section: String?, Key: String?, Value: String?)
225
226 external fun initGameIni(gameID: String?) 220 external fun initGameIni(gameID: String?)
227 221
228 /** 222 /**
@@ -413,14 +407,17 @@ object NativeLibrary {
413 details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } 407 details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
414 ) 408 )
415 } 409 }
410
416 CoreError.ErrorSavestate -> { 411 CoreError.ErrorSavestate -> {
417 title = emulationActivity.getString(R.string.save_load_error) 412 title = emulationActivity.getString(R.string.save_load_error)
418 message = details 413 message = details
419 } 414 }
415
420 CoreError.ErrorUnknown -> { 416 CoreError.ErrorUnknown -> {
421 title = emulationActivity.getString(R.string.fatal_error) 417 title = emulationActivity.getString(R.string.fatal_error)
422 message = emulationActivity.getString(R.string.fatal_error_message) 418 message = emulationActivity.getString(R.string.fatal_error_message)
423 } 419 }
420
424 else -> { 421 else -> {
425 return true 422 return true
426 } 423 }
@@ -454,6 +451,7 @@ object NativeLibrary {
454 captionId = R.string.loader_error_video_core 451 captionId = R.string.loader_error_video_core
455 descriptionId = R.string.loader_error_video_core_description 452 descriptionId = R.string.loader_error_video_core_description
456 } 453 }
454
457 else -> { 455 else -> {
458 captionId = R.string.loader_error_encrypted 456 captionId = R.string.loader_error_encrypted
459 descriptionId = R.string.loader_error_encrypted_roms_description 457 descriptionId = R.string.loader_error_encrypted_roms_description
@@ -465,7 +463,7 @@ object NativeLibrary {
465 463
466 val emulationActivity = sEmulationActivity.get() 464 val emulationActivity = sEmulationActivity.get()
467 if (emulationActivity == null) { 465 if (emulationActivity == null) {
468 warning("[NativeLibrary] EmulationActivity is null, can't exit.") 466 Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
469 return 467 return
470 } 468 }
471 469
@@ -490,15 +488,27 @@ object NativeLibrary {
490 } 488 }
491 489
492 fun setEmulationActivity(emulationActivity: EmulationActivity?) { 490 fun setEmulationActivity(emulationActivity: EmulationActivity?) {
493 verbose("[NativeLibrary] Registering EmulationActivity.") 491 Log.verbose("[NativeLibrary] Registering EmulationActivity.")
494 sEmulationActivity = WeakReference(emulationActivity) 492 sEmulationActivity = WeakReference(emulationActivity)
495 } 493 }
496 494
497 fun clearEmulationActivity() { 495 fun clearEmulationActivity() {
498 verbose("[NativeLibrary] Unregistering EmulationActivity.") 496 Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
499 sEmulationActivity.clear() 497 sEmulationActivity.clear()
500 } 498 }
501 499
500 @Keep
501 @JvmStatic
502 fun onEmulationStarted() {
503 sEmulationActivity.get()!!.onEmulationStarted()
504 }
505
506 @Keep
507 @JvmStatic
508 fun onEmulationStopped(status: Int) {
509 sEmulationActivity.get()!!.onEmulationStopped(status)
510 }
511
502 /** 512 /**
503 * Logs the Yuzu version, Android version and, CPU. 513 * Logs the Yuzu version, Android version and, CPU.
504 */ 514 */
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 04ab6a220..9561748cb 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -46,7 +46,7 @@ class YuzuApplication : Application() {
46 super.onCreate() 46 super.onCreate()
47 application = this 47 application = this
48 documentsTree = DocumentsTree() 48 documentsTree = DocumentsTree()
49 DirectoryInitialization.start(applicationContext) 49 DirectoryInitialization.start()
50 GpuDriverHelper.initializeDriverParameters(applicationContext) 50 GpuDriverHelper.initializeDriverParameters(applicationContext)
51 NativeLibrary.logDeviceInfo() 51 NativeLibrary.logDeviceInfo()
52 52
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index 7461fb093..bbd328c71 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
42import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 42import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
43import org.yuzu.yuzu_emu.features.settings.model.IntSetting 43import org.yuzu.yuzu_emu.features.settings.model.IntSetting
44import org.yuzu.yuzu_emu.features.settings.model.Settings 44import org.yuzu.yuzu_emu.features.settings.model.Settings
45import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel 45import org.yuzu.yuzu_emu.model.EmulationViewModel
46import org.yuzu.yuzu_emu.model.Game 46import org.yuzu.yuzu_emu.model.Game
47import org.yuzu.yuzu_emu.utils.ControllerMappingHelper 47import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
48import org.yuzu.yuzu_emu.utils.ForegroundService 48import org.yuzu.yuzu_emu.utils.ForegroundService
@@ -72,18 +72,17 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
72 private val actionMute = "ACTION_EMULATOR_MUTE" 72 private val actionMute = "ACTION_EMULATOR_MUTE"
73 private val actionUnmute = "ACTION_EMULATOR_UNMUTE" 73 private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
74 74
75 private val settingsViewModel: SettingsViewModel by viewModels() 75 private val emulationViewModel: EmulationViewModel by viewModels()
76 76
77 override fun onDestroy() { 77 override fun onDestroy() {
78 stopForegroundService(this) 78 stopForegroundService(this)
79 emulationViewModel.clear()
79 super.onDestroy() 80 super.onDestroy()
80 } 81 }
81 82
82 override fun onCreate(savedInstanceState: Bundle?) { 83 override fun onCreate(savedInstanceState: Bundle?) {
83 ThemeHelper.setTheme(this) 84 ThemeHelper.setTheme(this)
84 85
85 settingsViewModel.settings.loadSettings()
86
87 super.onCreate(savedInstanceState) 86 super.onCreate(savedInstanceState)
88 87
89 binding = ActivityEmulationBinding.inflate(layoutInflater) 88 binding = ActivityEmulationBinding.inflate(layoutInflater)
@@ -91,9 +90,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
91 90
92 val navHostFragment = 91 val navHostFragment =
93 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment 92 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
94 val navController = navHostFragment.navController 93 navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
95 navController
96 .setGraph(R.navigation.emulation_navigation, intent.extras)
97 94
98 isActivityRecreated = savedInstanceState != null 95 isActivityRecreated = savedInstanceState != null
99 96
@@ -424,6 +421,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
424 } 421 }
425 } 422 }
426 423
424 fun onEmulationStarted() {
425 emulationViewModel.setEmulationStarted(true)
426 }
427
428 fun onEmulationStopped(status: Int) {
429 if (status == 0) {
430 finish()
431 }
432 }
433
427 private fun startMotionSensorListener() { 434 private fun startMotionSensorListener() {
428 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager 435 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
429 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) 436 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index e91277d35..0013e8512 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -3,8 +3,8 @@
3 3
4package org.yuzu.yuzu_emu.adapters 4package org.yuzu.yuzu_emu.adapters
5 5
6import android.graphics.Bitmap 6import android.content.Intent
7import android.graphics.BitmapFactory 7import android.graphics.drawable.BitmapDrawable
8import android.net.Uri 8import android.net.Uri
9import android.text.TextUtils 9import android.text.TextUtils
10import android.view.LayoutInflater 10import android.view.LayoutInflater
@@ -13,25 +13,26 @@ import android.view.ViewGroup
13import android.widget.ImageView 13import android.widget.ImageView
14import android.widget.Toast 14import android.widget.Toast
15import androidx.appcompat.app.AppCompatActivity 15import androidx.appcompat.app.AppCompatActivity
16import androidx.core.content.pm.ShortcutInfoCompat
17import androidx.core.content.pm.ShortcutManagerCompat
18import androidx.core.graphics.drawable.IconCompat
16import androidx.documentfile.provider.DocumentFile 19import androidx.documentfile.provider.DocumentFile
17import androidx.lifecycle.ViewModelProvider 20import androidx.lifecycle.ViewModelProvider
18import androidx.lifecycle.lifecycleScope
19import androidx.navigation.findNavController 21import androidx.navigation.findNavController
20import androidx.preference.PreferenceManager 22import androidx.preference.PreferenceManager
21import androidx.recyclerview.widget.AsyncDifferConfig 23import androidx.recyclerview.widget.AsyncDifferConfig
22import androidx.recyclerview.widget.DiffUtil 24import androidx.recyclerview.widget.DiffUtil
23import androidx.recyclerview.widget.ListAdapter 25import androidx.recyclerview.widget.ListAdapter
24import androidx.recyclerview.widget.RecyclerView 26import androidx.recyclerview.widget.RecyclerView
25import coil.load
26import kotlinx.coroutines.launch
27import org.yuzu.yuzu_emu.HomeNavigationDirections 27import org.yuzu.yuzu_emu.HomeNavigationDirections
28import org.yuzu.yuzu_emu.NativeLibrary
29import org.yuzu.yuzu_emu.R 28import org.yuzu.yuzu_emu.R
30import org.yuzu.yuzu_emu.YuzuApplication 29import org.yuzu.yuzu_emu.YuzuApplication
30import org.yuzu.yuzu_emu.activities.EmulationActivity
31import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder 31import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
32import org.yuzu.yuzu_emu.databinding.CardGameBinding 32import org.yuzu.yuzu_emu.databinding.CardGameBinding
33import org.yuzu.yuzu_emu.model.Game 33import org.yuzu.yuzu_emu.model.Game
34import org.yuzu.yuzu_emu.model.GamesViewModel 34import org.yuzu.yuzu_emu.model.GamesViewModel
35import org.yuzu.yuzu_emu.utils.GameIconUtils
35 36
36class GameAdapter(private val activity: AppCompatActivity) : 37class GameAdapter(private val activity: AppCompatActivity) :
37 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), 38 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
@@ -82,6 +83,21 @@ class GameAdapter(private val activity: AppCompatActivity) :
82 ) 83 )
83 .apply() 84 .apply()
84 85
86 val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
87 action = Intent.ACTION_VIEW
88 data = Uri.parse(holder.game.path)
89 }
90 val shortcut = ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path)
91 .setShortLabel(holder.game.title)
92 .setIcon(
93 IconCompat.createWithBitmap(
94 (holder.binding.imageGameScreen.drawable as BitmapDrawable).bitmap
95 )
96 )
97 .setIntent(openIntent)
98 .build()
99 ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
100
85 val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game) 101 val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
86 view.findNavController().navigate(action) 102 view.findNavController().navigate(action)
87 } 103 }
@@ -98,12 +114,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
98 this.game = game 114 this.game = game
99 115
100 binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP 116 binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
101 activity.lifecycleScope.launch { 117 GameIconUtils.loadGameIcon(game, binding.imageGameScreen)
102 val bitmap = decodeGameIcon(game.path)
103 binding.imageGameScreen.load(bitmap) {
104 error(R.drawable.default_icon)
105 }
106 }
107 118
108 binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") 119 binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
109 120
@@ -126,14 +137,4 @@ class GameAdapter(private val activity: AppCompatActivity) :
126 return oldItem == newItem 137 return oldItem == newItem
127 } 138 }
128 } 139 }
129
130 private fun decodeGameIcon(uri: String): Bitmap? {
131 val data = NativeLibrary.getIcon(uri)
132 return BitmapFactory.decodeByteArray(
133 data,
134 0,
135 data.size,
136 BitmapFactory.Options()
137 )
138 }
139} 140}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
index 9f859b442..8d87d3bd7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
@@ -45,8 +45,8 @@ class HomeSettingAdapter(
45 holder.option.onClick.invoke() 45 holder.option.onClick.invoke()
46 } else { 46 } else {
47 MessageDialogFragment.newInstance( 47 MessageDialogFragment.newInstance(
48 holder.option.disabledTitleId, 48 titleId = holder.option.disabledTitleId,
49 holder.option.disabledMessageId 49 descriptionId = holder.option.disabledMessageId
50 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) 50 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
51 } 51 }
52 } 52 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt
index a18efef19..6f4b5b13f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress.kt
@@ -4,43 +4,43 @@
4package org.yuzu.yuzu_emu.disk_shader_cache 4package org.yuzu.yuzu_emu.disk_shader_cache
5 5
6import androidx.annotation.Keep 6import androidx.annotation.Keep
7import androidx.lifecycle.ViewModelProvider
7import org.yuzu.yuzu_emu.NativeLibrary 8import org.yuzu.yuzu_emu.NativeLibrary
8import org.yuzu.yuzu_emu.R 9import org.yuzu.yuzu_emu.R
9import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment 10import org.yuzu.yuzu_emu.activities.EmulationActivity
11import org.yuzu.yuzu_emu.model.EmulationViewModel
12import org.yuzu.yuzu_emu.utils.Log
10 13
11@Keep 14@Keep
12object DiskShaderCacheProgress { 15object DiskShaderCacheProgress {
13 val finishLock = Object() 16 private lateinit var emulationViewModel: EmulationViewModel
14 private lateinit var fragment: ShaderProgressDialogFragment
15 17
16 private fun prepareDialog() { 18 private fun prepareViewModel() {
17 val emulationActivity = NativeLibrary.sEmulationActivity.get()!! 19 emulationViewModel =
18 emulationActivity.runOnUiThread { 20 ViewModelProvider(
19 fragment = ShaderProgressDialogFragment.newInstance( 21 NativeLibrary.sEmulationActivity.get() as EmulationActivity
20 emulationActivity.getString(R.string.loading), 22 )[EmulationViewModel::class.java]
21 emulationActivity.getString(R.string.preparing_shaders)
22 )
23 fragment.show(
24 emulationActivity.supportFragmentManager,
25 ShaderProgressDialogFragment.TAG
26 )
27 }
28 synchronized(finishLock) { finishLock.wait() }
29 } 23 }
30 24
31 @JvmStatic 25 @JvmStatic
32 fun loadProgress(stage: Int, progress: Int, max: Int) { 26 fun loadProgress(stage: Int, progress: Int, max: Int) {
33 val emulationActivity = NativeLibrary.sEmulationActivity.get() 27 val emulationActivity = NativeLibrary.sEmulationActivity.get()
34 ?: error("[DiskShaderCacheProgress] EmulationActivity not present") 28 if (emulationActivity == null) {
35 29 Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
36 when (LoadCallbackStage.values()[stage]) { 30 return
37 LoadCallbackStage.Prepare -> prepareDialog() 31 }
38 LoadCallbackStage.Build -> fragment.onUpdateProgress( 32
39 emulationActivity.getString(R.string.building_shaders), 33 emulationActivity.runOnUiThread {
40 progress, 34 when (LoadCallbackStage.values()[stage]) {
41 max 35 LoadCallbackStage.Prepare -> prepareViewModel()
42 ) 36 LoadCallbackStage.Build -> emulationViewModel.updateProgress(
43 LoadCallbackStage.Complete -> fragment.dismiss() 37 emulationActivity.getString(R.string.building_shaders),
38 progress,
39 max
40 )
41
42 LoadCallbackStage.Complete -> {}
43 }
44 } 44 }
45 } 45 }
46 46
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt
deleted file mode 100644
index bf6f0366d..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ShaderProgressViewModel.kt
+++ /dev/null
@@ -1,31 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.disk_shader_cache
5
6import androidx.lifecycle.LiveData
7import androidx.lifecycle.MutableLiveData
8import androidx.lifecycle.ViewModel
9
10class ShaderProgressViewModel : ViewModel() {
11 private val _progress = MutableLiveData(0)
12 val progress: LiveData<Int> get() = _progress
13
14 private val _max = MutableLiveData(0)
15 val max: LiveData<Int> get() = _max
16
17 private val _message = MutableLiveData("")
18 val message: LiveData<String> get() = _message
19
20 fun setProgress(progress: Int) {
21 _progress.postValue(progress)
22 }
23
24 fun setMax(max: Int) {
25 _max.postValue(max)
26 }
27
28 fun setMessage(msg: String) {
29 _message.postValue(msg)
30 }
31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt
deleted file mode 100644
index 8a8e0a6e8..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/disk_shader_cache/ui/ShaderProgressDialogFragment.kt
+++ /dev/null
@@ -1,103 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.disk_shader_cache.ui
5
6import android.app.Dialog
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import androidx.appcompat.app.AlertDialog
12import androidx.fragment.app.DialogFragment
13import androidx.lifecycle.ViewModelProvider
14import com.google.android.material.dialog.MaterialAlertDialogBuilder
15import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
16import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
17import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
18
19class ShaderProgressDialogFragment : DialogFragment() {
20 private var _binding: DialogProgressBarBinding? = null
21 private val binding get() = _binding!!
22
23 private lateinit var alertDialog: AlertDialog
24
25 private lateinit var shaderProgressViewModel: ShaderProgressViewModel
26
27 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
28 _binding = DialogProgressBarBinding.inflate(layoutInflater)
29 shaderProgressViewModel =
30 ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
31
32 val title = requireArguments().getString(TITLE)
33 val message = requireArguments().getString(MESSAGE)
34
35 isCancelable = false
36 alertDialog = MaterialAlertDialogBuilder(requireActivity())
37 .setView(binding.root)
38 .setTitle(title)
39 .setMessage(message)
40 .create()
41 return alertDialog
42 }
43
44 override fun onCreateView(
45 inflater: LayoutInflater,
46 container: ViewGroup?,
47 savedInstanceState: Bundle?
48 ): View {
49 return binding.root
50 }
51
52 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
53 super.onViewCreated(view, savedInstanceState)
54 shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
55 binding.progressBar.progress = progress
56 setUpdateText()
57 }
58 shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
59 binding.progressBar.max = max
60 setUpdateText()
61 }
62 shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
63 alertDialog.setMessage(msg)
64 }
65 synchronized(DiskShaderCacheProgress.finishLock) {
66 DiskShaderCacheProgress.finishLock.notifyAll()
67 }
68 }
69
70 override fun onDestroyView() {
71 super.onDestroyView()
72 _binding = null
73 }
74
75 fun onUpdateProgress(msg: String, progress: Int, max: Int) {
76 shaderProgressViewModel.setProgress(progress)
77 shaderProgressViewModel.setMax(max)
78 shaderProgressViewModel.setMessage(msg)
79 }
80
81 private fun setUpdateText() {
82 binding.progressText.text = String.format(
83 "%d/%d",
84 shaderProgressViewModel.progress.value,
85 shaderProgressViewModel.max.value
86 )
87 }
88
89 companion object {
90 const val TAG = "ProgressDialogFragment"
91 const val TITLE = "title"
92 const val MESSAGE = "message"
93
94 fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
95 val frag = ShaderProgressDialogFragment()
96 val args = Bundle()
97 args.putString(TITLE, title)
98 args.putString(MESSAGE, message)
99 frag.arguments = args
100 return frag
101 }
102 }
103}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
index a6e9833ee..aeda8d222 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
@@ -4,5 +4,7 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractBooleanSetting : AbstractSetting { 6interface AbstractBooleanSetting : AbstractSetting {
7 var boolean: Boolean 7 val boolean: Boolean
8
9 fun setBoolean(value: Boolean)
8} 10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
index bd9233d62..606519ad8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
@@ -3,8 +3,8 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import androidx.lifecycle.ViewModel 6interface AbstractByteSetting : AbstractSetting {
7 val byte: Byte
7 8
8class SettingsViewModel : ViewModel() { 9 fun setByte(value: Byte)
9 val settings = Settings()
10} 10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
index 6fe4bc263..974925eed 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
@@ -4,5 +4,7 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractFloatSetting : AbstractSetting { 6interface AbstractFloatSetting : AbstractSetting {
7 var float: Float 7 val float: Float
8
9 fun setFloat(value: Float)
8} 10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
index 892b7dcfe..89b285b10 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
@@ -4,5 +4,7 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractIntSetting : AbstractSetting { 6interface AbstractIntSetting : AbstractSetting {
7 var int: Int 7 val int: Int
8
9 fun setInt(value: Int)
8} 10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
new file mode 100644
index 000000000..4873942db
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
@@ -0,0 +1,10 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractLongSetting : AbstractSetting {
7 val long: Long
8
9 fun setLong(value: Long)
10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
index 258580209..8b6d29fe5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
@@ -3,10 +3,22 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
6interface AbstractSetting { 8interface AbstractSetting {
7 val key: String? 9 val key: String
8 val section: String? 10 val category: Settings.Category
9 val isRuntimeEditable: Boolean
10 val valueAsString: String
11 val defaultValue: Any 11 val defaultValue: Any
12 val androidDefault: Any?
13 get() = null
14 val valueAsString: String
15 get() = ""
16
17 val isRuntimeModifiable: Boolean
18 get() = NativeConfig.getIsRuntimeModifiable(key)
19
20 val pairedSettingKey: String
21 get() = NativeConfig.getPairedSettingKey(key)
22
23 fun reset()
12} 24}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
new file mode 100644
index 000000000..91407ccbb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
@@ -0,0 +1,10 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6interface AbstractShortSetting : AbstractSetting {
7 val short: Short
8
9 fun setShort(value: Short)
10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
index 0d02c5997..c8935cc48 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
@@ -4,5 +4,7 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractStringSetting : AbstractSetting { 6interface AbstractStringSetting : AbstractSetting {
7 var string: String 7 val string: String
8
9 fun setString(value: String)
8} 10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index d41933766..e0c0538c7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -3,41 +3,37 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
6enum class BooleanSetting( 8enum class BooleanSetting(
7 override val key: String, 9 override val key: String,
8 override val section: String, 10 override val category: Settings.Category,
9 override val defaultValue: Boolean 11 override val androidDefault: Boolean? = null
10) : AbstractBooleanSetting { 12) : AbstractBooleanSetting {
11 CPU_DEBUG_MODE("cpu_debug_mode", Settings.SECTION_CPU, false), 13 CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
12 FASTMEM("cpuopt_fastmem", Settings.SECTION_CPU, true), 14 FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
13 FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.SECTION_CPU, true), 15 FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
14 PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true), 16 RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
15 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); 17 USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
16 18 RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
17 override var boolean: Boolean = defaultValue 19 RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
20 RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
21 RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
22 RENDERER_DEBUG("debug", Settings.Category.Renderer),
23 PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
24 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
25
26 override val boolean: Boolean
27 get() = NativeConfig.getBoolean(key, false)
28
29 override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
30
31 override val defaultValue: Boolean by lazy {
32 androidDefault ?: NativeConfig.getBoolean(key, true)
33 }
18 34
19 override val valueAsString: String 35 override val valueAsString: String
20 get() = boolean.toString() 36 get() = if (boolean) "1" else "0"
21 37
22 override val isRuntimeEditable: Boolean 38 override fun reset() = NativeConfig.setBoolean(key, defaultValue)
23 get() {
24 for (setting in NOT_RUNTIME_EDITABLE) {
25 if (setting == this) {
26 return false
27 }
28 }
29 return true
30 }
31
32 companion object {
33 private val NOT_RUNTIME_EDITABLE = listOf(
34 PICTURE_IN_PICTURE,
35 USE_CUSTOM_RTC
36 )
37
38 fun from(key: String): BooleanSetting? =
39 BooleanSetting.values().firstOrNull { it.key == key }
40
41 fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
42 }
43} 39}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
new file mode 100644
index 000000000..6ec0a765e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
@@ -0,0 +1,25 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
8enum class ByteSetting(
9 override val key: String,
10 override val category: Settings.Category
11) : AbstractByteSetting {
12 AUDIO_VOLUME("volume", Settings.Category.Audio);
13
14 override val byte: Byte
15 get() = NativeConfig.getByte(key, false)
16
17 override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
18
19 override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
20
21 override val valueAsString: String
22 get() = byte.toString()
23
24 override fun reset() = NativeConfig.setByte(key, defaultValue)
25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
index e5545a916..0181d06f2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
@@ -3,34 +3,24 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
6enum class FloatSetting( 8enum class FloatSetting(
7 override val key: String, 9 override val key: String,
8 override val section: String, 10 override val category: Settings.Category
9 override val defaultValue: Float
10) : AbstractFloatSetting { 11) : AbstractFloatSetting {
11 // No float settings currently exist 12 // No float settings currently exist
12 EMPTY_SETTING("", "", 0f); 13 EMPTY_SETTING("", Settings.Category.UiGeneral);
13
14 override var float: Float = defaultValue
15 14
16 override val valueAsString: String 15 override val float: Float
17 get() = float.toString() 16 get() = NativeConfig.getFloat(key, false)
18 17
19 override val isRuntimeEditable: Boolean 18 override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
20 get() {
21 for (setting in NOT_RUNTIME_EDITABLE) {
22 if (setting == this) {
23 return false
24 }
25 }
26 return true
27 }
28 19
29 companion object { 20 override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
30 private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
31 21
32 fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key } 22 override val valueAsString: String
23 get() = float.toString()
33 24
34 fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue } 25 override fun reset() = NativeConfig.setFloat(key, defaultValue)
35 }
36} 26}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index 4427a7d9d..151362124 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -3,139 +3,37 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
6enum class IntSetting( 8enum class IntSetting(
7 override val key: String, 9 override val key: String,
8 override val section: String, 10 override val category: Settings.Category,
9 override val defaultValue: Int 11 override val androidDefault: Int? = null
10) : AbstractIntSetting { 12) : AbstractIntSetting {
11 RENDERER_USE_SPEED_LIMIT( 13 CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
12 "use_speed_limit", 14 REGION_INDEX("region_index", Settings.Category.System),
13 Settings.SECTION_RENDERER, 15 LANGUAGE_INDEX("language_index", Settings.Category.System),
14 1 16 RENDERER_BACKEND("backend", Settings.Category.Renderer),
15 ), 17 RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
16 USE_DOCKED_MODE( 18 RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
17 "use_docked_mode", 19 RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
18 Settings.SECTION_SYSTEM, 20 RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
19 0 21 RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
20 ), 22 RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
21 RENDERER_USE_DISK_SHADER_CACHE( 23 RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
22 "use_disk_shader_cache", 24 AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
23 Settings.SECTION_RENDERER, 25
24 1 26 override val int: Int
25 ), 27 get() = NativeConfig.getInt(key, false)
26 RENDERER_FORCE_MAX_CLOCK( 28
27 "force_max_clock", 29 override fun setInt(value: Int) = NativeConfig.setInt(key, value)
28 Settings.SECTION_RENDERER, 30
29 0 31 override val defaultValue: Int by lazy {
30 ), 32 androidDefault ?: NativeConfig.getInt(key, true)
31 RENDERER_ASYNCHRONOUS_SHADERS( 33 }
32 "use_asynchronous_shaders",
33 Settings.SECTION_RENDERER,
34 0
35 ),
36 RENDERER_REACTIVE_FLUSHING(
37 "use_reactive_flushing",
38 Settings.SECTION_RENDERER,
39 0
40 ),
41 RENDERER_DEBUG(
42 "debug",
43 Settings.SECTION_RENDERER,
44 0
45 ),
46 RENDERER_SPEED_LIMIT(
47 "speed_limit",
48 Settings.SECTION_RENDERER,
49 100
50 ),
51 CPU_ACCURACY(
52 "cpu_accuracy",
53 Settings.SECTION_CPU,
54 0
55 ),
56 REGION_INDEX(
57 "region_index",
58 Settings.SECTION_SYSTEM,
59 -1
60 ),
61 LANGUAGE_INDEX(
62 "language_index",
63 Settings.SECTION_SYSTEM,
64 1
65 ),
66 RENDERER_BACKEND(
67 "backend",
68 Settings.SECTION_RENDERER,
69 1
70 ),
71 RENDERER_ACCURACY(
72 "gpu_accuracy",
73 Settings.SECTION_RENDERER,
74 0
75 ),
76 RENDERER_RESOLUTION(
77 "resolution_setup",
78 Settings.SECTION_RENDERER,
79 2
80 ),
81 RENDERER_VSYNC(
82 "use_vsync",
83 Settings.SECTION_RENDERER,
84 0
85 ),
86 RENDERER_SCALING_FILTER(
87 "scaling_filter",
88 Settings.SECTION_RENDERER,
89 1
90 ),
91 RENDERER_ANTI_ALIASING(
92 "anti_aliasing",
93 Settings.SECTION_RENDERER,
94 0
95 ),
96 RENDERER_SCREEN_LAYOUT(
97 "screen_layout",
98 Settings.SECTION_RENDERER,
99 Settings.LayoutOption_MobileLandscape
100 ),
101 RENDERER_ASPECT_RATIO(
102 "aspect_ratio",
103 Settings.SECTION_RENDERER,
104 0
105 ),
106 AUDIO_VOLUME(
107 "volume",
108 Settings.SECTION_AUDIO,
109 100
110 );
111
112 override var int: Int = defaultValue
113 34
114 override val valueAsString: String 35 override val valueAsString: String
115 get() = int.toString() 36 get() = int.toString()
116 37
117 override val isRuntimeEditable: Boolean 38 override fun reset() = NativeConfig.setInt(key, defaultValue)
118 get() {
119 for (setting in NOT_RUNTIME_EDITABLE) {
120 if (setting == this) {
121 return false
122 }
123 }
124 return true
125 }
126
127 companion object {
128 private val NOT_RUNTIME_EDITABLE = listOf(
129 RENDERER_USE_DISK_SHADER_CACHE,
130 RENDERER_ASYNCHRONOUS_SHADERS,
131 RENDERER_DEBUG,
132 RENDERER_BACKEND,
133 RENDERER_RESOLUTION,
134 RENDERER_VSYNC
135 )
136
137 fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
138
139 fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
140 }
141} 39}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
new file mode 100644
index 000000000..c526fc4cf
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
@@ -0,0 +1,25 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
8enum class LongSetting(
9 override val key: String,
10 override val category: Settings.Category
11) : AbstractLongSetting {
12 CUSTOM_RTC("custom_rtc", Settings.Category.System);
13
14 override val long: Long
15 get() = NativeConfig.getLong(key, false)
16
17 override fun setLong(value: Long) = NativeConfig.setLong(key, value)
18
19 override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
20
21 override val valueAsString: String
22 get() = long.toString()
23
24 override fun reset() = NativeConfig.setLong(key, defaultValue)
25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt
deleted file mode 100644
index 474f598a9..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt
+++ /dev/null
@@ -1,37 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6/**
7 * A semantically-related group of Settings objects. These Settings are
8 * internally stored as a HashMap.
9 */
10class SettingSection(val name: String) {
11 val settings = HashMap<String, AbstractSetting>()
12
13 /**
14 * Convenience method; inserts a value directly into the backing HashMap.
15 *
16 * @param setting The Setting to be inserted.
17 */
18 fun putSetting(setting: AbstractSetting) {
19 settings[setting.key!!] = setting
20 }
21
22 /**
23 * Convenience method; gets a value directly from the backing HashMap.
24 *
25 * @param key Used to retrieve the Setting.
26 * @return A Setting object (you should probably cast this before using)
27 */
28 fun getSetting(key: String): AbstractSetting? {
29 return settings[key]
30 }
31
32 fun mergeSection(settingSection: SettingSection) {
33 for (setting in settingSection.settings.values) {
34 putSetting(setting)
35 }
36 }
37}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index a6251bafd..0702236e8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -4,195 +4,151 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import android.text.TextUtils 6import android.text.TextUtils
7import java.util.* 7import android.widget.Toast
8import org.yuzu.yuzu_emu.R 8import org.yuzu.yuzu_emu.R
9import org.yuzu.yuzu_emu.YuzuApplication 9import org.yuzu.yuzu_emu.YuzuApplication
10import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
11import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 10import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
12 11
13class Settings { 12object Settings {
14 private var gameId: String? = null 13 private val context get() = YuzuApplication.appContext
15 14
16 var isLoaded = false 15 fun saveSettings(gameId: String = "") {
17
18 /**
19 * A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
20 * when getting a key not already in the map
21 */
22 class SettingsSectionMap : HashMap<String, SettingSection?>() {
23 override operator fun get(key: String): SettingSection? {
24 if (!super.containsKey(key)) {
25 val section = SettingSection(key)
26 super.put(key, section)
27 return section
28 }
29 return super.get(key)
30 }
31 }
32
33 var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
34
35 fun getSection(sectionName: String): SettingSection? {
36 return sections[sectionName]
37 }
38
39 val isEmpty: Boolean
40 get() = sections.isEmpty()
41
42 fun loadSettings(view: SettingsActivityView? = null) {
43 sections = SettingsSectionMap()
44 loadYuzuSettings(view)
45 if (!TextUtils.isEmpty(gameId)) {
46 loadCustomGameSettings(gameId!!, view)
47 }
48 isLoaded = true
49 }
50
51 private fun loadYuzuSettings(view: SettingsActivityView?) {
52 for ((fileName) in configFileSectionsMap) {
53 sections.putAll(SettingsFile.readFile(fileName, view))
54 }
55 }
56
57 private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
58 // Custom game settings
59 mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
60 }
61
62 private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
63 for ((key, updatedSection) in updatedSections) {
64 if (sections.containsKey(key)) {
65 val originalSection = sections[key]
66 originalSection!!.mergeSection(updatedSection!!)
67 } else {
68 sections[key] = updatedSection
69 }
70 }
71 }
72
73 fun loadSettings(gameId: String, view: SettingsActivityView) {
74 this.gameId = gameId
75 loadSettings(view)
76 }
77
78 fun saveSettings(view: SettingsActivityView) {
79 if (TextUtils.isEmpty(gameId)) { 16 if (TextUtils.isEmpty(gameId)) {
80 view.showToastMessage( 17 Toast.makeText(
81 YuzuApplication.appContext.getString(R.string.ini_saved), 18 context,
82 false 19 context.getString(R.string.ini_saved),
83 ) 20 Toast.LENGTH_SHORT
84 21 ).show()
85 for ((fileName, sectionNames) in configFileSectionsMap) { 22 SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG)
86 val iniSections = TreeMap<String, SettingSection>()
87 for (section in sectionNames) {
88 iniSections[section] = sections[section]!!
89 }
90
91 SettingsFile.saveFile(fileName, iniSections, view)
92 }
93 } else { 23 } else {
94 // Custom game settings 24 // TODO: Save custom game settings
95 view.showToastMessage( 25 Toast.makeText(
96 YuzuApplication.appContext.getString(R.string.gameid_saved, gameId), 26 context,
97 false 27 context.getString(R.string.gameid_saved, gameId),
98 ) 28 Toast.LENGTH_SHORT
99 29 ).show()
100 SettingsFile.saveCustomGameSettings(gameId, sections)
101 } 30 }
102 } 31 }
103 32
104 companion object { 33 enum class Category {
105 const val SECTION_GENERAL = "General" 34 Android,
106 const val SECTION_SYSTEM = "System" 35 Audio,
107 const val SECTION_RENDERER = "Renderer" 36 Core,
108 const val SECTION_AUDIO = "Audio" 37 Cpu,
109 const val SECTION_CPU = "Cpu" 38 CpuDebug,
110 const val SECTION_THEME = "Theme" 39 CpuUnsafe,
111 const val SECTION_DEBUG = "Debug" 40 Renderer,
112 41 RendererAdvanced,
113 const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" 42 RendererDebug,
114 43 System,
115 const val PREF_OVERLAY_VERSION = "OverlayVersion" 44 SystemAudio,
116 const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion" 45 DataStorage,
117 const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion" 46 Debugging,
118 const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion" 47 DebuggingGraphics,
119 val overlayLayoutPrefs = listOf( 48 Miscellaneous,
120 PREF_LANDSCAPE_OVERLAY_VERSION, 49 Network,
121 PREF_PORTRAIT_OVERLAY_VERSION, 50 WebService,
122 PREF_FOLDABLE_OVERLAY_VERSION 51 AddOns,
123 ) 52 Controls,
124 53 Ui,
125 const val PREF_CONTROL_SCALE = "controlScale" 54 UiGeneral,
126 const val PREF_CONTROL_OPACITY = "controlOpacity" 55 UiLayout,
127 const val PREF_TOUCH_ENABLED = "isTouchEnabled" 56 UiGameList,
128 const val PREF_BUTTON_A = "buttonToggle0" 57 Screenshots,
129 const val PREF_BUTTON_B = "buttonToggle1" 58 Shortcuts,
130 const val PREF_BUTTON_X = "buttonToggle2" 59 Multiplayer,
131 const val PREF_BUTTON_Y = "buttonToggle3" 60 Services,
132 const val PREF_BUTTON_L = "buttonToggle4" 61 Paths,
133 const val PREF_BUTTON_R = "buttonToggle5" 62 MaxEnum
134 const val PREF_BUTTON_ZL = "buttonToggle6"
135 const val PREF_BUTTON_ZR = "buttonToggle7"
136 const val PREF_BUTTON_PLUS = "buttonToggle8"
137 const val PREF_BUTTON_MINUS = "buttonToggle9"
138 const val PREF_BUTTON_DPAD = "buttonToggle10"
139 const val PREF_STICK_L = "buttonToggle11"
140 const val PREF_STICK_R = "buttonToggle12"
141 const val PREF_BUTTON_STICK_L = "buttonToggle13"
142 const val PREF_BUTTON_STICK_R = "buttonToggle14"
143 const val PREF_BUTTON_HOME = "buttonToggle15"
144 const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
145
146 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
147 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
148 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
149 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
150 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
151
152 const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
153 const val PREF_THEME = "Theme"
154 const val PREF_THEME_MODE = "ThemeMode"
155 const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
156
157 private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
158
159 val overlayPreferences = listOf(
160 PREF_OVERLAY_VERSION,
161 PREF_CONTROL_SCALE,
162 PREF_CONTROL_OPACITY,
163 PREF_TOUCH_ENABLED,
164 PREF_BUTTON_A,
165 PREF_BUTTON_B,
166 PREF_BUTTON_X,
167 PREF_BUTTON_Y,
168 PREF_BUTTON_L,
169 PREF_BUTTON_R,
170 PREF_BUTTON_ZL,
171 PREF_BUTTON_ZR,
172 PREF_BUTTON_PLUS,
173 PREF_BUTTON_MINUS,
174 PREF_BUTTON_DPAD,
175 PREF_STICK_L,
176 PREF_STICK_R,
177 PREF_BUTTON_HOME,
178 PREF_BUTTON_SCREENSHOT,
179 PREF_BUTTON_STICK_L,
180 PREF_BUTTON_STICK_R
181 )
182
183 const val LayoutOption_Unspecified = 0
184 const val LayoutOption_MobilePortrait = 4
185 const val LayoutOption_MobileLandscape = 5
186
187 init {
188 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
189 listOf(
190 SECTION_GENERAL,
191 SECTION_SYSTEM,
192 SECTION_RENDERER,
193 SECTION_AUDIO,
194 SECTION_CPU
195 )
196 }
197 } 63 }
64
65 val settingsList = listOf<AbstractSetting>(
66 *BooleanSetting.values(),
67 *ByteSetting.values(),
68 *ShortSetting.values(),
69 *IntSetting.values(),
70 *FloatSetting.values(),
71 *LongSetting.values(),
72 *StringSetting.values()
73 )
74
75 const val SECTION_GENERAL = "General"
76 const val SECTION_SYSTEM = "System"
77 const val SECTION_RENDERER = "Renderer"
78 const val SECTION_AUDIO = "Audio"
79 const val SECTION_CPU = "Cpu"
80 const val SECTION_THEME = "Theme"
81 const val SECTION_DEBUG = "Debug"
82
83 const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
84
85 const val PREF_OVERLAY_VERSION = "OverlayVersion"
86 const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion"
87 const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion"
88 const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion"
89 val overlayLayoutPrefs = listOf(
90 PREF_LANDSCAPE_OVERLAY_VERSION,
91 PREF_PORTRAIT_OVERLAY_VERSION,
92 PREF_FOLDABLE_OVERLAY_VERSION
93 )
94
95 const val PREF_CONTROL_SCALE = "controlScale"
96 const val PREF_CONTROL_OPACITY = "controlOpacity"
97 const val PREF_TOUCH_ENABLED = "isTouchEnabled"
98 const val PREF_BUTTON_A = "buttonToggle0"
99 const val PREF_BUTTON_B = "buttonToggle1"
100 const val PREF_BUTTON_X = "buttonToggle2"
101 const val PREF_BUTTON_Y = "buttonToggle3"
102 const val PREF_BUTTON_L = "buttonToggle4"
103 const val PREF_BUTTON_R = "buttonToggle5"
104 const val PREF_BUTTON_ZL = "buttonToggle6"
105 const val PREF_BUTTON_ZR = "buttonToggle7"
106 const val PREF_BUTTON_PLUS = "buttonToggle8"
107 const val PREF_BUTTON_MINUS = "buttonToggle9"
108 const val PREF_BUTTON_DPAD = "buttonToggle10"
109 const val PREF_STICK_L = "buttonToggle11"
110 const val PREF_STICK_R = "buttonToggle12"
111 const val PREF_BUTTON_STICK_L = "buttonToggle13"
112 const val PREF_BUTTON_STICK_R = "buttonToggle14"
113 const val PREF_BUTTON_HOME = "buttonToggle15"
114 const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
115
116 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
117 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
118 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
119 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
120 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
121
122 const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
123 const val PREF_THEME = "Theme"
124 const val PREF_THEME_MODE = "ThemeMode"
125 const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
126
127 val overlayPreferences = listOf(
128 PREF_OVERLAY_VERSION,
129 PREF_CONTROL_SCALE,
130 PREF_CONTROL_OPACITY,
131 PREF_TOUCH_ENABLED,
132 PREF_BUTTON_A,
133 PREF_BUTTON_B,
134 PREF_BUTTON_X,
135 PREF_BUTTON_Y,
136 PREF_BUTTON_L,
137 PREF_BUTTON_R,
138 PREF_BUTTON_ZL,
139 PREF_BUTTON_ZR,
140 PREF_BUTTON_PLUS,
141 PREF_BUTTON_MINUS,
142 PREF_BUTTON_DPAD,
143 PREF_STICK_L,
144 PREF_STICK_R,
145 PREF_BUTTON_HOME,
146 PREF_BUTTON_SCREENSHOT,
147 PREF_BUTTON_STICK_L,
148 PREF_BUTTON_STICK_R
149 )
150
151 const val LayoutOption_Unspecified = 0
152 const val LayoutOption_MobilePortrait = 4
153 const val LayoutOption_MobileLandscape = 5
198} 154}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
new file mode 100644
index 000000000..c9a0c664c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
@@ -0,0 +1,25 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.model
5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
8enum class ShortSetting(
9 override val key: String,
10 override val category: Settings.Category
11) : AbstractShortSetting {
12 RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
13
14 override val short: Short
15 get() = NativeConfig.getShort(key, false)
16
17 override fun setShort(value: Short) = NativeConfig.setShort(key, value)
18
19 override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
20
21 override val valueAsString: String
22 get() = short.toString()
23
24 override fun reset() = NativeConfig.setShort(key, defaultValue)
25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
index 6621289fd..9bb3e66d4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
@@ -3,36 +3,24 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig
7
6enum class StringSetting( 8enum class StringSetting(
7 override val key: String, 9 override val key: String,
8 override val section: String, 10 override val category: Settings.Category
9 override val defaultValue: String
10) : AbstractStringSetting { 11) : AbstractStringSetting {
11 AUDIO_OUTPUT_ENGINE("output_engine", Settings.SECTION_AUDIO, "auto"), 12 // No string settings currently exist
12 CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0"); 13 EMPTY_SETTING("", Settings.Category.UiGeneral);
14
15 override val string: String
16 get() = NativeConfig.getString(key, false)
17
18 override fun setString(value: String) = NativeConfig.setString(key, value)
13 19
14 override var string: String = defaultValue 20 override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
15 21
16 override val valueAsString: String 22 override val valueAsString: String
17 get() = string 23 get() = string
18 24
19 override val isRuntimeEditable: Boolean 25 override fun reset() = NativeConfig.setString(key, defaultValue)
20 get() {
21 for (setting in NOT_RUNTIME_EDITABLE) {
22 if (setting == this) {
23 return false
24 }
25 }
26 return true
27 }
28
29 companion object {
30 private val NOT_RUNTIME_EDITABLE = listOf(
31 CUSTOM_RTC
32 )
33
34 fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
35
36 fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
37 }
38} 26}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
index bc0bf7788..8bc164197 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
@@ -3,29 +3,16 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 6import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
8 7
9class DateTimeSetting( 8class DateTimeSetting(
10 setting: AbstractSetting?, 9 private val longSetting: AbstractLongSetting,
11 titleId: Int, 10 titleId: Int,
12 descriptionId: Int, 11 descriptionId: Int
13 val key: String? = null, 12) : SettingsItem(longSetting, titleId, descriptionId) {
14 private val defaultValue: String? = null
15) : SettingsItem(setting, titleId, descriptionId) {
16 override val type = TYPE_DATETIME_SETTING 13 override val type = TYPE_DATETIME_SETTING
17 14
18 val value: String 15 var value: Long
19 get() = if (setting != null) { 16 get() = longSetting.long
20 val setting = setting as AbstractStringSetting 17 set(value) = (setting as AbstractLongSetting).setLong(value)
21 setting.string
22 } else {
23 defaultValue!!
24 }
25
26 fun setSelectedValue(datetime: String): AbstractStringSetting {
27 val stringSetting = setting as AbstractStringSetting
28 stringSetting.string = datetime
29 return stringSetting
30 }
31} 18}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
index a67001311..d31ce1c31 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
@@ -5,6 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6class HeaderSetting( 6class HeaderSetting(
7 titleId: Int 7 titleId: Int
8) : SettingsItem(null, titleId, 0) { 8) : SettingsItem(emptySetting, titleId, 0) {
9 override val type = TYPE_HEADER 9 override val type = TYPE_HEADER
10} 10}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
index caaab50d8..522cc49df 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
@@ -8,6 +8,6 @@ class RunnableSetting(
8 descriptionId: Int, 8 descriptionId: Int,
9 val isRuntimeRunnable: Boolean, 9 val isRuntimeRunnable: Boolean,
10 val runnable: () -> Unit 10 val runnable: () -> Unit
11) : SettingsItem(null, titleId, descriptionId) { 11) : SettingsItem(emptySetting, titleId, descriptionId) {
12 override val type = TYPE_RUNNABLE 12 override val type = TYPE_RUNNABLE
13} 13}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 07520849e..b3b3fc209 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -4,7 +4,15 @@
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import org.yuzu.yuzu_emu.NativeLibrary 6import org.yuzu.yuzu_emu.NativeLibrary
7import org.yuzu.yuzu_emu.R
8import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 9import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
10import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
11import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
12import org.yuzu.yuzu_emu.features.settings.model.IntSetting
13import org.yuzu.yuzu_emu.features.settings.model.LongSetting
14import org.yuzu.yuzu_emu.features.settings.model.Settings
15import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
8 16
9/** 17/**
10 * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. 18 * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@@ -14,7 +22,7 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
14 * file.) 22 * file.)
15 */ 23 */
16abstract class SettingsItem( 24abstract class SettingsItem(
17 var setting: AbstractSetting?, 25 val setting: AbstractSetting,
18 val nameId: Int, 26 val nameId: Int,
19 val descriptionId: Int 27 val descriptionId: Int
20) { 28) {
@@ -23,7 +31,7 @@ abstract class SettingsItem(
23 val isEditable: Boolean 31 val isEditable: Boolean
24 get() { 32 get() {
25 if (!NativeLibrary.isRunning()) return true 33 if (!NativeLibrary.isRunning()) return true
26 return setting?.isRuntimeEditable ?: false 34 return setting.isRuntimeModifiable
27 } 35 }
28 36
29 companion object { 37 companion object {
@@ -35,5 +43,240 @@ abstract class SettingsItem(
35 const val TYPE_STRING_SINGLE_CHOICE = 5 43 const val TYPE_STRING_SINGLE_CHOICE = 5
36 const val TYPE_DATETIME_SETTING = 6 44 const val TYPE_DATETIME_SETTING = 6
37 const val TYPE_RUNNABLE = 7 45 const val TYPE_RUNNABLE = 7
46
47 const val FASTMEM_COMBINED = "fastmem_combined"
48
49 val emptySetting = object : AbstractSetting {
50 override val key: String = ""
51 override val category: Settings.Category = Settings.Category.Ui
52 override val defaultValue: Any = false
53 override fun reset() {}
54 }
55
56 // Extension for putting SettingsItems into a hashmap without repeating yourself
57 fun HashMap<String, SettingsItem>.put(item: SettingsItem) {
58 put(item.setting.key, item)
59 }
60
61 // List of all general
62 val settingsItems = HashMap<String, SettingsItem>().apply {
63 put(
64 SwitchSetting(
65 BooleanSetting.RENDERER_USE_SPEED_LIMIT,
66 R.string.frame_limit_enable,
67 R.string.frame_limit_enable_description
68 )
69 )
70 put(
71 SliderSetting(
72 ShortSetting.RENDERER_SPEED_LIMIT,
73 R.string.frame_limit_slider,
74 R.string.frame_limit_slider_description,
75 1,
76 200,
77 "%"
78 )
79 )
80 put(
81 SingleChoiceSetting(
82 IntSetting.CPU_ACCURACY,
83 R.string.cpu_accuracy,
84 0,
85 R.array.cpuAccuracyNames,
86 R.array.cpuAccuracyValues
87 )
88 )
89 put(
90 SwitchSetting(
91 BooleanSetting.PICTURE_IN_PICTURE,
92 R.string.picture_in_picture,
93 R.string.picture_in_picture_description
94 )
95 )
96 put(
97 SwitchSetting(
98 BooleanSetting.USE_DOCKED_MODE,
99 R.string.use_docked_mode,
100 R.string.use_docked_mode_description
101 )
102 )
103 put(
104 SingleChoiceSetting(
105 IntSetting.REGION_INDEX,
106 R.string.emulated_region,
107 0,
108 R.array.regionNames,
109 R.array.regionValues
110 )
111 )
112 put(
113 SingleChoiceSetting(
114 IntSetting.LANGUAGE_INDEX,
115 R.string.emulated_language,
116 0,
117 R.array.languageNames,
118 R.array.languageValues
119 )
120 )
121 put(
122 SwitchSetting(
123 BooleanSetting.USE_CUSTOM_RTC,
124 R.string.use_custom_rtc,
125 R.string.use_custom_rtc_description
126 )
127 )
128 put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0))
129 put(
130 SingleChoiceSetting(
131 IntSetting.RENDERER_ACCURACY,
132 R.string.renderer_accuracy,
133 0,
134 R.array.rendererAccuracyNames,
135 R.array.rendererAccuracyValues
136 )
137 )
138 put(
139 SingleChoiceSetting(
140 IntSetting.RENDERER_RESOLUTION,
141 R.string.renderer_resolution,
142 0,
143 R.array.rendererResolutionNames,
144 R.array.rendererResolutionValues
145 )
146 )
147 put(
148 SingleChoiceSetting(
149 IntSetting.RENDERER_VSYNC,
150 R.string.renderer_vsync,
151 0,
152 R.array.rendererVSyncNames,
153 R.array.rendererVSyncValues
154 )
155 )
156 put(
157 SingleChoiceSetting(
158 IntSetting.RENDERER_SCALING_FILTER,
159 R.string.renderer_scaling_filter,
160 0,
161 R.array.rendererScalingFilterNames,
162 R.array.rendererScalingFilterValues
163 )
164 )
165 put(
166 SingleChoiceSetting(
167 IntSetting.RENDERER_ANTI_ALIASING,
168 R.string.renderer_anti_aliasing,
169 0,
170 R.array.rendererAntiAliasingNames,
171 R.array.rendererAntiAliasingValues
172 )
173 )
174 put(
175 SingleChoiceSetting(
176 IntSetting.RENDERER_SCREEN_LAYOUT,
177 R.string.renderer_screen_layout,
178 0,
179 R.array.rendererScreenLayoutNames,
180 R.array.rendererScreenLayoutValues
181 )
182 )
183 put(
184 SingleChoiceSetting(
185 IntSetting.RENDERER_ASPECT_RATIO,
186 R.string.renderer_aspect_ratio,
187 0,
188 R.array.rendererAspectRatioNames,
189 R.array.rendererAspectRatioValues
190 )
191 )
192 put(
193 SwitchSetting(
194 BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
195 R.string.use_disk_shader_cache,
196 R.string.use_disk_shader_cache_description
197 )
198 )
199 put(
200 SwitchSetting(
201 BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
202 R.string.renderer_force_max_clock,
203 R.string.renderer_force_max_clock_description
204 )
205 )
206 put(
207 SwitchSetting(
208 BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
209 R.string.renderer_asynchronous_shaders,
210 R.string.renderer_asynchronous_shaders_description
211 )
212 )
213 put(
214 SwitchSetting(
215 BooleanSetting.RENDERER_REACTIVE_FLUSHING,
216 R.string.renderer_reactive_flushing,
217 R.string.renderer_reactive_flushing_description
218 )
219 )
220 put(
221 SingleChoiceSetting(
222 IntSetting.AUDIO_OUTPUT_ENGINE,
223 R.string.audio_output_engine,
224 0,
225 R.array.outputEngineEntries,
226 R.array.outputEngineValues
227 )
228 )
229 put(
230 SliderSetting(
231 ByteSetting.AUDIO_VOLUME,
232 R.string.audio_volume,
233 R.string.audio_volume_description,
234 0,
235 100,
236 "%"
237 )
238 )
239 put(
240 SingleChoiceSetting(
241 IntSetting.RENDERER_BACKEND,
242 R.string.renderer_api,
243 0,
244 R.array.rendererApiNames,
245 R.array.rendererApiValues
246 )
247 )
248 put(
249 SwitchSetting(
250 BooleanSetting.RENDERER_DEBUG,
251 R.string.renderer_debug,
252 R.string.renderer_debug_description
253 )
254 )
255 put(
256 SwitchSetting(
257 BooleanSetting.CPU_DEBUG_MODE,
258 R.string.cpu_debug_mode,
259 R.string.cpu_debug_mode_description
260 )
261 )
262
263 val fastmem = object : AbstractBooleanSetting {
264 override val boolean: Boolean
265 get() =
266 BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
267
268 override fun setBoolean(value: Boolean) {
269 BooleanSetting.FASTMEM.setBoolean(value)
270 BooleanSetting.FASTMEM_EXCLUSIVES.setBoolean(value)
271 }
272
273 override val key: String = FASTMEM_COMBINED
274 override val category = Settings.Category.Cpu
275 override val isRuntimeModifiable: Boolean = false
276 override val defaultValue: Boolean = true
277 override fun reset() = setBoolean(defaultValue)
278 }
279 put(SwitchSetting(fastmem, R.string.fastmem, 0))
280 }
38 } 281 }
39} 282}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
index 7306ec458..705527a73 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -4,36 +4,27 @@
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 6import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7 8
8class SingleChoiceSetting( 9class SingleChoiceSetting(
9 setting: AbstractIntSetting?, 10 setting: AbstractSetting,
10 titleId: Int, 11 titleId: Int,
11 descriptionId: Int, 12 descriptionId: Int,
12 val choicesId: Int, 13 val choicesId: Int,
13 val valuesId: Int, 14 val valuesId: Int
14 val key: String? = null,
15 val defaultValue: Int? = null
16) : SettingsItem(setting, titleId, descriptionId) { 15) : SettingsItem(setting, titleId, descriptionId) {
17 override val type = TYPE_SINGLE_CHOICE 16 override val type = TYPE_SINGLE_CHOICE
18 17
19 val selectedValue: Int 18 var selectedValue: Int
20 get() = if (setting != null) { 19 get() {
21 val setting = setting as AbstractIntSetting 20 return when (setting) {
22 setting.int 21 is AbstractIntSetting -> setting.int
23 } else { 22 else -> -1
24 defaultValue!! 23 }
24 }
25 set(value) {
26 when (setting) {
27 is AbstractIntSetting -> setting.setInt(value)
28 }
25 } 29 }
26
27 /**
28 * Write a value to the backing int. If that int was previously null,
29 * initializes a new one and returns it, so it can be added to the Hashmap.
30 *
31 * @param selection New value of the int.
32 * @return the existing setting with the new value applied.
33 */
34 fun setSelectedValue(selection: Int): AbstractIntSetting {
35 val intSetting = setting as AbstractIntSetting
36 intSetting.int = selection
37 return intSetting
38 }
39} 30}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
index 92d0167ae..c3b5df02c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
@@ -3,60 +3,39 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import kotlin.math.roundToInt 6import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting 7import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
8import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 8import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
9import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 9import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
10import org.yuzu.yuzu_emu.utils.Log 10import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting
11import kotlin.math.roundToInt
11 12
12class SliderSetting( 13class SliderSetting(
13 setting: AbstractSetting?, 14 setting: AbstractSetting,
14 titleId: Int, 15 titleId: Int,
15 descriptionId: Int, 16 descriptionId: Int,
16 val min: Int, 17 val min: Int,
17 val max: Int, 18 val max: Int,
18 val units: String, 19 val units: String
19 val key: String? = null,
20 val defaultValue: Int? = null
21) : SettingsItem(setting, titleId, descriptionId) { 20) : SettingsItem(setting, titleId, descriptionId) {
22 override val type = TYPE_SLIDER 21 override val type = TYPE_SLIDER
23 22
24 val selectedValue: Int 23 var selectedValue: Int
25 get() { 24 get() {
26 val setting = setting ?: return defaultValue!!
27 return when (setting) { 25 return when (setting) {
26 is AbstractByteSetting -> setting.byte.toInt()
27 is AbstractShortSetting -> setting.short.toInt()
28 is AbstractIntSetting -> setting.int 28 is AbstractIntSetting -> setting.int
29 is AbstractFloatSetting -> setting.float.roundToInt() 29 is AbstractFloatSetting -> setting.float.roundToInt()
30 else -> { 30 else -> -1
31 Log.error("[SliderSetting] Error casting setting type.") 31 }
32 -1 32 }
33 } 33 set(value) {
34 when (setting) {
35 is AbstractByteSetting -> setting.setByte(value.toByte())
36 is AbstractShortSetting -> setting.setShort(value.toShort())
37 is AbstractIntSetting -> setting.setInt(value)
38 is AbstractFloatSetting -> setting.setFloat(value.toFloat())
34 } 39 }
35 } 40 }
36
37 /**
38 * Write a value to the backing int. If that int was previously null,
39 * initializes a new one and returns it, so it can be added to the Hashmap.
40 *
41 * @param selection New value of the int.
42 * @return the existing setting with the new value applied.
43 */
44 fun setSelectedValue(selection: Int): AbstractIntSetting {
45 val intSetting = setting as AbstractIntSetting
46 intSetting.int = selection
47 return intSetting
48 }
49
50 /**
51 * Write a value to the backing float. If that float was previously null,
52 * initializes a new one and returns it, so it can be added to the Hashmap.
53 *
54 * @param selection New value of the float.
55 * @return the existing setting with the new value applied.
56 */
57 fun setSelectedValue(selection: Float): AbstractFloatSetting {
58 val floatSetting = setting as AbstractFloatSetting
59 floatSetting.float = selection
60 return floatSetting
61 }
62} 41}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
index 3b6731dcd..871dab4f3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -3,57 +3,31 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.model.view 4package org.yuzu.yuzu_emu.features.settings.model.view
5 5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting 6import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
8 7
9class StringSingleChoiceSetting( 8class StringSingleChoiceSetting(
10 setting: AbstractSetting?, 9 private val stringSetting: AbstractStringSetting,
11 titleId: Int, 10 titleId: Int,
12 descriptionId: Int, 11 descriptionId: Int,
13 val choices: Array<String>, 12 val choices: Array<String>,
14 val values: Array<String>?, 13 val values: Array<String>
15 val key: String? = null, 14) : SettingsItem(stringSetting, titleId, descriptionId) {
16 private val defaultValue: String? = null
17) : SettingsItem(setting, titleId, descriptionId) {
18 override val type = TYPE_STRING_SINGLE_CHOICE 15 override val type = TYPE_STRING_SINGLE_CHOICE
19 16
20 fun getValueAt(index: Int): String? { 17 fun getValueAt(index: Int): String =
21 if (values == null) return null 18 if (index >= 0 && index < values.size) values[index] else ""
22 return if (index >= 0 && index < values.size) { 19
23 values[index] 20 var selectedValue: String
24 } else { 21 get() = stringSetting.string
25 "" 22 set(value) = stringSetting.setString(value)
26 }
27 }
28 23
29 val selectedValue: String
30 get() = if (setting != null) {
31 val setting = setting as AbstractStringSetting
32 setting.string
33 } else {
34 defaultValue!!
35 }
36 val selectValueIndex: Int 24 val selectValueIndex: Int
37 get() { 25 get() {
38 val selectedValue = selectedValue 26 for (i in values.indices) {
39 for (i in values!!.indices) {
40 if (values[i] == selectedValue) { 27 if (values[i] == selectedValue) {
41 return i 28 return i
42 } 29 }
43 } 30 }
44 return -1 31 return -1
45 } 32 }
46
47 /**
48 * Write a value to the backing int. If that int was previously null,
49 * initializes a new one and returns it, so it can be added to the Hashmap.
50 *
51 * @param selection New value of the int.
52 * @return the existing setting with the new value applied.
53 */
54 fun setSelectedValue(selection: String): AbstractStringSetting {
55 val stringSetting = setting as AbstractStringSetting
56 stringSetting.string = selection
57 return stringSetting
58 }
59} 33}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
index 8a9d13a92..91c273964 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
@@ -7,6 +7,6 @@ class SubmenuSetting(
7 titleId: Int, 7 titleId: Int,
8 descriptionId: Int, 8 descriptionId: Int,
9 val menuKey: String 9 val menuKey: String
10) : SettingsItem(null, titleId, descriptionId) { 10) : SettingsItem(emptySetting, titleId, descriptionId) {
11 override val type = TYPE_SUBMENU 11 override val type = TYPE_SUBMENU
12} 12}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
index 90b198718..416967e64 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
@@ -10,53 +10,22 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
10class SwitchSetting( 10class SwitchSetting(
11 setting: AbstractSetting, 11 setting: AbstractSetting,
12 titleId: Int, 12 titleId: Int,
13 descriptionId: Int, 13 descriptionId: Int
14 val key: String? = null,
15 val defaultValue: Any? = null
16) : SettingsItem(setting, titleId, descriptionId) { 14) : SettingsItem(setting, titleId, descriptionId) {
17 override val type = TYPE_SWITCH 15 override val type = TYPE_SWITCH
18 16
19 val isChecked: Boolean 17 var checked: Boolean
20 get() { 18 get() {
21 if (setting == null) { 19 return when (setting) {
22 return defaultValue as Boolean 20 is AbstractIntSetting -> setting.int == 1
21 is AbstractBooleanSetting -> setting.boolean
22 else -> false
23 } 23 }
24
25 // Try integer setting
26 try {
27 val setting = setting as AbstractIntSetting
28 return setting.int == 1
29 } catch (_: ClassCastException) {
30 }
31
32 // Try boolean setting
33 try {
34 val setting = setting as AbstractBooleanSetting
35 return setting.boolean
36 } catch (_: ClassCastException) {
37 }
38 return defaultValue as Boolean
39 } 24 }
40 25 set(value) {
41 /** 26 when (setting) {
42 * Write a value to the backing boolean. If that boolean was previously null, 27 is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
43 * initializes a new one and returns it, so it can be added to the Hashmap. 28 is AbstractBooleanSetting -> setting.setBoolean(value)
44 * 29 }
45 * @param checked Pretty self explanatory.
46 * @return the existing setting with the new value applied.
47 */
48 fun setChecked(checked: Boolean): AbstractSetting {
49 // Try integer setting
50 try {
51 val setting = setting as AbstractIntSetting
52 setting.int = if (checked) 1 else 0
53 return setting
54 } catch (_: ClassCastException) {
55 } 30 }
56
57 // Try boolean setting
58 val setting = setting as AbstractBooleanSetting
59 setting.boolean = checked
60 return setting
61 }
62} 31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index a5af5a7ae..908c01265 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -3,42 +3,34 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.ui 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.content.Context
7import android.content.Intent
8import android.os.Bundle 6import android.os.Bundle
9import android.view.Menu
10import android.view.View 7import android.view.View
11import android.view.ViewGroup.MarginLayoutParams 8import android.view.ViewGroup.MarginLayoutParams
12import android.widget.Toast 9import android.widget.Toast
13import androidx.activity.OnBackPressedCallback 10import androidx.activity.OnBackPressedCallback
14import androidx.activity.result.ActivityResultLauncher
15import androidx.activity.viewModels 11import androidx.activity.viewModels
16import androidx.appcompat.app.AppCompatActivity 12import androidx.appcompat.app.AppCompatActivity
17import androidx.core.view.ViewCompat 13import androidx.core.view.ViewCompat
18import androidx.core.view.WindowCompat 14import androidx.core.view.WindowCompat
19import androidx.core.view.WindowInsetsCompat 15import androidx.core.view.WindowInsetsCompat
20import androidx.core.view.updatePadding 16import androidx.navigation.fragment.NavHostFragment
17import androidx.navigation.navArgs
21import com.google.android.material.color.MaterialColors 18import com.google.android.material.color.MaterialColors
22import java.io.IOException 19import java.io.IOException
23import org.yuzu.yuzu_emu.R 20import org.yuzu.yuzu_emu.R
24import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding 21import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
25import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
26import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
27import org.yuzu.yuzu_emu.features.settings.model.IntSetting
28import org.yuzu.yuzu_emu.features.settings.model.Settings 22import org.yuzu.yuzu_emu.features.settings.model.Settings
29import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
30import org.yuzu.yuzu_emu.features.settings.model.StringSetting
31import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 23import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
24import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
25import org.yuzu.yuzu_emu.model.SettingsViewModel
32import org.yuzu.yuzu_emu.utils.* 26import org.yuzu.yuzu_emu.utils.*
33 27
34class SettingsActivity : AppCompatActivity(), SettingsActivityView { 28class SettingsActivity : AppCompatActivity() {
35 private val presenter = SettingsActivityPresenter(this)
36
37 private lateinit var binding: ActivitySettingsBinding 29 private lateinit var binding: ActivitySettingsBinding
38 30
39 private val settingsViewModel: SettingsViewModel by viewModels() 31 private val args by navArgs<SettingsActivityArgs>()
40 32
41 override val settings: Settings get() = settingsViewModel.settings 33 private val settingsViewModel: SettingsViewModel by viewModels()
42 34
43 override fun onCreate(savedInstanceState: Bundle?) { 35 override fun onCreate(savedInstanceState: Bundle?) {
44 ThemeHelper.setTheme(this) 36 ThemeHelper.setTheme(this)
@@ -48,16 +40,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
48 binding = ActivitySettingsBinding.inflate(layoutInflater) 40 binding = ActivitySettingsBinding.inflate(layoutInflater)
49 setContentView(binding.root) 41 setContentView(binding.root)
50 42
51 WindowCompat.setDecorFitsSystemWindows(window, false) 43 settingsViewModel.game = args.game
52 44
53 val launcher = intent 45 val navHostFragment =
54 val gameID = launcher.getStringExtra(ARG_GAME_ID) 46 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
55 val menuTag = launcher.getStringExtra(ARG_MENU_TAG) 47 navHostFragment.navController.setGraph(R.navigation.settings_navigation, intent.extras)
56 presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
57 48
58 // Show "Back" button in the action bar for navigation 49 WindowCompat.setDecorFitsSystemWindows(window, false)
59 setSupportActionBar(binding.toolbarSettings) 50
60 supportActionBar!!.setDisplayHomeAsUpEnabled(true) 51 if (savedInstanceState != null) {
52 settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
53 }
61 54
62 if (InsetsHelper.getSystemGestureType(applicationContext) != 55 if (InsetsHelper.getSystemGestureType(applicationContext) !=
63 InsetsHelper.GESTURE_NAVIGATION 56 InsetsHelper.GESTURE_NAVIGATION
@@ -73,6 +66,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
73 ) 66 )
74 } 67 }
75 68
69 settingsViewModel.shouldRecreate.observe(this) {
70 if (it) {
71 settingsViewModel.setShouldRecreate(false)
72 recreate()
73 }
74 }
75 settingsViewModel.shouldNavigateBack.observe(this) {
76 if (it) {
77 settingsViewModel.setShouldNavigateBack(false)
78 navigateBack()
79 }
80 }
81 settingsViewModel.shouldShowResetSettingsDialog.observe(this) {
82 if (it) {
83 settingsViewModel.setShouldShowResetSettingsDialog(false)
84 ResetSettingsDialogFragment().show(
85 supportFragmentManager,
86 ResetSettingsDialogFragment.TAG
87 )
88 }
89 }
90
76 onBackPressedDispatcher.addCallback( 91 onBackPressedDispatcher.addCallback(
77 this, 92 this,
78 object : OnBackPressedCallback(true) { 93 object : OnBackPressedCallback(true) {
@@ -83,34 +98,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
83 setInsets() 98 setInsets()
84 } 99 }
85 100
86 override fun onSupportNavigateUp(): Boolean { 101 fun navigateBack() {
87 navigateBack() 102 val navHostFragment =
88 return true 103 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
89 } 104 if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
90 105 navHostFragment.navController.popBackStack()
91 private fun navigateBack() {
92 if (supportFragmentManager.backStackEntryCount > 0) {
93 supportFragmentManager.popBackStack()
94 } else { 106 } else {
95 finish() 107 finish()
96 } 108 }
97 } 109 }
98 110
99 override fun onCreateOptionsMenu(menu: Menu): Boolean {
100 val inflater = menuInflater
101 inflater.inflate(R.menu.menu_settings, menu)
102 return true
103 }
104
105 override fun onSaveInstanceState(outState: Bundle) { 111 override fun onSaveInstanceState(outState: Bundle) {
106 // Critical: If super method is not called, rotations will be busted. 112 // Critical: If super method is not called, rotations will be busted.
107 super.onSaveInstanceState(outState) 113 super.onSaveInstanceState(outState)
108 presenter.saveState(outState) 114 outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
109 } 115 }
110 116
111 override fun onStart() { 117 override fun onStart() {
112 super.onStart() 118 super.onStart()
113 presenter.onStart() 119 // TODO: Load custom settings contextually
120 if (!DirectoryInitialization.areDirectoriesReady) {
121 DirectoryInitialization.start()
122 }
114 } 123 }
115 124
116 /** 125 /**
@@ -120,143 +129,51 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
120 */ 129 */
121 override fun onStop() { 130 override fun onStop() {
122 super.onStop() 131 super.onStop()
123 presenter.onStop(isFinishing) 132 if (isFinishing && settingsViewModel.shouldSave) {
124 } 133 Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
125 134 Settings.saveSettings()
126 override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {
127 if (!addToStack && settingsFragment != null) {
128 return
129 }
130
131 val transaction = supportFragmentManager.beginTransaction()
132 if (addToStack) {
133 if (areSystemAnimationsEnabled()) {
134 transaction.setCustomAnimations(
135 R.anim.anim_settings_fragment_in,
136 R.anim.anim_settings_fragment_out,
137 0,
138 R.anim.anim_pop_settings_fragment_out
139 )
140 }
141 transaction.addToBackStack(null)
142 } 135 }
143 transaction.replace(
144 R.id.frame_content,
145 SettingsFragment.newInstance(menuTag, gameId),
146 FRAGMENT_TAG
147 )
148 transaction.commit()
149 } 136 }
150 137
151 private fun areSystemAnimationsEnabled(): Boolean { 138 override fun onDestroy() {
152 val duration = android.provider.Settings.Global.getFloat( 139 settingsViewModel.clear()
153 contentResolver, 140 super.onDestroy()
154 android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
155 1f
156 )
157 val transition = android.provider.Settings.Global.getFloat(
158 contentResolver,
159 android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
160 1f
161 )
162 return duration != 0f && transition != 0f
163 }
164
165 override fun onSettingsFileLoaded() {
166 val fragment: SettingsFragmentView? = settingsFragment
167 fragment?.loadSettingsList()
168 }
169
170 override fun onSettingsFileNotFound() {
171 val fragment: SettingsFragmentView? = settingsFragment
172 fragment?.loadSettingsList()
173 }
174
175 override fun showToastMessage(message: String, is_long: Boolean) {
176 Toast.makeText(
177 this,
178 message,
179 if (is_long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
180 ).show()
181 }
182
183 override fun onSettingChanged() {
184 presenter.onSettingChanged()
185 } 141 }
186 142
187 fun onSettingsReset() { 143 fun onSettingsReset() {
188 // Prevents saving to a non-existent settings file 144 // Prevents saving to a non-existent settings file
189 presenter.onSettingsReset() 145 settingsViewModel.shouldSave = false
190
191 // Reset the static memory representation of each setting
192 BooleanSetting.clear()
193 FloatSetting.clear()
194 IntSetting.clear()
195 StringSetting.clear()
196 146
197 // Delete settings file because the user may have changed values that do not exist in the UI 147 // Delete settings file because the user may have changed values that do not exist in the UI
198 val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) 148 val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
199 if (!settingsFile.delete()) { 149 if (!settingsFile.delete()) {
200 throw IOException("Failed to delete $settingsFile") 150 throw IOException("Failed to delete $settingsFile")
201 } 151 }
152 Settings.settingsList.forEach { it.reset() }
202 153
203 showToastMessage(getString(R.string.settings_reset), true) 154 Toast.makeText(
155 applicationContext,
156 getString(R.string.settings_reset),
157 Toast.LENGTH_LONG
158 ).show()
204 finish() 159 finish()
205 } 160 }
206 161
207 fun setToolbarTitle(title: String) {
208 binding.toolbarSettingsLayout.title = title
209 }
210
211 private val settingsFragment: SettingsFragment?
212 get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
213
214 private fun setInsets() { 162 private fun setInsets() {
215 ViewCompat.setOnApplyWindowInsetsListener( 163 ViewCompat.setOnApplyWindowInsetsListener(
216 binding.frameContent 164 binding.navigationBarShade
217 ) { view: View, windowInsets: WindowInsetsCompat -> 165 ) { view: View, windowInsets: WindowInsetsCompat ->
218 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 166 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
219 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
220 view.updatePadding(
221 left = barInsets.left + cutoutInsets.left,
222 right = barInsets.right + cutoutInsets.right
223 )
224 167
225 val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams 168 val mlpShade = view.layoutParams as MarginLayoutParams
226 mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
227 mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
228 binding.appbarSettings.layoutParams = mlpAppBar
229
230 val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
231 mlpShade.height = barInsets.bottom 169 mlpShade.height = barInsets.bottom
232 binding.navigationBarShade.layoutParams = mlpShade 170 view.layoutParams = mlpShade
233 171
234 windowInsets 172 windowInsets
235 } 173 }
236 } 174 }
237 175
238 companion object { 176 companion object {
239 private const val ARG_MENU_TAG = "menu_tag" 177 private const val KEY_SHOULD_SAVE = "should_save"
240 private const val ARG_GAME_ID = "game_id"
241 private const val FRAGMENT_TAG = "settings"
242
243 fun launch(context: Context, menuTag: String?, gameId: String?) {
244 val settings = Intent(context, SettingsActivity::class.java)
245 settings.putExtra(ARG_MENU_TAG, menuTag)
246 settings.putExtra(ARG_GAME_ID, gameId)
247 context.startActivity(settings)
248 }
249
250 fun launch(
251 context: Context,
252 launcher: ActivityResultLauncher<Intent>,
253 menuTag: String?,
254 gameId: String?
255 ) {
256 val settings = Intent(context, SettingsActivity::class.java)
257 settings.putExtra(ARG_MENU_TAG, menuTag)
258 settings.putExtra(ARG_GAME_ID, gameId)
259 launcher.launch(settings)
260 }
261 } 178 }
262} 179}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt
deleted file mode 100644
index 93e677b21..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt
+++ /dev/null
@@ -1,90 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import android.content.Context
7import android.os.Bundle
8import android.text.TextUtils
9import java.io.File
10import org.yuzu.yuzu_emu.NativeLibrary
11import org.yuzu.yuzu_emu.features.settings.model.Settings
12import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
13import org.yuzu.yuzu_emu.utils.DirectoryInitialization
14import org.yuzu.yuzu_emu.utils.Log
15
16class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
17 val settings: Settings get() = activityView.settings
18
19 private var shouldSave = false
20 private lateinit var menuTag: String
21 private lateinit var gameId: String
22
23 fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
24 this.menuTag = menuTag
25 this.gameId = gameId
26 if (savedInstanceState != null) {
27 shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
28 }
29 }
30
31 fun onStart() {
32 prepareDirectoriesIfNeeded()
33 }
34
35 private fun loadSettingsUI() {
36 if (!settings.isLoaded) {
37 if (!TextUtils.isEmpty(gameId)) {
38 settings.loadSettings(gameId, activityView)
39 } else {
40 settings.loadSettings(activityView)
41 }
42 }
43 activityView.showSettingsFragment(menuTag, false, gameId)
44 activityView.onSettingsFileLoaded()
45 }
46
47 private fun prepareDirectoriesIfNeeded() {
48 val configFile =
49 File(
50 "${DirectoryInitialization.userDirectory}/config/" +
51 "${SettingsFile.FILE_NAME_CONFIG}.ini"
52 )
53 if (!configFile.exists()) {
54 Log.error(
55 "${DirectoryInitialization.userDirectory}/config/" +
56 "${SettingsFile.FILE_NAME_CONFIG}.ini"
57 )
58 Log.error("yuzu config file could not be found!")
59 }
60
61 if (!DirectoryInitialization.areDirectoriesReady) {
62 DirectoryInitialization.start(activityView as Context)
63 }
64 loadSettingsUI()
65 }
66
67 fun onStop(finishing: Boolean) {
68 if (finishing && shouldSave) {
69 Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
70 settings.saveSettings(activityView)
71 }
72 NativeLibrary.reloadSettings()
73 }
74
75 fun onSettingChanged() {
76 shouldSave = true
77 }
78
79 fun onSettingsReset() {
80 shouldSave = false
81 }
82
83 fun saveState(outState: Bundle) {
84 outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
85 }
86
87 companion object {
88 private const val KEY_SHOULD_SAVE = "should_save"
89 }
90}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt
deleted file mode 100644
index c186fc388..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt
+++ /dev/null
@@ -1,57 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import org.yuzu.yuzu_emu.features.settings.model.Settings
7
8/**
9 * Abstraction for the Activity that manages SettingsFragments.
10 */
11interface SettingsActivityView {
12 /**
13 * Show a new SettingsFragment.
14 *
15 * @param menuTag Identifier for the settings group that should be displayed.
16 * @param addToStack Whether or not this fragment should replace a previous one.
17 */
18 fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
19
20 /**
21 * Called by a contained Fragment to get access to the Setting HashMap
22 * loaded from disk, so that each Fragment doesn't need to perform its own
23 * read operation.
24 *
25 * @return A HashMap of Settings.
26 */
27 val settings: Settings
28
29 /**
30 * Called when a load operation completes.
31 */
32 fun onSettingsFileLoaded()
33
34 /**
35 * Called when a load operation fails.
36 */
37 fun onSettingsFileNotFound()
38
39 /**
40 * Display a popup text message on screen.
41 *
42 * @param message The contents of the onscreen message.
43 * @param is_long Whether this should be a long Toast or short one.
44 */
45 fun showToastMessage(message: String, is_long: Boolean)
46
47 /**
48 * End the activity.
49 */
50 fun finish()
51
52 /**
53 * Called by a containing Fragment to tell the Activity that a setting was changed;
54 * unless this has been called, the Activity will not save to disk.
55 */
56 fun onSettingChanged()
57}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index 9711e2c51..a7a029fc1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -4,51 +4,54 @@
4package org.yuzu.yuzu_emu.features.settings.ui 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.content.Context 6import android.content.Context
7import android.content.DialogInterface
8import android.icu.util.Calendar 7import android.icu.util.Calendar
9import android.icu.util.TimeZone 8import android.icu.util.TimeZone
10import android.text.format.DateFormat 9import android.text.format.DateFormat
11import android.view.LayoutInflater 10import android.view.LayoutInflater
12import android.view.ViewGroup 11import android.view.ViewGroup
13import android.widget.TextView 12import androidx.fragment.app.Fragment
14import androidx.appcompat.app.AlertDialog 13import androidx.lifecycle.Lifecycle
15import androidx.appcompat.app.AppCompatActivity 14import androidx.lifecycle.ViewModelProvider
16import androidx.recyclerview.widget.RecyclerView 15import androidx.lifecycle.lifecycleScope
16import androidx.lifecycle.repeatOnLifecycle
17import androidx.navigation.findNavController
18import androidx.recyclerview.widget.AsyncDifferConfig
19import androidx.recyclerview.widget.DiffUtil
20import androidx.recyclerview.widget.ListAdapter
17import com.google.android.material.datepicker.MaterialDatePicker 21import com.google.android.material.datepicker.MaterialDatePicker
18import com.google.android.material.dialog.MaterialAlertDialogBuilder
19import com.google.android.material.slider.Slider
20import com.google.android.material.timepicker.MaterialTimePicker 22import com.google.android.material.timepicker.MaterialTimePicker
21import com.google.android.material.timepicker.TimeFormat 23import com.google.android.material.timepicker.TimeFormat
24import kotlinx.coroutines.launch
22import org.yuzu.yuzu_emu.R 25import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.DialogSliderBinding 26import org.yuzu.yuzu_emu.SettingsNavigationDirections
24import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding 27import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
25import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding 28import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
26import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding 29import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
27import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
28import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
29import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
30import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
31import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
32import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
33import org.yuzu.yuzu_emu.features.settings.model.view.* 30import org.yuzu.yuzu_emu.features.settings.model.view.*
34import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* 31import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
32import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment
33import org.yuzu.yuzu_emu.model.SettingsViewModel
35 34
36class SettingsAdapter( 35class SettingsAdapter(
37 private val fragmentView: SettingsFragmentView, 36 private val fragment: Fragment,
38 private val context: Context 37 private val context: Context
39) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener { 38) : ListAdapter<SettingsItem, SettingViewHolder>(
40 private var settings: ArrayList<SettingsItem>? = null 39 AsyncDifferConfig.Builder(DiffCallback()).build()
41 private var clickedItem: SettingsItem? = null 40) {
42 private var clickedPosition: Int 41 private val settingsViewModel: SettingsViewModel
43 private var dialog: AlertDialog? = null 42 get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
44 private var sliderProgress = 0
45 private var textSliderValue: TextView? = null
46
47 private var defaultCancelListener =
48 DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
49 43
50 init { 44 init {
51 clickedPosition = -1 45 fragment.viewLifecycleOwner.lifecycleScope.launch {
46 fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
47 settingsViewModel.adapterItemChanged.collect {
48 if (it != -1) {
49 notifyItemChanged(it)
50 settingsViewModel.setAdapterItemChanged(-1)
51 }
52 }
53 }
54 }
52 } 55 }
53 56
54 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { 57 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
@@ -90,67 +93,41 @@ class SettingsAdapter(
90 } 93 }
91 94
92 override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { 95 override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
93 holder.bind(getItem(position)) 96 holder.bind(currentList[position])
94 } 97 }
95 98
96 private fun getItem(position: Int): SettingsItem { 99 override fun getItemCount(): Int = currentList.size
97 return settings!![position]
98 }
99
100 override fun getItemCount(): Int {
101 return if (settings != null) {
102 settings!!.size
103 } else {
104 0
105 }
106 }
107 100
108 override fun getItemViewType(position: Int): Int { 101 override fun getItemViewType(position: Int): Int {
109 return getItem(position).type 102 return currentList[position].type
110 }
111
112 fun setSettingsList(settings: ArrayList<SettingsItem>?) {
113 this.settings = settings
114 notifyDataSetChanged()
115 }
116
117 fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
118 val setting = item.setChecked(checked)
119 fragmentView.putSetting(setting)
120 fragmentView.onSettingChanged()
121 } 103 }
122 104
123 private fun onSingleChoiceClick(item: SingleChoiceSetting) { 105 fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
124 clickedItem = item 106 item.checked = checked
125 val value = getSelectionForSingleChoiceValue(item) 107 settingsViewModel.setShouldReloadSettingsList(true)
126 dialog = MaterialAlertDialogBuilder(context) 108 settingsViewModel.shouldSave = true
127 .setTitle(item.nameId)
128 .setSingleChoiceItems(item.choicesId, value, this)
129 .show()
130 } 109 }
131 110
132 fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { 111 fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
133 clickedPosition = position 112 SettingsDialogFragment.newInstance(
134 onSingleChoiceClick(item) 113 settingsViewModel,
135 } 114 item,
136 115 SettingsItem.TYPE_SINGLE_CHOICE,
137 private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { 116 position
138 clickedItem = item 117 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
139 dialog = MaterialAlertDialogBuilder(context)
140 .setTitle(item.nameId)
141 .setSingleChoiceItems(item.choices, item.selectValueIndex, this)
142 .show()
143 } 118 }
144 119
145 fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { 120 fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
146 clickedPosition = position 121 SettingsDialogFragment.newInstance(
147 onStringSingleChoiceClick(item) 122 settingsViewModel,
123 item,
124 SettingsItem.TYPE_STRING_SINGLE_CHOICE,
125 position
126 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
148 } 127 }
149 128
150 fun onDateTimeClick(item: DateTimeSetting, position: Int) { 129 fun onDateTimeClick(item: DateTimeSetting, position: Int) {
151 clickedItem = item 130 val storedTime = item.value * 1000
152 clickedPosition = position
153 val storedTime = java.lang.Long.decode(item.value) * 1000
154 131
155 // Helper to extract hour and minute from epoch time 132 // Helper to extract hour and minute from epoch time
156 val calendar: Calendar = Calendar.getInstance() 133 val calendar: Calendar = Calendar.getInstance()
@@ -158,7 +135,7 @@ class SettingsAdapter(
158 calendar.timeZone = TimeZone.getTimeZone("UTC") 135 calendar.timeZone = TimeZone.getTimeZone("UTC")
159 136
160 var timeFormat: Int = TimeFormat.CLOCK_12H 137 var timeFormat: Int = TimeFormat.CLOCK_12H
161 if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) { 138 if (DateFormat.is24HourFormat(context)) {
162 timeFormat = TimeFormat.CLOCK_24H 139 timeFormat = TimeFormat.CLOCK_24H
163 } 140 }
164 141
@@ -175,7 +152,7 @@ class SettingsAdapter(
175 152
176 datePicker.addOnPositiveButtonClickListener { 153 datePicker.addOnPositiveButtonClickListener {
177 timePicker.show( 154 timePicker.show(
178 (fragmentView.activityView as AppCompatActivity).supportFragmentManager, 155 fragment.childFragmentManager,
179 "TimePicker" 156 "TimePicker"
180 ) 157 )
181 } 158 }
@@ -183,160 +160,50 @@ class SettingsAdapter(
183 var epochTime: Long = datePicker.selection!! / 1000 160 var epochTime: Long = datePicker.selection!! / 1000
184 epochTime += timePicker.hour.toLong() * 60 * 60 161 epochTime += timePicker.hour.toLong() * 60 * 60
185 epochTime += timePicker.minute.toLong() * 60 162 epochTime += timePicker.minute.toLong() * 60
186 val rtcString = epochTime.toString() 163 if (item.value != epochTime) {
187 if (item.value != rtcString) { 164 settingsViewModel.shouldSave = true
188 fragmentView.onSettingChanged() 165 notifyItemChanged(position)
166 item.value = epochTime
189 } 167 }
190 notifyItemChanged(clickedPosition)
191 val setting = item.setSelectedValue(rtcString)
192 fragmentView.putSetting(setting)
193 clickedItem = null
194 } 168 }
195 datePicker.show( 169 datePicker.show(
196 (fragmentView.activityView as AppCompatActivity).supportFragmentManager, 170 fragment.childFragmentManager,
197 "DatePicker" 171 "DatePicker"
198 ) 172 )
199 } 173 }
200 174
201 fun onSliderClick(item: SliderSetting, position: Int) { 175 fun onSliderClick(item: SliderSetting, position: Int) {
202 clickedItem = item 176 SettingsDialogFragment.newInstance(
203 clickedPosition = position 177 settingsViewModel,
204 sliderProgress = item.selectedValue 178 item,
205 179 SettingsItem.TYPE_SLIDER,
206 val inflater = LayoutInflater.from(context) 180 position
207 val sliderBinding = DialogSliderBinding.inflate(inflater) 181 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
208
209 textSliderValue = sliderBinding.textValue
210 textSliderValue!!.text = String.format(
211 context.getString(R.string.value_with_units),
212 sliderProgress.toString(),
213 item.units
214 )
215
216 sliderBinding.slider.apply {
217 valueFrom = item.min.toFloat()
218 valueTo = item.max.toFloat()
219 value = sliderProgress.toFloat()
220 addOnChangeListener { _: Slider, value: Float, _: Boolean ->
221 sliderProgress = value.toInt()
222 textSliderValue!!.text = String.format(
223 context.getString(R.string.value_with_units),
224 sliderProgress.toString(),
225 item.units
226 )
227 }
228 }
229
230 dialog = MaterialAlertDialogBuilder(context)
231 .setTitle(item.nameId)
232 .setView(sliderBinding.root)
233 .setPositiveButton(android.R.string.ok, this)
234 .setNegativeButton(android.R.string.cancel, defaultCancelListener)
235 .show()
236 } 182 }
237 183
238 fun onSubmenuClick(item: SubmenuSetting) { 184 fun onSubmenuClick(item: SubmenuSetting) {
239 fragmentView.loadSubMenu(item.menuKey) 185 val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
186 fragment.view?.findNavController()?.navigate(action)
240 } 187 }
241 188
242 override fun onClick(dialog: DialogInterface, which: Int) { 189 fun onLongClick(item: SettingsItem, position: Int): Boolean {
243 when (clickedItem) { 190 SettingsDialogFragment.newInstance(
244 is SingleChoiceSetting -> { 191 settingsViewModel,
245 val scSetting = clickedItem as SingleChoiceSetting 192 item,
246 val value = getValueForSingleChoiceSelection(scSetting, which) 193 SettingsDialogFragment.TYPE_RESET_SETTING,
247 if (scSetting.selectedValue != value) { 194 position
248 fragmentView.onSettingChanged() 195 ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
249 }
250
251 // Get the backing Setting, which may be null (if for example it was missing from the file)
252 val setting = scSetting.setSelectedValue(value)
253 fragmentView.putSetting(setting)
254 closeDialog()
255 }
256
257 is StringSingleChoiceSetting -> {
258 val scSetting = clickedItem as StringSingleChoiceSetting
259 val value = scSetting.getValueAt(which)
260 if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
261 val setting = scSetting.setSelectedValue(value!!)
262 fragmentView.putSetting(setting)
263 closeDialog()
264 }
265
266 is SliderSetting -> {
267 val sliderSetting = clickedItem as SliderSetting
268 if (sliderSetting.selectedValue != sliderProgress) {
269 fragmentView.onSettingChanged()
270 }
271 if (sliderSetting.setting is FloatSetting) {
272 val value = sliderProgress.toFloat()
273 val setting = sliderSetting.setSelectedValue(value)
274 fragmentView.putSetting(setting)
275 } else {
276 val setting = sliderSetting.setSelectedValue(sliderProgress)
277 fragmentView.putSetting(setting)
278 }
279 closeDialog()
280 }
281 }
282 clickedItem = null
283 sliderProgress = -1
284 }
285
286 fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
287 MaterialAlertDialogBuilder(context)
288 .setMessage(R.string.reset_setting_confirmation)
289 .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int ->
290 when (setting) {
291 is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
292 is AbstractFloatSetting -> setting.float = setting.defaultValue as Float
293 is AbstractIntSetting -> setting.int = setting.defaultValue as Int
294 is AbstractStringSetting -> setting.string = setting.defaultValue as String
295 }
296 notifyItemChanged(position)
297 fragmentView.onSettingChanged()
298 }
299 .setNegativeButton(android.R.string.cancel, null)
300 .show()
301 196
302 return true 197 return true
303 } 198 }
304 199
305 fun closeDialog() { 200 private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
306 if (dialog != null) { 201 override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
307 if (clickedPosition != -1) { 202 return oldItem.setting.key == newItem.setting.key
308 notifyItemChanged(clickedPosition)
309 clickedPosition = -1
310 }
311 dialog!!.dismiss()
312 dialog = null
313 } 203 }
314 }
315 204
316 private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { 205 override fun areContentsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
317 val valuesId = item.valuesId 206 return oldItem.setting.key == newItem.setting.key
318 return if (valuesId > 0) {
319 val valuesArray = context.resources.getIntArray(valuesId)
320 valuesArray[which]
321 } else {
322 which
323 }
324 }
325
326 private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
327 val value = item.selectedValue
328 val valuesId = item.valuesId
329 if (valuesId > 0) {
330 val valuesArray = context.resources.getIntArray(valuesId)
331 for (index in valuesArray.indices) {
332 val current = valuesArray[index]
333 if (current == value) {
334 return index
335 }
336 }
337 } else {
338 return value
339 } 207 }
340 return -1
341 } 208 }
342} 209}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
index 70a74c4dd..bc319714c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
@@ -3,40 +3,43 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.ui 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.content.Context
7import android.os.Bundle 6import android.os.Bundle
8import android.view.LayoutInflater 7import android.view.LayoutInflater
9import android.view.View 8import android.view.View
10import android.view.ViewGroup 9import android.view.ViewGroup
10import android.view.ViewGroup.MarginLayoutParams
11import androidx.core.view.ViewCompat 11import androidx.core.view.ViewCompat
12import androidx.core.view.WindowInsetsCompat 12import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding 13import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment 14import androidx.fragment.app.Fragment
15import androidx.fragment.app.activityViewModels
16import androidx.navigation.findNavController
17import androidx.navigation.fragment.navArgs
15import androidx.recyclerview.widget.LinearLayoutManager 18import androidx.recyclerview.widget.LinearLayoutManager
16import com.google.android.material.divider.MaterialDividerItemDecoration 19import com.google.android.material.divider.MaterialDividerItemDecoration
20import com.google.android.material.transition.MaterialSharedAxis
21import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding 22import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
18import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting 23import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
19import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 24import org.yuzu.yuzu_emu.model.SettingsViewModel
20 25
21class SettingsFragment : Fragment(), SettingsFragmentView { 26class SettingsFragment : Fragment() {
22 override var activityView: SettingsActivityView? = null 27 private lateinit var presenter: SettingsFragmentPresenter
23
24 private val fragmentPresenter = SettingsFragmentPresenter(this)
25 private var settingsAdapter: SettingsAdapter? = null 28 private var settingsAdapter: SettingsAdapter? = null
26 29
27 private var _binding: FragmentSettingsBinding? = null 30 private var _binding: FragmentSettingsBinding? = null
28 private val binding get() = _binding!! 31 private val binding get() = _binding!!
29 32
30 override fun onAttach(context: Context) { 33 private val args by navArgs<SettingsFragmentArgs>()
31 super.onAttach(context) 34
32 activityView = requireActivity() as SettingsActivityView 35 private val settingsViewModel: SettingsViewModel by activityViewModels()
33 }
34 36
35 override fun onCreate(savedInstanceState: Bundle?) { 37 override fun onCreate(savedInstanceState: Bundle?) {
36 super.onCreate(savedInstanceState) 38 super.onCreate(savedInstanceState)
37 val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG) 39 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
38 val gameId = requireArguments().getString(ARGUMENT_GAME_ID) 40 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
39 fragmentPresenter.onCreate(menuTag!!, gameId!!) 41 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
42 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
40 } 43 }
41 44
42 override fun onCreateView( 45 override fun onCreateView(
@@ -49,7 +52,14 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
49 } 52 }
50 53
51 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 54 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
52 settingsAdapter = SettingsAdapter(this, requireActivity()) 55 settingsAdapter = SettingsAdapter(this, requireContext())
56 presenter = SettingsFragmentPresenter(
57 settingsViewModel,
58 settingsAdapter!!,
59 args.menuTag,
60 args.game?.gameId ?: ""
61 )
62
53 val dividerDecoration = MaterialDividerItemDecoration( 63 val dividerDecoration = MaterialDividerItemDecoration(
54 requireContext(), 64 requireContext(),
55 LinearLayoutManager.VERTICAL 65 LinearLayoutManager.VERTICAL
@@ -57,71 +67,86 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
57 dividerDecoration.isLastItemDecorated = false 67 dividerDecoration.isLastItemDecorated = false
58 binding.listSettings.apply { 68 binding.listSettings.apply {
59 adapter = settingsAdapter 69 adapter = settingsAdapter
60 layoutManager = LinearLayoutManager(activity) 70 layoutManager = LinearLayoutManager(requireContext())
61 addItemDecoration(dividerDecoration) 71 addItemDecoration(dividerDecoration)
62 } 72 }
63 fragmentPresenter.onViewCreated()
64 73
65 setInsets() 74 binding.toolbarSettings.setNavigationOnClickListener {
66 } 75 settingsViewModel.setShouldNavigateBack(true)
76 }
67 77
68 override fun onDetach() { 78 settingsViewModel.toolbarTitle.observe(viewLifecycleOwner) {
69 super.onDetach() 79 if (it.isNotEmpty()) binding.toolbarSettingsLayout.title = it
70 activityView = null
71 if (settingsAdapter != null) {
72 settingsAdapter!!.closeDialog()
73 } 80 }
74 }
75 81
76 override fun showSettingsList(settingsList: ArrayList<SettingsItem>) { 82 settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) {
77 settingsAdapter!!.setSettingsList(settingsList) 83 if (it) {
78 } 84 settingsViewModel.setShouldReloadSettingsList(false)
85 presenter.loadSettingsList()
86 }
87 }
79 88
80 override fun loadSettingsList() { 89 settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) {
81 fragmentPresenter.loadSettingsList() 90 if (it) {
82 } 91 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
92 exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
93 } else {
94 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
95 exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
96 }
97 }
83 98
84 override fun loadSubMenu(menuKey: String) { 99 if (args.menuTag == SettingsFile.FILE_NAME_CONFIG) {
85 activityView!!.showSettingsFragment( 100 binding.toolbarSettings.inflateMenu(R.menu.menu_settings)
86 menuKey, 101 binding.toolbarSettings.setOnMenuItemClickListener {
87 true, 102 when (it.itemId) {
88 requireArguments().getString(ARGUMENT_GAME_ID)!! 103 R.id.action_search -> {
89 ) 104 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
90 } 105 exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
106 view.findNavController()
107 .navigate(R.id.action_settingsFragment_to_settingsSearchFragment)
108 true
109 }
110
111 else -> false
112 }
113 }
114 }
91 115
92 override fun showToastMessage(message: String?, is_long: Boolean) { 116 presenter.onViewCreated()
93 activityView!!.showToastMessage(message!!, is_long)
94 }
95 117
96 override fun putSetting(setting: AbstractSetting) { 118 setInsets()
97 fragmentPresenter.putSetting(setting)
98 } 119 }
99 120
100 override fun onSettingChanged() { 121 override fun onResume() {
101 activityView!!.onSettingChanged() 122 super.onResume()
123 settingsViewModel.setIsUsingSearch(false)
102 } 124 }
103 125
104 private fun setInsets() { 126 private fun setInsets() {
105 ViewCompat.setOnApplyWindowInsetsListener( 127 ViewCompat.setOnApplyWindowInsetsListener(
106 binding.listSettings 128 binding.root
107 ) { view: View, windowInsets: WindowInsetsCompat -> 129 ) { _: View, windowInsets: WindowInsetsCompat ->
108 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 130 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
109 view.updatePadding(bottom = insets.bottom) 131 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
132
133 val leftInsets = barInsets.left + cutoutInsets.left
134 val rightInsets = barInsets.right + cutoutInsets.right
135
136 val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
137 val mlpSettingsList = binding.listSettings.layoutParams as MarginLayoutParams
138 mlpSettingsList.leftMargin = sideMargin + leftInsets
139 mlpSettingsList.rightMargin = sideMargin + rightInsets
140 binding.listSettings.layoutParams = mlpSettingsList
141 binding.listSettings.updatePadding(
142 bottom = barInsets.bottom
143 )
144
145 val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
146 mlpAppBar.leftMargin = leftInsets
147 mlpAppBar.rightMargin = rightInsets
148 binding.appbarSettings.layoutParams = mlpAppBar
110 windowInsets 149 windowInsets
111 } 150 }
112 } 151 }
113
114 companion object {
115 private const val ARGUMENT_MENU_TAG = "menu_tag"
116 private const val ARGUMENT_GAME_ID = "game_id"
117
118 fun newInstance(menuTag: String?, gameId: String?): Fragment {
119 val fragment = SettingsFragment()
120 val arguments = Bundle()
121 arguments.putString(ARGUMENT_MENU_TAG, menuTag)
122 arguments.putString(ARGUMENT_GAME_ID, gameId)
123 fragment.arguments = arguments
124 return fragment
125 }
126 }
127} 152}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 59c1d9d54..22a529b1b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -3,63 +3,66 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.ui 4package org.yuzu.yuzu_emu.features.settings.ui
5 5
6import android.content.Context
6import android.content.SharedPreferences 7import android.content.SharedPreferences
7import android.os.Build 8import android.os.Build
8import android.text.TextUtils 9import android.text.TextUtils
10import android.widget.Toast
9import androidx.preference.PreferenceManager 11import androidx.preference.PreferenceManager
10import org.yuzu.yuzu_emu.R 12import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.YuzuApplication 13import org.yuzu.yuzu_emu.YuzuApplication
12import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting 14import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
13import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting 15import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
14import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
15import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting 16import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
17import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
16import org.yuzu.yuzu_emu.features.settings.model.IntSetting 18import org.yuzu.yuzu_emu.features.settings.model.IntSetting
19import org.yuzu.yuzu_emu.features.settings.model.LongSetting
17import org.yuzu.yuzu_emu.features.settings.model.Settings 20import org.yuzu.yuzu_emu.features.settings.model.Settings
18import org.yuzu.yuzu_emu.features.settings.model.StringSetting 21import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
19import org.yuzu.yuzu_emu.features.settings.model.view.* 22import org.yuzu.yuzu_emu.features.settings.model.view.*
20import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 23import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
21import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment 24import org.yuzu.yuzu_emu.model.SettingsViewModel
22import org.yuzu.yuzu_emu.utils.ThemeHelper 25import org.yuzu.yuzu_emu.utils.NativeConfig
23 26
24class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { 27class SettingsFragmentPresenter(
25 private var menuTag: String? = null 28 private val settingsViewModel: SettingsViewModel,
26 private lateinit var gameId: String 29 private val adapter: SettingsAdapter,
27 private var settingsList: ArrayList<SettingsItem>? = null 30 private var menuTag: String,
31 private var gameId: String
32) {
33 private var settingsList = ArrayList<SettingsItem>()
28 34
29 private val settingsActivity get() = fragmentView.activityView as SettingsActivity 35 private val preferences: SharedPreferences
30 private val settings get() = fragmentView.activityView!!.settings 36 get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
31 37
32 private lateinit var preferences: SharedPreferences 38 private val context: Context get() = YuzuApplication.appContext
33 39
34 fun onCreate(menuTag: String, gameId: String) { 40 // Extension for populating settings list based on paired settings
35 this.gameId = gameId 41 fun ArrayList<SettingsItem>.add(key: String) {
36 this.menuTag = menuTag 42 val item = SettingsItem.settingsItems[key]!!
43 val pairedSettingKey = item.setting.pairedSettingKey
44 if (pairedSettingKey.isNotEmpty()) {
45 val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
46 if (!pairedSettingValue) return
47 }
48 add(item)
37 } 49 }
38 50
39 fun onViewCreated() { 51 fun onViewCreated() {
40 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
41 loadSettingsList() 52 loadSettingsList()
42 } 53 }
43 54
44 fun putSetting(setting: AbstractSetting) {
45 if (setting.section == null || setting.key == null) {
46 return
47 }
48
49 val section = settings.getSection(setting.section!!)!!
50 if (section.getSetting(setting.key!!) == null) {
51 section.putSetting(setting)
52 }
53 }
54
55 fun loadSettingsList() { 55 fun loadSettingsList() {
56 if (!TextUtils.isEmpty(gameId)) { 56 if (!TextUtils.isEmpty(gameId)) {
57 settingsActivity.setToolbarTitle("Game Settings: $gameId") 57 settingsViewModel.setToolbarTitle(
58 context.getString(
59 R.string.advanced_settings_game,
60 gameId
61 )
62 )
58 } 63 }
64
59 val sl = ArrayList<SettingsItem>() 65 val sl = ArrayList<SettingsItem>()
60 if (menuTag == null) {
61 return
62 }
63 when (menuTag) { 66 when (menuTag) {
64 SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl) 67 SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl)
65 Settings.SECTION_GENERAL -> addGeneralSettings(sl) 68 Settings.SECTION_GENERAL -> addGeneralSettings(sl)
@@ -69,335 +72,104 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
69 Settings.SECTION_THEME -> addThemeSettings(sl) 72 Settings.SECTION_THEME -> addThemeSettings(sl)
70 Settings.SECTION_DEBUG -> addDebugSettings(sl) 73 Settings.SECTION_DEBUG -> addDebugSettings(sl)
71 else -> { 74 else -> {
72 fragmentView.showToastMessage("Unimplemented menu", false) 75 val context = YuzuApplication.appContext
76 Toast.makeText(
77 context,
78 context.getString(R.string.unimplemented_menu),
79 Toast.LENGTH_SHORT
80 ).show()
73 return 81 return
74 } 82 }
75 } 83 }
76 settingsList = sl 84 settingsList = sl
77 fragmentView.showSettingsList(settingsList!!) 85 adapter.submitList(settingsList)
78 } 86 }
79 87
80 private fun addConfigSettings(sl: ArrayList<SettingsItem>) { 88 private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
81 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.advanced_settings)) 89 settingsViewModel.setToolbarTitle(context.getString(R.string.advanced_settings))
82 sl.apply { 90 sl.apply {
83 add( 91 add(SubmenuSetting(R.string.preferences_general, 0, Settings.SECTION_GENERAL))
84 SubmenuSetting( 92 add(SubmenuSetting(R.string.preferences_system, 0, Settings.SECTION_SYSTEM))
85 R.string.preferences_general, 93 add(SubmenuSetting(R.string.preferences_graphics, 0, Settings.SECTION_RENDERER))
86 0, 94 add(SubmenuSetting(R.string.preferences_audio, 0, Settings.SECTION_AUDIO))
87 Settings.SECTION_GENERAL 95 add(SubmenuSetting(R.string.preferences_debug, 0, Settings.SECTION_DEBUG))
88 ) 96 add(
89 ) 97 RunnableSetting(R.string.reset_to_default, 0, false) {
90 add( 98 settingsViewModel.setShouldShowResetSettingsDialog(true)
91 SubmenuSetting(
92 R.string.preferences_system,
93 0,
94 Settings.SECTION_SYSTEM
95 )
96 )
97 add(
98 SubmenuSetting(
99 R.string.preferences_graphics,
100 0,
101 Settings.SECTION_RENDERER
102 )
103 )
104 add(
105 SubmenuSetting(
106 R.string.preferences_audio,
107 0,
108 Settings.SECTION_AUDIO
109 )
110 )
111 add(
112 SubmenuSetting(
113 R.string.preferences_debug,
114 0,
115 Settings.SECTION_DEBUG
116 )
117 )
118 add(
119 RunnableSetting(
120 R.string.reset_to_default,
121 0,
122 false
123 ) {
124 ResetSettingsDialogFragment().show(
125 settingsActivity.supportFragmentManager,
126 ResetSettingsDialogFragment.TAG
127 )
128 } 99 }
129 ) 100 )
130 } 101 }
131 } 102 }
132 103
133 private fun addGeneralSettings(sl: ArrayList<SettingsItem>) { 104 private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
134 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general)) 105 settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_general))
135 sl.apply { 106 sl.apply {
136 add( 107 add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
137 SwitchSetting( 108 add(ShortSetting.RENDERER_SPEED_LIMIT.key)
138 IntSetting.RENDERER_USE_SPEED_LIMIT, 109 add(IntSetting.CPU_ACCURACY.key)
139 R.string.frame_limit_enable, 110 add(BooleanSetting.PICTURE_IN_PICTURE.key)
140 R.string.frame_limit_enable_description,
141 IntSetting.RENDERER_USE_SPEED_LIMIT.key,
142 IntSetting.RENDERER_USE_SPEED_LIMIT.defaultValue
143 )
144 )
145 add(
146 SliderSetting(
147 IntSetting.RENDERER_SPEED_LIMIT,
148 R.string.frame_limit_slider,
149 R.string.frame_limit_slider_description,
150 1,
151 200,
152 "%",
153 IntSetting.RENDERER_SPEED_LIMIT.key,
154 IntSetting.RENDERER_SPEED_LIMIT.defaultValue
155 )
156 )
157 add(
158 SingleChoiceSetting(
159 IntSetting.CPU_ACCURACY,
160 R.string.cpu_accuracy,
161 0,
162 R.array.cpuAccuracyNames,
163 R.array.cpuAccuracyValues,
164 IntSetting.CPU_ACCURACY.key,
165 IntSetting.CPU_ACCURACY.defaultValue
166 )
167 )
168 add(
169 SwitchSetting(
170 BooleanSetting.PICTURE_IN_PICTURE,
171 R.string.picture_in_picture,
172 R.string.picture_in_picture_description,
173 BooleanSetting.PICTURE_IN_PICTURE.key,
174 BooleanSetting.PICTURE_IN_PICTURE.defaultValue
175 )
176 )
177 } 111 }
178 } 112 }
179 113
180 private fun addSystemSettings(sl: ArrayList<SettingsItem>) { 114 private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
181 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) 115 settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_system))
182 sl.apply { 116 sl.apply {
183 add( 117 add(BooleanSetting.USE_DOCKED_MODE.key)
184 SwitchSetting( 118 add(IntSetting.REGION_INDEX.key)
185 IntSetting.USE_DOCKED_MODE, 119 add(IntSetting.LANGUAGE_INDEX.key)
186 R.string.use_docked_mode, 120 add(BooleanSetting.USE_CUSTOM_RTC.key)
187 R.string.use_docked_mode_description, 121 add(LongSetting.CUSTOM_RTC.key)
188 IntSetting.USE_DOCKED_MODE.key,
189 IntSetting.USE_DOCKED_MODE.defaultValue
190 )
191 )
192 add(
193 SingleChoiceSetting(
194 IntSetting.REGION_INDEX,
195 R.string.emulated_region,
196 0,
197 R.array.regionNames,
198 R.array.regionValues,
199 IntSetting.REGION_INDEX.key,
200 IntSetting.REGION_INDEX.defaultValue
201 )
202 )
203 add(
204 SingleChoiceSetting(
205 IntSetting.LANGUAGE_INDEX,
206 R.string.emulated_language,
207 0,
208 R.array.languageNames,
209 R.array.languageValues,
210 IntSetting.LANGUAGE_INDEX.key,
211 IntSetting.LANGUAGE_INDEX.defaultValue
212 )
213 )
214 add(
215 SwitchSetting(
216 BooleanSetting.USE_CUSTOM_RTC,
217 R.string.use_custom_rtc,
218 R.string.use_custom_rtc_description,
219 BooleanSetting.USE_CUSTOM_RTC.key,
220 BooleanSetting.USE_CUSTOM_RTC.defaultValue
221 )
222 )
223 add(
224 DateTimeSetting(
225 StringSetting.CUSTOM_RTC,
226 R.string.set_custom_rtc,
227 0,
228 StringSetting.CUSTOM_RTC.key,
229 StringSetting.CUSTOM_RTC.defaultValue
230 )
231 )
232 } 122 }
233 } 123 }
234 124
235 private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { 125 private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
236 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) 126 settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_graphics))
237 sl.apply { 127 sl.apply {
238 add( 128 add(IntSetting.RENDERER_ACCURACY.key)
239 SingleChoiceSetting( 129 add(IntSetting.RENDERER_RESOLUTION.key)
240 IntSetting.RENDERER_ACCURACY, 130 add(IntSetting.RENDERER_VSYNC.key)
241 R.string.renderer_accuracy, 131 add(IntSetting.RENDERER_SCALING_FILTER.key)
242 0, 132 add(IntSetting.RENDERER_ANTI_ALIASING.key)
243 R.array.rendererAccuracyNames, 133 add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
244 R.array.rendererAccuracyValues, 134 add(IntSetting.RENDERER_ASPECT_RATIO.key)
245 IntSetting.RENDERER_ACCURACY.key, 135 add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
246 IntSetting.RENDERER_ACCURACY.defaultValue 136 add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
247 ) 137 add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
248 ) 138 add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
249 add(
250 SingleChoiceSetting(
251 IntSetting.RENDERER_RESOLUTION,
252 R.string.renderer_resolution,
253 0,
254 R.array.rendererResolutionNames,
255 R.array.rendererResolutionValues,
256 IntSetting.RENDERER_RESOLUTION.key,
257 IntSetting.RENDERER_RESOLUTION.defaultValue
258 )
259 )
260 add(
261 SingleChoiceSetting(
262 IntSetting.RENDERER_VSYNC,
263 R.string.renderer_vsync,
264 0,
265 R.array.rendererVSyncNames,
266 R.array.rendererVSyncValues,
267 IntSetting.RENDERER_VSYNC.key,
268 IntSetting.RENDERER_VSYNC.defaultValue
269 )
270 )
271 add(
272 SingleChoiceSetting(
273 IntSetting.RENDERER_SCALING_FILTER,
274 R.string.renderer_scaling_filter,
275 0,
276 R.array.rendererScalingFilterNames,
277 R.array.rendererScalingFilterValues,
278 IntSetting.RENDERER_SCALING_FILTER.key,
279 IntSetting.RENDERER_SCALING_FILTER.defaultValue
280 )
281 )
282 add(
283 SingleChoiceSetting(
284 IntSetting.RENDERER_ANTI_ALIASING,
285 R.string.renderer_anti_aliasing,
286 0,
287 R.array.rendererAntiAliasingNames,
288 R.array.rendererAntiAliasingValues,
289 IntSetting.RENDERER_ANTI_ALIASING.key,
290 IntSetting.RENDERER_ANTI_ALIASING.defaultValue
291 )
292 )
293 add(
294 SingleChoiceSetting(
295 IntSetting.RENDERER_SCREEN_LAYOUT,
296 R.string.renderer_screen_layout,
297 0,
298 R.array.rendererScreenLayoutNames,
299 R.array.rendererScreenLayoutValues,
300 IntSetting.RENDERER_SCREEN_LAYOUT.key,
301 IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
302 )
303 )
304 add(
305 SingleChoiceSetting(
306 IntSetting.RENDERER_ASPECT_RATIO,
307 R.string.renderer_aspect_ratio,
308 0,
309 R.array.rendererAspectRatioNames,
310 R.array.rendererAspectRatioValues,
311 IntSetting.RENDERER_ASPECT_RATIO.key,
312 IntSetting.RENDERER_ASPECT_RATIO.defaultValue
313 )
314 )
315 add(
316 SwitchSetting(
317 IntSetting.RENDERER_USE_DISK_SHADER_CACHE,
318 R.string.use_disk_shader_cache,
319 R.string.use_disk_shader_cache_description,
320 IntSetting.RENDERER_USE_DISK_SHADER_CACHE.key,
321 IntSetting.RENDERER_USE_DISK_SHADER_CACHE.defaultValue
322 )
323 )
324 add(
325 SwitchSetting(
326 IntSetting.RENDERER_FORCE_MAX_CLOCK,
327 R.string.renderer_force_max_clock,
328 R.string.renderer_force_max_clock_description,
329 IntSetting.RENDERER_FORCE_MAX_CLOCK.key,
330 IntSetting.RENDERER_FORCE_MAX_CLOCK.defaultValue
331 )
332 )
333 add(
334 SwitchSetting(
335 IntSetting.RENDERER_ASYNCHRONOUS_SHADERS,
336 R.string.renderer_asynchronous_shaders,
337 R.string.renderer_asynchronous_shaders_description,
338 IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.key,
339 IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.defaultValue
340 )
341 )
342 add(
343 SwitchSetting(
344 IntSetting.RENDERER_REACTIVE_FLUSHING,
345 R.string.renderer_reactive_flushing,
346 R.string.renderer_reactive_flushing_description,
347 IntSetting.RENDERER_REACTIVE_FLUSHING.key,
348 IntSetting.RENDERER_REACTIVE_FLUSHING.defaultValue
349 )
350 )
351 } 139 }
352 } 140 }
353 141
354 private fun addAudioSettings(sl: ArrayList<SettingsItem>) { 142 private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
355 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio)) 143 settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_audio))
356 sl.apply { 144 sl.apply {
357 add( 145 add(IntSetting.AUDIO_OUTPUT_ENGINE.key)
358 StringSingleChoiceSetting( 146 add(ByteSetting.AUDIO_VOLUME.key)
359 StringSetting.AUDIO_OUTPUT_ENGINE,
360 R.string.audio_output_engine,
361 0,
362 settingsActivity.resources.getStringArray(R.array.outputEngineEntries),
363 settingsActivity.resources.getStringArray(R.array.outputEngineValues),
364 StringSetting.AUDIO_OUTPUT_ENGINE.key,
365 StringSetting.AUDIO_OUTPUT_ENGINE.defaultValue
366 )
367 )
368 add(
369 SliderSetting(
370 IntSetting.AUDIO_VOLUME,
371 R.string.audio_volume,
372 R.string.audio_volume_description,
373 0,
374 100,
375 "%",
376 IntSetting.AUDIO_VOLUME.key,
377 IntSetting.AUDIO_VOLUME.defaultValue
378 )
379 )
380 } 147 }
381 } 148 }
382 149
383 private fun addThemeSettings(sl: ArrayList<SettingsItem>) { 150 private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
384 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme)) 151 settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_theme))
385 sl.apply { 152 sl.apply {
386 val theme: AbstractIntSetting = object : AbstractIntSetting { 153 val theme: AbstractIntSetting = object : AbstractIntSetting {
387 override var int: Int 154 override val int: Int
388 get() = preferences.getInt(Settings.PREF_THEME, 0) 155 get() = preferences.getInt(Settings.PREF_THEME, 0)
389 set(value) { 156
390 preferences.edit() 157 override fun setInt(value: Int) {
391 .putInt(Settings.PREF_THEME, value) 158 preferences.edit()
392 .apply() 159 .putInt(Settings.PREF_THEME, value)
393 settingsActivity.recreate() 160 .apply()
394 } 161 settingsViewModel.setShouldRecreate(true)
395 override val key: String? = null 162 }
396 override val section: String? = null 163
397 override val isRuntimeEditable: Boolean = false 164 override val key: String = Settings.PREF_THEME
398 override val valueAsString: String 165 override val category = Settings.Category.UiGeneral
399 get() = preferences.getInt(Settings.PREF_THEME, 0).toString() 166 override val isRuntimeModifiable: Boolean = false
400 override val defaultValue: Any = 0 167 override val defaultValue: Int = 0
168 override fun reset() {
169 preferences.edit()
170 .putInt(Settings.PREF_THEME, defaultValue)
171 .apply()
172 }
401 } 173 }
402 174
403 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 175 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -423,20 +195,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
423 } 195 }
424 196
425 val themeMode: AbstractIntSetting = object : AbstractIntSetting { 197 val themeMode: AbstractIntSetting = object : AbstractIntSetting {
426 override var int: Int 198 override val int: Int
427 get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) 199 get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
428 set(value) { 200
429 preferences.edit() 201 override fun setInt(value: Int) {
430 .putInt(Settings.PREF_THEME_MODE, value) 202 preferences.edit()
431 .apply() 203 .putInt(Settings.PREF_THEME_MODE, value)
432 ThemeHelper.setThemeMode(settingsActivity) 204 .apply()
433 } 205 settingsViewModel.setShouldRecreate(true)
434 override val key: String? = null 206 }
435 override val section: String? = null 207
436 override val isRuntimeEditable: Boolean = false 208 override val key: String = Settings.PREF_THEME_MODE
437 override val valueAsString: String 209 override val category = Settings.Category.UiGeneral
438 get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString() 210 override val isRuntimeModifiable: Boolean = false
439 override val defaultValue: Any = -1 211 override val defaultValue: Int = -1
212 override fun reset() {
213 preferences.edit()
214 .putInt(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
215 .apply()
216 settingsViewModel.setShouldRecreate(true)
217 }
440 } 218 }
441 219
442 add( 220 add(
@@ -450,21 +228,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
450 ) 228 )
451 229
452 val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { 230 val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
453 override var boolean: Boolean 231 override val boolean: Boolean
454 get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
455 set(value) {
456 preferences.edit()
457 .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
458 .apply()
459 settingsActivity.recreate()
460 }
461 override val key: String? = null
462 override val section: String? = null
463 override val isRuntimeEditable: Boolean = false
464 override val valueAsString: String
465 get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) 232 get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
466 .toString() 233
467 override val defaultValue: Any = false 234 override fun setBoolean(value: Boolean) {
235 preferences.edit()
236 .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
237 .apply()
238 settingsViewModel.setShouldRecreate(true)
239 }
240
241 override val key: String = Settings.PREF_BLACK_BACKGROUNDS
242 override val category = Settings.Category.UiGeneral
243 override val isRuntimeModifiable: Boolean = false
244 override val defaultValue: Boolean = false
245 override fun reset() {
246 preferences.edit()
247 .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
248 .apply()
249 settingsViewModel.setShouldRecreate(true)
250 }
468 } 251 }
469 252
470 add( 253 add(
@@ -478,62 +261,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
478 } 261 }
479 262
480 private fun addDebugSettings(sl: ArrayList<SettingsItem>) { 263 private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
481 settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug)) 264 settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_debug))
482 sl.apply { 265 sl.apply {
483 add(HeaderSetting(R.string.gpu)) 266 add(HeaderSetting(R.string.gpu))
484 add( 267 add(IntSetting.RENDERER_BACKEND.key)
485 SingleChoiceSetting( 268 add(BooleanSetting.RENDERER_DEBUG.key)
486 IntSetting.RENDERER_BACKEND,
487 R.string.renderer_api,
488 0,
489 R.array.rendererApiNames,
490 R.array.rendererApiValues,
491 IntSetting.RENDERER_BACKEND.key,
492 IntSetting.RENDERER_BACKEND.defaultValue
493 )
494 )
495 add(
496 SwitchSetting(
497 IntSetting.RENDERER_DEBUG,
498 R.string.renderer_debug,
499 R.string.renderer_debug_description,
500 IntSetting.RENDERER_DEBUG.key,
501 IntSetting.RENDERER_DEBUG.defaultValue
502 )
503 )
504 269
505 add(HeaderSetting(R.string.cpu)) 270 add(HeaderSetting(R.string.cpu))
506 add( 271 add(BooleanSetting.CPU_DEBUG_MODE.key)
507 SwitchSetting( 272 add(SettingsItem.FASTMEM_COMBINED)
508 BooleanSetting.CPU_DEBUG_MODE,
509 R.string.cpu_debug_mode,
510 R.string.cpu_debug_mode_description,
511 BooleanSetting.CPU_DEBUG_MODE.key,
512 BooleanSetting.CPU_DEBUG_MODE.defaultValue
513 )
514 )
515
516 val fastmem = object : AbstractBooleanSetting {
517 override var boolean: Boolean
518 get() =
519 BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
520 set(value) {
521 BooleanSetting.FASTMEM.boolean = value
522 BooleanSetting.FASTMEM_EXCLUSIVES.boolean = value
523 }
524 override val key: String? = null
525 override val section: String = Settings.SECTION_CPU
526 override val isRuntimeEditable: Boolean = false
527 override val valueAsString: String = ""
528 override val defaultValue: Any = true
529 }
530 add(
531 SwitchSetting(
532 fastmem,
533 R.string.fastmem,
534 0
535 )
536 )
537 } 273 }
538 } 274 }
539} 275}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt
deleted file mode 100644
index 1ebe35eaa..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt
+++ /dev/null
@@ -1,58 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.features.settings.ui
5
6import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
7import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
8
9/**
10 * Abstraction for a screen showing a list of settings. Instances of
11 * this type of view will each display a layer of the setting hierarchy.
12 */
13interface SettingsFragmentView {
14 /**
15 * Pass an ArrayList to the View so that it can be displayed on screen.
16 *
17 * @param settingsList The result of converting the HashMap to an ArrayList
18 */
19 fun showSettingsList(settingsList: ArrayList<SettingsItem>)
20
21 /**
22 * Instructs the Fragment to load the settings screen.
23 */
24 fun loadSettingsList()
25
26 /**
27 * @return The Fragment's containing activity.
28 */
29 val activityView: SettingsActivityView?
30
31 /**
32 * Tell the Fragment to tell the containing Activity to show a new
33 * Fragment containing a submenu of settings.
34 *
35 * @param menuKey Identifier for the settings group that should be shown.
36 */
37 fun loadSubMenu(menuKey: String)
38
39 /**
40 * Tell the Fragment to tell the containing activity to display a toast message.
41 *
42 * @param message Text to be shown in the Toast
43 * @param is_long Whether this should be a long Toast or short one.
44 */
45 fun showToastMessage(message: String?, is_long: Boolean)
46
47 /**
48 * Have the fragment add a setting to the HashMap.
49 *
50 * @param setting The (possibly previously missing) new setting.
51 */
52 fun putSetting(setting: AbstractSetting)
53
54 /**
55 * Have the fragment tell the containing Activity that a setting was modified.
56 */
57 fun onSettingChanged()
58}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
index 79572fc06..525f013f8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -29,7 +29,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
29 } 29 }
30 30
31 binding.textSettingValue.visibility = View.VISIBLE 31 binding.textSettingValue.visibility = View.VISIBLE
32 val epochTime = setting.value.toLong() 32 val epochTime = setting.value
33 val instant = Instant.ofEpochMilli(epochTime * 1000) 33 val instant = Instant.ofEpochMilli(epochTime * 1000)
34 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) 34 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
35 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) 35 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
@@ -46,7 +46,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
46 46
47 override fun onLongClick(clicked: View): Boolean { 47 override fun onLongClick(clicked: View): Boolean {
48 if (setting.isEditable) { 48 if (setting.isEditable) {
49 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) 49 return adapter.onLongClick(setting, bindingAdapterPosition)
50 } 50 }
51 return false 51 return false
52 } 52 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
index b42d955aa..80d1b22c1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -35,7 +35,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
35 } 35 }
36 } 36 }
37 } else if (item is StringSingleChoiceSetting) { 37 } else if (item is StringSingleChoiceSetting) {
38 for (i in item.values!!.indices) { 38 for (i in item.values.indices) {
39 if (item.values[i] == item.selectedValue) { 39 if (item.values[i] == item.selectedValue) {
40 binding.textSettingValue.text = item.choices[i] 40 binding.textSettingValue.text = item.choices[i]
41 break 41 break
@@ -66,7 +66,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
66 66
67 override fun onLongClick(clicked: View): Boolean { 67 override fun onLongClick(clicked: View): Boolean {
68 if (setting.isEditable) { 68 if (setting.isEditable) {
69 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) 69 return adapter.onLongClick(setting, bindingAdapterPosition)
70 } 70 }
71 return false 71 return false
72 } 72 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
index a23b5d109..b83c90100 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -41,7 +41,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
41 41
42 override fun onLongClick(clicked: View): Boolean { 42 override fun onLongClick(clicked: View): Boolean {
43 if (setting.isEditable) { 43 if (setting.isEditable) {
44 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) 44 return adapter.onLongClick(setting, bindingAdapterPosition)
45 } 45 }
46 return false 46 return false
47 } 47 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index ef34bf5f4..57fdeaa20 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -25,10 +25,12 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
25 binding.textSettingDescription.text = "" 25 binding.textSettingDescription.text = ""
26 binding.textSettingDescription.visibility = View.GONE 26 binding.textSettingDescription.visibility = View.GONE
27 } 27 }
28
29 binding.switchWidget.setOnCheckedChangeListener(null)
30 binding.switchWidget.isChecked = setting.checked
28 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> 31 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
29 adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked) 32 adapter.onBooleanClick(item, binding.switchWidget.isChecked)
30 } 33 }
31 binding.switchWidget.isChecked = setting.isChecked
32 34
33 setStyle(setting.isEditable, binding) 35 setStyle(setting.isEditable, binding)
34 } 36 }
@@ -41,7 +43,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
41 43
42 override fun onLongClick(clicked: View): Boolean { 44 override fun onLongClick(clicked: View): Boolean {
43 if (setting.isEditable) { 45 if (setting.isEditable) {
44 return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) 46 return adapter.onLongClick(setting, bindingAdapterPosition)
45 } 47 }
46 return false 48 return false
47 } 49 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
index 70a52df5d..2b04d666a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
@@ -3,18 +3,15 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.utils 4package org.yuzu.yuzu_emu.features.settings.utils
5 5
6import android.widget.Toast
6import java.io.* 7import java.io.*
7import java.util.*
8import org.ini4j.Wini 8import org.ini4j.Wini
9import org.yuzu.yuzu_emu.NativeLibrary
10import org.yuzu.yuzu_emu.R 9import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.YuzuApplication 10import org.yuzu.yuzu_emu.YuzuApplication
12import org.yuzu.yuzu_emu.features.settings.model.* 11import org.yuzu.yuzu_emu.features.settings.model.*
13import org.yuzu.yuzu_emu.features.settings.model.Settings.SettingsSectionMap
14import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
15import org.yuzu.yuzu_emu.utils.BiMap
16import org.yuzu.yuzu_emu.utils.DirectoryInitialization 12import org.yuzu.yuzu_emu.utils.DirectoryInitialization
17import org.yuzu.yuzu_emu.utils.Log 13import org.yuzu.yuzu_emu.utils.Log
14import org.yuzu.yuzu_emu.utils.NativeConfig
18 15
19/** 16/**
20 * Contains static methods for interacting with .ini files in which settings are stored. 17 * Contains static methods for interacting with .ini files in which settings are stored.
@@ -22,243 +19,41 @@ import org.yuzu.yuzu_emu.utils.Log
22object SettingsFile { 19object SettingsFile {
23 const val FILE_NAME_CONFIG = "config" 20 const val FILE_NAME_CONFIG = "config"
24 21
25 private var sectionsMap = BiMap<String?, String?>()
26
27 /**
28 * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
29 * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
30 * failed.
31 *
32 * @param ini The ini file to load the settings from
33 * @param isCustomGame
34 * @param view The current view.
35 * @return An Observable that emits a HashMap of the file's contents, then completes.
36 */
37 private fun readFile(
38 ini: File?,
39 isCustomGame: Boolean,
40 view: SettingsActivityView? = null
41 ): HashMap<String, SettingSection?> {
42 val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
43 var reader: BufferedReader? = null
44 try {
45 reader = BufferedReader(FileReader(ini))
46 var current: SettingSection? = null
47 var line: String?
48 while (reader.readLine().also { line = it } != null) {
49 if (line!!.startsWith("[") && line!!.endsWith("]")) {
50 current = sectionFromLine(line!!, isCustomGame)
51 sections[current.name] = current
52 } else if (current != null) {
53 val setting = settingFromLine(line!!)
54 if (setting != null) {
55 current.putSetting(setting)
56 }
57 }
58 }
59 } catch (e: FileNotFoundException) {
60 Log.error("[SettingsFile] File not found: " + e.message)
61 view?.onSettingsFileNotFound()
62 } catch (e: IOException) {
63 Log.error("[SettingsFile] Error reading from: " + e.message)
64 view?.onSettingsFileNotFound()
65 } finally {
66 if (reader != null) {
67 try {
68 reader.close()
69 } catch (e: IOException) {
70 Log.error("[SettingsFile] Error closing: " + e.message)
71 }
72 }
73 }
74 return sections
75 }
76
77 fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
78 return readFile(getSettingsFile(fileName), false, view)
79 }
80
81 fun readFile(fileName: String): HashMap<String, SettingSection?> =
82 readFile(getSettingsFile(fileName), false)
83
84 /**
85 * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
86 * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
87 * failed.
88 *
89 * @param gameId the id of the game to load it's settings.
90 * @param view The current view.
91 */
92 fun readCustomGameSettings(
93 gameId: String,
94 view: SettingsActivityView?
95 ): HashMap<String, SettingSection?> {
96 return readFile(getCustomGameSettingsFile(gameId), true, view)
97 }
98
99 /** 22 /**
100 * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error 23 * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
101 * telling why it failed. 24 * telling why it failed.
102 * 25 *
103 * @param fileName The target filename without a path or extension. 26 * @param fileName The target filename without a path or extension.
104 * @param sections The HashMap containing the Settings we want to serialize.
105 * @param view The current view.
106 */ 27 */
107 fun saveFile( 28 fun saveFile(fileName: String) {
108 fileName: String,
109 sections: TreeMap<String, SettingSection>,
110 view: SettingsActivityView
111 ) {
112 val ini = getSettingsFile(fileName) 29 val ini = getSettingsFile(fileName)
113 try { 30 try {
114 val writer = Wini(ini) 31 val wini = Wini(ini)
115 val keySet: Set<String> = sections.keys 32 for (specificCategory in Settings.Category.values()) {
116 for (key in keySet) { 33 val categoryHeader = NativeConfig.getConfigHeader(specificCategory.ordinal)
117 val section = sections[key] 34 for (setting in Settings.settingsList) {
118 writeSection(writer, section!!) 35 if (setting.key!!.isEmpty()) continue
36
37 val settingCategoryHeader =
38 NativeConfig.getConfigHeader(setting.category.ordinal)
39 val iniSetting: String? = wini.get(categoryHeader, setting.key)
40 if (iniSetting != null || settingCategoryHeader == categoryHeader) {
41 wini.put(settingCategoryHeader, setting.key, setting.valueAsString)
42 }
43 }
119 } 44 }
120 writer.store() 45 wini.store()
121 } catch (e: IOException) { 46 } catch (e: IOException) {
122 Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message) 47 Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message)
123 view.showToastMessage( 48 val context = YuzuApplication.appContext
124 YuzuApplication.appContext 49 Toast.makeText(
125 .getString(R.string.error_saving, fileName, e.message), 50 context,
126 false 51 context.getString(R.string.error_saving, fileName, e.message),
127 ) 52 Toast.LENGTH_SHORT
128 } 53 ).show()
129 }
130
131 fun saveCustomGameSettings(gameId: String?, sections: HashMap<String, SettingSection?>) {
132 val sortedSections: Set<String> = TreeSet(sections.keys)
133 for (sectionKey in sortedSections) {
134 val section = sections[sectionKey]
135 val settings = section!!.settings
136 val sortedKeySet: Set<String> = TreeSet(settings.keys)
137 for (settingKey in sortedKeySet) {
138 val setting = settings[settingKey]
139 NativeLibrary.setUserSetting(
140 gameId,
141 mapSectionNameFromIni(
142 section.name
143 ),
144 setting!!.key,
145 setting.valueAsString
146 )
147 }
148 }
149 }
150
151 private fun mapSectionNameFromIni(generalSectionName: String): String? {
152 return if (sectionsMap.getForward(generalSectionName) != null) {
153 sectionsMap.getForward(generalSectionName)
154 } else {
155 generalSectionName
156 }
157 }
158
159 private fun mapSectionNameToIni(generalSectionName: String): String {
160 return if (sectionsMap.getBackward(generalSectionName) != null) {
161 sectionsMap.getBackward(generalSectionName).toString()
162 } else {
163 generalSectionName
164 }
165 }
166
167 fun getSettingsFile(fileName: String): File {
168 return File(
169 DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini"
170 )
171 }
172
173 private fun getCustomGameSettingsFile(gameId: String): File {
174 return File(DirectoryInitialization.userDirectory + "/GameSettings/" + gameId + ".ini")
175 }
176
177 private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection {
178 var sectionName: String = line.substring(1, line.length - 1)
179 if (isCustomGame) {
180 sectionName = mapSectionNameToIni(sectionName)
181 } 54 }
182 return SettingSection(sectionName)
183 } 55 }
184 56
185 /** 57 fun getSettingsFile(fileName: String): File =
186 * For a line of text, determines what type of data is being represented, and returns 58 File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
187 * a Setting object containing this data.
188 *
189 * @param line The line of text being parsed.
190 * @return A typed Setting containing the key/value contained in the line.
191 */
192 private fun settingFromLine(line: String): AbstractSetting? {
193 val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
194 if (splitLine.size != 2) {
195 return null
196 }
197 val key = splitLine[0].trim { it <= ' ' }
198 val value = splitLine[1].trim { it <= ' ' }
199 if (value.isEmpty()) {
200 return null
201 }
202
203 val booleanSetting = BooleanSetting.from(key)
204 if (booleanSetting != null) {
205 booleanSetting.boolean = value.toBoolean()
206 return booleanSetting
207 }
208
209 val intSetting = IntSetting.from(key)
210 if (intSetting != null) {
211 intSetting.int = value.toInt()
212 return intSetting
213 }
214
215 val floatSetting = FloatSetting.from(key)
216 if (floatSetting != null) {
217 floatSetting.float = value.toFloat()
218 return floatSetting
219 }
220
221 val stringSetting = StringSetting.from(key)
222 if (stringSetting != null) {
223 stringSetting.string = value
224 return stringSetting
225 }
226
227 return null
228 }
229
230 /**
231 * Writes the contents of a Section HashMap to disk.
232 *
233 * @param parser A Wini pointed at a file on disk.
234 * @param section A section containing settings to be written to the file.
235 */
236 private fun writeSection(parser: Wini, section: SettingSection) {
237 // Write the section header.
238 val header = section.name
239
240 // Write this section's values.
241 val settings = section.settings
242 val keySet: Set<String> = settings.keys
243 for (key in keySet) {
244 val setting = settings[key]
245 parser.put(header, setting!!.key, setting.valueAsString)
246 }
247
248 BooleanSetting.values().forEach {
249 if (!keySet.contains(it.key)) {
250 parser.put(header, it.key, it.valueAsString)
251 }
252 }
253 IntSetting.values().forEach {
254 if (!keySet.contains(it.key)) {
255 parser.put(header, it.key, it.valueAsString)
256 }
257 }
258 StringSetting.values().forEach {
259 if (!keySet.contains(it.key)) {
260 parser.put(header, it.key, it.valueAsString)
261 }
262 }
263 }
264} 59}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 25b9d4018..944ae652e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -7,11 +7,11 @@ import android.annotation.SuppressLint
7import android.app.AlertDialog 7import android.app.AlertDialog
8import android.content.Context 8import android.content.Context
9import android.content.DialogInterface 9import android.content.DialogInterface
10import android.content.Intent
11import android.content.SharedPreferences 10import android.content.SharedPreferences
12import android.content.pm.ActivityInfo 11import android.content.pm.ActivityInfo
13import android.content.res.Configuration 12import android.content.res.Configuration
14import android.graphics.Color 13import android.graphics.Color
14import android.net.Uri
15import android.os.Bundle 15import android.os.Bundle
16import android.os.Handler 16import android.os.Handler
17import android.os.Looper 17import android.os.Looper
@@ -19,18 +19,18 @@ import android.util.Rational
19import android.view.* 19import android.view.*
20import android.widget.TextView 20import android.widget.TextView
21import androidx.activity.OnBackPressedCallback 21import androidx.activity.OnBackPressedCallback
22import androidx.activity.result.ActivityResultLauncher
23import androidx.activity.result.contract.ActivityResultContracts
24import androidx.appcompat.widget.PopupMenu 22import androidx.appcompat.widget.PopupMenu
25import androidx.core.content.res.ResourcesCompat 23import androidx.core.content.res.ResourcesCompat
26import androidx.core.graphics.Insets 24import androidx.core.graphics.Insets
27import androidx.core.view.ViewCompat 25import androidx.core.view.ViewCompat
28import androidx.core.view.WindowInsetsCompat 26import androidx.core.view.WindowInsetsCompat
29import androidx.core.view.isVisible 27import androidx.drawerlayout.widget.DrawerLayout
30import androidx.fragment.app.Fragment 28import androidx.fragment.app.Fragment
29import androidx.fragment.app.activityViewModels
31import androidx.lifecycle.Lifecycle 30import androidx.lifecycle.Lifecycle
32import androidx.lifecycle.lifecycleScope 31import androidx.lifecycle.lifecycleScope
33import androidx.lifecycle.repeatOnLifecycle 32import androidx.lifecycle.repeatOnLifecycle
33import androidx.navigation.findNavController
34import androidx.navigation.fragment.navArgs 34import androidx.navigation.fragment.navArgs
35import androidx.preference.PreferenceManager 35import androidx.preference.PreferenceManager
36import androidx.window.layout.FoldingFeature 36import androidx.window.layout.FoldingFeature
@@ -40,6 +40,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
40import com.google.android.material.slider.Slider 40import com.google.android.material.slider.Slider
41import kotlinx.coroutines.Dispatchers 41import kotlinx.coroutines.Dispatchers
42import kotlinx.coroutines.launch 42import kotlinx.coroutines.launch
43import org.yuzu.yuzu_emu.HomeNavigationDirections
43import org.yuzu.yuzu_emu.NativeLibrary 44import org.yuzu.yuzu_emu.NativeLibrary
44import org.yuzu.yuzu_emu.R 45import org.yuzu.yuzu_emu.R
45import org.yuzu.yuzu_emu.YuzuApplication 46import org.yuzu.yuzu_emu.YuzuApplication
@@ -48,8 +49,9 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
48import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding 49import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
49import org.yuzu.yuzu_emu.features.settings.model.IntSetting 50import org.yuzu.yuzu_emu.features.settings.model.IntSetting
50import org.yuzu.yuzu_emu.features.settings.model.Settings 51import org.yuzu.yuzu_emu.features.settings.model.Settings
51import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
52import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 52import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
53import org.yuzu.yuzu_emu.model.Game
54import org.yuzu.yuzu_emu.model.EmulationViewModel
53import org.yuzu.yuzu_emu.overlay.InputOverlay 55import org.yuzu.yuzu_emu.overlay.InputOverlay
54import org.yuzu.yuzu_emu.utils.* 56import org.yuzu.yuzu_emu.utils.*
55 57
@@ -62,11 +64,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
62 private var _binding: FragmentEmulationBinding? = null 64 private var _binding: FragmentEmulationBinding? = null
63 private val binding get() = _binding!! 65 private val binding get() = _binding!!
64 66
65 val args by navArgs<EmulationFragmentArgs>() 67 private val args by navArgs<EmulationFragmentArgs>()
66 68
67 private var isInFoldableLayout = false 69 private lateinit var game: Game
70
71 private val emulationViewModel: EmulationViewModel by activityViewModels()
68 72
69 private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent> 73 private var isInFoldableLayout = false
70 74
71 override fun onAttach(context: Context) { 75 override fun onAttach(context: Context) {
72 super.onAttach(context) 76 super.onAttach(context)
@@ -81,11 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
81 .collect { updateFoldableLayout(context, it) } 85 .collect { updateFoldableLayout(context, it) }
82 } 86 }
83 } 87 }
84
85 onReturnFromSettings = context.activityResultRegistry.register(
86 "SettingsResult",
87 ActivityResultContracts.StartActivityForResult()
88 ) { updateScreenLayout() }
89 } else { 88 } else {
90 throw IllegalStateException("EmulationFragment must have EmulationActivity parent") 89 throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
91 } 90 }
@@ -97,10 +96,25 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
97 override fun onCreate(savedInstanceState: Bundle?) { 96 override fun onCreate(savedInstanceState: Bundle?) {
98 super.onCreate(savedInstanceState) 97 super.onCreate(savedInstanceState)
99 98
99 val intentUri: Uri? = requireActivity().intent.data
100 var intentGame: Game? = null
101 if (intentUri != null) {
102 intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
103 GameHelper.getGame(requireActivity().intent.data!!, false)
104 } else {
105 null
106 }
107 }
108 game = if (args.game != null) {
109 args.game!!
110 } else {
111 intentGame ?: error("[EmulationFragment] No bootable game present!")
112 }
113
100 // So this fragment doesn't restart on configuration changes; i.e. rotation. 114 // So this fragment doesn't restart on configuration changes; i.e. rotation.
101 retainInstance = true 115 retainInstance = true
102 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 116 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
103 emulationState = EmulationState(args.game.path) 117 emulationState = EmulationState(game.path)
104 } 118 }
105 119
106 /** 120 /**
@@ -120,11 +134,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
120 binding.showFpsText.setTextColor(Color.YELLOW) 134 binding.showFpsText.setTextColor(Color.YELLOW)
121 binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } 135 binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
122 136
123 // Setup overlay. 137 binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
124 updateShowFpsOverlay()
125
126 binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text = 138 binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
127 args.game.title 139 game.title
128 binding.inGameMenu.setNavigationItemSelectedListener { 140 binding.inGameMenu.setNavigationItemSelectedListener {
129 when (it.itemId) { 141 when (it.itemId) {
130 R.id.menu_pause_emulation -> { 142 R.id.menu_pause_emulation -> {
@@ -149,12 +161,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
149 } 161 }
150 162
151 R.id.menu_settings -> { 163 R.id.menu_settings -> {
152 SettingsActivity.launch( 164 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
153 requireContext(), 165 null,
154 onReturnFromSettings, 166 SettingsFile.FILE_NAME_CONFIG
155 SettingsFile.FILE_NAME_CONFIG,
156 ""
157 ) 167 )
168 binding.root.findNavController().navigate(action)
158 true 169 true
159 } 170 }
160 171
@@ -165,7 +176,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
165 176
166 R.id.menu_exit -> { 177 R.id.menu_exit -> {
167 emulationState.stop() 178 emulationState.stop()
168 requireActivity().finish() 179 emulationViewModel.setIsEmulationStopping(true)
180 binding.drawerLayout.close()
181 binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
169 true 182 true
170 } 183 }
171 184
@@ -179,6 +192,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
179 requireActivity(), 192 requireActivity(),
180 object : OnBackPressedCallback(true) { 193 object : OnBackPressedCallback(true) {
181 override fun handleOnBackPressed() { 194 override fun handleOnBackPressed() {
195 if (!NativeLibrary.isRunning()) {
196 return
197 }
198
182 if (binding.drawerLayout.isOpen) { 199 if (binding.drawerLayout.isOpen) {
183 binding.drawerLayout.close() 200 binding.drawerLayout.close()
184 } else { 201 } else {
@@ -195,6 +212,54 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
195 .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } 212 .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
196 } 213 }
197 } 214 }
215
216 GameIconUtils.loadGameIcon(game, binding.loadingImage)
217 binding.loadingTitle.text = game.title
218 binding.loadingTitle.isSelected = true
219 binding.loadingText.isSelected = true
220
221 emulationViewModel.shaderProgress.observe(viewLifecycleOwner) {
222 if (it > 0 && it != emulationViewModel.totalShaders.value!!) {
223 binding.loadingProgressIndicator.isIndeterminate = false
224
225 if (it < binding.loadingProgressIndicator.max) {
226 binding.loadingProgressIndicator.progress = it
227 }
228 }
229
230 if (it == emulationViewModel.totalShaders.value!!) {
231 binding.loadingText.setText(R.string.loading)
232 binding.loadingProgressIndicator.isIndeterminate = true
233 }
234 }
235 emulationViewModel.totalShaders.observe(viewLifecycleOwner) {
236 binding.loadingProgressIndicator.max = it
237 }
238 emulationViewModel.shaderMessage.observe(viewLifecycleOwner) {
239 if (it.isNotEmpty()) {
240 binding.loadingText.text = it
241 }
242 }
243
244 emulationViewModel.emulationStarted.observe(viewLifecycleOwner) { started ->
245 if (started) {
246 binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
247 ViewUtils.showView(binding.surfaceInputOverlay)
248 ViewUtils.hideView(binding.loadingIndicator)
249
250 // Setup overlay
251 updateShowFpsOverlay()
252 }
253 }
254
255 emulationViewModel.isEmulationStopping.observe(viewLifecycleOwner) {
256 if (it) {
257 binding.loadingText.setText(R.string.shutting_down)
258 ViewUtils.showView(binding.loadingIndicator)
259 ViewUtils.hideView(binding.inputContainer)
260 ViewUtils.hideView(binding.showFpsText)
261 }
262 }
198 } 263 }
199 264
200 override fun onConfigurationChanged(newConfig: Configuration) { 265 override fun onConfigurationChanged(newConfig: Configuration) {
@@ -204,11 +269,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
204 binding.drawerLayout.close() 269 binding.drawerLayout.close()
205 } 270 }
206 if (EmulationMenuSettings.showOverlay) { 271 if (EmulationMenuSettings.showOverlay) {
207 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false } 272 binding.surfaceInputOverlay.post {
273 binding.surfaceInputOverlay.visibility = View.VISIBLE
274 }
208 } 275 }
209 } else { 276 } else {
210 if (EmulationMenuSettings.showOverlay) { 277 if (EmulationMenuSettings.showOverlay &&
211 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true } 278 emulationViewModel.emulationStarted.value == true
279 ) {
280 binding.surfaceInputOverlay.post {
281 binding.surfaceInputOverlay.visibility = View.VISIBLE
282 }
283 } else {
284 binding.surfaceInputOverlay.post {
285 binding.surfaceInputOverlay.visibility = View.INVISIBLE
286 }
212 } 287 }
213 if (!isInFoldableLayout) { 288 if (!isInFoldableLayout) {
214 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { 289 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
@@ -217,16 +292,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
217 binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE 292 binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
218 } 293 }
219 } 294 }
220 if (!binding.surfaceInputOverlay.isInEditMode) {
221 refreshInputOverlay()
222 }
223 } 295 }
224 } 296 }
225 297
226 override fun onResume() { 298 override fun onResume() {
227 super.onResume() 299 super.onResume()
228 if (!DirectoryInitialization.areDirectoriesReady) { 300 if (!DirectoryInitialization.areDirectoriesReady) {
229 DirectoryInitialization.start(requireContext()) 301 DirectoryInitialization.start()
230 } 302 }
231 303
232 updateScreenLayout() 304 updateScreenLayout()
@@ -251,10 +323,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
251 super.onDetach() 323 super.onDetach()
252 } 324 }
253 325
254 private fun refreshInputOverlay() {
255 binding.surfaceInputOverlay.refreshControls()
256 }
257
258 private fun resetInputOverlay() { 326 private fun resetInputOverlay() {
259 preferences.edit() 327 preferences.edit()
260 .remove(Settings.PREF_CONTROL_SCALE) 328 .remove(Settings.PREF_CONTROL_SCALE)
@@ -272,17 +340,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
272 val FRAMETIME = 2 340 val FRAMETIME = 2
273 val SPEED = 3 341 val SPEED = 3
274 perfStatsUpdater = { 342 perfStatsUpdater = {
275 val perfStats = NativeLibrary.getPerfStats() 343 if (emulationViewModel.emulationStarted.value == true) {
276 if (perfStats[FPS] > 0 && _binding != null) { 344 val perfStats = NativeLibrary.getPerfStats()
277 binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS]) 345 if (perfStats[FPS] > 0 && _binding != null) {
278 } 346 binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
279 347 }
280 if (!emulationState.isStopped) {
281 perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100) 348 perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
282 } 349 }
283 } 350 }
284 perfStatsUpdateHandler.post(perfStatsUpdater!!) 351 perfStatsUpdateHandler.post(perfStatsUpdater!!)
285 binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
286 binding.showFpsText.visibility = View.VISIBLE 352 binding.showFpsText.visibility = View.VISIBLE
287 } else { 353 } else {
288 if (perfStatsUpdater != null) { 354 if (perfStatsUpdater != null) {
@@ -340,7 +406,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
340 406
341 isInFoldableLayout = true 407 isInFoldableLayout = true
342 binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE 408 binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
343 refreshInputOverlay()
344 } 409 }
345 } 410 }
346 it.isSeparating 411 it.isSeparating
@@ -428,7 +493,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
428 .apply() 493 .apply()
429 } 494 }
430 .setPositiveButton(android.R.string.ok) { _, _ -> 495 .setPositiveButton(android.R.string.ok) { _, _ ->
431 refreshInputOverlay() 496 binding.surfaceInputOverlay.refreshControls()
432 } 497 }
433 .setNegativeButton(android.R.string.cancel, null) 498 .setNegativeButton(android.R.string.cancel, null)
434 .setNeutralButton(R.string.emulation_toggle_all) { _, _ -> } 499 .setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
@@ -452,7 +517,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
452 R.id.menu_show_overlay -> { 517 R.id.menu_show_overlay -> {
453 it.isChecked = !it.isChecked 518 it.isChecked = !it.isChecked
454 EmulationMenuSettings.showOverlay = it.isChecked 519 EmulationMenuSettings.showOverlay = it.isChecked
455 refreshInputOverlay() 520 binding.surfaceInputOverlay.refreshControls()
456 true 521 true
457 } 522 }
458 523
@@ -558,14 +623,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
558 preferences.edit() 623 preferences.edit()
559 .putInt(Settings.PREF_CONTROL_SCALE, scale) 624 .putInt(Settings.PREF_CONTROL_SCALE, scale)
560 .apply() 625 .apply()
561 refreshInputOverlay() 626 binding.surfaceInputOverlay.refreshControls()
562 } 627 }
563 628
564 private fun setControlOpacity(opacity: Int) { 629 private fun setControlOpacity(opacity: Int) {
565 preferences.edit() 630 preferences.edit()
566 .putInt(Settings.PREF_CONTROL_OPACITY, opacity) 631 .putInt(Settings.PREF_CONTROL_OPACITY, opacity)
567 .apply() 632 .apply()
568 refreshInputOverlay() 633 binding.surfaceInputOverlay.refreshControls()
569 } 634 }
570 635
571 private fun setInsets() { 636 private fun setInsets() {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index d5e793491..cbbe14d22 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -25,17 +25,18 @@ import androidx.core.view.updatePadding
25import androidx.documentfile.provider.DocumentFile 25import androidx.documentfile.provider.DocumentFile
26import androidx.fragment.app.Fragment 26import androidx.fragment.app.Fragment
27import androidx.fragment.app.activityViewModels 27import androidx.fragment.app.activityViewModels
28import androidx.navigation.findNavController
28import androidx.navigation.fragment.findNavController 29import androidx.navigation.fragment.findNavController
29import androidx.recyclerview.widget.LinearLayoutManager 30import androidx.recyclerview.widget.LinearLayoutManager
30import com.google.android.material.dialog.MaterialAlertDialogBuilder 31import com.google.android.material.dialog.MaterialAlertDialogBuilder
31import com.google.android.material.transition.MaterialSharedAxis 32import com.google.android.material.transition.MaterialSharedAxis
32import org.yuzu.yuzu_emu.BuildConfig 33import org.yuzu.yuzu_emu.BuildConfig
34import org.yuzu.yuzu_emu.HomeNavigationDirections
33import org.yuzu.yuzu_emu.R 35import org.yuzu.yuzu_emu.R
34import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter 36import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
35import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding 37import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
36import org.yuzu.yuzu_emu.features.DocumentProvider 38import org.yuzu.yuzu_emu.features.DocumentProvider
37import org.yuzu.yuzu_emu.features.settings.model.Settings 39import org.yuzu.yuzu_emu.features.settings.model.Settings
38import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
39import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 40import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
40import org.yuzu.yuzu_emu.model.HomeSetting 41import org.yuzu.yuzu_emu.model.HomeSetting
41import org.yuzu.yuzu_emu.model.HomeViewModel 42import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -74,7 +75,13 @@ class HomeSettingsFragment : Fragment() {
74 R.string.advanced_settings, 75 R.string.advanced_settings,
75 R.string.settings_description, 76 R.string.settings_description,
76 R.drawable.ic_settings, 77 R.drawable.ic_settings,
77 { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } 78 {
79 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
80 null,
81 SettingsFile.FILE_NAME_CONFIG
82 )
83 binding.root.findNavController().navigate(action)
84 }
78 ) 85 )
79 ) 86 )
80 add( 87 add(
@@ -90,7 +97,13 @@ class HomeSettingsFragment : Fragment() {
90 R.string.preferences_theme, 97 R.string.preferences_theme,
91 R.string.theme_and_color_description, 98 R.string.theme_and_color_description,
92 R.drawable.ic_palette, 99 R.drawable.ic_palette,
93 { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") } 100 {
101 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
102 null,
103 Settings.SECTION_THEME
104 )
105 binding.root.findNavController().navigate(action)
106 }
94 ) 107 )
95 ) 108 )
96 add( 109 add(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
index e1495ee8c..f38aeea53 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
@@ -187,8 +187,8 @@ class ImportExportSavesFragment : DialogFragment() {
187 withContext(Dispatchers.Main) { 187 withContext(Dispatchers.Main) {
188 if (!validZip) { 188 if (!validZip) {
189 MessageDialogFragment.newInstance( 189 MessageDialogFragment.newInstance(
190 R.string.save_file_invalid_zip_structure, 190 titleId = R.string.save_file_invalid_zip_structure,
191 R.string.save_file_invalid_zip_structure_description 191 descriptionId = R.string.save_file_invalid_zip_structure_description
192 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) 192 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
193 return@withContext 193 return@withContext
194 } 194 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
index 739b26f99..181bd983a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -34,7 +34,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
34 when (val result = taskViewModel.result.value) { 34 when (val result = taskViewModel.result.value) {
35 is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show() 35 is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
36 is MessageDialogFragment -> result.show( 36 is MessageDialogFragment -> result.show(
37 parentFragmentManager, 37 requireActivity().supportFragmentManager,
38 MessageDialogFragment.TAG 38 MessageDialogFragment.TAG
39 ) 39 )
40 } 40 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt
deleted file mode 100644
index b29b627e9..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt
+++ /dev/null
@@ -1,62 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.Intent
8import android.net.Uri
9import android.os.Bundle
10import androidx.fragment.app.DialogFragment
11import com.google.android.material.dialog.MaterialAlertDialogBuilder
12import org.yuzu.yuzu_emu.R
13
14class LongMessageDialogFragment : DialogFragment() {
15 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
16 val titleId = requireArguments().getInt(TITLE)
17 val description = requireArguments().getString(DESCRIPTION)
18 val helpLinkId = requireArguments().getInt(HELP_LINK)
19
20 val dialog = MaterialAlertDialogBuilder(requireContext())
21 .setPositiveButton(R.string.close, null)
22 .setTitle(titleId)
23 .setMessage(description)
24
25 if (helpLinkId != 0) {
26 dialog.setNeutralButton(R.string.learn_more) { _, _ ->
27 openLink(getString(helpLinkId))
28 }
29 }
30
31 return dialog.show()
32 }
33
34 private fun openLink(link: String) {
35 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
36 startActivity(intent)
37 }
38
39 companion object {
40 const val TAG = "LongMessageDialogFragment"
41
42 private const val TITLE = "Title"
43 private const val DESCRIPTION = "Description"
44 private const val HELP_LINK = "Link"
45
46 fun newInstance(
47 titleId: Int,
48 description: String,
49 helpLinkId: Int = 0
50 ): LongMessageDialogFragment {
51 val dialog = LongMessageDialogFragment()
52 val bundle = Bundle()
53 bundle.apply {
54 putInt(TITLE, titleId)
55 putString(DESCRIPTION, description)
56 putInt(HELP_LINK, helpLinkId)
57 }
58 dialog.arguments = bundle
59 return dialog
60 }
61 }
62}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
index 2db38fdc2..7d1c2c8dd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -13,14 +13,20 @@ import org.yuzu.yuzu_emu.R
13 13
14class MessageDialogFragment : DialogFragment() { 14class MessageDialogFragment : DialogFragment() {
15 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 15 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
16 val titleId = requireArguments().getInt(TITLE) 16 val titleId = requireArguments().getInt(TITLE_ID)
17 val descriptionId = requireArguments().getInt(DESCRIPTION) 17 val titleString = requireArguments().getString(TITLE_STRING)!!
18 val descriptionId = requireArguments().getInt(DESCRIPTION_ID)
19 val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
18 val helpLinkId = requireArguments().getInt(HELP_LINK) 20 val helpLinkId = requireArguments().getInt(HELP_LINK)
19 21
20 val dialog = MaterialAlertDialogBuilder(requireContext()) 22 val dialog = MaterialAlertDialogBuilder(requireContext())
21 .setPositiveButton(R.string.close, null) 23 .setPositiveButton(R.string.close, null)
22 .setTitle(titleId) 24
23 .setMessage(descriptionId) 25 if (titleId != 0) dialog.setTitle(titleId)
26 if (titleString.isNotEmpty()) dialog.setTitle(titleString)
27
28 if (descriptionId != 0) dialog.setMessage(descriptionId)
29 if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
24 30
25 if (helpLinkId != 0) { 31 if (helpLinkId != 0) {
26 dialog.setNeutralButton(R.string.learn_more) { _, _ -> 32 dialog.setNeutralButton(R.string.learn_more) { _, _ ->
@@ -39,20 +45,26 @@ class MessageDialogFragment : DialogFragment() {
39 companion object { 45 companion object {
40 const val TAG = "MessageDialogFragment" 46 const val TAG = "MessageDialogFragment"
41 47
42 private const val TITLE = "Title" 48 private const val TITLE_ID = "Title"
43 private const val DESCRIPTION = "Description" 49 private const val TITLE_STRING = "TitleString"
50 private const val DESCRIPTION_ID = "DescriptionId"
51 private const val DESCRIPTION_STRING = "DescriptionString"
44 private const val HELP_LINK = "Link" 52 private const val HELP_LINK = "Link"
45 53
46 fun newInstance( 54 fun newInstance(
47 titleId: Int, 55 titleId: Int = 0,
48 descriptionId: Int, 56 titleString: String = "",
57 descriptionId: Int = 0,
58 descriptionString: String = "",
49 helpLinkId: Int = 0 59 helpLinkId: Int = 0
50 ): MessageDialogFragment { 60 ): MessageDialogFragment {
51 val dialog = MessageDialogFragment() 61 val dialog = MessageDialogFragment()
52 val bundle = Bundle() 62 val bundle = Bundle()
53 bundle.apply { 63 bundle.apply {
54 putInt(TITLE, titleId) 64 putInt(TITLE_ID, titleId)
55 putInt(DESCRIPTION, descriptionId) 65 putString(TITLE_STRING, titleString)
66 putInt(DESCRIPTION_ID, descriptionId)
67 putString(DESCRIPTION_STRING, descriptionString)
56 putInt(HELP_LINK, helpLinkId) 68 putInt(HELP_LINK, helpLinkId)
57 } 69 }
58 dialog.arguments = bundle 70 dialog.arguments = bundle
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
new file mode 100644
index 000000000..d18ec6974
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
@@ -0,0 +1,235 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import androidx.fragment.app.DialogFragment
13import androidx.fragment.app.activityViewModels
14import androidx.lifecycle.Lifecycle
15import androidx.lifecycle.lifecycleScope
16import androidx.lifecycle.repeatOnLifecycle
17import com.google.android.material.dialog.MaterialAlertDialogBuilder
18import com.google.android.material.slider.Slider
19import kotlinx.coroutines.launch
20import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
22import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
23import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
24import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
25import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
26import org.yuzu.yuzu_emu.model.SettingsViewModel
27
28class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
29 private var type = 0
30 private var position = 0
31
32 private var defaultCancelListener =
33 DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
34
35 private val settingsViewModel: SettingsViewModel by activityViewModels()
36
37 private lateinit var sliderBinding: DialogSliderBinding
38
39 override fun onCreate(savedInstanceState: Bundle?) {
40 super.onCreate(savedInstanceState)
41 type = requireArguments().getInt(TYPE)
42 position = requireArguments().getInt(POSITION)
43
44 if (settingsViewModel.clickedItem == null) dismiss()
45 }
46
47 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
48 return when (type) {
49 TYPE_RESET_SETTING -> {
50 MaterialAlertDialogBuilder(requireContext())
51 .setMessage(R.string.reset_setting_confirmation)
52 .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
53 settingsViewModel.clickedItem!!.setting.reset()
54 settingsViewModel.setAdapterItemChanged(position)
55 settingsViewModel.shouldSave = true
56 }
57 .setNegativeButton(android.R.string.cancel, null)
58 .create()
59 }
60
61 SettingsItem.TYPE_SINGLE_CHOICE -> {
62 val item = settingsViewModel.clickedItem as SingleChoiceSetting
63 val value = getSelectionForSingleChoiceValue(item)
64 MaterialAlertDialogBuilder(requireContext())
65 .setTitle(item.nameId)
66 .setSingleChoiceItems(item.choicesId, value, this)
67 .create()
68 }
69
70 SettingsItem.TYPE_SLIDER -> {
71 sliderBinding = DialogSliderBinding.inflate(layoutInflater)
72 val item = settingsViewModel.clickedItem as SliderSetting
73
74 settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
75 sliderBinding.slider.apply {
76 valueFrom = item.min.toFloat()
77 valueTo = item.max.toFloat()
78 value = settingsViewModel.sliderProgress.value.toFloat()
79 addOnChangeListener { _: Slider, value: Float, _: Boolean ->
80 settingsViewModel.setSliderTextValue(value, item.units)
81 }
82 }
83
84 MaterialAlertDialogBuilder(requireContext())
85 .setTitle(item.nameId)
86 .setView(sliderBinding.root)
87 .setPositiveButton(android.R.string.ok, this)
88 .setNegativeButton(android.R.string.cancel, defaultCancelListener)
89 .create()
90 }
91
92 SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
93 val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
94 MaterialAlertDialogBuilder(requireContext())
95 .setTitle(item.nameId)
96 .setSingleChoiceItems(item.choices, item.selectValueIndex, this)
97 .create()
98 }
99
100 else -> super.onCreateDialog(savedInstanceState)
101 }
102 }
103
104 override fun onCreateView(
105 inflater: LayoutInflater,
106 container: ViewGroup?,
107 savedInstanceState: Bundle?
108 ): View? {
109 return when (type) {
110 SettingsItem.TYPE_SLIDER -> sliderBinding.root
111 else -> super.onCreateView(inflater, container, savedInstanceState)
112 }
113 }
114
115 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
116 super.onViewCreated(view, savedInstanceState)
117 when (type) {
118 SettingsItem.TYPE_SLIDER -> {
119 viewLifecycleOwner.lifecycleScope.launch {
120 repeatOnLifecycle(Lifecycle.State.CREATED) {
121 settingsViewModel.sliderTextValue.collect {
122 sliderBinding.textValue.text = it
123 }
124 }
125 repeatOnLifecycle(Lifecycle.State.CREATED) {
126 settingsViewModel.sliderProgress.collect {
127 sliderBinding.slider.value = it.toFloat()
128 }
129 }
130 }
131 }
132 }
133 }
134
135 override fun onClick(dialog: DialogInterface, which: Int) {
136 when (settingsViewModel.clickedItem) {
137 is SingleChoiceSetting -> {
138 val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
139 val value = getValueForSingleChoiceSelection(scSetting, which)
140 if (scSetting.selectedValue != value) {
141 settingsViewModel.shouldSave = true
142 }
143 scSetting.selectedValue = value
144 }
145
146 is StringSingleChoiceSetting -> {
147 val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
148 val value = scSetting.getValueAt(which)
149 if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
150 scSetting.selectedValue = value
151 }
152
153 is SliderSetting -> {
154 val sliderSetting = settingsViewModel.clickedItem as SliderSetting
155 if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
156 settingsViewModel.shouldSave = true
157 }
158 sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
159 }
160 }
161 closeDialog()
162 }
163
164 private fun closeDialog() {
165 settingsViewModel.setAdapterItemChanged(position)
166 settingsViewModel.clickedItem = null
167 settingsViewModel.setSliderProgress(-1f)
168 dismiss()
169 }
170
171 private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
172 val valuesId = item.valuesId
173 return if (valuesId > 0) {
174 val valuesArray = requireContext().resources.getIntArray(valuesId)
175 valuesArray[which]
176 } else {
177 which
178 }
179 }
180
181 private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
182 val value = item.selectedValue
183 val valuesId = item.valuesId
184 if (valuesId > 0) {
185 val valuesArray = requireContext().resources.getIntArray(valuesId)
186 for (index in valuesArray.indices) {
187 val current = valuesArray[index]
188 if (current == value) {
189 return index
190 }
191 }
192 } else {
193 return value
194 }
195 return -1
196 }
197
198 companion object {
199 const val TAG = "SettingsDialogFragment"
200
201 const val TYPE_RESET_SETTING = -1
202
203 const val TITLE = "Title"
204 const val TYPE = "Type"
205 const val POSITION = "Position"
206
207 fun newInstance(
208 settingsViewModel: SettingsViewModel,
209 clickedItem: SettingsItem,
210 type: Int,
211 position: Int
212 ): SettingsDialogFragment {
213 when (type) {
214 SettingsItem.TYPE_HEADER,
215 SettingsItem.TYPE_SWITCH,
216 SettingsItem.TYPE_SUBMENU,
217 SettingsItem.TYPE_DATETIME_SETTING,
218 SettingsItem.TYPE_RUNNABLE ->
219 throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
220
221 SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
222 (clickedItem as SliderSetting).selectedValue.toFloat()
223 )
224 }
225 settingsViewModel.clickedItem = clickedItem
226
227 val args = Bundle()
228 args.putInt(TYPE, type)
229 args.putInt(POSITION, position)
230 val fragment = SettingsDialogFragment()
231 fragment.arguments = args
232 return fragment
233 }
234 }
235}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt
new file mode 100644
index 000000000..55b6a0367
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt
@@ -0,0 +1,184 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.Context
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import android.view.inputmethod.InputMethodManager
12import androidx.core.view.ViewCompat
13import androidx.core.view.WindowInsetsCompat
14import androidx.core.view.updatePadding
15import androidx.core.widget.doOnTextChanged
16import androidx.fragment.app.Fragment
17import androidx.fragment.app.activityViewModels
18import androidx.recyclerview.widget.LinearLayoutManager
19import com.google.android.material.divider.MaterialDividerItemDecoration
20import com.google.android.material.transition.MaterialSharedAxis
21import info.debatty.java.stringsimilarity.Cosine
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
24import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
25import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
26import org.yuzu.yuzu_emu.model.SettingsViewModel
27import org.yuzu.yuzu_emu.utils.NativeConfig
28
29class SettingsSearchFragment : Fragment() {
30 private var _binding: FragmentSettingsSearchBinding? = null
31 private val binding get() = _binding!!
32
33 private var settingsAdapter: SettingsAdapter? = null
34
35 private val settingsViewModel: SettingsViewModel by activityViewModels()
36
37 override fun onCreate(savedInstanceState: Bundle?) {
38 super.onCreate(savedInstanceState)
39 enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
40 returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
41 }
42
43 override fun onCreateView(
44 inflater: LayoutInflater,
45 container: ViewGroup?,
46 savedInstanceState: Bundle?
47 ): View {
48 _binding = FragmentSettingsSearchBinding.inflate(layoutInflater)
49 return binding.root
50 }
51
52 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
53 super.onViewCreated(view, savedInstanceState)
54 settingsViewModel.setIsUsingSearch(true)
55
56 if (savedInstanceState != null) {
57 binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
58 }
59
60 settingsAdapter = SettingsAdapter(this, requireContext())
61
62 val dividerDecoration = MaterialDividerItemDecoration(
63 requireContext(),
64 LinearLayoutManager.VERTICAL
65 )
66 dividerDecoration.isLastItemDecorated = false
67 binding.settingsList.apply {
68 adapter = settingsAdapter
69 layoutManager = LinearLayoutManager(requireContext())
70 addItemDecoration(dividerDecoration)
71 }
72
73 focusSearch()
74
75 binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) }
76 binding.searchBackground.setOnClickListener { focusSearch() }
77 binding.clearButton.setOnClickListener { binding.searchText.setText("") }
78 binding.searchText.doOnTextChanged { _, _, _, _ ->
79 search()
80 binding.settingsList.smoothScrollToPosition(0)
81 }
82 settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) {
83 if (it) {
84 settingsViewModel.setShouldReloadSettingsList(false)
85 search()
86 }
87 }
88
89 search()
90
91 setInsets()
92 }
93
94 override fun onSaveInstanceState(outState: Bundle) {
95 super.onSaveInstanceState(outState)
96 outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
97 }
98
99 private fun search() {
100 val searchTerm = binding.searchText.text.toString().lowercase()
101 binding.clearButton.visibility =
102 if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE
103 if (searchTerm.isEmpty()) {
104 binding.noResultsView.visibility = View.VISIBLE
105 settingsAdapter?.submitList(emptyList())
106 return
107 }
108
109 val baseList = SettingsItem.settingsItems
110 val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
111 val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
112 val title = getString(item.value.nameId).lowercase()
113 val similarity = similarityAlgorithm.similarity(searchTerm, title)
114 if (similarity > 0.08) {
115 Pair(similarity, item)
116 } else {
117 null
118 }
119 }.sortedByDescending { it.first }.mapNotNull {
120 val item = it.second.value
121 val pairedSettingKey = item.setting.pairedSettingKey
122 val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) {
123 val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
124 if (pairedSettingValue) it.second.value else null
125 } else {
126 it.second.value
127 }
128 optionalSetting
129 }
130 settingsAdapter?.submitList(sortedList)
131 binding.noResultsView.visibility =
132 if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE
133 }
134
135 private fun focusSearch() {
136 binding.searchText.requestFocus()
137 val imm = requireActivity()
138 .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
139 imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
140 }
141
142 private fun setInsets() =
143 ViewCompat.setOnApplyWindowInsetsListener(
144 binding.root
145 ) { _: View, windowInsets: WindowInsetsCompat ->
146 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
147 val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
148 val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip)
149
150 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
151 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
152
153 val leftInsets = barInsets.left + cutoutInsets.left
154 val rightInsets = barInsets.right + cutoutInsets.right
155
156 binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing)
157 binding.frameSearch.updatePadding(
158 left = leftInsets + sideMargin,
159 top = barInsets.top + topMargin,
160 right = rightInsets + sideMargin
161 )
162 binding.noResultsView.updatePadding(
163 left = leftInsets,
164 right = rightInsets,
165 bottom = barInsets.bottom
166 )
167
168 val mlpSettingsList = binding.settingsList.layoutParams as ViewGroup.MarginLayoutParams
169 mlpSettingsList.leftMargin = leftInsets + sideMargin
170 mlpSettingsList.rightMargin = rightInsets + sideMargin
171 binding.settingsList.layoutParams = mlpSettingsList
172
173 val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
174 mlpDivider.leftMargin = leftInsets + sideMargin
175 mlpDivider.rightMargin = rightInsets + sideMargin
176 binding.divider.layoutParams = mlpDivider
177
178 windowInsets
179 }
180
181 companion object {
182 const val SEARCH_TEXT = "SearchText"
183 }
184}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt
new file mode 100644
index 000000000..e35f51bc3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt
@@ -0,0 +1,59 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.lifecycle.LiveData
7import androidx.lifecycle.MutableLiveData
8import androidx.lifecycle.ViewModel
9
10class EmulationViewModel : ViewModel() {
11 private val _emulationStarted = MutableLiveData(false)
12 val emulationStarted: LiveData<Boolean> get() = _emulationStarted
13
14 private val _isEmulationStopping = MutableLiveData(false)
15 val isEmulationStopping: LiveData<Boolean> get() = _isEmulationStopping
16
17 private val _shaderProgress = MutableLiveData(0)
18 val shaderProgress: LiveData<Int> get() = _shaderProgress
19
20 private val _totalShaders = MutableLiveData(0)
21 val totalShaders: LiveData<Int> get() = _totalShaders
22
23 private val _shaderMessage = MutableLiveData("")
24 val shaderMessage: LiveData<String> get() = _shaderMessage
25
26 fun setEmulationStarted(started: Boolean) {
27 _emulationStarted.postValue(started)
28 }
29
30 fun setIsEmulationStopping(value: Boolean) {
31 _isEmulationStopping.value = value
32 }
33
34 fun setShaderProgress(progress: Int) {
35 _shaderProgress.value = progress
36 }
37
38 fun setTotalShaders(max: Int) {
39 _totalShaders.value = max
40 }
41
42 fun setShaderMessage(msg: String) {
43 _shaderMessage.value = msg
44 }
45
46 fun updateProgress(msg: String, progress: Int, max: Int) {
47 setShaderMessage(msg)
48 setShaderProgress(progress)
49 setTotalShaders(max)
50 }
51
52 fun clear() {
53 _emulationStarted.value = false
54 _isEmulationStopping.value = false
55 _shaderProgress.value = 0
56 _totalShaders.value = 0
57 _shaderMessage.value = ""
58 }
59}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
new file mode 100644
index 000000000..d16d15fa6
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
@@ -0,0 +1,96 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.lifecycle.LiveData
7import androidx.lifecycle.MutableLiveData
8import androidx.lifecycle.SavedStateHandle
9import androidx.lifecycle.ViewModel
10import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.YuzuApplication
12import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
13
14class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
15 var game: Game? = null
16
17 var shouldSave = false
18
19 var clickedItem: SettingsItem? = null
20
21 private val _toolbarTitle = MutableLiveData("")
22 val toolbarTitle: LiveData<String> get() = _toolbarTitle
23
24 private val _shouldRecreate = MutableLiveData(false)
25 val shouldRecreate: LiveData<Boolean> get() = _shouldRecreate
26
27 private val _shouldNavigateBack = MutableLiveData(false)
28 val shouldNavigateBack: LiveData<Boolean> get() = _shouldNavigateBack
29
30 private val _shouldShowResetSettingsDialog = MutableLiveData(false)
31 val shouldShowResetSettingsDialog: LiveData<Boolean> get() = _shouldShowResetSettingsDialog
32
33 private val _shouldReloadSettingsList = MutableLiveData(false)
34 val shouldReloadSettingsList: LiveData<Boolean> get() = _shouldReloadSettingsList
35
36 private val _isUsingSearch = MutableLiveData(false)
37 val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch
38
39 val sliderProgress = savedStateHandle.getStateFlow(KEY_SLIDER_PROGRESS, -1)
40
41 val sliderTextValue = savedStateHandle.getStateFlow(KEY_SLIDER_TEXT_VALUE, "")
42
43 val adapterItemChanged = savedStateHandle.getStateFlow(KEY_ADAPTER_ITEM_CHANGED, -1)
44
45 fun setToolbarTitle(value: String) {
46 _toolbarTitle.value = value
47 }
48
49 fun setShouldRecreate(value: Boolean) {
50 _shouldRecreate.value = value
51 }
52
53 fun setShouldNavigateBack(value: Boolean) {
54 _shouldNavigateBack.value = value
55 }
56
57 fun setShouldShowResetSettingsDialog(value: Boolean) {
58 _shouldShowResetSettingsDialog.value = value
59 }
60
61 fun setShouldReloadSettingsList(value: Boolean) {
62 _shouldReloadSettingsList.value = value
63 }
64
65 fun setIsUsingSearch(value: Boolean) {
66 _isUsingSearch.value = value
67 }
68
69 fun setSliderTextValue(value: Float, units: String) {
70 savedStateHandle[KEY_SLIDER_PROGRESS] = value
71 savedStateHandle[KEY_SLIDER_TEXT_VALUE] = String.format(
72 YuzuApplication.appContext.getString(R.string.value_with_units),
73 value.toInt().toString(),
74 units
75 )
76 }
77
78 fun setSliderProgress(value: Float) {
79 savedStateHandle[KEY_SLIDER_PROGRESS] = value
80 }
81
82 fun setAdapterItemChanged(value: Int) {
83 savedStateHandle[KEY_ADAPTER_ITEM_CHANGED] = value
84 }
85
86 fun clear() {
87 game = null
88 shouldSave = false
89 }
90
91 companion object {
92 const val KEY_SLIDER_TEXT_VALUE = "SliderTextValue"
93 const val KEY_SLIDER_PROGRESS = "SliderProgress"
94 const val KEY_ADAPTER_ITEM_CHANGED = "AdapterItemChanged"
95 }
96}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index aaf3a0ec1..7d8e06ad8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -33,17 +33,15 @@ import java.io.IOException
33import kotlinx.coroutines.Dispatchers 33import kotlinx.coroutines.Dispatchers
34import kotlinx.coroutines.launch 34import kotlinx.coroutines.launch
35import kotlinx.coroutines.withContext 35import kotlinx.coroutines.withContext
36import org.yuzu.yuzu_emu.HomeNavigationDirections
36import org.yuzu.yuzu_emu.NativeLibrary 37import org.yuzu.yuzu_emu.NativeLibrary
37import org.yuzu.yuzu_emu.R 38import org.yuzu.yuzu_emu.R
38import org.yuzu.yuzu_emu.activities.EmulationActivity 39import org.yuzu.yuzu_emu.activities.EmulationActivity
39import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 40import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
40import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 41import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
41import org.yuzu.yuzu_emu.features.settings.model.Settings 42import org.yuzu.yuzu_emu.features.settings.model.Settings
42import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
43import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
44import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 43import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
45import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment 44import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
46import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment
47import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 45import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
48import org.yuzu.yuzu_emu.model.GamesViewModel 46import org.yuzu.yuzu_emu.model.GamesViewModel
49import org.yuzu.yuzu_emu.model.HomeViewModel 47import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -54,7 +52,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
54 52
55 private val homeViewModel: HomeViewModel by viewModels() 53 private val homeViewModel: HomeViewModel by viewModels()
56 private val gamesViewModel: GamesViewModel by viewModels() 54 private val gamesViewModel: GamesViewModel by viewModels()
57 private val settingsViewModel: SettingsViewModel by viewModels()
58 55
59 override var themeId: Int = 0 56 override var themeId: Int = 0
60 57
@@ -62,8 +59,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
62 val splashScreen = installSplashScreen() 59 val splashScreen = installSplashScreen()
63 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } 60 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
64 61
65 settingsViewModel.settings.loadSettings()
66
67 ThemeHelper.setTheme(this) 62 ThemeHelper.setTheme(this)
68 63
69 super.onCreate(savedInstanceState) 64 super.onCreate(savedInstanceState)
@@ -109,11 +104,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
109 when (it.itemId) { 104 when (it.itemId) {
110 R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true) 105 R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
111 R.id.searchFragment -> gamesViewModel.setSearchFocused(true) 106 R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
112 R.id.homeSettingsFragment -> SettingsActivity.launch( 107 R.id.homeSettingsFragment -> {
113 this, 108 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
114 SettingsFile.FILE_NAME_CONFIG, 109 null,
115 "" 110 SettingsFile.FILE_NAME_CONFIG
116 ) 111 )
112 navHostFragment.navController.navigate(action)
113 }
117 } 114 }
118 } 115 }
119 116
@@ -303,8 +300,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
303 fun processKey(result: Uri): Boolean { 300 fun processKey(result: Uri): Boolean {
304 if (FileUtil.getExtension(result) != "keys") { 301 if (FileUtil.getExtension(result) != "keys") {
305 MessageDialogFragment.newInstance( 302 MessageDialogFragment.newInstance(
306 R.string.reading_keys_failure, 303 titleId = R.string.reading_keys_failure,
307 R.string.install_prod_keys_failure_extension_description 304 descriptionId = R.string.install_prod_keys_failure_extension_description
308 ).show(supportFragmentManager, MessageDialogFragment.TAG) 305 ).show(supportFragmentManager, MessageDialogFragment.TAG)
309 return false 306 return false
310 } 307 }
@@ -332,9 +329,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
332 return true 329 return true
333 } else { 330 } else {
334 MessageDialogFragment.newInstance( 331 MessageDialogFragment.newInstance(
335 R.string.invalid_keys_error, 332 titleId = R.string.invalid_keys_error,
336 R.string.install_keys_failure_description, 333 descriptionId = R.string.install_keys_failure_description,
337 R.string.dumping_keys_quickstart_link 334 helpLinkId = R.string.dumping_keys_quickstart_link
338 ).show(supportFragmentManager, MessageDialogFragment.TAG) 335 ).show(supportFragmentManager, MessageDialogFragment.TAG)
339 return false 336 return false
340 } 337 }
@@ -372,8 +369,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
372 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 369 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
373 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { 370 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
374 MessageDialogFragment.newInstance( 371 MessageDialogFragment.newInstance(
375 R.string.firmware_installed_failure, 372 titleId = R.string.firmware_installed_failure,
376 R.string.firmware_installed_failure_description 373 descriptionId = R.string.firmware_installed_failure_description
377 ) 374 )
378 } else { 375 } else {
379 firmwarePath.deleteRecursively() 376 firmwarePath.deleteRecursively()
@@ -403,8 +400,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
403 400
404 if (FileUtil.getExtension(result) != "bin") { 401 if (FileUtil.getExtension(result) != "bin") {
405 MessageDialogFragment.newInstance( 402 MessageDialogFragment.newInstance(
406 R.string.reading_keys_failure, 403 titleId = R.string.reading_keys_failure,
407 R.string.install_amiibo_keys_failure_extension_description 404 descriptionId = R.string.install_amiibo_keys_failure_extension_description
408 ).show(supportFragmentManager, MessageDialogFragment.TAG) 405 ).show(supportFragmentManager, MessageDialogFragment.TAG)
409 return@registerForActivityResult 406 return@registerForActivityResult
410 } 407 }
@@ -430,9 +427,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
430 ).show() 427 ).show()
431 } else { 428 } else {
432 MessageDialogFragment.newInstance( 429 MessageDialogFragment.newInstance(
433 R.string.invalid_keys_error, 430 titleId = R.string.invalid_keys_error,
434 R.string.install_keys_failure_description, 431 descriptionId = R.string.install_keys_failure_description,
435 R.string.dumping_keys_quickstart_link 432 helpLinkId = R.string.dumping_keys_quickstart_link
436 ).show(supportFragmentManager, MessageDialogFragment.TAG) 433 ).show(supportFragmentManager, MessageDialogFragment.TAG)
437 } 434 }
438 } 435 }
@@ -504,96 +501,91 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
504 var errorBaseGame = 0 501 var errorBaseGame = 0
505 var errorExtension = 0 502 var errorExtension = 0
506 var errorOther = 0 503 var errorOther = 0
507 var errorTotal = 0 504 documents.forEach {
508 lifecycleScope.launch { 505 when (NativeLibrary.installFileToNand(it.toString())) {
509 documents.forEach { 506 NativeLibrary.InstallFileToNandResult.Success -> {
510 when (NativeLibrary.installFileToNand(it.toString())) { 507 installSuccess += 1
511 NativeLibrary.InstallFileToNandResult.Success -> {
512 installSuccess += 1
513 }
514
515 NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
516 installOverwrite += 1
517 }
518
519 NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
520 errorBaseGame += 1
521 }
522
523 NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
524 errorExtension += 1
525 }
526
527 else -> {
528 errorOther += 1
529 }
530 } 508 }
531 } 509
532 withContext(Dispatchers.Main) { 510 NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
533 val separator = System.getProperty("line.separator") ?: "\n" 511 installOverwrite += 1
534 val installResult = StringBuilder()
535 if (installSuccess > 0) {
536 installResult.append(
537 getString(
538 R.string.install_game_content_success_install,
539 installSuccess
540 )
541 )
542 installResult.append(separator)
543 } 512 }
544 if (installOverwrite > 0) { 513
545 installResult.append( 514 NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
546 getString( 515 errorBaseGame += 1
547 R.string.install_game_content_success_overwrite,
548 installOverwrite
549 )
550 )
551 installResult.append(separator)
552 } 516 }
553 errorTotal = errorBaseGame + errorExtension + errorOther 517
554 if (errorTotal > 0) { 518 NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
555 installResult.append(separator) 519 errorExtension += 1
556 installResult.append( 520 }
557 getString( 521
558 R.string.install_game_content_failed_count, 522 else -> {
559 errorTotal 523 errorOther += 1
560 )
561 )
562 installResult.append(separator)
563 if (errorBaseGame > 0) {
564 installResult.append(separator)
565 installResult.append(
566 getString(R.string.install_game_content_failure_base)
567 )
568 installResult.append(separator)
569 }
570 if (errorExtension > 0) {
571 installResult.append(separator)
572 installResult.append(
573 getString(R.string.install_game_content_failure_file_extension)
574 )
575 installResult.append(separator)
576 }
577 if (errorOther > 0) {
578 installResult.append(
579 getString(R.string.install_game_content_failure_description)
580 )
581 installResult.append(separator)
582 }
583 LongMessageDialogFragment.newInstance(
584 R.string.install_game_content_failure,
585 installResult.toString().trim(),
586 R.string.install_game_content_help_link
587 ).show(supportFragmentManager, LongMessageDialogFragment.TAG)
588 } else {
589 LongMessageDialogFragment.newInstance(
590 R.string.install_game_content_success,
591 installResult.toString().trim()
592 ).show(supportFragmentManager, LongMessageDialogFragment.TAG)
593 } 524 }
594 } 525 }
595 } 526 }
596 return@newInstance installSuccess + installOverwrite + errorTotal 527
528 val separator = System.getProperty("line.separator") ?: "\n"
529 val installResult = StringBuilder()
530 if (installSuccess > 0) {
531 installResult.append(
532 getString(
533 R.string.install_game_content_success_install,
534 installSuccess
535 )
536 )
537 installResult.append(separator)
538 }
539 if (installOverwrite > 0) {
540 installResult.append(
541 getString(
542 R.string.install_game_content_success_overwrite,
543 installOverwrite
544 )
545 )
546 installResult.append(separator)
547 }
548 val errorTotal: Int = errorBaseGame + errorExtension + errorOther
549 if (errorTotal > 0) {
550 installResult.append(separator)
551 installResult.append(
552 getString(
553 R.string.install_game_content_failed_count,
554 errorTotal
555 )
556 )
557 installResult.append(separator)
558 if (errorBaseGame > 0) {
559 installResult.append(separator)
560 installResult.append(
561 getString(R.string.install_game_content_failure_base)
562 )
563 installResult.append(separator)
564 }
565 if (errorExtension > 0) {
566 installResult.append(separator)
567 installResult.append(
568 getString(R.string.install_game_content_failure_file_extension)
569 )
570 installResult.append(separator)
571 }
572 if (errorOther > 0) {
573 installResult.append(
574 getString(R.string.install_game_content_failure_description)
575 )
576 installResult.append(separator)
577 }
578 return@newInstance MessageDialogFragment.newInstance(
579 titleId = R.string.install_game_content_failure,
580 descriptionString = installResult.toString().trim(),
581 helpLinkId = R.string.install_game_content_help_link
582 )
583 } else {
584 return@newInstance MessageDialogFragment.newInstance(
585 titleId = R.string.install_game_content_success,
586 descriptionString = installResult.toString().trim()
587 )
588 }
597 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 589 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
598 } 590 }
599 } 591 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt
deleted file mode 100644
index 9cfda74ee..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt
+++ /dev/null
@@ -1,25 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6class BiMap<K, V> {
7 private val forward: MutableMap<K, V> = HashMap()
8 private val backward: MutableMap<V, K> = HashMap()
9
10 @Synchronized
11 fun add(key: K, value: V) {
12 forward[key] = value
13 backward[value] = key
14 }
15
16 @Synchronized
17 fun getForward(key: K): V? {
18 return forward[key]
19 }
20
21 @Synchronized
22 fun getBackward(key: V): K? {
23 return backward[key]
24 }
25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
index 2ee63697e..3c9f6bad0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -3,18 +3,18 @@
3 3
4package org.yuzu.yuzu_emu.utils 4package org.yuzu.yuzu_emu.utils
5 5
6import android.content.Context
7import java.io.IOException 6import java.io.IOException
8import org.yuzu.yuzu_emu.NativeLibrary 7import org.yuzu.yuzu_emu.NativeLibrary
8import org.yuzu.yuzu_emu.YuzuApplication
9 9
10object DirectoryInitialization { 10object DirectoryInitialization {
11 private var userPath: String? = null 11 private var userPath: String? = null
12 12
13 var areDirectoriesReady: Boolean = false 13 var areDirectoriesReady: Boolean = false
14 14
15 fun start(context: Context) { 15 fun start() {
16 if (!areDirectoriesReady) { 16 if (!areDirectoriesReady) {
17 initializeInternalStorage(context) 17 initializeInternalStorage()
18 NativeLibrary.initializeEmulation() 18 NativeLibrary.initializeEmulation()
19 areDirectoriesReady = true 19 areDirectoriesReady = true
20 } 20 }
@@ -26,9 +26,9 @@ object DirectoryInitialization {
26 return userPath 26 return userPath
27 } 27 }
28 28
29 private fun initializeInternalStorage(context: Context) { 29 private fun initializeInternalStorage() {
30 try { 30 try {
31 userPath = context.getExternalFilesDir(null)!!.canonicalPath 31 userPath = YuzuApplication.appContext.getExternalFilesDir(null)!!.canonicalPath
32 NativeLibrary.setAppDirectory(userPath!!) 32 NativeLibrary.setAppDirectory(userPath!!)
33 } catch (e: IOException) { 33 } catch (e: IOException) {
34 e.printStackTrace() 34 e.printStackTrace()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index f71d0a098..e0ee29c9b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -63,13 +63,13 @@ object GameHelper {
63 ) 63 )
64 } else { 64 } else {
65 if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { 65 if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
66 games.add(getGame(it.uri)) 66 games.add(getGame(it.uri, true))
67 } 67 }
68 } 68 }
69 } 69 }
70 } 70 }
71 71
72 private fun getGame(uri: Uri): Game { 72 fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
73 val filePath = uri.toString() 73 val filePath = uri.toString()
74 var name = NativeLibrary.getTitle(filePath) 74 var name = NativeLibrary.getTitle(filePath)
75 75
@@ -94,11 +94,13 @@ object GameHelper {
94 NativeLibrary.isHomebrew(filePath) 94 NativeLibrary.isHomebrew(filePath)
95 ) 95 )
96 96
97 val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L) 97 if (addedToLibrary) {
98 if (addedTime == 0L) { 98 val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
99 preferences.edit() 99 if (addedTime == 0L) {
100 .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis()) 100 preferences.edit()
101 .apply() 101 .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
102 .apply()
103 }
102 } 104 }
103 105
104 return newGame 106 return newGame
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt
new file mode 100644
index 000000000..c0fe596d7
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt
@@ -0,0 +1,77 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6import android.graphics.Bitmap
7import android.graphics.BitmapFactory
8import android.widget.ImageView
9import androidx.core.graphics.drawable.toDrawable
10import coil.ImageLoader
11import coil.decode.DataSource
12import coil.fetch.DrawableResult
13import coil.fetch.FetchResult
14import coil.fetch.Fetcher
15import coil.key.Keyer
16import coil.memory.MemoryCache
17import coil.request.ImageRequest
18import coil.request.Options
19import org.yuzu.yuzu_emu.NativeLibrary
20import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.YuzuApplication
22import org.yuzu.yuzu_emu.model.Game
23
24class GameIconFetcher(
25 private val game: Game,
26 private val options: Options
27) : Fetcher {
28 override suspend fun fetch(): FetchResult {
29 return DrawableResult(
30 drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
31 isSampled = false,
32 dataSource = DataSource.DISK
33 )
34 }
35
36 private fun decodeGameIcon(uri: String): Bitmap? {
37 val data = NativeLibrary.getIcon(uri)
38 return BitmapFactory.decodeByteArray(
39 data,
40 0,
41 data.size,
42 BitmapFactory.Options()
43 )
44 }
45
46 class Factory : Fetcher.Factory<Game> {
47 override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
48 GameIconFetcher(data, options)
49 }
50}
51
52class GameIconKeyer : Keyer<Game> {
53 override fun key(data: Game, options: Options): String = data.path
54}
55
56object GameIconUtils {
57 private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
58 .components {
59 add(GameIconKeyer())
60 add(GameIconFetcher.Factory())
61 }
62 .memoryCache {
63 MemoryCache.Builder(YuzuApplication.appContext)
64 .maxSizePercent(0.25)
65 .build()
66 }
67 .build()
68
69 fun loadGameIcon(game: Game, imageView: ImageView) {
70 val request = ImageRequest.Builder(YuzuApplication.appContext)
71 .data(game)
72 .target(imageView)
73 .error(R.drawable.default_icon)
74 .build()
75 imageLoader.enqueue(request)
76 }
77}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
new file mode 100644
index 000000000..9425f8b99
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -0,0 +1,33 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6object NativeConfig {
7 external fun getBoolean(key: String, getDefault: Boolean): Boolean
8 external fun setBoolean(key: String, value: Boolean)
9
10 external fun getByte(key: String, getDefault: Boolean): Byte
11 external fun setByte(key: String, value: Byte)
12
13 external fun getShort(key: String, getDefault: Boolean): Short
14 external fun setShort(key: String, value: Short)
15
16 external fun getInt(key: String, getDefault: Boolean): Int
17 external fun setInt(key: String, value: Int)
18
19 external fun getFloat(key: String, getDefault: Boolean): Float
20 external fun setFloat(key: String, value: Float)
21
22 external fun getLong(key: String, getDefault: Boolean): Long
23 external fun setLong(key: String, value: Long)
24
25 external fun getString(key: String, getDefault: Boolean): String
26 external fun setString(key: String, value: String)
27
28 external fun getIsRuntimeModifiable(key: String): Boolean
29
30 external fun getConfigHeader(category: Int): String
31
32 external fun getPairedSettingKey(key: String): String
33}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt
index 685ccaa76..2f0868c63 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/FixedRatioSurfaceView.kt
@@ -7,7 +7,6 @@ import android.content.Context
7import android.util.AttributeSet 7import android.util.AttributeSet
8import android.util.Rational 8import android.util.Rational
9import android.view.SurfaceView 9import android.view.SurfaceView
10import kotlin.math.roundToInt
11 10
12class FixedRatioSurfaceView @JvmOverloads constructor( 11class FixedRatioSurfaceView @JvmOverloads constructor(
13 context: Context, 12 context: Context,
@@ -22,27 +21,44 @@ class FixedRatioSurfaceView @JvmOverloads constructor(
22 */ 21 */
23 fun setAspectRatio(ratio: Rational?) { 22 fun setAspectRatio(ratio: Rational?) {
24 aspectRatio = ratio?.toFloat() ?: 0f 23 aspectRatio = ratio?.toFloat() ?: 0f
24 requestLayout()
25 } 25 }
26 26
27 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 27 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
28 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 28 val displayWidth: Float = MeasureSpec.getSize(widthMeasureSpec).toFloat()
29 val width = MeasureSpec.getSize(widthMeasureSpec) 29 val displayHeight: Float = MeasureSpec.getSize(heightMeasureSpec).toFloat()
30 val height = MeasureSpec.getSize(heightMeasureSpec)
31 if (aspectRatio != 0f) { 30 if (aspectRatio != 0f) {
32 val newWidth: Int 31 val displayAspect = displayWidth / displayHeight
33 val newHeight: Int 32 if (displayAspect < aspectRatio) {
34 if (height * aspectRatio < width) { 33 // Max out width
35 newWidth = (height * aspectRatio).roundToInt() 34 val halfHeight = displayHeight / 2
36 newHeight = height 35 val surfaceHeight = displayWidth / aspectRatio
36 val newTop: Float = halfHeight - (surfaceHeight / 2)
37 val newBottom: Float = halfHeight + (surfaceHeight / 2)
38 super.onMeasure(
39 widthMeasureSpec,
40 MeasureSpec.makeMeasureSpec(
41 newBottom.toInt() - newTop.toInt(),
42 MeasureSpec.EXACTLY
43 )
44 )
45 return
37 } else { 46 } else {
38 newWidth = width 47 // Max out height
39 newHeight = (width / aspectRatio).roundToInt() 48 val halfWidth = displayWidth / 2
49 val surfaceWidth = displayHeight * aspectRatio
50 val newLeft: Float = halfWidth - (surfaceWidth / 2)
51 val newRight: Float = halfWidth + (surfaceWidth / 2)
52 super.onMeasure(
53 MeasureSpec.makeMeasureSpec(
54 newRight.toInt() - newLeft.toInt(),
55 MeasureSpec.EXACTLY
56 ),
57 heightMeasureSpec
58 )
59 return
40 } 60 }
41 val left = (width - newWidth) / 2
42 val top = (height - newHeight) / 2
43 setLeftTopRightBottom(left, top, left + newWidth, top + newHeight)
44 } else {
45 setLeftTopRightBottom(0, 0, width, height)
46 } 61 }
62 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
47 } 63 }
48} 64}
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
index e2ed08e9f..e15d1480b 100644
--- a/src/android/app/src/main/jni/CMakeLists.txt
+++ b/src/android/app/src/main/jni/CMakeLists.txt
@@ -14,6 +14,8 @@ add_library(yuzu-android SHARED
14 id_cache.cpp 14 id_cache.cpp
15 id_cache.h 15 id_cache.h
16 native.cpp 16 native.cpp
17 native_config.cpp
18 uisettings.cpp
17) 19)
18 20
19set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) 21set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
index 9de9bd93e..34b425cb4 100644
--- a/src/android/app/src/main/jni/config.cpp
+++ b/src/android/app/src/main/jni/config.cpp
@@ -16,18 +16,20 @@
16#include "input_common/main.h" 16#include "input_common/main.h"
17#include "jni/config.h" 17#include "jni/config.h"
18#include "jni/default_ini.h" 18#include "jni/default_ini.h"
19#include "uisettings.h"
19 20
20namespace FS = Common::FS; 21namespace FS = Common::FS;
21 22
22Config::Config(std::optional<std::filesystem::path> config_path) 23Config::Config(const std::string& config_name, ConfigType config_type)
23 : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")}, 24 : type(config_type), global{config_type == ConfigType::GlobalConfig} {
24 config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} { 25 Initialize(config_name);
25 Reload();
26} 26}
27 27
28Config::~Config() = default; 28Config::~Config() = default;
29 29
30bool Config::LoadINI(const std::string& default_contents, bool retry) { 30bool Config::LoadINI(const std::string& default_contents, bool retry) {
31 void(FS::CreateParentDir(config_loc));
32 config = std::make_unique<INIReader>(FS::PathToUTF8String(config_loc));
31 const auto config_loc_str = FS::PathToUTF8String(config_loc); 33 const auto config_loc_str = FS::PathToUTF8String(config_loc);
32 if (config->ParseError() < 0) { 34 if (config->ParseError() < 0) {
33 if (retry) { 35 if (retry) {
@@ -301,9 +303,28 @@ void Config::ReadValues() {
301 303
302 // Network 304 // Network
303 ReadSetting("Network", Settings::values.network_interface); 305 ReadSetting("Network", Settings::values.network_interface);
306
307 // Android
308 ReadSetting("Android", AndroidSettings::values.picture_in_picture);
309 ReadSetting("Android", AndroidSettings::values.screen_layout);
304} 310}
305 311
306void Config::Reload() { 312void Config::Initialize(const std::string& config_name) {
313 const auto fs_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir);
314 const auto config_file = fmt::format("{}.ini", config_name);
315
316 switch (type) {
317 case ConfigType::GlobalConfig:
318 config_loc = FS::PathToUTF8String(fs_config_loc / config_file);
319 break;
320 case ConfigType::PerGameConfig:
321 config_loc = FS::PathToUTF8String(fs_config_loc / "custom" / FS::ToU8String(config_file));
322 break;
323 case ConfigType::InputProfile:
324 config_loc = FS::PathToUTF8String(fs_config_loc / "input" / config_file);
325 LoadINI(DefaultINI::android_config_file);
326 return;
327 }
307 LoadINI(DefaultINI::android_config_file); 328 LoadINI(DefaultINI::android_config_file);
308 ReadValues(); 329 ReadValues();
309} 330}
diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h
index 0d7d6e94d..e1e8f47ed 100644
--- a/src/android/app/src/main/jni/config.h
+++ b/src/android/app/src/main/jni/config.h
@@ -13,25 +13,35 @@
13class INIReader; 13class INIReader;
14 14
15class Config { 15class Config {
16 std::filesystem::path config_loc;
17 std::unique_ptr<INIReader> config;
18
19 bool LoadINI(const std::string& default_contents = "", bool retry = true); 16 bool LoadINI(const std::string& default_contents = "", bool retry = true);
20 void ReadValues();
21 17
22public: 18public:
23 explicit Config(std::optional<std::filesystem::path> config_path = std::nullopt); 19 enum class ConfigType {
20 GlobalConfig,
21 PerGameConfig,
22 InputProfile,
23 };
24
25 explicit Config(const std::string& config_name = "config",
26 ConfigType config_type = ConfigType::GlobalConfig);
24 ~Config(); 27 ~Config();
25 28
26 void Reload(); 29 void Initialize(const std::string& config_name);
27 30
28private: 31private:
29 /** 32 /**
30 * Applies a value read from the sdl2_config to a Setting. 33 * Applies a value read from the config to a Setting.
31 * 34 *
32 * @param group The name of the INI group 35 * @param group The name of the INI group
33 * @param setting The yuzu setting to modify 36 * @param setting The yuzu setting to modify
34 */ 37 */
35 template <typename Type, bool ranged> 38 template <typename Type, bool ranged>
36 void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting); 39 void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
40
41 void ReadValues();
42
43 const ConfigType type;
44 std::unique_ptr<INIReader> config;
45 std::string config_loc;
46 const bool global;
37}; 47};
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index 9cbbf23a3..960abf95a 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -15,6 +15,8 @@ static jclass s_disk_cache_progress_class;
15static jclass s_load_callback_stage_class; 15static jclass s_load_callback_stage_class;
16static jmethodID s_exit_emulation_activity; 16static jmethodID s_exit_emulation_activity;
17static jmethodID s_disk_cache_load_progress; 17static jmethodID s_disk_cache_load_progress;
18static jmethodID s_on_emulation_started;
19static jmethodID s_on_emulation_stopped;
18 20
19static constexpr jint JNI_VERSION = JNI_VERSION_1_6; 21static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
20 22
@@ -59,6 +61,14 @@ jmethodID GetDiskCacheLoadProgress() {
59 return s_disk_cache_load_progress; 61 return s_disk_cache_load_progress;
60} 62}
61 63
64jmethodID GetOnEmulationStarted() {
65 return s_on_emulation_started;
66}
67
68jmethodID GetOnEmulationStopped() {
69 return s_on_emulation_stopped;
70}
71
62} // namespace IDCache 72} // namespace IDCache
63 73
64#ifdef __cplusplus 74#ifdef __cplusplus
@@ -85,6 +95,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
85 env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); 95 env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
86 s_disk_cache_load_progress = 96 s_disk_cache_load_progress =
87 env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V"); 97 env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
98 s_on_emulation_started =
99 env->GetStaticMethodID(s_native_library_class, "onEmulationStarted", "()V");
100 s_on_emulation_stopped =
101 env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
88 102
89 // Initialize Android Storage 103 // Initialize Android Storage
90 Common::FS::Android::RegisterCallbacks(env, s_native_library_class); 104 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index be535fe1e..b76158928 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -15,5 +15,7 @@ jclass GetDiskCacheProgressClass();
15jclass GetDiskCacheLoadCallbackStageClass(); 15jclass GetDiskCacheLoadCallbackStageClass();
16jmethodID GetExitEmulationActivity(); 16jmethodID GetExitEmulationActivity();
17jmethodID GetDiskCacheLoadProgress(); 17jmethodID GetDiskCacheLoadProgress();
18jmethodID GetOnEmulationStarted();
19jmethodID GetOnEmulationStopped();
18 20
19} // namespace IDCache 21} // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 7e17833a0..0f2a6d9e4 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -203,12 +203,10 @@ public:
203 } 203 }
204 204
205 bool IsRunning() const { 205 bool IsRunning() const {
206 std::scoped_lock lock(m_mutex);
207 return m_is_running; 206 return m_is_running;
208 } 207 }
209 208
210 bool IsPaused() const { 209 bool IsPaused() const {
211 std::scoped_lock lock(m_mutex);
212 return m_is_running && m_is_paused; 210 return m_is_running && m_is_paused;
213 } 211 }
214 212
@@ -335,6 +333,8 @@ public:
335 333
336 // Tear down the render window. 334 // Tear down the render window.
337 m_window.reset(); 335 m_window.reset();
336
337 OnEmulationStopped(m_load_result);
338 } 338 }
339 339
340 void PauseEmulation() { 340 void PauseEmulation() {
@@ -376,6 +376,8 @@ public:
376 m_system.InitializeDebugger(); 376 m_system.InitializeDebugger();
377 } 377 }
378 378
379 OnEmulationStarted();
380
379 while (true) { 381 while (true) {
380 { 382 {
381 [[maybe_unused]] std::unique_lock lock(m_mutex); 383 [[maybe_unused]] std::unique_lock lock(m_mutex);
@@ -511,6 +513,18 @@ private:
511 static_cast<jint>(progress), static_cast<jint>(max)); 513 static_cast<jint>(progress), static_cast<jint>(max));
512 } 514 }
513 515
516 static void OnEmulationStarted() {
517 JNIEnv* env = IDCache::GetEnvForThread();
518 env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
519 IDCache::GetOnEmulationStarted());
520 }
521
522 static void OnEmulationStopped(Core::SystemResultStatus result) {
523 JNIEnv* env = IDCache::GetEnvForThread();
524 env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
525 IDCache::GetOnEmulationStopped(), static_cast<jint>(result));
526 }
527
514private: 528private:
515 static EmulationSession s_instance; 529 static EmulationSession s_instance;
516 530
@@ -528,8 +542,8 @@ private:
528 Core::PerfStatsResults m_perf_stats{}; 542 Core::PerfStatsResults m_perf_stats{};
529 std::shared_ptr<FileSys::VfsFilesystem> m_vfs; 543 std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
530 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; 544 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
531 bool m_is_running{}; 545 std::atomic<bool> m_is_running = false;
532 bool m_is_paused{}; 546 std::atomic<bool> m_is_paused = false;
533 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; 547 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
534 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; 548 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
535 std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider; 549 std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
@@ -824,34 +838,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings(JNIEnv* env, jclass cl
824 Config{}; 838 Config{};
825} 839}
826 840
827jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserSetting(JNIEnv* env, jclass clazz,
828 jstring j_game_id, jstring j_section,
829 jstring j_key) {
830 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
831 std::string_view section = env->GetStringUTFChars(j_section, 0);
832 std::string_view key = env->GetStringUTFChars(j_key, 0);
833
834 env->ReleaseStringUTFChars(j_game_id, game_id.data());
835 env->ReleaseStringUTFChars(j_section, section.data());
836 env->ReleaseStringUTFChars(j_key, key.data());
837
838 return env->NewStringUTF("");
839}
840
841void Java_org_yuzu_yuzu_1emu_NativeLibrary_setUserSetting(JNIEnv* env, jclass clazz,
842 jstring j_game_id, jstring j_section,
843 jstring j_key, jstring j_value) {
844 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
845 std::string_view section = env->GetStringUTFChars(j_section, 0);
846 std::string_view key = env->GetStringUTFChars(j_key, 0);
847 std::string_view value = env->GetStringUTFChars(j_value, 0);
848
849 env->ReleaseStringUTFChars(j_game_id, game_id.data());
850 env->ReleaseStringUTFChars(j_section, section.data());
851 env->ReleaseStringUTFChars(j_key, key.data());
852 env->ReleaseStringUTFChars(j_value, value.data());
853}
854
855void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz, 841void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz,
856 jstring j_game_id) { 842 jstring j_game_id) {
857 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); 843 std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
new file mode 100644
index 000000000..8a704960c
--- /dev/null
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -0,0 +1,237 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <string>
5
6#include <jni.h>
7
8#include "common/logging/log.h"
9#include "common/settings.h"
10#include "jni/android_common/android_common.h"
11#include "jni/config.h"
12#include "uisettings.h"
13
14template <typename T>
15Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
16 auto key = GetJString(env, jkey);
17 auto basicSetting = Settings::values.linkage.by_key[key];
18 auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
19 if (basicSetting != 0) {
20 return static_cast<Settings::Setting<T>*>(basicSetting);
21 }
22 if (basicAndroidSetting != 0) {
23 return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
24 }
25 LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
26 return nullptr;
27}
28
29extern "C" {
30
31jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
32 jstring jkey, jboolean getDefault) {
33 auto setting = getSetting<bool>(env, jkey);
34 if (setting == nullptr) {
35 return false;
36 }
37 setting->SetGlobal(true);
38
39 if (static_cast<bool>(getDefault)) {
40 return setting->GetDefault();
41 }
42
43 return setting->GetValue();
44}
45
46void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey,
47 jboolean value) {
48 auto setting = getSetting<bool>(env, jkey);
49 if (setting == nullptr) {
50 return;
51 }
52 setting->SetGlobal(true);
53 setting->SetValue(static_cast<bool>(value));
54}
55
56jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey,
57 jboolean getDefault) {
58 auto setting = getSetting<u8>(env, jkey);
59 if (setting == nullptr) {
60 return -1;
61 }
62 setting->SetGlobal(true);
63
64 if (static_cast<bool>(getDefault)) {
65 return setting->GetDefault();
66 }
67
68 return setting->GetValue();
69}
70
71void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey,
72 jbyte value) {
73 auto setting = getSetting<u8>(env, jkey);
74 if (setting == nullptr) {
75 return;
76 }
77 setting->SetGlobal(true);
78 setting->SetValue(value);
79}
80
81jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey,
82 jboolean getDefault) {
83 auto setting = getSetting<u16>(env, jkey);
84 if (setting == nullptr) {
85 return -1;
86 }
87 setting->SetGlobal(true);
88
89 if (static_cast<bool>(getDefault)) {
90 return setting->GetDefault();
91 }
92
93 return setting->GetValue();
94}
95
96void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey,
97 jshort value) {
98 auto setting = getSetting<u16>(env, jkey);
99 if (setting == nullptr) {
100 return;
101 }
102 setting->SetGlobal(true);
103 setting->SetValue(value);
104}
105
106jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
107 jboolean getDefault) {
108 auto setting = getSetting<int>(env, jkey);
109 if (setting == nullptr) {
110 return -1;
111 }
112 setting->SetGlobal(true);
113
114 if (static_cast<bool>(getDefault)) {
115 return setting->GetDefault();
116 }
117
118 return setting->GetValue();
119}
120
121void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey,
122 jint value) {
123 auto setting = getSetting<int>(env, jkey);
124 if (setting == nullptr) {
125 return;
126 }
127 setting->SetGlobal(true);
128 setting->SetValue(value);
129}
130
131jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey,
132 jboolean getDefault) {
133 auto setting = getSetting<float>(env, jkey);
134 if (setting == nullptr) {
135 return -1;
136 }
137 setting->SetGlobal(true);
138
139 if (static_cast<bool>(getDefault)) {
140 return setting->GetDefault();
141 }
142
143 return setting->GetValue();
144}
145
146void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey,
147 jfloat value) {
148 auto setting = getSetting<float>(env, jkey);
149 if (setting == nullptr) {
150 return;
151 }
152 setting->SetGlobal(true);
153 setting->SetValue(value);
154}
155
156jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
157 jboolean getDefault) {
158 auto setting = getSetting<long>(env, jkey);
159 if (setting == nullptr) {
160 return -1;
161 }
162 setting->SetGlobal(true);
163
164 if (static_cast<bool>(getDefault)) {
165 return setting->GetDefault();
166 }
167
168 return setting->GetValue();
169}
170
171void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey,
172 jlong value) {
173 auto setting = getSetting<long>(env, jkey);
174 if (setting == nullptr) {
175 return;
176 }
177 setting->SetGlobal(true);
178 setting->SetValue(value);
179}
180
181jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey,
182 jboolean getDefault) {
183 auto setting = getSetting<std::string>(env, jkey);
184 if (setting == nullptr) {
185 return ToJString(env, "");
186 }
187 setting->SetGlobal(true);
188
189 if (static_cast<bool>(getDefault)) {
190 return ToJString(env, setting->GetDefault());
191 }
192
193 return ToJString(env, setting->GetValue());
194}
195
196void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
197 jstring value) {
198 auto setting = getSetting<std::string>(env, jkey);
199 if (setting == nullptr) {
200 return;
201 }
202
203 setting->SetGlobal(true);
204 setting->SetValue(GetJString(env, value));
205}
206
207jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
208 jstring jkey) {
209 auto key = GetJString(env, jkey);
210 auto setting = Settings::values.linkage.by_key[key];
211 if (setting != 0) {
212 return setting->RuntimeModfiable();
213 }
214 LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
215 return true;
216}
217
218jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj,
219 jint jcategory) {
220 auto category = static_cast<Settings::Category>(jcategory);
221 return ToJString(env, Settings::TranslateCategory(category));
222}
223
224jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
225 jstring jkey) {
226 auto setting = getSetting<std::string>(env, jkey);
227 if (setting == nullptr) {
228 return ToJString(env, "");
229 }
230 if (setting->PairedSetting() == nullptr) {
231 return ToJString(env, "");
232 }
233
234 return ToJString(env, setting->PairedSetting()->GetLabel());
235}
236
237} // extern "C"
diff --git a/src/android/app/src/main/jni/uisettings.cpp b/src/android/app/src/main/jni/uisettings.cpp
new file mode 100644
index 000000000..f2f0bad50
--- /dev/null
+++ b/src/android/app/src/main/jni/uisettings.cpp
@@ -0,0 +1,10 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "uisettings.h"
5
6namespace AndroidSettings {
7
8Values values;
9
10} // namespace AndroidSettings
diff --git a/src/android/app/src/main/jni/uisettings.h b/src/android/app/src/main/jni/uisettings.h
new file mode 100644
index 000000000..494654af7
--- /dev/null
+++ b/src/android/app/src/main/jni/uisettings.h
@@ -0,0 +1,29 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <common/settings_common.h>
7#include "common/common_types.h"
8#include "common/settings_setting.h"
9
10namespace AndroidSettings {
11
12struct Values {
13 Settings::Linkage linkage;
14
15 // Android
16 Settings::Setting<bool> picture_in_picture{linkage, true, "picture_in_picture",
17 Settings::Category::Android};
18 Settings::Setting<s32> screen_layout{linkage,
19 5,
20 "screen_layout",
21 Settings::Category::Android,
22 Settings::Specialization::Default,
23 true,
24 true};
25};
26
27extern Values values;
28
29} // namespace AndroidSettings
diff --git a/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml b/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml
deleted file mode 100644
index 9f49c133a..000000000
--- a/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml
+++ /dev/null
@@ -1,16 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="125"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="1"
8 android:toAlpha="0" />
9
10 <translate
11 android:duration="125"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="0"
14 android:toXDelta="-75" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml b/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml
deleted file mode 100644
index 82fd719db..000000000
--- a/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml
+++ /dev/null
@@ -1,16 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="@android:integer/config_shortAnimTime"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="0"
8 android:toAlpha="1" />
9
10 <translate
11 android:duration="@android:integer/config_shortAnimTime"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="-200"
14 android:toXDelta="0" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml b/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml
deleted file mode 100644
index 5892128f1..000000000
--- a/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml
+++ /dev/null
@@ -1,16 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="125"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="1"
8 android:toAlpha="0" />
9
10 <translate
11 android:duration="125"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="0"
14 android:toXDelta="75" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml b/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml
deleted file mode 100644
index 98e0cf8bd..000000000
--- a/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml
+++ /dev/null
@@ -1,16 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="@android:integer/config_shortAnimTime"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="0"
8 android:toAlpha="1" />
9
10 <translate
11 android:duration="@android:integer/config_shortAnimTime"
12 android:interpolator="@android:anim/decelerate_interpolator"
13 android:fromXDelta="200"
14 android:toXDelta="0" />
15
16</set>
diff --git a/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml b/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml
deleted file mode 100644
index 77a40a4d1..000000000
--- a/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml
+++ /dev/null
@@ -1,10 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <alpha
5 android:duration="@android:integer/config_shortAnimTime"
6 android:interpolator="@android:anim/decelerate_interpolator"
7 android:fromAlpha="1"
8 android:toAlpha="0" />
9
10</set>
diff --git a/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml b/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml
deleted file mode 100644
index 4612aee13..000000000
--- a/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml
+++ /dev/null
@@ -1,20 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <objectAnimator
5 android:propertyName="translationX"
6 android:valueType="floatType"
7 android:valueFrom="-1280dp"
8 android:valueTo="0"
9 android:interpolator="@android:interpolator/decelerate_quad"
10 android:duration="300"/>
11
12 <objectAnimator
13 android:propertyName="alpha"
14 android:valueType="floatType"
15 android:valueFrom="0"
16 android:valueTo="1"
17 android:interpolator="@android:interpolator/accelerate_quad"
18 android:duration="300"/>
19
20</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml b/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml
deleted file mode 100644
index c00478946..000000000
--- a/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml
+++ /dev/null
@@ -1,21 +0,0 @@
1<?xml version="1.0" encoding="utf-8"?>
2<set xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <!-- This animation is used ONLY when a submenu is replaced. -->
5 <objectAnimator
6 android:propertyName="translationX"
7 android:valueType="floatType"
8 android:valueFrom="0"
9 android:valueTo="-1280dp"
10 android:interpolator="@android:interpolator/decelerate_quad"
11 android:duration="200"/>
12
13 <objectAnimator
14 android:propertyName="alpha"
15 android:valueType="floatType"
16 android:valueFrom="1"
17 android:valueTo="0"
18 android:interpolator="@android:interpolator/decelerate_quad"
19 android:duration="200"/>
20
21</set> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml
index 14ae83b04..8a026a30a 100644
--- a/src/android/app/src/main/res/layout/activity_settings.xml
+++ b/src/android/app/src/main/res/layout/activity_settings.xml
@@ -1,42 +1,24 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout 2<androidx.constraintlayout.widget.ConstraintLayout
3 android:id="@+id/coordinator_main"
4 xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:android="http://schemas.android.com/apk/res/android"
5 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/constraint_settings"
6 android:layout_width="match_parent" 7 android:layout_width="match_parent"
7 android:layout_height="match_parent" 8 android:layout_height="match_parent"
8 android:background="?attr/colorSurface"> 9 android:background="?attr/colorSurface">
9 10
10 <com.google.android.material.appbar.AppBarLayout 11 <androidx.fragment.app.FragmentContainerView
11 android:id="@+id/appbar_settings" 12 android:id="@+id/fragment_container"
12 android:layout_width="match_parent" 13 android:name="androidx.navigation.fragment.NavHostFragment"
13 android:layout_height="wrap_content" 14 android:layout_width="0dp"
14 android:fitsSystemWindows="true" 15 android:layout_height="0dp"
15 app:elevation="0dp"> 16 app:defaultNavHost="true"
16 17 app:layout_constraintBottom_toBottomOf="parent"
17 <com.google.android.material.appbar.CollapsingToolbarLayout 18 app:layout_constraintLeft_toLeftOf="parent"
18 style="?attr/collapsingToolbarLayoutMediumStyle" 19 app:layout_constraintRight_toRightOf="parent"
19 android:id="@+id/toolbar_settings_layout" 20 app:layout_constraintTop_toTopOf="parent"
20 android:layout_width="match_parent" 21 tools:layout="@layout/fragment_settings" />
21 android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
22 app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
23
24 <com.google.android.material.appbar.MaterialToolbar
25 android:id="@+id/toolbar_settings"
26 android:layout_width="match_parent"
27 android:layout_height="?attr/actionBarSize"
28 app:layout_collapseMode="pin" />
29
30 </com.google.android.material.appbar.CollapsingToolbarLayout>
31
32 </com.google.android.material.appbar.AppBarLayout>
33
34 <FrameLayout
35 android:id="@+id/frame_content"
36 android:layout_width="match_parent"
37 android:layout_height="match_parent"
38 android:layout_marginHorizontal="12dp"
39 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
40 22
41 <View 23 <View
42 android:id="@+id/navigation_bar_shade" 24 android:id="@+id/navigation_bar_shade"
@@ -45,6 +27,8 @@
45 android:background="@android:color/transparent" 27 android:background="@android:color/transparent"
46 android:clickable="false" 28 android:clickable="false"
47 android:focusable="false" 29 android:focusable="false"
48 android:layout_gravity="bottom|center_horizontal" /> 30 app:layout_constraintBottom_toBottomOf="parent"
31 app:layout_constraintEnd_toEndOf="parent"
32 app:layout_constraintStart_toStartOf="parent" />
49 33
50</androidx.coordinatorlayout.widget.CoordinatorLayout> 34</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
index e54a10e8f..da97d85c1 100644
--- a/src/android/app/src/main/res/layout/fragment_emulation.xml
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -26,6 +26,81 @@
26 android:focusable="false" 26 android:focusable="false"
27 android:focusableInTouchMode="false" /> 27 android:focusableInTouchMode="false" />
28 28
29 <com.google.android.material.card.MaterialCardView
30 android:id="@+id/loading_indicator"
31 style="?attr/materialCardViewOutlinedStyle"
32 android:layout_width="wrap_content"
33 android:layout_height="wrap_content"
34 android:layout_gravity="center"
35 android:focusable="false">
36
37 <androidx.constraintlayout.widget.ConstraintLayout
38 android:id="@+id/loading_layout"
39 android:layout_width="wrap_content"
40 android:layout_height="wrap_content"
41 android:gravity="center_horizontal">
42
43 <ImageView
44 android:id="@+id/loading_image"
45 android:layout_width="wrap_content"
46 android:layout_height="0dp"
47 android:adjustViewBounds="true"
48 app:layout_constraintBottom_toBottomOf="@+id/linearLayout"
49 app:layout_constraintStart_toStartOf="parent"
50 app:layout_constraintTop_toTopOf="@+id/linearLayout"
51 tools:src="@drawable/default_icon" />
52
53 <LinearLayout
54 android:id="@+id/linearLayout"
55 android:layout_width="wrap_content"
56 android:layout_height="wrap_content"
57 android:orientation="vertical"
58 android:paddingHorizontal="24dp"
59 android:paddingVertical="36dp"
60 app:layout_constraintBottom_toBottomOf="parent"
61 app:layout_constraintEnd_toEndOf="parent"
62 app:layout_constraintStart_toEndOf="@id/loading_image"
63 app:layout_constraintTop_toTopOf="parent">
64
65 <com.google.android.material.textview.MaterialTextView
66 android:id="@+id/loading_title"
67 style="@style/TextAppearance.Material3.TitleMedium"
68 android:layout_width="match_parent"
69 android:layout_height="wrap_content"
70 android:ellipsize="marquee"
71 android:marqueeRepeatLimit="marquee_forever"
72 android:requiresFadingEdge="horizontal"
73 android:singleLine="true"
74 android:textAlignment="viewStart"
75 tools:text="@string/games" />
76
77 <com.google.android.material.textview.MaterialTextView
78 android:id="@+id/loading_text"
79 style="@style/TextAppearance.Material3.TitleSmall"
80 android:layout_width="match_parent"
81 android:layout_height="wrap_content"
82 android:layout_marginTop="4dp"
83 android:ellipsize="marquee"
84 android:marqueeRepeatLimit="marquee_forever"
85 android:requiresFadingEdge="horizontal"
86 android:singleLine="true"
87 android:text="@string/loading"
88 android:textAlignment="viewStart" />
89
90 <com.google.android.material.progressindicator.LinearProgressIndicator
91 android:id="@+id/loading_progress_indicator"
92 android:layout_width="192dp"
93 android:layout_height="wrap_content"
94 android:layout_marginTop="12dp"
95 android:indeterminate="true"
96 app:trackCornerRadius="8dp" />
97
98 </LinearLayout>
99
100 </androidx.constraintlayout.widget.ConstraintLayout>
101
102 </com.google.android.material.card.MaterialCardView>
103
29 </FrameLayout> 104 </FrameLayout>
30 105
31 <FrameLayout 106 <FrameLayout
@@ -41,11 +116,12 @@
41 android:layout_height="match_parent" 116 android:layout_height="match_parent"
42 android:layout_gravity="center" 117 android:layout_gravity="center"
43 android:focusable="true" 118 android:focusable="true"
44 android:focusableInTouchMode="true" /> 119 android:focusableInTouchMode="true"
120 android:visibility="invisible" />
45 121
46 <Button 122 <Button
47 style="@style/Widget.Material3.Button.ElevatedButton"
48 android:id="@+id/done_control_config" 123 android:id="@+id/done_control_config"
124 style="@style/Widget.Material3.Button.ElevatedButton"
49 android:layout_width="wrap_content" 125 android:layout_width="wrap_content"
50 android:layout_height="wrap_content" 126 android:layout_height="wrap_content"
51 android:layout_gravity="center" 127 android:layout_gravity="center"
@@ -81,6 +157,7 @@
81 android:layout_height="match_parent" 157 android:layout_height="match_parent"
82 android:layout_gravity="start|bottom" 158 android:layout_gravity="start|bottom"
83 app:headerLayout="@layout/header_in_game" 159 app:headerLayout="@layout/header_in_game"
84 app:menu="@menu/menu_in_game" /> 160 app:menu="@menu/menu_in_game"
161 tools:visibility="gone" />
85 162
86</androidx.drawerlayout.widget.DrawerLayout> 163</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml
index 167720347..ebedbf1ec 100644
--- a/src/android/app/src/main/res/layout/fragment_settings.xml
+++ b/src/android/app/src/main/res/layout/fragment_settings.xml
@@ -1,14 +1,41 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout 2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/coordinator_main"
4 android:layout_width="match_parent" 5 android:layout_width="match_parent"
5 android:layout_height="match_parent"> 6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface">
8
9 <com.google.android.material.appbar.AppBarLayout
10 android:id="@+id/appbar_settings"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:fitsSystemWindows="true"
14 app:elevation="0dp">
15
16 <com.google.android.material.appbar.CollapsingToolbarLayout
17 android:id="@+id/toolbar_settings_layout"
18 style="?attr/collapsingToolbarLayoutMediumStyle"
19 android:layout_width="match_parent"
20 android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
21 app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
22
23 <com.google.android.material.appbar.MaterialToolbar
24 android:id="@+id/toolbar_settings"
25 android:layout_width="match_parent"
26 android:layout_height="?attr/actionBarSize"
27 app:layout_collapseMode="pin"
28 app:navigationIcon="@drawable/ic_back" />
29
30 </com.google.android.material.appbar.CollapsingToolbarLayout>
31
32 </com.google.android.material.appbar.AppBarLayout>
6 33
7 <androidx.recyclerview.widget.RecyclerView 34 <androidx.recyclerview.widget.RecyclerView
8 android:id="@+id/list_settings" 35 android:id="@+id/list_settings"
9 android:layout_width="match_parent" 36 android:layout_width="match_parent"
10 android:layout_height="match_parent" 37 android:layout_height="match_parent"
11 android:background="?attr/colorSurface" 38 android:clipToPadding="false"
12 android:clipToPadding="false" /> 39 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
13 40
14</FrameLayout> 41</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_settings_search.xml b/src/android/app/src/main/res/layout/fragment_settings_search.xml
new file mode 100644
index 000000000..c779ed2fc
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_settings_search.xml
@@ -0,0 +1,120 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <RelativeLayout
9 android:id="@+id/relativeLayout"
10 android:layout_width="0dp"
11 android:layout_height="0dp"
12 app:layout_constraintBottom_toBottomOf="parent"
13 app:layout_constraintEnd_toEndOf="parent"
14 app:layout_constraintStart_toStartOf="parent"
15 app:layout_constraintTop_toBottomOf="@+id/divider">
16
17 <LinearLayout
18 android:id="@+id/no_results_view"
19 android:layout_width="match_parent"
20 android:layout_height="match_parent"
21 android:gravity="center"
22 android:orientation="vertical">
23
24 <ImageView
25 android:id="@+id/icon_no_results"
26 android:layout_width="match_parent"
27 android:layout_height="80dp"
28 android:src="@drawable/ic_search" />
29
30 <com.google.android.material.textview.MaterialTextView
31 android:id="@+id/notice_text"
32 style="@style/TextAppearance.Material3.TitleLarge"
33 android:layout_width="match_parent"
34 android:layout_height="wrap_content"
35 android:gravity="center"
36 android:paddingTop="8dp"
37 android:text="@string/search_settings"
38 tools:visibility="visible" />
39
40 </LinearLayout>
41
42 <androidx.recyclerview.widget.RecyclerView
43 android:id="@+id/settings_list"
44 android:layout_width="match_parent"
45 android:layout_height="match_parent"
46 android:clipToPadding="false" />
47
48 </RelativeLayout>
49
50 <FrameLayout
51 android:id="@+id/frame_search"
52 android:layout_width="match_parent"
53 android:layout_height="wrap_content"
54 android:clipToPadding="false"
55 app:layout_constraintEnd_toEndOf="parent"
56 app:layout_constraintStart_toStartOf="parent"
57 app:layout_constraintTop_toTopOf="parent">
58
59 <com.google.android.material.card.MaterialCardView
60 android:id="@+id/search_background"
61 style="?attr/materialCardViewFilledStyle"
62 android:layout_width="match_parent"
63 android:layout_height="56dp"
64 app:cardCornerRadius="28dp">
65
66 <LinearLayout
67 android:id="@+id/search_container"
68 android:layout_width="match_parent"
69 android:layout_height="match_parent"
70 android:layout_marginEnd="56dp"
71 android:orientation="horizontal">
72
73 <Button
74 android:id="@+id/back_button"
75 style="?attr/materialIconButtonFilledTonalStyle"
76 android:layout_width="wrap_content"
77 android:layout_height="wrap_content"
78 android:layout_gravity="center_vertical"
79 android:layout_marginStart="8dp"
80 app:backgroundTint="@android:color/transparent"
81 app:icon="@drawable/ic_back" />
82
83 <EditText
84 android:id="@+id/search_text"
85 android:layout_width="match_parent"
86 android:layout_height="match_parent"
87 android:background="@android:color/transparent"
88 android:hint="@string/search_settings"
89 android:imeOptions="flagNoFullscreen"
90 android:inputType="text"
91 android:maxLines="1" />
92
93 </LinearLayout>
94
95 <Button
96 android:id="@+id/clear_button"
97 style="?attr/materialIconButtonFilledTonalStyle"
98 android:layout_width="wrap_content"
99 android:layout_height="wrap_content"
100 android:layout_gravity="center_vertical|end"
101 android:layout_marginEnd="8dp"
102 android:visibility="invisible"
103 app:backgroundTint="@android:color/transparent"
104 app:icon="@drawable/ic_clear"
105 tools:visibility="visible" />
106
107 </com.google.android.material.card.MaterialCardView>
108
109 </FrameLayout>
110
111 <com.google.android.material.divider.MaterialDivider
112 android:id="@+id/divider"
113 android:layout_width="match_parent"
114 android:layout_height="wrap_content"
115 android:layout_marginTop="20dp"
116 app:layout_constraintEnd_toEndOf="parent"
117 app:layout_constraintStart_toStartOf="parent"
118 app:layout_constraintTop_toBottomOf="@+id/frame_search" />
119
120</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/menu/menu_settings.xml b/src/android/app/src/main/res/menu/menu_settings.xml
index 1fe7aa6d4..21501a471 100644
--- a/src/android/app/src/main/res/menu/menu_settings.xml
+++ b/src/android/app/src/main/res/menu/menu_settings.xml
@@ -1,2 +1,11 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<menu /> \ No newline at end of file 2<menu xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto">
4
5 <item
6 android:id="@+id/action_search"
7 android:icon="@drawable/ic_search"
8 android:title="@string/home_search"
9 app:showAsAction="always" />
10
11</menu>
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml
index 8208f4c2c..c7be37f9b 100644
--- a/src/android/app/src/main/res/navigation/emulation_navigation.xml
+++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml
@@ -12,7 +12,26 @@
12 tools:layout="@layout/fragment_emulation" > 12 tools:layout="@layout/fragment_emulation" >
13 <argument 13 <argument
14 android:name="game" 14 android:name="game"
15 app:argType="org.yuzu.yuzu_emu.model.Game" /> 15 app:argType="org.yuzu.yuzu_emu.model.Game"
16 app:nullable="true"
17 android:defaultValue="@null" />
16 </fragment> 18 </fragment>
17 19
20 <activity
21 android:id="@+id/settingsActivity"
22 android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
23 android:label="SettingsActivity">
24 <argument
25 android:name="game"
26 app:argType="org.yuzu.yuzu_emu.model.Game"
27 app:nullable="true" />
28 <argument
29 android:name="menuTag"
30 app:argType="string" />
31 </activity>
32
33 <action
34 android:id="@+id/action_global_settingsActivity"
35 app:destination="@id/settingsActivity" />
36
18</navigation> 37</navigation>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index fcebba726..2085430bf 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -62,7 +62,9 @@
62 android:label="EmulationActivity"> 62 android:label="EmulationActivity">
63 <argument 63 <argument
64 android:name="game" 64 android:name="game"
65 app:argType="org.yuzu.yuzu_emu.model.Game" /> 65 app:argType="org.yuzu.yuzu_emu.model.Game"
66 app:nullable="true"
67 android:defaultValue="@null" />
66 </activity> 68 </activity>
67 69
68 <action 70 <action
@@ -70,4 +72,21 @@
70 app:destination="@id/emulationActivity" 72 app:destination="@id/emulationActivity"
71 app:launchSingleTop="true" /> 73 app:launchSingleTop="true" />
72 74
75 <activity
76 android:id="@+id/settingsActivity"
77 android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
78 android:label="SettingsActivity">
79 <argument
80 android:name="game"
81 app:argType="org.yuzu.yuzu_emu.model.Game"
82 app:nullable="true" />
83 <argument
84 android:name="menuTag"
85 app:argType="string" />
86 </activity>
87
88 <action
89 android:id="@+id/action_global_settingsActivity"
90 app:destination="@id/settingsActivity" />
91
73</navigation> 92</navigation>
diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml
new file mode 100644
index 000000000..88e1b4587
--- /dev/null
+++ b/src/android/app/src/main/res/navigation/settings_navigation.xml
@@ -0,0 +1,32 @@
1<?xml version="1.0" encoding="utf-8"?>
2<navigation xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/settings_navigation"
5 app:startDestination="@id/settingsFragment">
6
7 <fragment
8 android:id="@+id/settingsFragment"
9 android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsFragment"
10 android:label="SettingsFragment">
11 <argument
12 android:name="menuTag"
13 app:argType="string" />
14 <argument
15 android:name="game"
16 app:argType="org.yuzu.yuzu_emu.model.Game"
17 app:nullable="true" />
18 <action
19 android:id="@+id/action_settingsFragment_to_settingsSearchFragment"
20 app:destination="@id/settingsSearchFragment" />
21 </fragment>
22
23 <action
24 android:id="@+id/action_global_settingsFragment"
25 app:destination="@id/settingsFragment" />
26
27 <fragment
28 android:id="@+id/settingsSearchFragment"
29 android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment"
30 android:label="SettingsSearchFragment" />
31
32</navigation>
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
index 0c1d91264..daaa7ffde 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -209,7 +209,6 @@
209 <string name="emulation_pause">Emulation pausieren</string> 209 <string name="emulation_pause">Emulation pausieren</string>
210 <string name="emulation_unpause">Emulation fortsetzen</string> 210 <string name="emulation_unpause">Emulation fortsetzen</string>
211 <string name="emulation_input_overlay">Overlay-Optionen</string> 211 <string name="emulation_input_overlay">Overlay-Optionen</string>
212 <string name="emulation_game_loading">Spiel lädt…</string>
213 212
214 <string name="load_settings">Lädt Einstellungen...</string> 213 <string name="load_settings">Lädt Einstellungen...</string>
215 214
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
index 357f956d1..e9129cb00 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Pausar Emulación</string> 213 <string name="emulation_pause">Pausar Emulación</string>
214 <string name="emulation_unpause">Reanudar Emulación</string> 214 <string name="emulation_unpause">Reanudar Emulación</string>
215 <string name="emulation_input_overlay">Opciones de pantalla </string> 215 <string name="emulation_input_overlay">Opciones de pantalla </string>
216 <string name="emulation_game_loading">Cargando juego...</string>
217 216
218 <string name="load_settings">Cargando configuración...</string> 217 <string name="load_settings">Cargando configuración...</string>
219 218
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
index dfca1c830..2d99d618e 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Mettre en pause l\'émulation</string> 213 <string name="emulation_pause">Mettre en pause l\'émulation</string>
214 <string name="emulation_unpause">Reprendre l\'émulation</string> 214 <string name="emulation_unpause">Reprendre l\'émulation</string>
215 <string name="emulation_input_overlay">Options de l\'overlay</string> 215 <string name="emulation_input_overlay">Options de l\'overlay</string>
216 <string name="emulation_game_loading">Chargement du jeu...</string>
217 216
218 <string name="load_settings">Chargement des paramètres…</string> 217 <string name="load_settings">Chargement des paramètres…</string>
219 218
diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml
index 089d93ed6..d9c3de385 100644
--- a/src/android/app/src/main/res/values-it/strings.xml
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Metti in pausa l\'emulazione</string> 213 <string name="emulation_pause">Metti in pausa l\'emulazione</string>
214 <string name="emulation_unpause">Riprendi Emulazione</string> 214 <string name="emulation_unpause">Riprendi Emulazione</string>
215 <string name="emulation_input_overlay">Impostazioni Overlay</string> 215 <string name="emulation_input_overlay">Impostazioni Overlay</string>
216 <string name="emulation_game_loading">Caricamento del gioco...</string>
217 216
218 <string name="load_settings">Caricamento delle impostazioni...</string> 217 <string name="load_settings">Caricamento delle impostazioni...</string>
219 218
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml
index 39b590bee..7a226cd5c 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -211,7 +211,6 @@
211 <string name="emulation_pause">エミュレーションを一時停止</string> 211 <string name="emulation_pause">エミュレーションを一時停止</string>
212 <string name="emulation_unpause">エミュレーションを再開</string> 212 <string name="emulation_unpause">エミュレーションを再開</string>
213 <string name="emulation_input_overlay">オーバーレイオプション</string> 213 <string name="emulation_input_overlay">オーバーレイオプション</string>
214 <string name="emulation_game_loading">ロード中…</string>
215 214
216 <string name="load_settings">設定をロード中…</string> 215 <string name="load_settings">設定をロード中…</string>
217 216
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml
index cbcb2873f..427b6e5a0 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">에뮬레이션 일시 중지</string> 213 <string name="emulation_pause">에뮬레이션 일시 중지</string>
214 <string name="emulation_unpause">에뮬레이션 일시 중지 해제</string> 214 <string name="emulation_unpause">에뮬레이션 일시 중지 해제</string>
215 <string name="emulation_input_overlay">오버레이 옵션</string> 215 <string name="emulation_input_overlay">오버레이 옵션</string>
216 <string name="emulation_game_loading">게임 불러오기 중...</string>
217 216
218 <string name="load_settings">설정 불러오기 중...</string> 217 <string name="load_settings">설정 불러오기 중...</string>
219 218
diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml
index e48a4be38..ce8d7a9e4 100644
--- a/src/android/app/src/main/res/values-nb/strings.xml
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Pause Emulering</string> 213 <string name="emulation_pause">Pause Emulering</string>
214 <string name="emulation_unpause">Opphev pausing av emulering</string> 214 <string name="emulation_unpause">Opphev pausing av emulering</string>
215 <string name="emulation_input_overlay">Alternativer for overlegg</string> 215 <string name="emulation_input_overlay">Alternativer for overlegg</string>
216 <string name="emulation_game_loading">Spillet lastes inn...</string>
217 216
218 <string name="load_settings">Laster inn innstillinger...</string> 217 <string name="load_settings">Laster inn innstillinger...</string>
219 218
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml
index bc9c0f7f4..c2c24b48f 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Wstrzymaj emulację</string> 213 <string name="emulation_pause">Wstrzymaj emulację</string>
214 <string name="emulation_unpause">Wznów emulację</string> 214 <string name="emulation_unpause">Wznów emulację</string>
215 <string name="emulation_input_overlay">Opcje nakładki</string> 215 <string name="emulation_input_overlay">Opcje nakładki</string>
216 <string name="emulation_game_loading">Wczytywanie gry...</string>
217 216
218 <string name="load_settings">Wczytywanie ustawień...</string> 217 <string name="load_settings">Wczytywanie ustawień...</string>
219 218
diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml
index 75fe0edbf..04f276108 100644
--- a/src/android/app/src/main/res/values-pt-rBR/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Pausa emulação</string> 213 <string name="emulation_pause">Pausa emulação</string>
214 <string name="emulation_unpause">Retomar emulação</string> 214 <string name="emulation_unpause">Retomar emulação</string>
215 <string name="emulation_input_overlay">Opções de sobreposição </string> 215 <string name="emulation_input_overlay">Opções de sobreposição </string>
216 <string name="emulation_game_loading">Jogo a carregar...</string>
217 216
218 <string name="load_settings">Configurações a carregar...</string> 217 <string name="load_settings">Configurações a carregar...</string>
219 218
diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml
index 96b040c66..66a3a1a2e 100644
--- a/src/android/app/src/main/res/values-pt-rPT/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Pausa emulação</string> 213 <string name="emulation_pause">Pausa emulação</string>
214 <string name="emulation_unpause">Retomar emulação</string> 214 <string name="emulation_unpause">Retomar emulação</string>
215 <string name="emulation_input_overlay">Opções de sobreposição </string> 215 <string name="emulation_input_overlay">Opções de sobreposição </string>
216 <string name="emulation_game_loading">Jogo a carregar...</string>
217 216
218 <string name="load_settings">Configurações a carregar...</string> 217 <string name="load_settings">Configurações a carregar...</string>
219 218
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
index 8d954f59e..f770e954f 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Пауза эмуляции</string> 213 <string name="emulation_pause">Пауза эмуляции</string>
214 <string name="emulation_unpause">Возобновление эмуляции</string> 214 <string name="emulation_unpause">Возобновление эмуляции</string>
215 <string name="emulation_input_overlay">Настройки оверлея</string> 215 <string name="emulation_input_overlay">Настройки оверлея</string>
216 <string name="emulation_game_loading">Загрузка игры...</string>
217 216
218 <string name="load_settings">Загрузка настроек...</string> 217 <string name="load_settings">Загрузка настроек...</string>
219 218
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml
index 6c028535b..ea3ab1b15 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">Пауза емуляції</string> 213 <string name="emulation_pause">Пауза емуляції</string>
214 <string name="emulation_unpause">Відновлення емуляції</string> 214 <string name="emulation_unpause">Відновлення емуляції</string>
215 <string name="emulation_input_overlay">Налаштування оверлея</string> 215 <string name="emulation_input_overlay">Налаштування оверлея</string>
216 <string name="emulation_game_loading">Завантаження гри...</string>
217 216
218 <string name="load_settings">Завантаження налаштувань...</string> 217 <string name="load_settings">Завантаження налаштувань...</string>
219 218
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml
index e4ad2ed07..b45a5a528 100644
--- a/src/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">暂停模拟</string> 213 <string name="emulation_pause">暂停模拟</string>
214 <string name="emulation_unpause">继续模拟</string> 214 <string name="emulation_unpause">继续模拟</string>
215 <string name="emulation_input_overlay">虚拟按键选项</string> 215 <string name="emulation_input_overlay">虚拟按键选项</string>
216 <string name="emulation_game_loading">载入游戏中…</string>
217 216
218 <string name="load_settings">正在载入设定…</string> 217 <string name="load_settings">正在载入设定…</string>
219 218
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml
index 0d32f23df..3aab889e4 100644
--- a/src/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -213,7 +213,6 @@
213 <string name="emulation_pause">暫停模擬</string> 213 <string name="emulation_pause">暫停模擬</string>
214 <string name="emulation_unpause">取消暫停模擬</string> 214 <string name="emulation_unpause">取消暫停模擬</string>
215 <string name="emulation_input_overlay">覆疊選項</string> 215 <string name="emulation_input_overlay">覆疊選項</string>
216 <string name="emulation_game_loading">遊戲正在載入…</string>
217 216
218 <string name="load_settings">正在載入設定…</string> 217 <string name="load_settings">正在載入設定…</string>
219 218
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 200b99185..dc10159c9 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -243,10 +243,10 @@
243 <item>@string/cubeb</item> 243 <item>@string/cubeb</item>
244 <item>@string/string_null</item> 244 <item>@string/string_null</item>
245 </string-array> 245 </string-array>
246 <string-array name="outputEngineValues"> 246 <integer-array name="outputEngineValues">
247 <item>auto</item> 247 <item>0</item>
248 <item>cubeb</item> 248 <item>1</item>
249 <item>null</item> 249 <item>3</item>
250 </string-array> 250 </integer-array>
251 251
252</resources> 252</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index de1b2909b..b163e6fc1 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -43,6 +43,7 @@
43 <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string> 43 <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
44 <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string> 44 <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
45 <string name="home_search_games">Search games</string> 45 <string name="home_search_games">Search games</string>
46 <string name="search_settings">Search settings</string>
46 <string name="games_dir_selected">Games directory selected</string> 47 <string name="games_dir_selected">Games directory selected</string>
47 <string name="install_prod_keys">Install prod.keys</string> 48 <string name="install_prod_keys">Install prod.keys</string>
48 <string name="install_prod_keys_description">Required to decrypt retail games</string> 49 <string name="install_prod_keys_description">Required to decrypt retail games</string>
@@ -74,6 +75,7 @@
74 <string name="install_gpu_driver">Install GPU driver</string> 75 <string name="install_gpu_driver">Install GPU driver</string>
75 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> 76 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
76 <string name="advanced_settings">Advanced settings</string> 77 <string name="advanced_settings">Advanced settings</string>
78 <string name="advanced_settings_game">Advanced settings: %1$s</string>
77 <string name="settings_description">Configure emulator settings</string> 79 <string name="settings_description">Configure emulator settings</string>
78 <string name="search_recently_played">Recently played</string> 80 <string name="search_recently_played">Recently played</string>
79 <string name="search_recently_added">Recently added</string> 81 <string name="search_recently_added">Recently added</string>
@@ -200,7 +202,9 @@
200 <string name="ini_saved">Saved settings</string> 202 <string name="ini_saved">Saved settings</string>
201 <string name="gameid_saved">Saved settings for %1$s</string> 203 <string name="gameid_saved">Saved settings for %1$s</string>
202 <string name="error_saving">Error saving %1$s.ini: %2$s</string> 204 <string name="error_saving">Error saving %1$s.ini: %2$s</string>
205 <string name="unimplemented_menu">Unimplemented Menu</string>
203 <string name="loading">Loading…</string> 206 <string name="loading">Loading…</string>
207 <string name="shutting_down">Shutting down…</string>
204 <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string> 208 <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
205 <string name="reset_to_default">Reset to default</string> 209 <string name="reset_to_default">Reset to default</string>
206 <string name="reset_all_settings">Reset all settings?</string> 210 <string name="reset_all_settings">Reset all settings?</string>
@@ -259,7 +263,6 @@
259 <string name="emulation_pause">Pause emulation</string> 263 <string name="emulation_pause">Pause emulation</string>
260 <string name="emulation_unpause">Unpause emulation</string> 264 <string name="emulation_unpause">Unpause emulation</string>
261 <string name="emulation_input_overlay">Overlay options</string> 265 <string name="emulation_input_overlay">Overlay options</string>
262 <string name="emulation_game_loading">Game loading…</string>
263 266
264 <string name="load_settings">Loading settings…</string> 267 <string name="load_settings">Loading settings…</string>
265 268
diff --git a/src/audio_core/renderer/system.cpp b/src/audio_core/renderer/system.cpp
index a23627472..6e07baa54 100644
--- a/src/audio_core/renderer/system.cpp
+++ b/src/audio_core/renderer/system.cpp
@@ -778,7 +778,7 @@ u32 System::DropVoices(CommandBuffer& command_buffer, u32 estimated_process_time
778 while (i < command_buffer.count) { 778 while (i < command_buffer.count) {
779 const auto node_id{cmd->node_id}; 779 const auto node_id{cmd->node_id};
780 const auto node_id_type{cmd->node_id >> 28}; 780 const auto node_id_type{cmd->node_id >> 28};
781 const auto node_id_base{cmd->node_id & 0xFFF}; 781 const auto node_id_base{(cmd->node_id >> 16) & 0xFFF};
782 782
783 // If the new estimated process time falls below the limit, we're done dropping. 783 // If the new estimated process time falls below the limit, we're done dropping.
784 if (estimated_process_time <= time_limit) { 784 if (estimated_process_time <= time_limit) {
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 524056841..4ecaf550b 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -159,6 +159,8 @@ float Volume() {
159 159
160const char* TranslateCategory(Category category) { 160const char* TranslateCategory(Category category) {
161 switch (category) { 161 switch (category) {
162 case Category::Android:
163 return "Android";
162 case Category::Audio: 164 case Category::Audio:
163 return "Audio"; 165 return "Audio";
164 case Category::Core: 166 case Category::Core:
diff --git a/src/common/settings_common.cpp b/src/common/settings_common.cpp
index 137b65d5f..5960b78aa 100644
--- a/src/common/settings_common.cpp
+++ b/src/common/settings_common.cpp
@@ -14,6 +14,7 @@ BasicSetting::BasicSetting(Linkage& linkage, const std::string& name, enum Categ
14 : label{name}, category{category_}, id{linkage.count}, save{save_}, 14 : label{name}, category{category_}, id{linkage.count}, save{save_},
15 runtime_modifiable{runtime_modifiable_}, specialization{specialization_}, 15 runtime_modifiable{runtime_modifiable_}, specialization{specialization_},
16 other_setting{other_setting_} { 16 other_setting{other_setting_} {
17 linkage.by_key.insert({name, this});
17 linkage.by_category[category].push_back(this); 18 linkage.by_category[category].push_back(this);
18 linkage.count++; 19 linkage.count++;
19} 20}
diff --git a/src/common/settings_common.h b/src/common/settings_common.h
index 3082e0ce1..5b170dfd5 100644
--- a/src/common/settings_common.h
+++ b/src/common/settings_common.h
@@ -12,6 +12,7 @@
12namespace Settings { 12namespace Settings {
13 13
14enum class Category : u32 { 14enum class Category : u32 {
15 Android,
15 Audio, 16 Audio,
16 Core, 17 Core,
17 Cpu, 18 Cpu,
@@ -68,6 +69,7 @@ public:
68 explicit Linkage(u32 initial_count = 0); 69 explicit Linkage(u32 initial_count = 0);
69 ~Linkage(); 70 ~Linkage();
70 std::map<Category, std::vector<BasicSetting*>> by_category{}; 71 std::map<Category, std::vector<BasicSetting*>> by_category{};
72 std::map<std::string, Settings::BasicSetting*> by_key{};
71 std::vector<std::function<void()>> restore_functions{}; 73 std::vector<std::function<void()>> restore_functions{};
72 u32 count; 74 u32 count;
73}; 75};
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 2f67e60a9..e95ae80da 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -273,7 +273,8 @@ struct System::Impl {
273 time_manager.Initialize(); 273 time_manager.Initialize();
274 274
275 is_powered_on = true; 275 is_powered_on = true;
276 exit_lock = false; 276 exit_locked = false;
277 exit_requested = false;
277 278
278 microprofile_cpu[0] = MICROPROFILE_TOKEN(ARM_CPU0); 279 microprofile_cpu[0] = MICROPROFILE_TOKEN(ARM_CPU0);
279 microprofile_cpu[1] = MICROPROFILE_TOKEN(ARM_CPU1); 280 microprofile_cpu[1] = MICROPROFILE_TOKEN(ARM_CPU1);
@@ -398,7 +399,8 @@ struct System::Impl {
398 } 399 }
399 400
400 is_powered_on = false; 401 is_powered_on = false;
401 exit_lock = false; 402 exit_locked = false;
403 exit_requested = false;
402 404
403 if (gpu_core != nullptr) { 405 if (gpu_core != nullptr) {
404 gpu_core->NotifyShutdown(); 406 gpu_core->NotifyShutdown();
@@ -507,7 +509,8 @@ struct System::Impl {
507 509
508 CpuManager cpu_manager; 510 CpuManager cpu_manager;
509 std::atomic_bool is_powered_on{}; 511 std::atomic_bool is_powered_on{};
510 bool exit_lock = false; 512 bool exit_locked = false;
513 bool exit_requested = false;
511 514
512 bool nvdec_active{}; 515 bool nvdec_active{};
513 516
@@ -943,12 +946,20 @@ const Service::Time::TimeManager& System::GetTimeManager() const {
943 return impl->time_manager; 946 return impl->time_manager;
944} 947}
945 948
946void System::SetExitLock(bool locked) { 949void System::SetExitLocked(bool locked) {
947 impl->exit_lock = locked; 950 impl->exit_locked = locked;
948} 951}
949 952
950bool System::GetExitLock() const { 953bool System::GetExitLocked() const {
951 return impl->exit_lock; 954 return impl->exit_locked;
955}
956
957void System::SetExitRequested(bool requested) {
958 impl->exit_requested = requested;
959}
960
961bool System::GetExitRequested() const {
962 return impl->exit_requested;
952} 963}
953 964
954void System::SetApplicationProcessBuildID(const CurrentBuildProcessID& id) { 965void System::SetApplicationProcessBuildID(const CurrentBuildProcessID& id) {
diff --git a/src/core/core.h b/src/core/core.h
index c70ea1965..a9ff9315e 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -412,8 +412,11 @@ public:
412 /// Gets an immutable reference to the Room Network. 412 /// Gets an immutable reference to the Room Network.
413 [[nodiscard]] const Network::RoomNetwork& GetRoomNetwork() const; 413 [[nodiscard]] const Network::RoomNetwork& GetRoomNetwork() const;
414 414
415 void SetExitLock(bool locked); 415 void SetExitLocked(bool locked);
416 [[nodiscard]] bool GetExitLock() const; 416 bool GetExitLocked() const;
417
418 void SetExitRequested(bool requested);
419 bool GetExitRequested() const;
417 420
418 void SetApplicationProcessBuildID(const CurrentBuildProcessID& id); 421 void SetApplicationProcessBuildID(const CurrentBuildProcessID& id);
419 [[nodiscard]] const CurrentBuildProcessID& GetApplicationProcessBuildID() const; 422 [[nodiscard]] const CurrentBuildProcessID& GetApplicationProcessBuildID() const;
diff --git a/src/core/file_sys/content_archive.cpp b/src/core/file_sys/content_archive.cpp
index 44e6852fe..7d2f0abb8 100644
--- a/src/core/file_sys/content_archive.cpp
+++ b/src/core/file_sys/content_archive.cpp
@@ -22,6 +22,10 @@
22 22
23namespace FileSys { 23namespace FileSys {
24 24
25static u8 MasterKeyIdForKeyGeneration(u8 key_generation) {
26 return std::max<u8>(key_generation, 1) - 1;
27}
28
25NCA::NCA(VirtualFile file_, const NCA* base_nca) 29NCA::NCA(VirtualFile file_, const NCA* base_nca)
26 : file(std::move(file_)), keys{Core::Crypto::KeyManager::Instance()} { 30 : file(std::move(file_)), keys{Core::Crypto::KeyManager::Instance()} {
27 if (file == nullptr) { 31 if (file == nullptr) {
@@ -41,12 +45,17 @@ NCA::NCA(VirtualFile file_, const NCA* base_nca)
41 return; 45 return;
42 } 46 }
43 47
48 // Ensure we have the proper key area keys to continue.
49 const u8 master_key_id = MasterKeyIdForKeyGeneration(reader->GetKeyGeneration());
50 if (!keys.HasKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, reader->GetKeyIndex())) {
51 status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
52 return;
53 }
54
44 RightsId rights_id{}; 55 RightsId rights_id{};
45 reader->GetRightsId(rights_id.data(), rights_id.size()); 56 reader->GetRightsId(rights_id.data(), rights_id.size());
46 if (rights_id != RightsId{}) { 57 if (rights_id != RightsId{}) {
47 // External decryption key required; provide it here. 58 // External decryption key required; provide it here.
48 const auto key_generation = std::max<s32>(reader->GetKeyGeneration(), 1) - 1;
49
50 u128 rights_id_u128; 59 u128 rights_id_u128;
51 std::memcpy(rights_id_u128.data(), rights_id.data(), sizeof(rights_id)); 60 std::memcpy(rights_id_u128.data(), rights_id.data(), sizeof(rights_id));
52 61
@@ -57,12 +66,12 @@ NCA::NCA(VirtualFile file_, const NCA* base_nca)
57 return; 66 return;
58 } 67 }
59 68
60 if (!keys.HasKey(Core::Crypto::S128KeyType::Titlekek, key_generation)) { 69 if (!keys.HasKey(Core::Crypto::S128KeyType::Titlekek, master_key_id)) {
61 status = Loader::ResultStatus::ErrorMissingTitlekek; 70 status = Loader::ResultStatus::ErrorMissingTitlekek;
62 return; 71 return;
63 } 72 }
64 73
65 auto titlekek = keys.GetKey(Core::Crypto::S128KeyType::Titlekek, key_generation); 74 auto titlekek = keys.GetKey(Core::Crypto::S128KeyType::Titlekek, master_key_id);
66 Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(titlekek, Core::Crypto::Mode::ECB); 75 Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(titlekek, Core::Crypto::Mode::ECB);
67 cipher.Transcode(titlekey.data(), titlekey.size(), titlekey.data(), 76 cipher.Transcode(titlekey.data(), titlekey.size(), titlekey.data(),
68 Core::Crypto::Op::Decrypt); 77 Core::Crypto::Op::Decrypt);
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index da33f0e44..e92f400de 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -341,7 +341,7 @@ void ISelfController::Exit(HLERequestContext& ctx) {
341void ISelfController::LockExit(HLERequestContext& ctx) { 341void ISelfController::LockExit(HLERequestContext& ctx) {
342 LOG_DEBUG(Service_AM, "called"); 342 LOG_DEBUG(Service_AM, "called");
343 343
344 system.SetExitLock(true); 344 system.SetExitLocked(true);
345 345
346 IPC::ResponseBuilder rb{ctx, 2}; 346 IPC::ResponseBuilder rb{ctx, 2};
347 rb.Push(ResultSuccess); 347 rb.Push(ResultSuccess);
@@ -350,10 +350,14 @@ void ISelfController::LockExit(HLERequestContext& ctx) {
350void ISelfController::UnlockExit(HLERequestContext& ctx) { 350void ISelfController::UnlockExit(HLERequestContext& ctx) {
351 LOG_DEBUG(Service_AM, "called"); 351 LOG_DEBUG(Service_AM, "called");
352 352
353 system.SetExitLock(false); 353 system.SetExitLocked(false);
354 354
355 IPC::ResponseBuilder rb{ctx, 2}; 355 IPC::ResponseBuilder rb{ctx, 2};
356 rb.Push(ResultSuccess); 356 rb.Push(ResultSuccess);
357
358 if (system.GetExitRequested()) {
359 system.Exit();
360 }
357} 361}
358 362
359void ISelfController::EnterFatalSection(HLERequestContext& ctx) { 363void ISelfController::EnterFatalSection(HLERequestContext& ctx) {
diff --git a/src/core/hle/service/audio/hwopus.cpp b/src/core/hle/service/audio/hwopus.cpp
index 4a8276ed1..1557e6088 100644
--- a/src/core/hle/service/audio/hwopus.cpp
+++ b/src/core/hle/service/audio/hwopus.cpp
@@ -267,6 +267,10 @@ void HwOpus::GetWorkBufferSizeEx(HLERequestContext& ctx) {
267 GetWorkBufferSize(ctx); 267 GetWorkBufferSize(ctx);
268} 268}
269 269
270void HwOpus::GetWorkBufferSizeExEx(HLERequestContext& ctx) {
271 GetWorkBufferSizeEx(ctx);
272}
273
270void HwOpus::GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx) { 274void HwOpus::GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx) {
271 OpusMultiStreamParametersEx param; 275 OpusMultiStreamParametersEx param;
272 std::memcpy(&param, ctx.ReadBuffer().data(), ctx.GetReadBufferSize()); 276 std::memcpy(&param, ctx.ReadBuffer().data(), ctx.GetReadBufferSize());
@@ -409,7 +413,7 @@ HwOpus::HwOpus(Core::System& system_) : ServiceFramework{system_, "hwopus"} {
409 {6, &HwOpus::OpenHardwareOpusDecoderForMultiStreamEx, 413 {6, &HwOpus::OpenHardwareOpusDecoderForMultiStreamEx,
410 "OpenHardwareOpusDecoderForMultiStreamEx"}, 414 "OpenHardwareOpusDecoderForMultiStreamEx"},
411 {7, &HwOpus::GetWorkBufferSizeForMultiStreamEx, "GetWorkBufferSizeForMultiStreamEx"}, 415 {7, &HwOpus::GetWorkBufferSizeForMultiStreamEx, "GetWorkBufferSizeForMultiStreamEx"},
412 {8, nullptr, "GetWorkBufferSizeExEx"}, 416 {8, &HwOpus::GetWorkBufferSizeExEx, "GetWorkBufferSizeExEx"},
413 {9, nullptr, "GetWorkBufferSizeForMultiStreamExEx"}, 417 {9, nullptr, "GetWorkBufferSizeForMultiStreamExEx"},
414 }; 418 };
415 RegisterHandlers(functions); 419 RegisterHandlers(functions);
diff --git a/src/core/hle/service/audio/hwopus.h b/src/core/hle/service/audio/hwopus.h
index 91d9998ac..90867bf74 100644
--- a/src/core/hle/service/audio/hwopus.h
+++ b/src/core/hle/service/audio/hwopus.h
@@ -34,6 +34,7 @@ private:
34 void OpenHardwareOpusDecoderForMultiStreamEx(HLERequestContext& ctx); 34 void OpenHardwareOpusDecoderForMultiStreamEx(HLERequestContext& ctx);
35 void GetWorkBufferSize(HLERequestContext& ctx); 35 void GetWorkBufferSize(HLERequestContext& ctx);
36 void GetWorkBufferSizeEx(HLERequestContext& ctx); 36 void GetWorkBufferSizeEx(HLERequestContext& ctx);
37 void GetWorkBufferSizeExEx(HLERequestContext& ctx);
37 void GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx); 38 void GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx);
38}; 39};
39 40
diff --git a/src/core/hle/service/sockets/nsd.cpp b/src/core/hle/service/sockets/nsd.cpp
index bac21752a..491b76d48 100644
--- a/src/core/hle/service/sockets/nsd.cpp
+++ b/src/core/hle/service/sockets/nsd.cpp
@@ -19,6 +19,12 @@ enum class ServerEnvironmentType : u8 {
19 Dp, 19 Dp,
20}; 20};
21 21
22// This is nn::nsd::EnvironmentIdentifier
23struct EnvironmentIdentifier {
24 std::array<u8, 8> identifier;
25};
26static_assert(sizeof(EnvironmentIdentifier) == 0x8);
27
22NSD::NSD(Core::System& system_, const char* name) : ServiceFramework{system_, name} { 28NSD::NSD(Core::System& system_, const char* name) : ServiceFramework{system_, name} {
23 // clang-format off 29 // clang-format off
24 static const FunctionInfo functions[] = { 30 static const FunctionInfo functions[] = {
@@ -101,8 +107,9 @@ void NSD::ResolveEx(HLERequestContext& ctx) {
101} 107}
102 108
103void NSD::GetEnvironmentIdentifier(HLERequestContext& ctx) { 109void NSD::GetEnvironmentIdentifier(HLERequestContext& ctx) {
104 const std::string environment_identifier = "lp1"; 110 constexpr EnvironmentIdentifier lp1 = {
105 ctx.WriteBuffer(environment_identifier); 111 .identifier = {'l', 'p', '1', '\0', '\0', '\0', '\0', '\0'}};
112 ctx.WriteBuffer(lp1);
106 113
107 IPC::ResponseBuilder rb{ctx, 2}; 114 IPC::ResponseBuilder rb{ctx, 2};
108 rb.Push(ResultSuccess); 115 rb.Push(ResultSuccess);
diff --git a/src/core/hle/service/sockets/sfdnsres.cpp b/src/core/hle/service/sockets/sfdnsres.cpp
index 22e4a6f49..c657c4efd 100644
--- a/src/core/hle/service/sockets/sfdnsres.cpp
+++ b/src/core/hle/service/sockets/sfdnsres.cpp
@@ -150,6 +150,12 @@ static std::pair<u32, GetAddrInfoError> GetHostByNameRequestImpl(HLERequestConte
150 const std::string host = Common::StringFromBuffer(host_buffer); 150 const std::string host = Common::StringFromBuffer(host_buffer);
151 // For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions. 151 // For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions.
152 152
153 // Prevent resolution of Nintendo servers
154 if (host.find("srv.nintendo.net") != std::string::npos) {
155 LOG_WARNING(Network, "Resolution of hostname {} requested, returning EAI_AGAIN", host);
156 return {0, GetAddrInfoError::AGAIN};
157 }
158
153 auto res = Network::GetAddressInfo(host, /*service*/ std::nullopt); 159 auto res = Network::GetAddressInfo(host, /*service*/ std::nullopt);
154 if (!res.has_value()) { 160 if (!res.has_value()) {
155 return {0, Translate(res.error())}; 161 return {0, Translate(res.error())};
@@ -261,6 +267,12 @@ static std::pair<u32, GetAddrInfoError> GetAddrInfoRequestImpl(HLERequestContext
261 const auto host_buffer = ctx.ReadBuffer(0); 267 const auto host_buffer = ctx.ReadBuffer(0);
262 const std::string host = Common::StringFromBuffer(host_buffer); 268 const std::string host = Common::StringFromBuffer(host_buffer);
263 269
270 // Prevent resolution of Nintendo servers
271 if (host.find("srv.nintendo.net") != std::string::npos) {
272 LOG_WARNING(Network, "Resolution of hostname {} requested, returning EAI_AGAIN", host);
273 return {0, GetAddrInfoError::AGAIN};
274 }
275
264 std::optional<std::string> service = std::nullopt; 276 std::optional<std::string> service = std::nullopt;
265 if (ctx.CanReadBuffer(1)) { 277 if (ctx.CanReadBuffer(1)) {
266 const std::span<const u8> service_buffer = ctx.ReadBuffer(1); 278 const std::span<const u8> service_buffer = ctx.ReadBuffer(1);
diff --git a/src/shader_recompiler/backend/glsl/emit_glsl_image.cpp b/src/shader_recompiler/backend/glsl/emit_glsl_image.cpp
index 3ad668a47..d9872ecc2 100644
--- a/src/shader_recompiler/backend/glsl/emit_glsl_image.cpp
+++ b/src/shader_recompiler/backend/glsl/emit_glsl_image.cpp
@@ -558,7 +558,7 @@ void EmitImageGradient(EmitContext& ctx, IR::Inst& inst, const IR::Value& index,
558 if (multi_component) { 558 if (multi_component) {
559 if (info.num_derivates >= 3) { 559 if (info.num_derivates >= 3) {
560 const auto offset_vec{ctx.var_alloc.Consume(offset)}; 560 const auto offset_vec{ctx.var_alloc.Consume(offset)};
561 ctx.Add("{}=textureGrad({},{},vec3({}.xz, {}.x),vec3({}.yz, {}.y));", texel, texture, 561 ctx.Add("{}=textureGrad({},{},vec3({}.xz, {}.x),vec3({}.yw, {}.y));", texel, texture,
562 coords, derivatives_vec, offset_vec, derivatives_vec, offset_vec); 562 coords, derivatives_vec, offset_vec, derivatives_vec, offset_vec);
563 return; 563 return;
564 } 564 }
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp
index 7d901c04b..34240b36f 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp
@@ -91,6 +91,34 @@ public:
91 } 91 }
92 } 92 }
93 93
94 explicit ImageOperands(EmitContext& ctx, bool has_lod_clamp, Id derivates_1, Id derivates_2,
95 Id offset, Id lod_clamp) {
96 if (!Sirit::ValidId(derivates_1) || !Sirit::ValidId(derivates_2)) {
97 throw LogicError("Derivates must be present");
98 }
99 boost::container::static_vector<Id, 3> deriv_1_accum{
100 ctx.OpCompositeExtract(ctx.F32[1], derivates_1, 0),
101 ctx.OpCompositeExtract(ctx.F32[1], derivates_1, 2),
102 ctx.OpCompositeExtract(ctx.F32[1], derivates_2, 0),
103 };
104 boost::container::static_vector<Id, 3> deriv_2_accum{
105 ctx.OpCompositeExtract(ctx.F32[1], derivates_1, 1),
106 ctx.OpCompositeExtract(ctx.F32[1], derivates_1, 3),
107 ctx.OpCompositeExtract(ctx.F32[1], derivates_2, 1),
108 };
109 const Id derivates_id1{ctx.OpCompositeConstruct(
110 ctx.F32[3], std::span{deriv_1_accum.data(), deriv_1_accum.size()})};
111 const Id derivates_id2{ctx.OpCompositeConstruct(
112 ctx.F32[3], std::span{deriv_2_accum.data(), deriv_2_accum.size()})};
113 Add(spv::ImageOperandsMask::Grad, derivates_id1, derivates_id2);
114 if (Sirit::ValidId(offset)) {
115 Add(spv::ImageOperandsMask::Offset, offset);
116 }
117 if (has_lod_clamp) {
118 Add(spv::ImageOperandsMask::MinLod, lod_clamp);
119 }
120 }
121
94 std::span<const Id> Span() const noexcept { 122 std::span<const Id> Span() const noexcept {
95 return std::span{operands.data(), operands.size()}; 123 return std::span{operands.data(), operands.size()};
96 } 124 }
@@ -524,8 +552,11 @@ Id EmitImageQueryLod(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, I
524Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords, 552Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords,
525 Id derivates, Id offset, Id lod_clamp) { 553 Id derivates, Id offset, Id lod_clamp) {
526 const auto info{inst->Flags<IR::TextureInstInfo>()}; 554 const auto info{inst->Flags<IR::TextureInstInfo>()};
527 const ImageOperands operands(ctx, info.has_lod_clamp != 0, derivates, info.num_derivates, 555 const auto operands =
528 offset, lod_clamp); 556 info.num_derivates == 3
557 ? ImageOperands(ctx, info.has_lod_clamp != 0, derivates, offset, {}, lod_clamp)
558 : ImageOperands(ctx, info.has_lod_clamp != 0, derivates, info.num_derivates, offset,
559 lod_clamp);
529 return Emit(&EmitContext::OpImageSparseSampleExplicitLod, 560 return Emit(&EmitContext::OpImageSparseSampleExplicitLod,
530 &EmitContext::OpImageSampleExplicitLod, ctx, inst, ctx.F32[4], 561 &EmitContext::OpImageSampleExplicitLod, ctx, inst, ctx.F32[4],
531 Texture(ctx, info, index), coords, operands.Mask(), operands.Span()); 562 Texture(ctx, info, index), coords, operands.Mask(), operands.Span());
diff --git a/src/shader_recompiler/frontend/maxwell/translate/impl/move_special_register.cpp b/src/shader_recompiler/frontend/maxwell/translate/impl/move_special_register.cpp
index 753c62098..e593132e6 100644
--- a/src/shader_recompiler/frontend/maxwell/translate/impl/move_special_register.cpp
+++ b/src/shader_recompiler/frontend/maxwell/translate/impl/move_special_register.cpp
@@ -161,7 +161,8 @@ enum class SpecialRegister : u64 {
161 LOG_WARNING(Shader, "(STUBBED) SR_AFFINITY"); 161 LOG_WARNING(Shader, "(STUBBED) SR_AFFINITY");
162 return ir.Imm32(0); // This is the default value hardware returns. 162 return ir.Imm32(0); // This is the default value hardware returns.
163 default: 163 default:
164 throw NotImplementedException("S2R special register {}", special_register); 164 LOG_CRITICAL(Shader, "(STUBBED) Special register {}", special_register);
165 return ir.Imm32(0); // This is the default value hardware returns.
165 } 166 }
166} 167}
167} // Anonymous namespace 168} // Anonymous namespace
diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp
index 9f1b340a9..58ce0d8c2 100644
--- a/src/video_core/dma_pusher.cpp
+++ b/src/video_core/dma_pusher.cpp
@@ -14,6 +14,7 @@
14namespace Tegra { 14namespace Tegra {
15 15
16constexpr u32 MacroRegistersStart = 0xE00; 16constexpr u32 MacroRegistersStart = 0xE00;
17constexpr u32 ComputeInline = 0x6D;
17 18
18DmaPusher::DmaPusher(Core::System& system_, GPU& gpu_, MemoryManager& memory_manager_, 19DmaPusher::DmaPusher(Core::System& system_, GPU& gpu_, MemoryManager& memory_manager_,
19 Control::ChannelState& channel_state_) 20 Control::ChannelState& channel_state_)
@@ -83,12 +84,35 @@ bool DmaPusher::Step() {
83 dma_state.dma_get, command_list_header.size * sizeof(u32)); 84 dma_state.dma_get, command_list_header.size * sizeof(u32));
84 } 85 }
85 } 86 }
86 Core::Memory::GpuGuestMemory<Tegra::CommandHeader, 87 const auto safe_process = [&] {
87 Core::Memory::GuestMemoryFlags::UnsafeRead> 88 Core::Memory::GpuGuestMemory<Tegra::CommandHeader,
88 headers(memory_manager, dma_state.dma_get, command_list_header.size, &command_headers); 89 Core::Memory::GuestMemoryFlags::SafeRead>
89 ProcessCommands(headers); 90 headers(memory_manager, dma_state.dma_get, command_list_header.size,
91 &command_headers);
92 ProcessCommands(headers);
93 };
94 const auto unsafe_process = [&] {
95 Core::Memory::GpuGuestMemory<Tegra::CommandHeader,
96 Core::Memory::GuestMemoryFlags::UnsafeRead>
97 headers(memory_manager, dma_state.dma_get, command_list_header.size,
98 &command_headers);
99 ProcessCommands(headers);
100 };
101 if (Settings::IsGPULevelHigh()) {
102 if (dma_state.method >= MacroRegistersStart) {
103 unsafe_process();
104 return true;
105 }
106 if (subchannel_type[dma_state.subchannel] == Engines::EngineTypes::KeplerCompute &&
107 dma_state.method == ComputeInline) {
108 unsafe_process();
109 return true;
110 }
111 safe_process();
112 return true;
113 }
114 unsafe_process();
90 } 115 }
91
92 return true; 116 return true;
93} 117}
94 118
diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h
index 8a2784cdc..c9fab2d90 100644
--- a/src/video_core/dma_pusher.h
+++ b/src/video_core/dma_pusher.h
@@ -130,8 +130,10 @@ public:
130 130
131 void DispatchCalls(); 131 void DispatchCalls();
132 132
133 void BindSubchannel(Engines::EngineInterface* engine, u32 subchannel_id) { 133 void BindSubchannel(Engines::EngineInterface* engine, u32 subchannel_id,
134 Engines::EngineTypes engine_type) {
134 subchannels[subchannel_id] = engine; 135 subchannels[subchannel_id] = engine;
136 subchannel_type[subchannel_id] = engine_type;
135 } 137 }
136 138
137 void BindRasterizer(VideoCore::RasterizerInterface* rasterizer); 139 void BindRasterizer(VideoCore::RasterizerInterface* rasterizer);
@@ -170,6 +172,7 @@ private:
170 const bool ib_enable{true}; ///< IB mode enabled 172 const bool ib_enable{true}; ///< IB mode enabled
171 173
172 std::array<Engines::EngineInterface*, max_subchannels> subchannels{}; 174 std::array<Engines::EngineInterface*, max_subchannels> subchannels{};
175 std::array<Engines::EngineTypes, max_subchannels> subchannel_type;
173 176
174 GPU& gpu; 177 GPU& gpu;
175 Core::System& system; 178 Core::System& system;
diff --git a/src/video_core/engines/engine_interface.h b/src/video_core/engines/engine_interface.h
index 392322358..54631ee6c 100644
--- a/src/video_core/engines/engine_interface.h
+++ b/src/video_core/engines/engine_interface.h
@@ -11,6 +11,14 @@
11 11
12namespace Tegra::Engines { 12namespace Tegra::Engines {
13 13
14enum class EngineTypes : u32 {
15 KeplerCompute,
16 Maxwell3D,
17 Fermi2D,
18 MaxwellDMA,
19 KeplerMemory,
20};
21
14class EngineInterface { 22class EngineInterface {
15public: 23public:
16 virtual ~EngineInterface() = default; 24 virtual ~EngineInterface() = default;
diff --git a/src/video_core/engines/engine_upload.h b/src/video_core/engines/engine_upload.h
index 7242d2529..21bf8aeb4 100644
--- a/src/video_core/engines/engine_upload.h
+++ b/src/video_core/engines/engine_upload.h
@@ -69,6 +69,14 @@ public:
69 /// Binds a rasterizer to this engine. 69 /// Binds a rasterizer to this engine.
70 void BindRasterizer(VideoCore::RasterizerInterface* rasterizer); 70 void BindRasterizer(VideoCore::RasterizerInterface* rasterizer);
71 71
72 GPUVAddr ExecTargetAddress() const {
73 return regs.dest.Address();
74 }
75
76 u32 GetUploadSize() const {
77 return copy_size;
78 }
79
72private: 80private:
73 void ProcessData(std::span<const u8> read_buffer); 81 void ProcessData(std::span<const u8> read_buffer);
74 82
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index a38d9528a..cd61ab222 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -43,16 +43,33 @@ void KeplerCompute::CallMethod(u32 method, u32 method_argument, bool is_last_cal
43 43
44 switch (method) { 44 switch (method) {
45 case KEPLER_COMPUTE_REG_INDEX(exec_upload): { 45 case KEPLER_COMPUTE_REG_INDEX(exec_upload): {
46 UploadInfo info{.upload_address = upload_address,
47 .exec_address = upload_state.ExecTargetAddress(),
48 .copy_size = upload_state.GetUploadSize()};
49 uploads.push_back(info);
46 upload_state.ProcessExec(regs.exec_upload.linear != 0); 50 upload_state.ProcessExec(regs.exec_upload.linear != 0);
47 break; 51 break;
48 } 52 }
49 case KEPLER_COMPUTE_REG_INDEX(data_upload): { 53 case KEPLER_COMPUTE_REG_INDEX(data_upload): {
54 upload_address = current_dma_segment;
50 upload_state.ProcessData(method_argument, is_last_call); 55 upload_state.ProcessData(method_argument, is_last_call);
51 break; 56 break;
52 } 57 }
53 case KEPLER_COMPUTE_REG_INDEX(launch): 58 case KEPLER_COMPUTE_REG_INDEX(launch): {
59 const GPUVAddr launch_desc_loc = regs.launch_desc_loc.Address();
60
61 for (auto& data : uploads) {
62 const GPUVAddr offset = data.exec_address - launch_desc_loc;
63 if (offset / sizeof(u32) == LAUNCH_REG_INDEX(grid_dim_x) &&
64 memory_manager.IsMemoryDirty(data.upload_address, data.copy_size)) {
65 indirect_compute = {data.upload_address};
66 }
67 }
68 uploads.clear();
54 ProcessLaunch(); 69 ProcessLaunch();
70 indirect_compute = std::nullopt;
55 break; 71 break;
72 }
56 default: 73 default:
57 break; 74 break;
58 } 75 }
@@ -62,6 +79,7 @@ void KeplerCompute::CallMultiMethod(u32 method, const u32* base_start, u32 amoun
62 u32 methods_pending) { 79 u32 methods_pending) {
63 switch (method) { 80 switch (method) {
64 case KEPLER_COMPUTE_REG_INDEX(data_upload): 81 case KEPLER_COMPUTE_REG_INDEX(data_upload):
82 upload_address = current_dma_segment;
65 upload_state.ProcessData(base_start, amount); 83 upload_state.ProcessData(base_start, amount);
66 return; 84 return;
67 default: 85 default:
diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h
index 2092e685f..735e05fb4 100644
--- a/src/video_core/engines/kepler_compute.h
+++ b/src/video_core/engines/kepler_compute.h
@@ -5,6 +5,7 @@
5 5
6#include <array> 6#include <array>
7#include <cstddef> 7#include <cstddef>
8#include <optional>
8#include <vector> 9#include <vector>
9#include "common/bit_field.h" 10#include "common/bit_field.h"
10#include "common/common_funcs.h" 11#include "common/common_funcs.h"
@@ -36,6 +37,9 @@ namespace Tegra::Engines {
36#define KEPLER_COMPUTE_REG_INDEX(field_name) \ 37#define KEPLER_COMPUTE_REG_INDEX(field_name) \
37 (offsetof(Tegra::Engines::KeplerCompute::Regs, field_name) / sizeof(u32)) 38 (offsetof(Tegra::Engines::KeplerCompute::Regs, field_name) / sizeof(u32))
38 39
40#define LAUNCH_REG_INDEX(field_name) \
41 (offsetof(Tegra::Engines::KeplerCompute::LaunchParams, field_name) / sizeof(u32))
42
39class KeplerCompute final : public EngineInterface { 43class KeplerCompute final : public EngineInterface {
40public: 44public:
41 explicit KeplerCompute(Core::System& system, MemoryManager& memory_manager); 45 explicit KeplerCompute(Core::System& system, MemoryManager& memory_manager);
@@ -201,6 +205,10 @@ public:
201 void CallMultiMethod(u32 method, const u32* base_start, u32 amount, 205 void CallMultiMethod(u32 method, const u32* base_start, u32 amount,
202 u32 methods_pending) override; 206 u32 methods_pending) override;
203 207
208 std::optional<GPUVAddr> GetIndirectComputeAddress() const {
209 return indirect_compute;
210 }
211
204private: 212private:
205 void ProcessLaunch(); 213 void ProcessLaunch();
206 214
@@ -216,6 +224,15 @@ private:
216 MemoryManager& memory_manager; 224 MemoryManager& memory_manager;
217 VideoCore::RasterizerInterface* rasterizer = nullptr; 225 VideoCore::RasterizerInterface* rasterizer = nullptr;
218 Upload::State upload_state; 226 Upload::State upload_state;
227 GPUVAddr upload_address;
228
229 struct UploadInfo {
230 GPUVAddr upload_address;
231 GPUVAddr exec_address;
232 u32 copy_size;
233 };
234 std::vector<UploadInfo> uploads;
235 std::optional<GPUVAddr> indirect_compute{};
219}; 236};
220 237
221#define ASSERT_REG_POSITION(field_name, position) \ 238#define ASSERT_REG_POSITION(field_name, position) \
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index c3696096d..06e349e43 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -257,6 +257,7 @@ u32 Maxwell3D::GetMaxCurrentVertices() {
257 const u32 address_size = static_cast<u32>(gpu_addr_end - gpu_addr_begin); 257 const u32 address_size = static_cast<u32>(gpu_addr_end - gpu_addr_begin);
258 num_vertices = std::max( 258 num_vertices = std::max(
259 num_vertices, address_size / std::max(attribute.SizeInBytes(), array.stride.Value())); 259 num_vertices, address_size / std::max(attribute.SizeInBytes(), array.stride.Value()));
260 break;
260 } 261 }
261 return num_vertices; 262 return num_vertices;
262} 263}
@@ -269,10 +270,13 @@ size_t Maxwell3D::EstimateIndexBufferSize() {
269 std::numeric_limits<u32>::max()}; 270 std::numeric_limits<u32>::max()};
270 const size_t byte_size = regs.index_buffer.FormatSizeInBytes(); 271 const size_t byte_size = regs.index_buffer.FormatSizeInBytes();
271 const size_t log2_byte_size = Common::Log2Ceil64(byte_size); 272 const size_t log2_byte_size = Common::Log2Ceil64(byte_size);
273 const size_t cap{GetMaxCurrentVertices() * 3 * byte_size};
274 const size_t lower_cap =
275 std::min<size_t>(static_cast<size_t>(end_address - start_address), cap);
272 return std::min<size_t>( 276 return std::min<size_t>(
273 memory_manager.GetMemoryLayoutSize(start_address, byte_size * max_sizes[log2_byte_size]) / 277 memory_manager.GetMemoryLayoutSize(start_address, byte_size * max_sizes[log2_byte_size]) /
274 byte_size, 278 byte_size,
275 static_cast<size_t>(end_address - start_address)); 279 lower_cap);
276} 280}
277 281
278u32 Maxwell3D::ProcessShadowRam(u32 method, u32 argument) { 282u32 Maxwell3D::ProcessShadowRam(u32 method, u32 argument) {
diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp
index 7718a09b3..6de2543b7 100644
--- a/src/video_core/engines/puller.cpp
+++ b/src/video_core/engines/puller.cpp
@@ -34,19 +34,24 @@ void Puller::ProcessBindMethod(const MethodCall& method_call) {
34 bound_engines[method_call.subchannel] = engine_id; 34 bound_engines[method_call.subchannel] = engine_id;
35 switch (engine_id) { 35 switch (engine_id) {
36 case EngineID::FERMI_TWOD_A: 36 case EngineID::FERMI_TWOD_A:
37 dma_pusher.BindSubchannel(channel_state.fermi_2d.get(), method_call.subchannel); 37 dma_pusher.BindSubchannel(channel_state.fermi_2d.get(), method_call.subchannel,
38 EngineTypes::Fermi2D);
38 break; 39 break;
39 case EngineID::MAXWELL_B: 40 case EngineID::MAXWELL_B:
40 dma_pusher.BindSubchannel(channel_state.maxwell_3d.get(), method_call.subchannel); 41 dma_pusher.BindSubchannel(channel_state.maxwell_3d.get(), method_call.subchannel,
42 EngineTypes::Maxwell3D);
41 break; 43 break;
42 case EngineID::KEPLER_COMPUTE_B: 44 case EngineID::KEPLER_COMPUTE_B:
43 dma_pusher.BindSubchannel(channel_state.kepler_compute.get(), method_call.subchannel); 45 dma_pusher.BindSubchannel(channel_state.kepler_compute.get(), method_call.subchannel,
46 EngineTypes::KeplerCompute);
44 break; 47 break;
45 case EngineID::MAXWELL_DMA_COPY_A: 48 case EngineID::MAXWELL_DMA_COPY_A:
46 dma_pusher.BindSubchannel(channel_state.maxwell_dma.get(), method_call.subchannel); 49 dma_pusher.BindSubchannel(channel_state.maxwell_dma.get(), method_call.subchannel,
50 EngineTypes::MaxwellDMA);
47 break; 51 break;
48 case EngineID::KEPLER_INLINE_TO_MEMORY_B: 52 case EngineID::KEPLER_INLINE_TO_MEMORY_B:
49 dma_pusher.BindSubchannel(channel_state.kepler_memory.get(), method_call.subchannel); 53 dma_pusher.BindSubchannel(channel_state.kepler_memory.get(), method_call.subchannel,
54 EngineTypes::KeplerMemory);
50 break; 55 break;
51 default: 56 default:
52 UNIMPLEMENTED_MSG("Unimplemented engine {:04X}", engine_id); 57 UNIMPLEMENTED_MSG("Unimplemented engine {:04X}", engine_id);
diff --git a/src/video_core/host1x/codecs/codec.cpp b/src/video_core/host1x/codecs/codec.cpp
index 220cce28a..8d7da50fc 100644
--- a/src/video_core/host1x/codecs/codec.cpp
+++ b/src/video_core/host1x/codecs/codec.cpp
@@ -319,6 +319,7 @@ void Codec::Decode() {
319 LOG_WARNING(Service_NVDRV, "Zero width or height in frame"); 319 LOG_WARNING(Service_NVDRV, "Zero width or height in frame");
320 return; 320 return;
321 } 321 }
322 bool is_interlaced = initial_frame->interlaced_frame != 0;
322 if (av_codec_ctx->hw_device_ctx) { 323 if (av_codec_ctx->hw_device_ctx) {
323 final_frame = AVFramePtr{av_frame_alloc(), AVFrameDeleter}; 324 final_frame = AVFramePtr{av_frame_alloc(), AVFrameDeleter};
324 ASSERT_MSG(final_frame, "av_frame_alloc final_frame failed"); 325 ASSERT_MSG(final_frame, "av_frame_alloc final_frame failed");
@@ -334,7 +335,7 @@ void Codec::Decode() {
334 UNIMPLEMENTED_MSG("Unexpected video format: {}", final_frame->format); 335 UNIMPLEMENTED_MSG("Unexpected video format: {}", final_frame->format);
335 return; 336 return;
336 } 337 }
337 if (!final_frame->interlaced_frame) { 338 if (!is_interlaced) {
338 av_frames.push(std::move(final_frame)); 339 av_frames.push(std::move(final_frame));
339 } else { 340 } else {
340 if (!filters_initialized) { 341 if (!filters_initialized) {
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index 1ba31be88..dd03efecd 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -380,6 +380,17 @@ void RasterizerOpenGL::DispatchCompute() {
380 pipeline->SetEngine(kepler_compute, gpu_memory); 380 pipeline->SetEngine(kepler_compute, gpu_memory);
381 pipeline->Configure(); 381 pipeline->Configure();
382 const auto& qmd{kepler_compute->launch_description}; 382 const auto& qmd{kepler_compute->launch_description};
383 auto indirect_address = kepler_compute->GetIndirectComputeAddress();
384 if (indirect_address) {
385 // DispatchIndirect
386 static constexpr auto sync_info = VideoCommon::ObtainBufferSynchronize::FullSynchronize;
387 const auto post_op = VideoCommon::ObtainBufferOperation::DiscardWrite;
388 const auto [buffer, offset] =
389 buffer_cache.ObtainBuffer(*indirect_address, 12, sync_info, post_op);
390 glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, buffer->Handle());
391 glDispatchComputeIndirect(static_cast<GLintptr>(offset));
392 return;
393 }
383 glDispatchCompute(qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z); 394 glDispatchCompute(qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z);
384 ++num_queued_commands; 395 ++num_queued_commands;
385 has_written_global_memory |= pipeline->WritesGlobalMemory(); 396 has_written_global_memory |= pipeline->WritesGlobalMemory();
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index fe432dfe1..4f83a88e1 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -665,6 +665,19 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
665 std::move(modules), infos); 665 std::move(modules), infos);
666 666
667} catch (const Shader::Exception& exception) { 667} catch (const Shader::Exception& exception) {
668 auto hash = key.Hash();
669 size_t env_index{0};
670 for (size_t index = 0; index < Maxwell::MaxShaderProgram; ++index) {
671 if (key.unique_hashes[index] == 0) {
672 continue;
673 }
674 Shader::Environment& env{*envs[env_index]};
675 ++env_index;
676
677 const u32 cfg_offset{static_cast<u32>(env.StartAddress() + sizeof(Shader::ProgramHeader))};
678 Shader::Maxwell::Flow::CFG cfg(env, pools.flow_block, cfg_offset, index == 0);
679 env.Dump(hash, key.unique_hashes[index]);
680 }
668 LOG_ERROR(Render_Vulkan, "{}", exception.what()); 681 LOG_ERROR(Render_Vulkan, "{}", exception.what());
669 return nullptr; 682 return nullptr;
670} 683}
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 032f694bc..01e76a82c 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -463,6 +463,20 @@ void RasterizerVulkan::DispatchCompute() {
463 pipeline->Configure(*kepler_compute, *gpu_memory, scheduler, buffer_cache, texture_cache); 463 pipeline->Configure(*kepler_compute, *gpu_memory, scheduler, buffer_cache, texture_cache);
464 464
465 const auto& qmd{kepler_compute->launch_description}; 465 const auto& qmd{kepler_compute->launch_description};
466 auto indirect_address = kepler_compute->GetIndirectComputeAddress();
467 if (indirect_address) {
468 // DispatchIndirect
469 static constexpr auto sync_info = VideoCommon::ObtainBufferSynchronize::FullSynchronize;
470 const auto post_op = VideoCommon::ObtainBufferOperation::DiscardWrite;
471 const auto [buffer, offset] =
472 buffer_cache.ObtainBuffer(*indirect_address, 12, sync_info, post_op);
473 scheduler.RequestOutsideRenderPassOperationContext();
474 scheduler.Record([indirect_buffer = buffer->Handle(),
475 indirect_offset = offset](vk::CommandBuffer cmdbuf) {
476 cmdbuf.DispatchIndirect(indirect_buffer, indirect_offset);
477 });
478 return;
479 }
466 const std::array<u32, 3> dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z}; 480 const std::array<u32, 3> dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z};
467 scheduler.RequestOutsideRenderPassOperationContext(); 481 scheduler.RequestOutsideRenderPassOperationContext();
468 scheduler.Record([dim](vk::CommandBuffer cmdbuf) { cmdbuf.Dispatch(dim[0], dim[1], dim[2]); }); 482 scheduler.Record([dim](vk::CommandBuffer cmdbuf) { cmdbuf.Dispatch(dim[0], dim[1], dim[2]); });
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp
index 78e5a248f..c3f388d89 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.cpp
+++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp
@@ -92,6 +92,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept {
92 X(vkCmdCopyImage); 92 X(vkCmdCopyImage);
93 X(vkCmdCopyImageToBuffer); 93 X(vkCmdCopyImageToBuffer);
94 X(vkCmdDispatch); 94 X(vkCmdDispatch);
95 X(vkCmdDispatchIndirect);
95 X(vkCmdDraw); 96 X(vkCmdDraw);
96 X(vkCmdDrawIndexed); 97 X(vkCmdDrawIndexed);
97 X(vkCmdDrawIndirect); 98 X(vkCmdDrawIndirect);
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h
index c226a2a29..049fa8038 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.h
+++ b/src/video_core/vulkan_common/vulkan_wrapper.h
@@ -203,6 +203,7 @@ struct DeviceDispatch : InstanceDispatch {
203 PFN_vkCmdCopyImage vkCmdCopyImage{}; 203 PFN_vkCmdCopyImage vkCmdCopyImage{};
204 PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer{}; 204 PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer{};
205 PFN_vkCmdDispatch vkCmdDispatch{}; 205 PFN_vkCmdDispatch vkCmdDispatch{};
206 PFN_vkCmdDispatchIndirect vkCmdDispatchIndirect{};
206 PFN_vkCmdDraw vkCmdDraw{}; 207 PFN_vkCmdDraw vkCmdDraw{};
207 PFN_vkCmdDrawIndexed vkCmdDrawIndexed{}; 208 PFN_vkCmdDrawIndexed vkCmdDrawIndexed{};
208 PFN_vkCmdDrawIndirect vkCmdDrawIndirect{}; 209 PFN_vkCmdDrawIndirect vkCmdDrawIndirect{};
@@ -1209,6 +1210,10 @@ public:
1209 dld->vkCmdDispatch(handle, x, y, z); 1210 dld->vkCmdDispatch(handle, x, y, z);
1210 } 1211 }
1211 1212
1213 void DispatchIndirect(VkBuffer indirect_buffer, VkDeviceSize offset) const noexcept {
1214 dld->vkCmdDispatchIndirect(handle, indirect_buffer, offset);
1215 }
1216
1212 void PipelineBarrier(VkPipelineStageFlags src_stage_mask, VkPipelineStageFlags dst_stage_mask, 1217 void PipelineBarrier(VkPipelineStageFlags src_stage_mask, VkPipelineStageFlags dst_stage_mask,
1213 VkDependencyFlags dependency_flags, Span<VkMemoryBarrier> memory_barriers, 1218 VkDependencyFlags dependency_flags, Span<VkMemoryBarrier> memory_barriers,
1214 Span<VkBufferMemoryBarrier> buffer_barriers, 1219 Span<VkBufferMemoryBarrier> buffer_barriers,
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index ba4084840..f2e6c03f0 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -2010,8 +2010,16 @@ bool GMainWindow::OnShutdownBegin() {
2010 2010
2011 emit EmulationStopping(); 2011 emit EmulationStopping();
2012 2012
2013 int shutdown_time = 1000;
2014
2015 if (system->DebuggerEnabled()) {
2016 shutdown_time = 0;
2017 } else if (system->GetExitLocked()) {
2018 shutdown_time = 5000;
2019 }
2020
2013 shutdown_timer.setSingleShot(true); 2021 shutdown_timer.setSingleShot(true);
2014 shutdown_timer.start(system->DebuggerEnabled() ? 0 : 5000); 2022 shutdown_timer.start(shutdown_time);
2015 connect(&shutdown_timer, &QTimer::timeout, this, &GMainWindow::OnEmulationStopTimeExpired); 2023 connect(&shutdown_timer, &QTimer::timeout, this, &GMainWindow::OnEmulationStopTimeExpired);
2016 connect(emu_thread.get(), &QThread::finished, this, &GMainWindow::OnEmulationStopped); 2024 connect(emu_thread.get(), &QThread::finished, this, &GMainWindow::OnEmulationStopped);
2017 2025
@@ -3265,7 +3273,7 @@ void GMainWindow::OnPauseContinueGame() {
3265} 3273}
3266 3274
3267void GMainWindow::OnStopGame() { 3275void GMainWindow::OnStopGame() {
3268 if (system->GetExitLock() && !ConfirmForceLockedExit()) { 3276 if (system->GetExitLocked() && !ConfirmForceLockedExit()) {
3269 return; 3277 return;
3270 } 3278 }
3271 3279
@@ -4535,6 +4543,8 @@ void GMainWindow::RequestGameExit() {
4535 auto applet_ae = sm.GetService<Service::AM::AppletAE>("appletAE"); 4543 auto applet_ae = sm.GetService<Service::AM::AppletAE>("appletAE");
4536 bool has_signalled = false; 4544 bool has_signalled = false;
4537 4545
4546 system->SetExitRequested(true);
4547
4538 if (applet_oe != nullptr) { 4548 if (applet_oe != nullptr) {
4539 applet_oe->GetMessageQueue()->RequestExit(); 4549 applet_oe->GetMessageQueue()->RequestExit();
4540 has_signalled = true; 4550 has_signalled = true;