summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/android/app/build.gradle.kts16
-rw-r--r--src/android/app/src/main/AndroidManifest.xml1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt14
-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/adapters/DriverAdapter.kt117
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt186
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt75
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt41
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt158
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt62
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt97
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt228
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt112
-rw-r--r--src/android/app/src/main/jni/native.cpp1
-rw-r--r--src/android/app/src/main/res/drawable/ic_build.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_delete.xml9
-rw-r--r--src/android/app/src/main/res/layout/card_driver_option.xml89
-rw-r--r--src/android/app/src/main/res/layout/fragment_driver_manager.xml48
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml7
-rw-r--r--src/android/app/src/main/res/values-de/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-es/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-fr/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-it/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-ja/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-ko/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-nb/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-pl/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-pt-rBR/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-pt-rPT/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-ru/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-uk/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-zh-rTW/strings.xml2
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/strings.xml7
-rw-r--r--src/android/build.gradle.kts4
-rw-r--r--src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp9
-rw-r--r--src/audio_core/adsp/apps/audio_renderer/audio_renderer.h2
-rw-r--r--src/audio_core/sink/sink_stream.cpp4
-rw-r--r--src/common/CMakeLists.txt8
-rw-r--r--src/common/arm64/native_clock.cpp72
-rw-r--r--src/common/arm64/native_clock.h47
-rw-r--r--src/common/common_funcs.h4
-rw-r--r--src/common/elf.h8
-rw-r--r--src/common/fs/fs_paths.h2
-rw-r--r--src/common/fs/path_util.cpp2
-rw-r--r--src/common/fs/path_util.h2
-rw-r--r--src/common/polyfill_thread.h21
-rw-r--r--src/common/settings.cpp5
-rw-r--r--src/common/settings.h5
-rw-r--r--src/common/settings_enums.h2
-rw-r--r--src/common/string_util.cpp5
-rw-r--r--src/common/string_util.h1
-rw-r--r--src/common/wall_clock.cpp8
-rw-r--r--src/core/CMakeLists.txt8
-rw-r--r--src/core/core.cpp11
-rw-r--r--src/core/debugger/gdbstub.cpp17
-rw-r--r--src/core/file_sys/fsmitm_romfsbuild.cpp84
-rw-r--r--src/core/file_sys/patch_manager.cpp8
-rw-r--r--src/core/file_sys/program_metadata.cpp8
-rw-r--r--src/core/file_sys/program_metadata.h1
-rw-r--r--src/core/file_sys/registered_cache.cpp3
-rw-r--r--src/core/file_sys/romfs.cpp5
-rw-r--r--src/core/file_sys/vfs_cached.cpp6
-rw-r--r--src/core/file_sys/vfs_cached.h2
-rw-r--r--src/core/file_sys/vfs_concat.cpp27
-rw-r--r--src/core/file_sys/vfs_concat.h12
-rw-r--r--src/core/file_sys/vfs_layered.cpp8
-rw-r--r--src/core/hle/kernel/k_page_table.cpp22
-rw-r--r--src/core/hle/kernel/k_page_table.h3
-rw-r--r--src/core/hle/kernel/k_transfer_memory.cpp89
-rw-r--r--src/core/hle/kernel/k_transfer_memory.h14
-rw-r--r--src/core/hle/kernel/kernel.cpp16
-rw-r--r--src/core/hle/kernel/svc/svc_transfer_memory.cpp54
-rw-r--r--src/core/hle/service/acc/acc.cpp55
-rw-r--r--src/core/hle/service/acc/acc.h3
-rw-r--r--src/core/hle/service/acc/acc_su.cpp6
-rw-r--r--src/core/hle/service/acc/profile_manager.h3
-rw-r--r--src/core/hle/service/am/am.cpp147
-rw-r--r--src/core/hle/service/am/am.h23
-rw-r--r--src/core/hle/service/am/applet_ae.cpp20
-rw-r--r--src/core/hle/service/am/applets/applet_error.cpp5
-rw-r--r--src/core/hle/service/caps/caps.cpp21
-rw-r--r--src/core/hle/service/caps/caps.h81
-rw-r--r--src/core/hle/service/caps/caps_a.cpp239
-rw-r--r--src/core/hle/service/caps/caps_a.h21
-rw-r--r--src/core/hle/service/caps/caps_c.cpp50
-rw-r--r--src/core/hle/service/caps/caps_c.h10
-rw-r--r--src/core/hle/service/caps/caps_manager.cpp386
-rw-r--r--src/core/hle/service/caps/caps_manager.h79
-rw-r--r--src/core/hle/service/caps/caps_result.h35
-rw-r--r--src/core/hle/service/caps/caps_sc.cpp5
-rw-r--r--src/core/hle/service/caps/caps_sc.h6
-rw-r--r--src/core/hle/service/caps/caps_ss.cpp5
-rw-r--r--src/core/hle/service/caps/caps_ss.h6
-rw-r--r--src/core/hle/service/caps/caps_su.cpp9
-rw-r--r--src/core/hle/service/caps/caps_su.h6
-rw-r--r--src/core/hle/service/caps/caps_types.h184
-rw-r--r--src/core/hle/service/caps/caps_u.cpp146
-rw-r--r--src/core/hle/service/caps/caps_u.h12
-rw-r--r--src/core/hle/service/hle_ipc.cpp58
-rw-r--r--src/core/hle/service/hle_ipc.h6
-rw-r--r--src/core/hle/service/jit/jit_context.cpp36
-rw-r--r--src/core/hle/service/nifm/nifm.cpp12
-rw-r--r--src/core/hle/service/nifm/nifm.h1
-rw-r--r--src/core/hle/service/ns/ns.cpp27
-rw-r--r--src/core/hle/service/ns/ns.h4
-rw-r--r--src/core/hle/service/pctl/pctl_module.cpp9
-rw-r--r--src/core/hle/service/prepo/prepo.cpp40
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp2
-rw-r--r--src/input_common/drivers/udp_client.cpp1
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h46
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h4
-rw-r--r--src/video_core/engines/draw_manager.cpp24
-rw-r--r--src/video_core/engines/draw_manager.h2
-rw-r--r--src/video_core/host1x/codecs/codec.cpp10
-rw-r--r--src/video_core/host_shaders/convert_d24s8_to_abgr8.frag8
-rw-r--r--src/video_core/host_shaders/convert_s8d24_to_abgr8.frag8
-rw-r--r--src/video_core/renderer_base.h3
-rw-r--r--src/video_core/renderer_opengl/gl_texture_cache.cpp14
-rw-r--r--src/video_core/renderer_opengl/gl_texture_cache.h2
-rw-r--r--src/video_core/renderer_opengl/maxwell_to_gl.h1
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp4
-rw-r--r--src/video_core/renderer_vulkan/maxwell_to_vk.cpp5
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.h4
-rw-r--r--src/video_core/renderer_vulkan/vk_blit_screen.cpp1
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.cpp67
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.h10
-rw-r--r--src/video_core/renderer_vulkan/vk_query_cache.cpp1
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp16
-rw-r--r--src/video_core/renderer_vulkan/vk_render_pass_cache.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp33
-rw-r--r--src/video_core/renderer_vulkan/vk_staging_buffer_pool.h5
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp45
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h2
-rw-r--r--src/video_core/surface.cpp3
-rw-r--r--src/video_core/surface.h4
-rw-r--r--src/video_core/texture_cache/format_lookup_table.cpp6
-rw-r--r--src/video_core/texture_cache/formatter.cpp8
-rw-r--r--src/video_core/texture_cache/formatter.h2
-rw-r--r--src/video_core/texture_cache/image_view_base.cpp1
-rw-r--r--src/video_core/texture_cache/samples_helper.h2
-rw-r--r--src/video_core/texture_cache/texture_cache.h1
-rw-r--r--src/video_core/texture_cache/util.cpp36
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp56
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp29
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h14
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h3
-rw-r--r--src/yuzu/CMakeLists.txt4
-rw-r--r--src/yuzu/applets/qt_controller.cpp39
-rw-r--r--src/yuzu/applets/qt_controller.h6
-rw-r--r--src/yuzu/applets/qt_controller.ui54
-rw-r--r--src/yuzu/configuration/config.cpp4
-rw-r--r--src/yuzu/configuration/configure_audio.cpp3
-rw-r--r--src/yuzu/configuration/configure_input.cpp36
-rw-r--r--src/yuzu/configuration/configure_input.h1
-rw-r--r--src/yuzu/configuration/configure_ui.cpp5
-rw-r--r--src/yuzu/configuration/configure_ui.ui7
-rw-r--r--src/yuzu/configuration/shared_translation.cpp11
-rw-r--r--src/yuzu/game_list.cpp23
-rw-r--r--src/yuzu/game_list.h7
-rw-r--r--src/yuzu/game_list_p.h26
-rw-r--r--src/yuzu/game_list_worker.cpp30
-rw-r--r--src/yuzu/game_list_worker.h11
-rw-r--r--src/yuzu/main.cpp291
-rw-r--r--src/yuzu/main.h26
-rw-r--r--src/yuzu/main.ui6
-rw-r--r--src/yuzu/play_time_manager.cpp179
-rw-r--r--src/yuzu/play_time_manager.h44
-rw-r--r--src/yuzu/uisettings.h16
-rw-r--r--src/yuzu/util/util.cpp102
-rw-r--r--src/yuzu/util/util.h14
176 files changed, 4126 insertions, 1026 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index 84a3308b7..ac43d84b7 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -27,7 +27,7 @@ android {
namespace = "org.yuzu.yuzu_emu"
compileSdkVersion = "android-34"
- ndkVersion = "25.2.9519653"
+ ndkVersion = "26.1.10909125"
buildFeatures {
viewBinding = true
@@ -203,23 +203,23 @@ ktlint {
}
dependencies {
- implementation("androidx.core:core-ktx:1.10.1")
+ implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
- implementation("androidx.recyclerview:recyclerview:1.3.0")
+ implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
- implementation("androidx.fragment:fragment-ktx:1.6.0")
+ implementation("androidx.fragment:fragment-ktx:1.6.1")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.google.android.material:material:1.9.0")
- implementation("androidx.preference:preference:1.2.0")
- implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
+ implementation("androidx.preference:preference-ktx:1.2.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.window:window:1.2.0-beta03")
implementation("org.ini4j:ini4j:0.5.4")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
- implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
- implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
+ implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
}
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 832c08e15..a67351727 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -28,7 +28,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:appCategory="game"
android:localeConfig="@xml/locales_config"
android:banner="@drawable/tv_banner"
- android:extractNativeLibs="true"
android:fullBackupContent="@xml/data_extraction_rules"
android:dataExtractionRules="@xml/data_extraction_rules_api_31"
android:enableOnBackInvokedCallback="true">
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 6e39e542b..115f72710 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
@@ -15,13 +15,9 @@ import androidx.annotation.Keep
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
-import org.yuzu.yuzu_emu.YuzuApplication.Companion.appContext
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath
-import org.yuzu.yuzu_emu.utils.FileUtil.exists
-import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
-import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
-import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
+import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
@@ -75,7 +71,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.openContentUri(path, openmode)
} else {
- openContentUri(appContext, path, openmode)
+ FileUtil.openContentUri(path, openmode)
}
}
@@ -85,7 +81,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.getFileSize(path)
} else {
- getFileSize(appContext, path)
+ FileUtil.getFileSize(path)
}
}
@@ -95,7 +91,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.exists(path)
} else {
- exists(appContext, path)
+ FileUtil.exists(path)
}
}
@@ -105,7 +101,7 @@ object NativeLibrary {
return if (isNativePath(path!!)) {
YuzuApplication.documentsTree!!.isDirectory(path)
} else {
- isDirectory(appContext, path)
+ FileUtil.isDirectory(path)
}
}
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 9561748cb..8c053670c 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
@@ -47,7 +47,7 @@ class YuzuApplication : Application() {
application = this
documentsTree = DocumentsTree()
DirectoryInitialization.start()
- GpuDriverHelper.initializeDriverParameters(applicationContext)
+ GpuDriverHelper.initializeDriverParameters()
NativeLibrary.logDeviceInfo()
createNotificationChannels()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
new file mode 100644
index 000000000..0e818cab9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -0,0 +1,117 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+
+class DriverAdapter(private val driverViewModel: DriverViewModel) :
+ ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
+ AsyncDifferConfig.Builder(DiffCallback()).build()
+ ) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
+ val binding =
+ CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return DriverViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = currentList.size
+
+ override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
+ holder.bind(currentList[position])
+
+ private fun onSelectDriver(position: Int) {
+ driverViewModel.setSelectedDriverIndex(position)
+ notifyItemChanged(driverViewModel.previouslySelectedDriver)
+ notifyItemChanged(driverViewModel.selectedDriver)
+ }
+
+ private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
+ if (driverViewModel.selectedDriver > position) {
+ driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
+ }
+ if (GpuDriverHelper.customDriverData == driverData.second) {
+ driverViewModel.setSelectedDriverIndex(0)
+ }
+ driverViewModel.driversToDelete.add(driverData.first)
+ driverViewModel.removeDriver(driverData)
+ notifyItemRemoved(position)
+ notifyItemChanged(driverViewModel.selectedDriver)
+ }
+
+ inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ private lateinit var driverData: Pair<String, GpuDriverMetadata>
+
+ fun bind(driverData: Pair<String, GpuDriverMetadata>) {
+ this.driverData = driverData
+ val driver = driverData.second
+
+ binding.apply {
+ radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
+ root.setOnClickListener {
+ onSelectDriver(bindingAdapterPosition)
+ }
+ buttonDelete.setOnClickListener {
+ onDeleteDriver(driverData, bindingAdapterPosition)
+ }
+
+ // Delay marquee by 3s
+ title.postDelayed(
+ {
+ title.isSelected = true
+ title.ellipsize = TextUtils.TruncateAt.MARQUEE
+ version.isSelected = true
+ version.ellipsize = TextUtils.TruncateAt.MARQUEE
+ description.isSelected = true
+ description.ellipsize = TextUtils.TruncateAt.MARQUEE
+ },
+ 3000
+ )
+ if (driver.name == null) {
+ title.setText(R.string.system_gpu_driver)
+ description.text = ""
+ version.text = ""
+ version.visibility = View.GONE
+ description.visibility = View.GONE
+ buttonDelete.visibility = View.GONE
+ } else {
+ title.text = driver.name
+ version.text = driver.version
+ description.text = driver.description
+ version.visibility = View.VISIBLE
+ description.visibility = View.VISIBLE
+ buttonDelete.visibility = View.VISIBLE
+ }
+ }
+ }
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
+ override fun areItemsTheSame(
+ oldItem: Pair<String, GpuDriverMetadata>,
+ newItem: Pair<String, GpuDriverMetadata>
+ ): Boolean {
+ return oldItem.first == newItem.first
+ }
+
+ override fun areContentsTheSame(
+ oldItem: Pair<String, GpuDriverMetadata>,
+ newItem: Pair<String, GpuDriverMetadata>
+ ): Boolean {
+ return oldItem.second == newItem.second
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
new file mode 100644
index 000000000..df21d74b2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -0,0 +1,186 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.DriverAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import java.io.File
+import java.io.IOException
+
+class DriverManagerFragment : Fragment() {
+ private var _binding: FragmentDriverManagerBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentDriverManagerBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ if (!driverViewModel.isInteractionAllowed) {
+ DriversLoadingDialogFragment().show(
+ childFragmentManager,
+ DriversLoadingDialogFragment.TAG
+ )
+ }
+
+ binding.toolbarDrivers.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ binding.buttonInstall.setOnClickListener {
+ getDriver.launch(arrayOf("application/zip"))
+ }
+
+ binding.listDrivers.apply {
+ layoutManager = GridLayoutManager(
+ requireContext(),
+ resources.getInteger(R.integer.grid_columns)
+ )
+ adapter = DriverAdapter(driverViewModel)
+ }
+
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ driverViewModel.driverList.collectLatest {
+ (binding.listDrivers.adapter as DriverAdapter).submitList(it)
+ }
+ }
+ launch {
+ driverViewModel.newDriverInstalled.collect {
+ if (_binding != null && it) {
+ (binding.listDrivers.adapter as DriverAdapter).apply {
+ notifyItemChanged(driverViewModel.previouslySelectedDriver)
+ notifyItemChanged(driverViewModel.selectedDriver)
+ driverViewModel.setNewDriverInstalled(false)
+ }
+ }
+ }
+ }
+ }
+
+ setInsets()
+ }
+
+ // Start installing requested driver
+ override fun onStop() {
+ super.onStop()
+ driverViewModel.onCloseDriverManager()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.toolbarDrivers.layoutParams = mlpAppBar
+
+ val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams
+ mlplistDrivers.leftMargin = leftInsets
+ mlplistDrivers.rightMargin = rightInsets
+ binding.listDrivers.layoutParams = mlplistDrivers
+
+ val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+ val mlpFab =
+ binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
+ mlpFab.leftMargin = leftInsets + fabSpacing
+ mlpFab.rightMargin = rightInsets + fabSpacing
+ mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+ binding.buttonInstall.layoutParams = mlpFab
+
+ binding.listDrivers.updatePadding(
+ bottom = barInsets.bottom +
+ resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+ )
+
+ windowInsets
+ }
+
+ private val getDriver =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.installing_driver,
+ false
+ ) {
+ val driverPath =
+ "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}"
+ val driverFile = File(driverPath)
+
+ // Ignore file exceptions when a user selects an invalid zip
+ try {
+ if (!GpuDriverHelper.copyDriverToInternalStorage(result)) {
+ throw IOException("Driver failed validation!")
+ }
+ } catch (_: IOException) {
+ if (driverFile.exists()) {
+ driverFile.delete()
+ }
+ return@newInstance getString(R.string.select_gpu_driver_error)
+ }
+
+ val driverData = GpuDriverHelper.getMetadataFromZip(driverFile)
+ val driverInList =
+ driverViewModel.driverList.value.firstOrNull { it.second == driverData }
+ if (driverInList != null) {
+ return@newInstance getString(R.string.driver_already_installed)
+ } else {
+ driverViewModel.addDriver(Pair(driverPath, driverData))
+ driverViewModel.setNewDriverInstalled(true)
+ }
+ return@newInstance Any()
+ }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
new file mode 100644
index 000000000..f8c34346a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+
+class DriversLoadingDialogFragment : DialogFragment() {
+ private val driverViewModel: DriverViewModel by activityViewModels()
+
+ private lateinit var binding: DialogProgressBarBinding
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ binding = DialogProgressBarBinding.inflate(layoutInflater)
+ binding.progressBar.isIndeterminate = true
+
+ isCancelable = false
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.loading)
+ .setView(binding.root)
+ .create()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = binding.root
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.areDriversLoading.collect { checkForDismiss() }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.isDriverReady.collect { checkForDismiss() }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
+ }
+ }
+ }
+ }
+
+ private fun checkForDismiss() {
+ if (driverViewModel.isInteractionAllowed) {
+ dismiss()
+ }
+ }
+
+ companion object {
+ const val TAG = "DriversLoadingDialogFragment"
+ }
+}
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 e6ad2aa77..598a9d42b 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
@@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -50,6 +51,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.overlay.InputOverlay
@@ -70,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var game: Game
private val emulationViewModel: EmulationViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
private var isInFoldableLayout = false
@@ -299,6 +302,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
}
+ launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ driverViewModel.isDriverReady.collect {
+ if (it && !emulationState.isRunning) {
+ if (!DirectoryInitialization.areDirectoriesReady) {
+ DirectoryInitialization.start()
+ }
+
+ updateScreenLayout()
+
+ emulationState.run(emulationActivity!!.isActivityRecreated)
+ }
+ }
+ }
+ }
}
}
@@ -332,17 +350,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
- override fun onResume() {
- super.onResume()
- if (!DirectoryInitialization.areDirectoriesReady) {
- DirectoryInitialization.start()
- }
-
- updateScreenLayout()
-
- emulationState.run(emulationActivity!!.isActivityRecreated)
- }
-
override fun onPause() {
if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
emulationState.pause()
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 8923c0ea2..fd9785075 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
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments
import android.Manifest
import android.content.ActivityNotFoundException
-import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
@@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -37,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
@@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() {
private lateinit var mainActivity: MainActivity
private val homeViewModel: HomeViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -107,13 +107,17 @@ class HomeSettingsFragment : Fragment() {
)
add(
HomeSetting(
- R.string.install_gpu_driver,
+ R.string.gpu_driver_manager,
R.string.install_gpu_driver_description,
- R.drawable.ic_exit,
- { driverInstaller() },
+ R.drawable.ic_build,
+ {
+ binding.root.findNavController()
+ .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
+ },
{ GpuDriverHelper.supportsCustomDriverLoading() },
R.string.custom_driver_not_supported,
- R.string.custom_driver_not_supported_description
+ R.string.custom_driver_not_supported_description,
+ driverViewModel.selectedDriverMetadata
)
)
add(
@@ -292,31 +296,6 @@ class HomeSettingsFragment : Fragment() {
}
}
- private fun driverInstaller() {
- // Get the driver name for the dialog message.
- var driverName = GpuDriverHelper.customDriverName
- if (driverName == null) {
- driverName = getString(R.string.system_gpu_driver)
- }
-
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(getString(R.string.select_gpu_driver_title))
- .setMessage(driverName)
- .setNegativeButton(android.R.string.cancel, null)
- .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
- GpuDriverHelper.installDefaultDriver(requireContext())
- Toast.makeText(
- requireContext(),
- R.string.select_gpu_driver_use_default,
- Toast.LENGTH_SHORT
- ).show()
- }
- .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
- mainActivity.getDriver.launch(arrayOf("application/zip"))
- }
- .show()
- }
-
private fun shareLog() {
val file = DocumentFile.fromSingleUri(
mainActivity,
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 f128deda8..7e467814d 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
@@ -10,8 +10,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
@@ -78,6 +78,10 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
requireActivity().supportFragmentManager,
MessageDialogFragment.TAG
)
+
+ else -> {
+ // Do nothing
+ }
}
taskViewModel.clear()
}
@@ -115,7 +119,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
private const val CANCELLABLE = "Cancellable"
fun newInstance(
- activity: AppCompatActivity,
+ activity: FragmentActivity,
titleId: Int,
cancellable: Boolean = false,
task: () -> Any
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
new file mode 100644
index 000000000..62945ad65
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+import java.io.BufferedOutputStream
+import java.io.File
+
+class DriverViewModel : ViewModel() {
+ private val _areDriversLoading = MutableStateFlow(false)
+ val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
+
+ private val _isDriverReady = MutableStateFlow(true)
+ val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
+
+ private val _isDeletingDrivers = MutableStateFlow(false)
+ val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
+
+ private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
+ val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
+
+ var previouslySelectedDriver = 0
+ var selectedDriver = -1
+
+ private val _selectedDriverMetadata =
+ MutableStateFlow(
+ GpuDriverHelper.customDriverData.name
+ ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+ )
+ val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
+
+ private val _newDriverInstalled = MutableStateFlow(false)
+ val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
+
+ val driversToDelete = mutableListOf<String>()
+
+ val isInteractionAllowed
+ get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
+
+ init {
+ _areDriversLoading.value = true
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ val drivers = GpuDriverHelper.getDrivers()
+ val currentDriverMetadata = GpuDriverHelper.customDriverData
+ for (i in drivers.indices) {
+ if (drivers[i].second == currentDriverMetadata) {
+ setSelectedDriverIndex(i)
+ break
+ }
+ }
+
+ // If a user had installed a driver before the manager was implemented, this zips
+ // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
+ // be indexed and exported as expected.
+ if (selectedDriver == -1) {
+ val driverToSave =
+ File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
+ driverToSave.createNewFile()
+ FileUtil.zipFromInternalStorage(
+ File(GpuDriverHelper.driverInstallationPath!!),
+ GpuDriverHelper.driverInstallationPath!!,
+ BufferedOutputStream(driverToSave.outputStream())
+ )
+ drivers.add(Pair(driverToSave.path, currentDriverMetadata))
+ setSelectedDriverIndex(drivers.size - 1)
+ }
+
+ _driverList.value = drivers
+ _areDriversLoading.value = false
+ }
+ }
+ }
+
+ fun setSelectedDriverIndex(value: Int) {
+ if (selectedDriver != -1) {
+ previouslySelectedDriver = selectedDriver
+ }
+ selectedDriver = value
+ }
+
+ fun setNewDriverInstalled(value: Boolean) {
+ _newDriverInstalled.value = value
+ }
+
+ fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
+ val driverIndex = _driverList.value.indexOfFirst { it == driverData }
+ if (driverIndex == -1) {
+ setSelectedDriverIndex(_driverList.value.size)
+ _driverList.value.add(driverData)
+ _selectedDriverMetadata.value = driverData.second.name
+ ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+ } else {
+ setSelectedDriverIndex(driverIndex)
+ }
+ }
+
+ fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
+ _driverList.value.remove(driverData)
+ }
+
+ fun onCloseDriverManager() {
+ _isDeletingDrivers.value = true
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ driversToDelete.forEach {
+ val driver = File(it)
+ if (driver.exists()) {
+ driver.delete()
+ }
+ }
+ driversToDelete.clear()
+ _isDeletingDrivers.value = false
+ }
+ }
+
+ if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
+ return
+ }
+
+ _isDriverReady.value = false
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ if (selectedDriver == 0) {
+ GpuDriverHelper.installDefaultDriver()
+ setDriverReady()
+ return@withContext
+ }
+
+ val driverToInstall = File(driverList.value[selectedDriver].first)
+ if (driverToInstall.exists()) {
+ GpuDriverHelper.installCustomDriver(driverToInstall)
+ } else {
+ GpuDriverHelper.installDefaultDriver()
+ }
+ setDriverReady()
+ }
+ }
+ }
+
+ private fun setDriverReady() {
+ _isDriverReady.value = true
+ _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
+ ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+ }
+}
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 0fa5df5e5..233aa4101 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
@@ -29,12 +29,10 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import java.io.File
import java.io.FilenameFilter
-import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -43,7 +41,6 @@ import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
-import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
@@ -343,11 +340,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
if (FileUtil.copyUriToInternalStorage(
- applicationContext,
result,
dstPath,
"prod.keys"
- )
+ ) != null
) {
if (NativeLibrary.reloadKeys()) {
Toast.makeText(
@@ -446,11 +442,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
if (FileUtil.copyUriToInternalStorage(
- applicationContext,
result,
dstPath,
"key_retail.bin"
- )
+ ) != null
) {
if (NativeLibrary.reloadKeys()) {
Toast.makeText(
@@ -469,59 +464,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
- val getDriver =
- registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
- if (result == null) {
- return@registerForActivityResult
- }
-
- val takeFlags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
- contentResolver.takePersistableUriPermission(
- result,
- takeFlags
- )
-
- val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
- progressBinding.progressBar.isIndeterminate = true
- val installationDialog = MaterialAlertDialogBuilder(this)
- .setTitle(R.string.installing_driver)
- .setView(progressBinding.root)
- .show()
-
- lifecycleScope.launch {
- withContext(Dispatchers.IO) {
- // Ignore file exceptions when a user selects an invalid zip
- try {
- GpuDriverHelper.installCustomDriver(applicationContext, result)
- } catch (_: IOException) {
- }
-
- withContext(Dispatchers.Main) {
- installationDialog.dismiss()
-
- val driverName = GpuDriverHelper.customDriverName
- if (driverName != null) {
- Toast.makeText(
- applicationContext,
- getString(
- R.string.select_gpu_driver_install_success,
- driverName
- ),
- Toast.LENGTH_SHORT
- ).show()
- } else {
- Toast.makeText(
- applicationContext,
- R.string.select_gpu_driver_error,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
- }
- }
-
val installGameUpdate = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments()
) { documents: List<Uri> ->
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
index cf226ad94..eafcf9e42 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
@@ -7,7 +7,6 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import java.io.File
import java.util.*
-import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
class DocumentsTree {
@@ -22,7 +21,7 @@ class DocumentsTree {
fun openContentUri(filepath: String, openMode: String?): Int {
val node = resolvePath(filepath) ?: return -1
- return FileUtil.openContentUri(YuzuApplication.appContext, node.uri.toString(), openMode)
+ return FileUtil.openContentUri(node.uri.toString(), openMode)
}
fun getFileSize(filepath: String): Long {
@@ -30,7 +29,7 @@ class DocumentsTree {
return if (node == null || node.isDirectory) {
0
} else {
- FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString())
+ FileUtil.getFileSize(node.uri.toString())
}
}
@@ -67,7 +66,7 @@ class DocumentsTree {
* @param parent parent node of this level
*/
private fun structTree(parent: DocumentsNode) {
- val documents = FileUtil.listFiles(YuzuApplication.appContext, parent.uri!!)
+ val documents = FileUtil.listFiles(parent.uri!!)
for (document in documents) {
val node = DocumentsNode(document)
node.parent = parent
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index c3f53f1c5..5ee74a52c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -3,7 +3,6 @@
package org.yuzu.yuzu_emu.utils
-import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
@@ -11,7 +10,6 @@ import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.flow.StateFlow
import java.io.BufferedInputStream
import java.io.File
-import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.URLDecoder
@@ -21,6 +19,8 @@ import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
import org.yuzu.yuzu_emu.model.TaskState
import java.io.BufferedOutputStream
+import java.lang.NullPointerException
+import java.nio.charset.StandardCharsets
import java.util.zip.ZipOutputStream
object FileUtil {
@@ -29,6 +29,8 @@ object FileUtil {
const val APPLICATION_OCTET_STREAM = "application/octet-stream"
const val TEXT_PLAIN = "text/plain"
+ private val context get() = YuzuApplication.appContext
+
/**
* Create a file from directory with filename.
* @param context Application context
@@ -36,11 +38,11 @@ object FileUtil {
* @param filename file display name.
* @return boolean
*/
- fun createFile(context: Context?, directory: String?, filename: String): DocumentFile? {
+ fun createFile(directory: String?, filename: String): DocumentFile? {
var decodedFilename = filename
try {
val directoryUri = Uri.parse(directory)
- val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+ val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)
var mimeType = APPLICATION_OCTET_STREAM
if (decodedFilename.endsWith(".txt")) {
@@ -56,16 +58,15 @@ object FileUtil {
/**
* Create a directory from directory with filename.
- * @param context Application context
* @param directory parent path for directory.
* @param directoryName directory display name.
* @return boolean
*/
- fun createDir(context: Context?, directory: String?, directoryName: String?): DocumentFile? {
+ fun createDir(directory: String?, directoryName: String?): DocumentFile? {
var decodedDirectoryName = directoryName
try {
val directoryUri = Uri.parse(directory)
- val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+ val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)
val isExist = parent.findFile(decodedDirectoryName)
return isExist ?: parent.createDirectory(decodedDirectoryName)
@@ -77,13 +78,12 @@ object FileUtil {
/**
* Open content uri and return file descriptor to JNI.
- * @param context Application context
* @param path Native content uri path
* @param openMode will be one of "r", "r", "rw", "wa", "rwa"
* @return file descriptor
*/
@JvmStatic
- fun openContentUri(context: Context, path: String, openMode: String?): Int {
+ fun openContentUri(path: String, openMode: String?): Int {
try {
val uri = Uri.parse(path)
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!)
@@ -103,11 +103,10 @@ object FileUtil {
/**
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
* This function will be faster than DoucmentFile.listFiles
- * @param context Application context
* @param uri Directory uri.
* @return CheapDocument lists.
*/
- fun listFiles(context: Context, uri: Uri): Array<MinimalDocumentFile> {
+ fun listFiles(uri: Uri): Array<MinimalDocumentFile> {
val resolver = context.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
@@ -145,7 +144,7 @@ object FileUtil {
* @param path Native content uri path
* @return bool
*/
- fun exists(context: Context, path: String?): Boolean {
+ fun exists(path: String?): Boolean {
var c: Cursor? = null
try {
val mUri = Uri.parse(path)
@@ -165,7 +164,7 @@ object FileUtil {
* @param path content uri path
* @return bool
*/
- fun isDirectory(context: Context, path: String): Boolean {
+ fun isDirectory(path: String): Boolean {
val resolver = context.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_MIME_TYPE
@@ -210,10 +209,10 @@ object FileUtil {
return filename
}
- fun getFilesName(context: Context, path: String): Array<String> {
+ fun getFilesName(path: String): Array<String> {
val uri = Uri.parse(path)
val files: MutableList<String> = ArrayList()
- for (file in listFiles(context, uri)) {
+ for (file in listFiles(uri)) {
files.add(file.filename)
}
return files.toTypedArray()
@@ -225,7 +224,7 @@ object FileUtil {
* @return long file size
*/
@JvmStatic
- fun getFileSize(context: Context, path: String): Long {
+ fun getFileSize(path: String): Long {
val resolver = context.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_SIZE
@@ -245,44 +244,38 @@ object FileUtil {
return size
}
+ /**
+ * Creates an input stream with a given [Uri] and copies its data to the given path. This will
+ * overwrite any pre-existing files.
+ *
+ * @param sourceUri The [Uri] to copy data from
+ * @param destinationParentPath Destination directory
+ * @param destinationFilename Optionally renames the file once copied
+ */
fun copyUriToInternalStorage(
- context: Context,
- sourceUri: Uri?,
+ sourceUri: Uri,
destinationParentPath: String,
- destinationFilename: String
- ): Boolean {
- var input: InputStream? = null
- var output: FileOutputStream? = null
+ destinationFilename: String = ""
+ ): File? =
try {
- input = context.contentResolver.openInputStream(sourceUri!!)
- output = FileOutputStream("$destinationParentPath/$destinationFilename")
- val buffer = ByteArray(1024)
- var len: Int
- while (input!!.read(buffer).also { len = it } != -1) {
- output.write(buffer, 0, len)
- }
- output.flush()
- return true
- } catch (e: Exception) {
- Log.error("[FileUtil]: Cannot copy file, error: " + e.message)
- } finally {
- if (input != null) {
- try {
- input.close()
- } catch (e: IOException) {
- Log.error("[FileUtil]: Cannot close input file, error: " + e.message)
- }
+ val fileName =
+ if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename"
+ val inputStream = context.contentResolver.openInputStream(sourceUri)!!
+
+ val destinationFile = File("$destinationParentPath$fileName")
+ if (destinationFile.exists()) {
+ destinationFile.delete()
}
- if (output != null) {
- try {
- output.close()
- } catch (e: IOException) {
- Log.error("[FileUtil]: Cannot close output file, error: " + e.message)
- }
+
+ destinationFile.outputStream().use { fos ->
+ inputStream.use { it.copyTo(fos) }
}
+ destinationFile
+ } catch (e: IOException) {
+ null
+ } catch (e: NullPointerException) {
+ null
}
- return false
- }
/**
* Extracts the given zip file into the given directory.
@@ -368,4 +361,12 @@ object FileUtil {
return fileName.substring(fileName.lastIndexOf(".") + 1)
.lowercase()
}
+
+ @Throws(IOException::class)
+ fun getStringFromFile(file: File): String =
+ String(file.readBytes(), StandardCharsets.UTF_8)
+
+ @Throws(IOException::class)
+ fun getStringFromInputStream(stream: InputStream): String =
+ String(stream.readBytes(), StandardCharsets.UTF_8)
}
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 e0ee29c9b..9001ca9ab 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
@@ -30,7 +30,7 @@ object GameHelper {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
- addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3)
+ addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
// Cache list of games found on disk
val serializedGames = mutableSetOf<String>()
@@ -58,7 +58,7 @@ object GameHelper {
if (it.isDirectory) {
addGamesRecursive(
games,
- FileUtil.listFiles(YuzuApplication.appContext, it.uri),
+ FileUtil.listFiles(it.uri),
depth - 1
)
} else {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index 1d4695a2a..f6882ce6c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -3,64 +3,33 @@
package org.yuzu.yuzu_emu.utils
-import android.content.Context
import android.net.Uri
+import android.os.Build
import java.io.BufferedInputStream
import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
import java.io.IOException
-import java.util.zip.ZipInputStream
import org.yuzu.yuzu_emu.NativeLibrary
-import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
+import org.yuzu.yuzu_emu.YuzuApplication
+import java.util.zip.ZipException
+import java.util.zip.ZipFile
object GpuDriverHelper {
private const val META_JSON_FILENAME = "meta.json"
- private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"
private var fileRedirectionPath: String? = null
- private var driverInstallationPath: String? = null
+ var driverInstallationPath: String? = null
private var hookLibPath: String? = null
- @Throws(IOException::class)
- private fun unzip(zipFilePath: String, destDir: String) {
- val dir = File(destDir)
-
- // Create output directory if it doesn't exist
- if (!dir.exists()) dir.mkdirs()
-
- // Unpack the files.
- val inputStream = FileInputStream(zipFilePath)
- val zis = ZipInputStream(BufferedInputStream(inputStream))
- val buffer = ByteArray(1024)
- var ze = zis.nextEntry
- while (ze != null) {
- val newFile = File(destDir, ze.name)
- val canonicalPath = newFile.canonicalPath
- if (!canonicalPath.startsWith(destDir + ze.name)) {
- throw SecurityException("Zip file attempted path traversal! " + ze.name)
- }
-
- newFile.parentFile!!.mkdirs()
- val fos = FileOutputStream(newFile)
- var len: Int
- while (zis.read(buffer).also { len = it } > 0) {
- fos.write(buffer, 0, len)
- }
- fos.close()
- zis.closeEntry()
- ze = zis.nextEntry
- }
- zis.closeEntry()
- }
+ val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
- fun initializeDriverParameters(context: Context) {
+ fun initializeDriverParameters() {
try {
// Initialize the file redirection directory.
- fileRedirectionPath =
- context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
+ fileRedirectionPath = YuzuApplication.appContext
+ .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
// Initialize the driver installation directory.
- driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/"
+ driverInstallationPath = YuzuApplication.appContext
+ .filesDir.canonicalPath + "/gpu_driver/"
} catch (e: IOException) {
throw RuntimeException(e)
}
@@ -69,68 +38,169 @@ object GpuDriverHelper {
initializeDirectories()
// Initialize hook libraries directory.
- hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
+ hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"
// Initialize GPU driver.
NativeLibrary.initializeGpuDriver(
hookLibPath,
driverInstallationPath,
- customDriverLibraryName,
+ customDriverData.libraryName,
fileRedirectionPath
)
}
- fun installDefaultDriver(context: Context) {
+ fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> {
+ val driverZips = File(driverStoragePath).listFiles()
+ val drivers: MutableList<Pair<String, GpuDriverMetadata>> =
+ driverZips
+ ?.mapNotNull {
+ val metadata = getMetadataFromZip(it)
+ metadata.name?.let { _ -> Pair(it.path, metadata) }
+ }
+ ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
+ ?.distinct()
+ ?.toMutableList() ?: mutableListOf()
+
+ // TODO: Get system driver information
+ drivers.add(0, Pair("", GpuDriverMetadata()))
+ return drivers
+ }
+
+ fun installDefaultDriver() {
// Removing the installed driver will result in the backend using the default system driver.
- val driverInstallationDir = File(driverInstallationPath!!)
- deleteRecursive(driverInstallationDir)
- initializeDriverParameters(context)
+ File(driverInstallationPath!!).deleteRecursively()
+ initializeDriverParameters()
+ }
+
+ fun copyDriverToInternalStorage(driverUri: Uri): Boolean {
+ // Ensure we have directories.
+ initializeDirectories()
+
+ // Copy the zip file URI to user data
+ val copiedFile =
+ FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
+
+ // Validate driver
+ val metadata = getMetadataFromZip(copiedFile)
+ if (metadata.name == null) {
+ copiedFile.delete()
+ return false
+ }
+
+ if (metadata.minApi > Build.VERSION.SDK_INT) {
+ copiedFile.delete()
+ return false
+ }
+ return true
}
- fun installCustomDriver(context: Context, driverPathUri: Uri?) {
+ /**
+ * Copies driver zip into user data directory so that it can be exported along with
+ * other user data and also unzipped into the installation directory
+ */
+ fun installCustomDriver(driverUri: Uri): Boolean {
// Revert to system default in the event the specified driver is bad.
- installDefaultDriver(context)
+ installDefaultDriver()
// Ensure we have directories.
initializeDirectories()
- // Copy the zip file URI into our private storage.
- copyUriToInternalStorage(
- context,
- driverPathUri,
- driverInstallationPath!!,
- DRIVER_INTERNAL_FILENAME
- )
+ // Copy the zip file URI to user data
+ val copiedFile =
+ FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
+
+ // Validate driver
+ val metadata = getMetadataFromZip(copiedFile)
+ if (metadata.name == null) {
+ copiedFile.delete()
+ return false
+ }
+
+ if (metadata.minApi > Build.VERSION.SDK_INT) {
+ copiedFile.delete()
+ return false
+ }
// Unzip the driver.
try {
- unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
+ FileUtil.unzipToInternalStorage(
+ BufferedInputStream(copiedFile.inputStream()),
+ File(driverInstallationPath!!)
+ )
} catch (e: SecurityException) {
- return
+ return false
}
// Initialize the driver parameters.
- initializeDriverParameters(context)
+ initializeDriverParameters()
+
+ return true
}
- external fun supportsCustomDriverLoading(): Boolean
+ /**
+ * Unzips driver into installation directory
+ */
+ fun installCustomDriver(driver: File): Boolean {
+ // Revert to system default in the event the specified driver is bad.
+ installDefaultDriver()
- // Parse the custom driver metadata to retrieve the name.
- val customDriverName: String?
- get() {
- val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
- return metadata.name
+ // Ensure we have directories.
+ initializeDirectories()
+
+ // Validate driver
+ val metadata = getMetadataFromZip(driver)
+ if (metadata.name == null) {
+ driver.delete()
+ return false
}
- // Parse the custom driver metadata to retrieve the library name.
- private val customDriverLibraryName: String?
- get() {
- // Parse the custom driver metadata to retrieve the library name.
- val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
- return metadata.libraryName
+ // Unzip the driver to the private installation directory
+ try {
+ FileUtil.unzipToInternalStorage(
+ BufferedInputStream(driver.inputStream()),
+ File(driverInstallationPath!!)
+ )
+ } catch (e: SecurityException) {
+ return false
}
- private fun initializeDirectories() {
+ // Initialize the driver parameters.
+ initializeDriverParameters()
+
+ return true
+ }
+
+ /**
+ * Takes in a zip file and reads the meta.json file for presentation to the UI
+ *
+ * @param driver Zip containing driver and meta.json file
+ * @return A non-null [GpuDriverMetadata] instance that may have null members
+ */
+ fun getMetadataFromZip(driver: File): GpuDriverMetadata {
+ try {
+ ZipFile(driver).use { zf ->
+ val entries = zf.entries()
+ while (entries.hasMoreElements()) {
+ val entry = entries.nextElement()
+ if (!entry.isDirectory && entry.name.lowercase().contains(".json")) {
+ zf.getInputStream(entry).use {
+ return GpuDriverMetadata(it, entry.size)
+ }
+ }
+ }
+ }
+ } catch (_: ZipException) {
+ }
+ return GpuDriverMetadata()
+ }
+
+ external fun supportsCustomDriverLoading(): Boolean
+
+ // Parse the custom driver metadata to retrieve the name.
+ val customDriverData: GpuDriverMetadata
+ get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
+
+ fun initializeDirectories() {
// Ensure the file redirection directory exists.
val fileRedirectionDir = File(fileRedirectionPath!!)
if (!fileRedirectionDir.exists()) {
@@ -141,14 +211,10 @@ object GpuDriverHelper {
if (!driverInstallationDir.exists()) {
driverInstallationDir.mkdirs()
}
- }
-
- private fun deleteRecursive(fileOrDirectory: File) {
- if (fileOrDirectory.isDirectory) {
- for (child in fileOrDirectory.listFiles()!!) {
- deleteRecursive(child)
- }
+ // Ensure the driver storage directory exists
+ val driverStorageDirectory = File(driverStoragePath)
+ if (!driverStorageDirectory.exists()) {
+ driverStorageDirectory.mkdirs()
}
- fileOrDirectory.delete()
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
index a4e64070a..511a4171a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
@@ -4,29 +4,29 @@
package org.yuzu.yuzu_emu.utils
import java.io.IOException
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.nio.file.Paths
import org.json.JSONException
import org.json.JSONObject
+import java.io.File
+import java.io.InputStream
-class GpuDriverMetadata(metadataFilePath: String) {
- var name: String? = null
- var description: String? = null
- var author: String? = null
- var vendor: String? = null
- var driverVersion: String? = null
- var minApi = 0
- var libraryName: String? = null
+class GpuDriverMetadata {
+ /**
+ * Tries to get driver metadata information from a meta.json [File]
+ *
+ * @param metadataFile meta.json file provided with a GPU driver
+ */
+ constructor(metadataFile: File) {
+ if (metadataFile.length() > MAX_META_SIZE_BYTES) {
+ return
+ }
- init {
try {
- val json = JSONObject(getStringFromFile(metadataFilePath))
+ val json = JSONObject(FileUtil.getStringFromFile(metadataFile))
name = json.getString("name")
description = json.getString("description")
author = json.getString("author")
vendor = json.getString("vendor")
- driverVersion = json.getString("driverVersion")
+ version = json.getString("driverVersion")
minApi = json.getInt("minApi")
libraryName = json.getString("libraryName")
} catch (e: JSONException) {
@@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) {
}
}
- companion object {
- @Throws(IOException::class)
- private fun getStringFromFile(filePath: String): String {
- val path = Paths.get(filePath)
- val bytes = Files.readAllBytes(path)
- return String(bytes, StandardCharsets.UTF_8)
+ /**
+ * Tries to get driver metadata information from an input stream that's intended to be
+ * from a zip file
+ *
+ * @param metadataStream ZipEntry input stream
+ * @param size Size of the file in bytes
+ */
+ constructor(metadataStream: InputStream, size: Long) {
+ if (size > MAX_META_SIZE_BYTES) {
+ return
}
+
+ try {
+ val json = JSONObject(FileUtil.getStringFromInputStream(metadataStream))
+ name = json.getString("name")
+ description = json.getString("description")
+ author = json.getString("author")
+ vendor = json.getString("vendor")
+ version = json.getString("driverVersion")
+ minApi = json.getInt("minApi")
+ libraryName = json.getString("libraryName")
+ } catch (e: JSONException) {
+ // JSON is malformed, ignore and treat as unsupported metadata.
+ } catch (e: IOException) {
+ // File is inaccessible, ignore and treat as unsupported metadata.
+ }
+ }
+
+ /**
+ * Creates an empty metadata instance
+ */
+ constructor()
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is GpuDriverMetadata) {
+ return false
+ }
+
+ return other.name == name &&
+ other.description == description &&
+ other.author == author &&
+ other.vendor == vendor &&
+ other.version == version &&
+ other.minApi == minApi &&
+ other.libraryName == libraryName
+ }
+
+ override fun hashCode(): Int {
+ var result = name?.hashCode() ?: 0
+ result = 31 * result + (description?.hashCode() ?: 0)
+ result = 31 * result + (author?.hashCode() ?: 0)
+ result = 31 * result + (vendor?.hashCode() ?: 0)
+ result = 31 * result + (version?.hashCode() ?: 0)
+ result = 31 * result + minApi
+ result = 31 * result + (libraryName?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString(): String =
+ """
+ Name - $name
+ Description - $description
+ Author - $author
+ Vendor - $vendor
+ Version - $version
+ Min API - $minApi
+ Library Name - $libraryName
+ """.trimMargin().trimIndent()
+
+ var name: String? = null
+ var description: String? = null
+ var author: String? = null
+ var vendor: String? = null
+ var version: String? = null
+ var minApi = 0
+ var libraryName: String? = null
+
+ companion object {
+ private const val MAX_META_SIZE_BYTES = 500000
}
}
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 9cf71680c..598f4e8bf 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -218,7 +218,6 @@ public:
return;
}
m_window->OnSurfaceChanged(m_native_window);
- m_system.Renderer().NotifySurfaceChanged();
}
void ConfigureFilesystemProvider(const std::string& filepath) {
diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml
new file mode 100644
index 000000000..91d52f1b8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_build.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_delete.xml b/src/android/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 000000000..d26a79711
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
+</vector>
diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml
new file mode 100644
index 000000000..1dd9a6d7d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_driver_option.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/materialCardViewOutlinedStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="12dp"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="center"
+ android:padding="16dp">
+
+ <RadioButton
+ android:id="@+id/radio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:clickable="false"
+ android:checked="false" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/title"
+ style="@style/TextAppearance.Material3.TitleMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="@string/select_gpu_driver_default" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/version"
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="@string/install_gpu_driver_description" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/description"
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="@string/install_gpu_driver_description" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/button_delete"
+ style="@style/Widget.Material3.Button.IconButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/delete"
+ android:tooltipText="@string/delete"
+ app:icon="@drawable/ic_delete"
+ app:iconTint="?attr/colorControlNormal" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
new file mode 100644
index 000000000..6cea2d164
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_licenses"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_drivers"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ app:liftOnScrollTargetViewId="@id/list_drivers">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_drivers"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:navigationIcon="@drawable/ic_back"
+ app:title="@string/gpu_driver_manager" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_drivers"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+ android:id="@+id/button_install"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:text="@string/install"
+ app:icon="@drawable/ic_add"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
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 2356b802b..82749359d 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -22,6 +22,9 @@
<action
android:id="@+id/action_homeSettingsFragment_to_installableFragment"
app:destination="@id/installableFragment" />
+ <action
+ android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"
+ app:destination="@id/driverManagerFragment" />
</fragment>
<fragment
@@ -95,5 +98,9 @@
android:id="@+id/installableFragment"
android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
android:label="InstallableFragment" />
+ <fragment
+ android:id="@+id/driverManagerFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
+ android:label="DriverManagerFragment" />
</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 dd0f36392..72a47fbdb 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -168,9 +168,7 @@
<string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string>
<string name="select_gpu_driver_install">Installieren</string>
<string name="select_gpu_driver_default">Standard</string>
- <string name="select_gpu_driver_install_success">%s wurde installiert</string>
<string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string>
- <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string>
<string name="system_gpu_driver">System GPU-Treiber</string>
<string name="installing_driver">Treiber wird installiert...</string>
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 d398f862f..e5bdd5889 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string>
<string name="select_gpu_driver_install">Instalar</string>
<string name="select_gpu_driver_default">Predeterminado</string>
- <string name="select_gpu_driver_install_success">Instalado %s</string>
<string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string>
- <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string>
<string name="system_gpu_driver">Driver GPU del sistema</string>
<string name="installing_driver">Instalando driver...</string>
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 a7abd9077..1e02828aa 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string>
<string name="select_gpu_driver_install">Installer</string>
<string name="select_gpu_driver_default">Défaut</string>
- <string name="select_gpu_driver_install_success">%s Installé</string>
<string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string>
- <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string>
<string name="system_gpu_driver">Pilote du GPU du système</string>
<string name="installing_driver">Installation du pilote...</string>
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 b18161801..09c9345b0 100644
--- a/src/android/app/src/main/res/values-it/strings.xml
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string>
<string name="select_gpu_driver_install">Installa</string>
<string name="select_gpu_driver_default">Predefinito</string>
- <string name="select_gpu_driver_install_success">Installato%s</string>
<string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string>
- <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string>
<string name="system_gpu_driver">Driver GPU del sistema</string>
<string name="installing_driver">Installando i driver...</string>
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 88fa5a0bb..a0ea78bef 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -170,9 +170,7 @@
<string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string>
<string name="select_gpu_driver_install">インストール</string>
<string name="select_gpu_driver_default">デフォルト</string>
- <string name="select_gpu_driver_install_success">%s をインストールしました</string>
<string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string>
- <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string>
<string name="system_gpu_driver">システムのGPUドライバ</string>
<string name="installing_driver">インストール中…</string>
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 4b658255c..214f95706 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string>
<string name="select_gpu_driver_install">설치</string>
<string name="select_gpu_driver_default">기본값</string>
- <string name="select_gpu_driver_install_success">설치된 %s</string>
<string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string>
- <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string>
<string name="system_gpu_driver">시스템 GPU 드라이버</string>
<string name="installing_driver">드라이버 설치 중...</string>
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 dd602a389..5443cef42 100644
--- a/src/android/app/src/main/res/values-nb/strings.xml
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string>
<string name="select_gpu_driver_install">Installer</string>
<string name="select_gpu_driver_default">Standard</string>
- <string name="select_gpu_driver_install_success">Installert %s</string>
<string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string>
- <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string>
<string name="system_gpu_driver">Systemets GPU-driver</string>
<string name="installing_driver">Installerer driver...</string>
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 2fdd1f952..899e233d0 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string>
<string name="select_gpu_driver_install">Zainstaluj</string>
<string name="select_gpu_driver_default">Domyślne</string>
- <string name="select_gpu_driver_install_success">Zainstalowano %s</string>
<string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string>
- <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string>
<string name="system_gpu_driver">Systemowy sterownik GPU</string>
<string name="installing_driver">Instalowanie sterownika...</string>
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 2f26367fe..caa095364 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
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
<string name="select_gpu_driver_install">Instalar</string>
<string name="select_gpu_driver_default">Padrão</string>
- <string name="select_gpu_driver_install_success">Instalado%s</string>
<string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
- <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
<string name="system_gpu_driver">Driver do GPU padrão</string>
<string name="installing_driver">A instalar o Driver...</string>
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 4e1eb4cd7..0a1a47fbb 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
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
<string name="select_gpu_driver_install">Instalar</string>
<string name="select_gpu_driver_default">Padrão</string>
- <string name="select_gpu_driver_install_success">Instalado%s</string>
<string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
- <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
<string name="system_gpu_driver">Driver do GPU padrão</string>
<string name="installing_driver">A instalar o Driver...</string>
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 f5695dc93..0bef035d6 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string>
<string name="select_gpu_driver_install">Установить</string>
<string name="select_gpu_driver_default">По умолчанию</string>
- <string name="select_gpu_driver_install_success">Установлено %s</string>
<string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string>
- <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string>
<string name="system_gpu_driver">Системный драйвер ГП</string>
<string name="installing_driver">Установка драйвера...</string>
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 061bc6f04..5b789ee98 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string>
<string name="select_gpu_driver_install">Встановити</string>
<string name="select_gpu_driver_default">За замовчуванням</string>
- <string name="select_gpu_driver_install_success">Встановлено %s</string>
<string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string>
- <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string>
<string name="system_gpu_driver">Системний драйвер ГП</string>
<string name="installing_driver">Встановлення драйвера...</string>
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 fe6dd5eaa..c0e885751 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
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string>
<string name="select_gpu_driver_install">安装</string>
<string name="select_gpu_driver_default">系统默认</string>
- <string name="select_gpu_driver_install_success">已安装 %s</string>
<string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string>
- <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string>
<string name="system_gpu_driver">系统 GPU 驱动程序</string>
<string name="installing_driver">正在安装驱动程序…</string>
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 9b3e54224..4a21bf893 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
@@ -171,9 +171,7 @@
<string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string>
<string name="select_gpu_driver_install">安裝</string>
<string name="select_gpu_driver_default">預設</string>
- <string name="select_gpu_driver_install_success">已安裝 %s</string>
<string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string>
- <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string>
<string name="system_gpu_driver">系統 GPU 驅動程式</string>
<string name="installing_driver">正在安裝驅動程式…</string>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 7b2296d95..ef855ea6f 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,6 +13,8 @@
<dimen name="menu_width">256dp</dimen>
<dimen name="card_width">165dp</dimen>
<dimen name="icon_inset">24dp</dimen>
+ <dimen name="spacing_bottom_list_fab">72dp</dimen>
+ <dimen name="spacing_fab">24dp</dimen>
<dimen name="dialog_margin">20dp</dimen>
<dimen name="elevated_app_bar">3dp</dimen>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index e51edf872..9e4854221 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -72,6 +72,7 @@
<string name="invalid_keys_error">Invalid encryption keys</string>
<string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
<string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
+ <string name="gpu_driver_manager">GPU Driver Manager</string>
<string name="install_gpu_driver">Install GPU driver</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
<string name="advanced_settings">Advanced settings</string>
@@ -234,15 +235,17 @@
<string name="export_failed">Export failed</string>
<string name="import_failed">Import failed</string>
<string name="cancelling">Cancelling</string>
+ <string name="install">Install</string>
+ <string name="delete">Delete</string>
<!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string>
<string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>
<string name="select_gpu_driver_install">Install</string>
<string name="select_gpu_driver_default">Default</string>
- <string name="select_gpu_driver_install_success">Installed %s</string>
<string name="select_gpu_driver_use_default">Using default GPU driver</string>
- <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string>
+ <string name="select_gpu_driver_error">Invalid driver selected</string>
+ <string name="driver_already_installed">Driver already installed</string>
<string name="system_gpu_driver">System GPU driver</string>
<string name="installing_driver">Installing driver…</string>
diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts
index 80f370c16..51e559321 100644
--- a/src/android/build.gradle.kts
+++ b/src/android/build.gradle.kts
@@ -3,8 +3,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id("com.android.application") version "8.0.2" apply false
- id("com.android.library") version "8.0.2" apply false
+ id("com.android.application") version "8.1.2" apply false
+ id("com.android.library") version "8.1.2" apply false
id("org.jetbrains.kotlin.android") version "1.8.21" apply false
}
diff --git a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp
index 972d5e45b..ef301d8b4 100644
--- a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp
+++ b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp
@@ -77,6 +77,7 @@ void AudioRenderer::Wait() {
"{}, got {}",
Message::RenderResponse, msg);
}
+ PostDSPClearCommandBuffer();
}
void AudioRenderer::Send(Direction dir, u32 message) {
@@ -96,6 +97,14 @@ void AudioRenderer::SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u
command_buffers[session_id].reset_buffer = reset;
}
+void AudioRenderer::PostDSPClearCommandBuffer() noexcept {
+ for (auto& buffer : command_buffers) {
+ buffer.buffer = 0;
+ buffer.size = 0;
+ buffer.reset_buffer = false;
+ }
+}
+
u32 AudioRenderer::GetRemainCommandCount(s32 session_id) const noexcept {
return command_buffers[session_id].remaining_command_count;
}
diff --git a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
index 85874d88a..57b89d9fe 100644
--- a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
+++ b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
@@ -85,6 +85,8 @@ private:
*/
void CreateSinkStreams();
+ void PostDSPClearCommandBuffer() noexcept;
+
/// Core system
Core::System& system;
/// The output sink the AudioRenderer will send samples to
diff --git a/src/audio_core/sink/sink_stream.cpp b/src/audio_core/sink/sink_stream.cpp
index 6081352a2..d66d04fae 100644
--- a/src/audio_core/sink/sink_stream.cpp
+++ b/src/audio_core/sink/sink_stream.cpp
@@ -204,6 +204,10 @@ void SinkStream::ProcessAudioOutAndRender(std::span<s16> output_buffer, std::siz
// paused and we'll desync, so just play silence.
if (system.IsPaused() || system.IsShuttingDown()) {
if (system.IsShuttingDown()) {
+ {
+ std::scoped_lock lk{release_mutex};
+ queued_buffers.store(0);
+ }
release_cv.notify_one();
}
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 416203c59..8a1861051 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -189,6 +189,14 @@ if(ARCHITECTURE_x86_64)
target_link_libraries(common PRIVATE xbyak::xbyak)
endif()
+if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
+ target_sources(common
+ PRIVATE
+ arm64/native_clock.cpp
+ arm64/native_clock.h
+ )
+endif()
+
if (MSVC)
target_compile_definitions(common PRIVATE
# The standard library doesn't provide any replacement for codecvt yet
diff --git a/src/common/arm64/native_clock.cpp b/src/common/arm64/native_clock.cpp
new file mode 100644
index 000000000..88fdba527
--- /dev/null
+++ b/src/common/arm64/native_clock.cpp
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/arm64/native_clock.h"
+
+namespace Common::Arm64 {
+
+namespace {
+
+NativeClock::FactorType GetFixedPointFactor(u64 num, u64 den) {
+ return (static_cast<NativeClock::FactorType>(num) << 64) / den;
+}
+
+u64 MultiplyHigh(u64 m, NativeClock::FactorType factor) {
+ return static_cast<u64>((m * factor) >> 64);
+}
+
+} // namespace
+
+NativeClock::NativeClock() {
+ const u64 host_cntfrq = GetHostCNTFRQ();
+ ns_cntfrq_factor = GetFixedPointFactor(NsRatio::den, host_cntfrq);
+ us_cntfrq_factor = GetFixedPointFactor(UsRatio::den, host_cntfrq);
+ ms_cntfrq_factor = GetFixedPointFactor(MsRatio::den, host_cntfrq);
+ guest_cntfrq_factor = GetFixedPointFactor(CNTFRQ, host_cntfrq);
+ gputick_cntfrq_factor = GetFixedPointFactor(GPUTickFreq, host_cntfrq);
+}
+
+std::chrono::nanoseconds NativeClock::GetTimeNS() const {
+ return std::chrono::nanoseconds{MultiplyHigh(GetHostTicksElapsed(), ns_cntfrq_factor)};
+}
+
+std::chrono::microseconds NativeClock::GetTimeUS() const {
+ return std::chrono::microseconds{MultiplyHigh(GetHostTicksElapsed(), us_cntfrq_factor)};
+}
+
+std::chrono::milliseconds NativeClock::GetTimeMS() const {
+ return std::chrono::milliseconds{MultiplyHigh(GetHostTicksElapsed(), ms_cntfrq_factor)};
+}
+
+u64 NativeClock::GetCNTPCT() const {
+ return MultiplyHigh(GetHostTicksElapsed(), guest_cntfrq_factor);
+}
+
+u64 NativeClock::GetGPUTick() const {
+ return MultiplyHigh(GetHostTicksElapsed(), gputick_cntfrq_factor);
+}
+
+u64 NativeClock::GetHostTicksNow() const {
+ u64 cntvct_el0 = 0;
+ asm volatile("dsb ish\n\t"
+ "mrs %[cntvct_el0], cntvct_el0\n\t"
+ "dsb ish\n\t"
+ : [cntvct_el0] "=r"(cntvct_el0));
+ return cntvct_el0;
+}
+
+u64 NativeClock::GetHostTicksElapsed() const {
+ return GetHostTicksNow();
+}
+
+bool NativeClock::IsNative() const {
+ return true;
+}
+
+u64 NativeClock::GetHostCNTFRQ() {
+ u64 cntfrq_el0 = 0;
+ asm("mrs %[cntfrq_el0], cntfrq_el0" : [cntfrq_el0] "=r"(cntfrq_el0));
+ return cntfrq_el0;
+}
+
+} // namespace Common::Arm64
diff --git a/src/common/arm64/native_clock.h b/src/common/arm64/native_clock.h
new file mode 100644
index 000000000..a28b419f2
--- /dev/null
+++ b/src/common/arm64/native_clock.h
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/wall_clock.h"
+
+namespace Common::Arm64 {
+
+class NativeClock final : public WallClock {
+public:
+ explicit NativeClock();
+
+ std::chrono::nanoseconds GetTimeNS() const override;
+
+ std::chrono::microseconds GetTimeUS() const override;
+
+ std::chrono::milliseconds GetTimeMS() const override;
+
+ u64 GetCNTPCT() const override;
+
+ u64 GetGPUTick() const override;
+
+ u64 GetHostTicksNow() const override;
+
+ u64 GetHostTicksElapsed() const override;
+
+ bool IsNative() const override;
+
+ static u64 GetHostCNTFRQ();
+
+public:
+ using FactorType = unsigned __int128;
+
+ FactorType GetGuestCNTFRQFactor() const {
+ return guest_cntfrq_factor;
+ }
+
+private:
+ FactorType ns_cntfrq_factor;
+ FactorType us_cntfrq_factor;
+ FactorType ms_cntfrq_factor;
+ FactorType guest_cntfrq_factor;
+ FactorType gputick_cntfrq_factor;
+};
+
+} // namespace Common::Arm64
diff --git a/src/common/common_funcs.h b/src/common/common_funcs.h
index 0dad9338a..47d028d48 100644
--- a/src/common/common_funcs.h
+++ b/src/common/common_funcs.h
@@ -39,8 +39,12 @@
#define Crash() exit(1)
#endif
+#define LTO_NOINLINE __attribute__((noinline))
+
#else // _MSC_VER
+#define LTO_NOINLINE
+
// Locale Cross-Compatibility
#define locale_t _locale_t
diff --git a/src/common/elf.h b/src/common/elf.h
index 14a5e9597..0b728dc54 100644
--- a/src/common/elf.h
+++ b/src/common/elf.h
@@ -211,6 +211,11 @@ struct Elf64_Rela {
Elf64_Sxword r_addend; /* Addend */
};
+/* RELR relocation table entry */
+
+using Elf32_Relr = Elf32_Word;
+using Elf64_Relr = Elf64_Xword;
+
/* How to extract and insert information held in the r_info field. */
static inline u32 Elf32RelSymIndex(Elf32_Word r_info) {
@@ -328,6 +333,9 @@ constexpr u32 ElfDtFiniArray = 26; /* Array with addresses of fini fct */
constexpr u32 ElfDtInitArraySz = 27; /* Size in bytes of DT_INIT_ARRAY */
constexpr u32 ElfDtFiniArraySz = 28; /* Size in bytes of DT_FINI_ARRAY */
constexpr u32 ElfDtSymtabShndx = 34; /* Address of SYMTAB_SHNDX section */
+constexpr u32 ElfDtRelrsz = 35; /* Size of RELR relative relocations */
+constexpr u32 ElfDtRelr = 36; /* Address of RELR relative relocations */
+constexpr u32 ElfDtRelrent = 37; /* Size of one RELR relative relocation */
} // namespace ELF
} // namespace Common
diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h
index 61bac9eba..441c8af97 100644
--- a/src/common/fs/fs_paths.h
+++ b/src/common/fs/fs_paths.h
@@ -18,10 +18,12 @@
#define LOAD_DIR "load"
#define LOG_DIR "log"
#define NAND_DIR "nand"
+#define PLAY_TIME_DIR "play_time"
#define SCREENSHOTS_DIR "screenshots"
#define SDMC_DIR "sdmc"
#define SHADER_DIR "shader"
#define TAS_DIR "tas"
+#define ICONS_DIR "icons"
// yuzu-specific files
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index dce219fcf..0abd81a45 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -124,10 +124,12 @@ public:
GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR);
GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR);
GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR);
+ GenerateYuzuPath(YuzuPath::PlayTimeDir, yuzu_path / PLAY_TIME_DIR);
GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR);
GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
+ GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR);
}
private:
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index ba28964d0..63801c924 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -20,10 +20,12 @@ enum class YuzuPath {
LoadDir, // Where cheat/mod files are stored.
LogDir, // Where log files are stored.
NANDDir, // Where the emulated NAND is stored.
+ PlayTimeDir, // Where play time data is stored.
ScreenshotsDir, // Where yuzu screenshots are stored.
SDMCDir, // Where the emulated SDMC is stored.
ShaderDir, // Where shaders are stored.
TASDir, // Where TAS scripts are stored.
+ IconsDir, // Where Icons for Windows shortcuts are stored.
};
/**
diff --git a/src/common/polyfill_thread.h b/src/common/polyfill_thread.h
index 41cbb9ed5..12e59a893 100644
--- a/src/common/polyfill_thread.h
+++ b/src/common/polyfill_thread.h
@@ -15,12 +15,13 @@
#include <condition_variable>
#include <stop_token>
#include <thread>
+#include <utility>
namespace Common {
template <typename Condvar, typename Lock, typename Pred>
void CondvarWait(Condvar& cv, std::unique_lock<Lock>& lk, std::stop_token token, Pred&& pred) {
- cv.wait(lk, token, std::move(pred));
+ cv.wait(lk, token, std::forward<Pred>(pred));
}
template <typename Rep, typename Period>
@@ -109,7 +110,7 @@ public:
// Insert the callback.
stop_state_callback ret = ++m_next_callback;
- m_callbacks.emplace(ret, move(f));
+ m_callbacks.emplace(ret, std::move(f));
return ret;
}
@@ -162,7 +163,7 @@ private:
friend class stop_source;
template <typename Callback>
friend class stop_callback;
- stop_token(shared_ptr<polyfill::stop_state> stop_state) : m_stop_state(move(stop_state)) {}
+ stop_token(shared_ptr<polyfill::stop_state> stop_state) : m_stop_state(std::move(stop_state)) {}
private:
shared_ptr<polyfill::stop_state> m_stop_state;
@@ -198,7 +199,7 @@ public:
private:
friend class jthread;
explicit stop_source(shared_ptr<polyfill::stop_state> stop_state)
- : m_stop_state(move(stop_state)) {}
+ : m_stop_state(std::move(stop_state)) {}
private:
shared_ptr<polyfill::stop_state> m_stop_state;
@@ -218,16 +219,16 @@ public:
C&& cb) noexcept(is_nothrow_constructible_v<Callback, C>)
: m_stop_state(st.m_stop_state) {
if (m_stop_state) {
- m_callback = m_stop_state->insert_callback(move(cb));
+ m_callback = m_stop_state->insert_callback(std::move(cb));
}
}
template <typename C>
requires constructible_from<Callback, C>
explicit stop_callback(stop_token&& st,
C&& cb) noexcept(is_nothrow_constructible_v<Callback, C>)
- : m_stop_state(move(st.m_stop_state)) {
+ : m_stop_state(std::move(st.m_stop_state)) {
if (m_stop_state) {
- m_callback = m_stop_state->insert_callback(move(cb));
+ m_callback = m_stop_state->insert_callback(std::move(cb));
}
}
~stop_callback() {
@@ -260,7 +261,7 @@ public:
typename = enable_if_t<!is_same_v<remove_cvref_t<F>, jthread>>>
explicit jthread(F&& f, Args&&... args)
: m_stop_state(make_shared<polyfill::stop_state>()),
- m_thread(make_thread(move(f), move(args)...)) {}
+ m_thread(make_thread(std::forward<F>(f), std::forward<Args>(args)...)) {}
~jthread() {
if (joinable()) {
@@ -317,9 +318,9 @@ private:
template <typename F, typename... Args>
thread make_thread(F&& f, Args&&... args) {
if constexpr (is_invocable_v<decay_t<F>, stop_token, decay_t<Args>...>) {
- return thread(move(f), get_stop_token(), move(args)...);
+ return thread(std::forward<F>(f), get_stop_token(), std::forward<Args>(args)...);
} else {
- return thread(move(f), move(args)...);
+ return thread(std::forward<F>(f), std::forward<Args>(args)...);
}
}
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 3fde3cae6..98b43e49c 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -45,6 +45,7 @@ SWITCHABLE(CpuAccuracy, true);
SWITCHABLE(FullscreenMode, true);
SWITCHABLE(GpuAccuracy, true);
SWITCHABLE(Language, true);
+SWITCHABLE(MemoryLayout, true);
SWITCHABLE(NvdecEmulation, false);
SWITCHABLE(Region, true);
SWITCHABLE(RendererBackend, true);
@@ -61,6 +62,10 @@ SWITCHABLE(u32, false);
SWITCHABLE(u8, false);
SWITCHABLE(u8, true);
+// Used in UISettings
+// TODO see if we can move this to uisettings.cpp
+SWITCHABLE(ConfirmStop, true);
+
#undef SETTING
#undef SWITCHABLE
#endif
diff --git a/src/common/settings.h b/src/common/settings.h
index 98ab0ec2e..236e33bee 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -67,6 +67,7 @@ SWITCHABLE(CpuAccuracy, true);
SWITCHABLE(FullscreenMode, true);
SWITCHABLE(GpuAccuracy, true);
SWITCHABLE(Language, true);
+SWITCHABLE(MemoryLayout, true);
SWITCHABLE(NvdecEmulation, false);
SWITCHABLE(Region, true);
SWITCHABLE(RendererBackend, true);
@@ -83,6 +84,10 @@ SWITCHABLE(u32, false);
SWITCHABLE(u8, false);
SWITCHABLE(u8, true);
+// Used in UISettings
+// TODO see if we can move this to uisettings.h
+SWITCHABLE(ConfirmStop, true);
+
#undef SETTING
#undef SWITCHABLE
#endif
diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h
index 815cafe15..11429d7a8 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -133,6 +133,8 @@ ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid);
ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb);
+ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never);
+
ENUM(FullscreenMode, Borderless, Exclusive);
ENUM(NvdecEmulation, Off, Cpu, Gpu);
diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp
index feab1653d..4c7aba3f5 100644
--- a/src/common/string_util.cpp
+++ b/src/common/string_util.cpp
@@ -135,6 +135,11 @@ std::u16string UTF8ToUTF16(std::string_view input) {
return convert.from_bytes(input.data(), input.data() + input.size());
}
+std::u32string UTF8ToUTF32(std::string_view input) {
+ std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert;
+ return convert.from_bytes(input.data(), input.data() + input.size());
+}
+
#ifdef _WIN32
static std::wstring CPToUTF16(u32 code_page, std::string_view input) {
const auto size =
diff --git a/src/common/string_util.h b/src/common/string_util.h
index c351f1a0c..9da1ca4e9 100644
--- a/src/common/string_util.h
+++ b/src/common/string_util.h
@@ -38,6 +38,7 @@ bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _
[[nodiscard]] std::string UTF16ToUTF8(std::u16string_view input);
[[nodiscard]] std::u16string UTF8ToUTF16(std::string_view input);
+[[nodiscard]] std::u32string UTF8ToUTF32(std::string_view input);
#ifdef _WIN32
[[nodiscard]] std::string UTF16ToUTF8(std::wstring_view input);
diff --git a/src/common/wall_clock.cpp b/src/common/wall_clock.cpp
index 71e15ab4c..caca9a123 100644
--- a/src/common/wall_clock.cpp
+++ b/src/common/wall_clock.cpp
@@ -10,6 +10,10 @@
#include "common/x64/rdtsc.h"
#endif
+#if defined(ARCHITECTURE_arm64) && defined(__linux__)
+#include "common/arm64/native_clock.h"
+#endif
+
namespace Common {
class StandardWallClock final : public WallClock {
@@ -53,7 +57,7 @@ private:
};
std::unique_ptr<WallClock> CreateOptimalClock() {
-#ifdef ARCHITECTURE_x86_64
+#if defined(ARCHITECTURE_x86_64)
const auto& caps = GetCPUCaps();
if (caps.invariant_tsc && caps.tsc_frequency >= std::nano::den) {
@@ -64,6 +68,8 @@ std::unique_ptr<WallClock> CreateOptimalClock() {
// - Is not more precise than 1 GHz (1ns resolution)
return std::make_unique<StandardWallClock>();
}
+#elif defined(ARCHITECTURE_arm64) && defined(__linux__)
+ return std::make_unique<Arm64::NativeClock>();
#else
return std::make_unique<StandardWallClock>();
#endif
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index e02ededfc..e4f499135 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -466,14 +466,18 @@ add_library(core STATIC
hle/service/caps/caps_a.h
hle/service/caps/caps_c.cpp
hle/service/caps/caps_c.h
- hle/service/caps/caps_u.cpp
- hle/service/caps/caps_u.h
+ hle/service/caps/caps_manager.cpp
+ hle/service/caps/caps_manager.h
+ hle/service/caps/caps_result.h
hle/service/caps/caps_sc.cpp
hle/service/caps/caps_sc.h
hle/service/caps/caps_ss.cpp
hle/service/caps/caps_ss.h
hle/service/caps/caps_su.cpp
hle/service/caps/caps_su.h
+ hle/service/caps/caps_types.h
+ hle/service/caps/caps_u.cpp
+ hle/service/caps/caps_u.h
hle/service/erpt/erpt.cpp
hle/service/erpt/erpt.h
hle/service/es/es.cpp
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 08cbb8978..d7e2efbd7 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -116,11 +116,8 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
}
}
- if (concat.empty()) {
- return nullptr;
- }
-
- return FileSys::ConcatenatedVfsFile::MakeConcatenatedFile(concat, dir->GetName());
+ return FileSys::ConcatenatedVfsFile::MakeConcatenatedFile(dir->GetName(),
+ std::move(concat));
}
if (Common::FS::IsDir(path)) {
@@ -1078,6 +1075,10 @@ void System::ApplySettings() {
impl->RefreshTime();
if (IsPoweredOn()) {
+ if (Settings::values.custom_rtc_enabled) {
+ const s64 posix_time{Settings::values.custom_rtc.GetValue()};
+ GetTimeManager().UpdateLocalSystemClockTime(posix_time);
+ }
Renderer().RefreshBaseSettings();
}
}
diff --git a/src/core/debugger/gdbstub.cpp b/src/core/debugger/gdbstub.cpp
index e55831f27..82964f0a1 100644
--- a/src/core/debugger/gdbstub.cpp
+++ b/src/core/debugger/gdbstub.cpp
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
+#include <codecvt>
+#include <locale>
#include <numeric>
#include <optional>
#include <thread>
@@ -12,6 +14,7 @@
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "common/settings.h"
+#include "common/string_util.h"
#include "core/arm/arm_interface.h"
#include "core/core.h"
#include "core/debugger/gdbstub.h"
@@ -68,10 +71,16 @@ static std::string EscapeGDB(std::string_view data) {
}
static std::string EscapeXML(std::string_view data) {
+ std::u32string converted = U"[Encoding error]";
+ try {
+ converted = Common::UTF8ToUTF32(data);
+ } catch (std::range_error&) {
+ }
+
std::string escaped;
escaped.reserve(data.size());
- for (char c : data) {
+ for (char32_t c : converted) {
switch (c) {
case '&':
escaped += "&amp;";
@@ -86,7 +95,11 @@ static std::string EscapeXML(std::string_view data) {
escaped += "&gt;";
break;
default:
- escaped += c;
+ if (c > 0x7f) {
+ escaped += fmt::format("&#{};", static_cast<u32>(c));
+ } else {
+ escaped += static_cast<char>(c);
+ }
break;
}
}
diff --git a/src/core/file_sys/fsmitm_romfsbuild.cpp b/src/core/file_sys/fsmitm_romfsbuild.cpp
index e39c7b62b..f1d3e4129 100644
--- a/src/core/file_sys/fsmitm_romfsbuild.cpp
+++ b/src/core/file_sys/fsmitm_romfsbuild.cpp
@@ -107,62 +107,56 @@ static u64 romfs_get_hash_table_count(u64 num_entries) {
void RomFSBuildContext::VisitDirectory(VirtualDir romfs_dir, VirtualDir ext_dir,
std::shared_ptr<RomFSBuildDirectoryContext> parent) {
- std::vector<std::shared_ptr<RomFSBuildDirectoryContext>> child_dirs;
+ for (auto& child_romfs_file : romfs_dir->GetFiles()) {
+ const auto name = child_romfs_file->GetName();
+ const auto child = std::make_shared<RomFSBuildFileContext>();
+ // Set child's path.
+ child->cur_path_ofs = parent->path_len + 1;
+ child->path_len = child->cur_path_ofs + static_cast<u32>(name.size());
+ child->path = parent->path + "/" + name;
+
+ if (ext_dir != nullptr && ext_dir->GetFile(name + ".stub") != nullptr) {
+ continue;
+ }
- const auto entries = romfs_dir->GetEntries();
+ // Sanity check on path_len
+ ASSERT(child->path_len < FS_MAX_PATH);
- for (const auto& kv : entries) {
- if (kv.second == VfsEntryType::Directory) {
- const auto child = std::make_shared<RomFSBuildDirectoryContext>();
- // Set child's path.
- child->cur_path_ofs = parent->path_len + 1;
- child->path_len = child->cur_path_ofs + static_cast<u32>(kv.first.size());
- child->path = parent->path + "/" + kv.first;
+ child->source = std::move(child_romfs_file);
- if (ext_dir != nullptr && ext_dir->GetFile(kv.first + ".stub") != nullptr) {
- continue;
+ if (ext_dir != nullptr) {
+ if (const auto ips = ext_dir->GetFile(name + ".ips")) {
+ if (auto patched = PatchIPS(child->source, ips)) {
+ child->source = std::move(patched);
+ }
}
+ }
- // Sanity check on path_len
- ASSERT(child->path_len < FS_MAX_PATH);
-
- if (AddDirectory(parent, child)) {
- child_dirs.push_back(child);
- }
- } else {
- const auto child = std::make_shared<RomFSBuildFileContext>();
- // Set child's path.
- child->cur_path_ofs = parent->path_len + 1;
- child->path_len = child->cur_path_ofs + static_cast<u32>(kv.first.size());
- child->path = parent->path + "/" + kv.first;
-
- if (ext_dir != nullptr && ext_dir->GetFile(kv.first + ".stub") != nullptr) {
- continue;
- }
+ child->size = child->source->GetSize();
- // Sanity check on path_len
- ASSERT(child->path_len < FS_MAX_PATH);
+ AddFile(parent, child);
+ }
- child->source = romfs_dir->GetFile(kv.first);
+ for (auto& child_romfs_dir : romfs_dir->GetSubdirectories()) {
+ const auto name = child_romfs_dir->GetName();
+ const auto child = std::make_shared<RomFSBuildDirectoryContext>();
+ // Set child's path.
+ child->cur_path_ofs = parent->path_len + 1;
+ child->path_len = child->cur_path_ofs + static_cast<u32>(name.size());
+ child->path = parent->path + "/" + name;
- if (ext_dir != nullptr) {
- if (const auto ips = ext_dir->GetFile(kv.first + ".ips")) {
- if (auto patched = PatchIPS(child->source, ips)) {
- child->source = std::move(patched);
- }
- }
- }
+ if (ext_dir != nullptr && ext_dir->GetFile(name + ".stub") != nullptr) {
+ continue;
+ }
- child->size = child->source->GetSize();
+ // Sanity check on path_len
+ ASSERT(child->path_len < FS_MAX_PATH);
- AddFile(parent, child);
+ if (!AddDirectory(parent, child)) {
+ continue;
}
- }
- for (auto& child : child_dirs) {
- auto subdir_name = std::string_view(child->path).substr(child->cur_path_ofs);
- auto child_romfs_dir = romfs_dir->GetSubdirectory(subdir_name);
- auto child_ext_dir = ext_dir != nullptr ? ext_dir->GetSubdirectory(subdir_name) : nullptr;
+ auto child_ext_dir = ext_dir != nullptr ? ext_dir->GetSubdirectory(name) : nullptr;
this->VisitDirectory(child_romfs_dir, child_ext_dir, child);
}
}
@@ -293,7 +287,7 @@ std::multimap<u64, VirtualFile> RomFSBuildContext::Build() {
cur_entry.name_size = name_size;
- out.emplace(cur_file->offset + ROMFS_FILEPARTITION_OFS, cur_file->source);
+ out.emplace(cur_file->offset + ROMFS_FILEPARTITION_OFS, std::move(cur_file->source));
std::memcpy(file_table.data() + cur_file->entry_offset, &cur_entry, sizeof(RomFSFileEntry));
std::memset(file_table.data() + cur_file->entry_offset + sizeof(RomFSFileEntry), 0,
Common::AlignUp(cur_entry.name_size, 4));
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
index 8e475f25a..0bca05587 100644
--- a/src/core/file_sys/patch_manager.cpp
+++ b/src/core/file_sys/patch_manager.cpp
@@ -377,16 +377,16 @@ static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType t
auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs");
if (romfs_dir != nullptr)
- layers.push_back(std::make_shared<CachedVfsDirectory>(romfs_dir));
+ layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(romfs_dir)));
auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext");
if (ext_dir != nullptr)
- layers_ext.push_back(std::make_shared<CachedVfsDirectory>(ext_dir));
+ layers_ext.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(ext_dir)));
if (type == ContentRecordType::HtmlDocument) {
auto manual_dir = FindSubdirectoryCaseless(subdir, "manual_html");
if (manual_dir != nullptr)
- layers.push_back(std::make_shared<CachedVfsDirectory>(manual_dir));
+ layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(manual_dir)));
}
}
@@ -400,7 +400,7 @@ static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType t
return;
}
- layers.push_back(std::move(extracted));
+ layers.emplace_back(std::move(extracted));
auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
if (layered == nullptr) {
diff --git a/src/core/file_sys/program_metadata.cpp b/src/core/file_sys/program_metadata.cpp
index f00479bd3..8e291ff67 100644
--- a/src/core/file_sys/program_metadata.cpp
+++ b/src/core/file_sys/program_metadata.cpp
@@ -5,6 +5,7 @@
#include <vector>
#include "common/logging/log.h"
+#include "common/scope_exit.h"
#include "core/file_sys/program_metadata.h"
#include "core/file_sys/vfs.h"
#include "core/loader/loader.h"
@@ -95,6 +96,13 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
return Loader::ResultStatus::Success;
}
+Loader::ResultStatus ProgramMetadata::Reload(VirtualFile file) {
+ const u64 original_program_id = aci_header.title_id;
+ SCOPE_EXIT({ aci_header.title_id = original_program_id; });
+
+ return this->Load(file);
+}
+
/*static*/ ProgramMetadata ProgramMetadata::GetDefault() {
// Allow use of cores 0~3 and thread priorities 1~63.
constexpr u32 default_thread_info_capability = 0x30007F7;
diff --git a/src/core/file_sys/program_metadata.h b/src/core/file_sys/program_metadata.h
index 2e8960b07..9f8e74b13 100644
--- a/src/core/file_sys/program_metadata.h
+++ b/src/core/file_sys/program_metadata.h
@@ -56,6 +56,7 @@ public:
static ProgramMetadata GetDefault();
Loader::ResultStatus Load(VirtualFile file);
+ Loader::ResultStatus Reload(VirtualFile file);
/// Load from parameters instead of NPDM file, used for KIP
void LoadManual(bool is_64_bit, ProgramAddressSpaceType address_space, s32 main_thread_prio,
diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp
index 04da93d5c..1cc77ad14 100644
--- a/src/core/file_sys/registered_cache.cpp
+++ b/src/core/file_sys/registered_cache.cpp
@@ -322,7 +322,8 @@ VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& open_di
return nullptr;
}
- return ConcatenatedVfsFile::MakeConcatenatedFile(concat, concat.front()->GetName());
+ auto name = concat.front()->GetName();
+ return ConcatenatedVfsFile::MakeConcatenatedFile(std::move(name), std::move(concat));
}
VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
diff --git a/src/core/file_sys/romfs.cpp b/src/core/file_sys/romfs.cpp
index 614da2130..1c580de57 100644
--- a/src/core/file_sys/romfs.cpp
+++ b/src/core/file_sys/romfs.cpp
@@ -133,7 +133,7 @@ VirtualDir ExtractRomFS(VirtualFile file, RomFSExtractionType type) {
out = out->GetSubdirectories().front();
}
- return std::make_shared<CachedVfsDirectory>(out);
+ return std::make_shared<CachedVfsDirectory>(std::move(out));
}
VirtualFile CreateRomFS(VirtualDir dir, VirtualDir ext) {
@@ -141,8 +141,7 @@ VirtualFile CreateRomFS(VirtualDir dir, VirtualDir ext) {
return nullptr;
RomFSBuildContext ctx{dir, ext};
- auto file_map = ctx.Build();
- return ConcatenatedVfsFile::MakeConcatenatedFile(0, file_map, dir->GetName());
+ return ConcatenatedVfsFile::MakeConcatenatedFile(0, dir->GetName(), ctx.Build());
}
} // namespace FileSys
diff --git a/src/core/file_sys/vfs_cached.cpp b/src/core/file_sys/vfs_cached.cpp
index c3154ee81..7ee5300e5 100644
--- a/src/core/file_sys/vfs_cached.cpp
+++ b/src/core/file_sys/vfs_cached.cpp
@@ -6,13 +6,13 @@
namespace FileSys {
-CachedVfsDirectory::CachedVfsDirectory(VirtualDir& source_dir)
+CachedVfsDirectory::CachedVfsDirectory(VirtualDir&& source_dir)
: name(source_dir->GetName()), parent(source_dir->GetParentDirectory()) {
for (auto& dir : source_dir->GetSubdirectories()) {
- dirs.emplace(dir->GetName(), std::make_shared<CachedVfsDirectory>(dir));
+ dirs.emplace(dir->GetName(), std::make_shared<CachedVfsDirectory>(std::move(dir)));
}
for (auto& file : source_dir->GetFiles()) {
- files.emplace(file->GetName(), file);
+ files.emplace(file->GetName(), std::move(file));
}
}
diff --git a/src/core/file_sys/vfs_cached.h b/src/core/file_sys/vfs_cached.h
index 113acac12..1e5300784 100644
--- a/src/core/file_sys/vfs_cached.h
+++ b/src/core/file_sys/vfs_cached.h
@@ -11,7 +11,7 @@ namespace FileSys {
class CachedVfsDirectory : public ReadOnlyVfsDirectory {
public:
- CachedVfsDirectory(VirtualDir& source_directory);
+ CachedVfsDirectory(VirtualDir&& source_directory);
~CachedVfsDirectory() override;
VirtualFile GetFile(std::string_view file_name) const override;
diff --git a/src/core/file_sys/vfs_concat.cpp b/src/core/file_sys/vfs_concat.cpp
index 311a59e5f..168b9cbec 100644
--- a/src/core/file_sys/vfs_concat.cpp
+++ b/src/core/file_sys/vfs_concat.cpp
@@ -10,7 +10,7 @@
namespace FileSys {
-ConcatenatedVfsFile::ConcatenatedVfsFile(ConcatenationMap&& concatenation_map_, std::string&& name_)
+ConcatenatedVfsFile::ConcatenatedVfsFile(std::string&& name_, ConcatenationMap&& concatenation_map_)
: concatenation_map(std::move(concatenation_map_)), name(std::move(name_)) {
DEBUG_ASSERT(this->VerifyContinuity());
}
@@ -30,8 +30,8 @@ bool ConcatenatedVfsFile::VerifyContinuity() const {
ConcatenatedVfsFile::~ConcatenatedVfsFile() = default;
-VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(const std::vector<VirtualFile>& files,
- std::string&& name) {
+VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(std::string&& name,
+ std::vector<VirtualFile>&& files) {
// Fold trivial cases.
if (files.empty()) {
return nullptr;
@@ -46,20 +46,21 @@ VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(const std::vector<VirtualF
u64 last_offset = 0;
for (auto& file : files) {
+ const auto size = file->GetSize();
+
concatenation_map.emplace_back(ConcatenationEntry{
.offset = last_offset,
- .file = file,
+ .file = std::move(file),
});
- last_offset += file->GetSize();
+ last_offset += size;
}
- return VirtualFile(new ConcatenatedVfsFile(std::move(concatenation_map), std::move(name)));
+ return VirtualFile(new ConcatenatedVfsFile(std::move(name), std::move(concatenation_map)));
}
-VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(u8 filler_byte,
- const std::multimap<u64, VirtualFile>& files,
- std::string&& name) {
+VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(u8 filler_byte, std::string&& name,
+ std::multimap<u64, VirtualFile>&& files) {
// Fold trivial cases.
if (files.empty()) {
return nullptr;
@@ -76,6 +77,8 @@ VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(u8 filler_byte,
// Iteration of a multimap is ordered, so offset will be strictly non-decreasing.
for (auto& [offset, file] : files) {
+ const auto size = file->GetSize();
+
if (offset > last_offset) {
concatenation_map.emplace_back(ConcatenationEntry{
.offset = last_offset,
@@ -85,13 +88,13 @@ VirtualFile ConcatenatedVfsFile::MakeConcatenatedFile(u8 filler_byte,
concatenation_map.emplace_back(ConcatenationEntry{
.offset = offset,
- .file = file,
+ .file = std::move(file),
});
- last_offset = offset + file->GetSize();
+ last_offset = offset + size;
}
- return VirtualFile(new ConcatenatedVfsFile(std::move(concatenation_map), std::move(name)));
+ return VirtualFile(new ConcatenatedVfsFile(std::move(name), std::move(concatenation_map)));
}
std::string ConcatenatedVfsFile::GetName() const {
diff --git a/src/core/file_sys/vfs_concat.h b/src/core/file_sys/vfs_concat.h
index 6b329d545..cbddd12bd 100644
--- a/src/core/file_sys/vfs_concat.h
+++ b/src/core/file_sys/vfs_concat.h
@@ -24,22 +24,20 @@ private:
};
using ConcatenationMap = std::vector<ConcatenationEntry>;
- explicit ConcatenatedVfsFile(std::vector<ConcatenationEntry>&& concatenation_map,
- std::string&& name);
+ explicit ConcatenatedVfsFile(std::string&& name,
+ std::vector<ConcatenationEntry>&& concatenation_map);
bool VerifyContinuity() const;
public:
~ConcatenatedVfsFile() override;
/// Wrapper function to allow for more efficient handling of files.size() == 0, 1 cases.
- static VirtualFile MakeConcatenatedFile(const std::vector<VirtualFile>& files,
- std::string&& name);
+ static VirtualFile MakeConcatenatedFile(std::string&& name, std::vector<VirtualFile>&& files);
/// Convenience function that turns a map of offsets to files into a concatenated file, filling
/// gaps with a given filler byte.
- static VirtualFile MakeConcatenatedFile(u8 filler_byte,
- const std::multimap<u64, VirtualFile>& files,
- std::string&& name);
+ static VirtualFile MakeConcatenatedFile(u8 filler_byte, std::string&& name,
+ std::multimap<u64, VirtualFile>&& files);
std::string GetName() const override;
std::size_t GetSize() const override;
diff --git a/src/core/file_sys/vfs_layered.cpp b/src/core/file_sys/vfs_layered.cpp
index 3e6426afc..08daca397 100644
--- a/src/core/file_sys/vfs_layered.cpp
+++ b/src/core/file_sys/vfs_layered.cpp
@@ -38,7 +38,7 @@ VirtualDir LayeredVfsDirectory::GetDirectoryRelative(std::string_view path) cons
for (const auto& layer : dirs) {
auto dir = layer->GetDirectoryRelative(path);
if (dir != nullptr) {
- out.push_back(std::move(dir));
+ out.emplace_back(std::move(dir));
}
}
@@ -62,11 +62,11 @@ std::vector<VirtualFile> LayeredVfsDirectory::GetFiles() const {
std::set<std::string, std::less<>> out_names;
for (const auto& layer : dirs) {
- for (const auto& file : layer->GetFiles()) {
+ for (auto& file : layer->GetFiles()) {
auto file_name = file->GetName();
if (!out_names.contains(file_name)) {
out_names.emplace(std::move(file_name));
- out.push_back(file);
+ out.emplace_back(std::move(file));
}
}
}
@@ -86,7 +86,7 @@ std::vector<VirtualDir> LayeredVfsDirectory::GetSubdirectories() const {
std::vector<VirtualDir> out;
out.reserve(names.size());
for (const auto& subdir : names)
- out.push_back(GetSubdirectory(subdir));
+ out.emplace_back(GetSubdirectory(subdir));
return out;
}
diff --git a/src/core/hle/kernel/k_page_table.cpp b/src/core/hle/kernel/k_page_table.cpp
index 5b51edf30..0b0cef984 100644
--- a/src/core/hle/kernel/k_page_table.cpp
+++ b/src/core/hle/kernel/k_page_table.cpp
@@ -2949,6 +2949,23 @@ Result KPageTable::UnlockForIpcUserBuffer(KProcessAddress address, size_t size)
KMemoryAttribute::Locked, nullptr));
}
+Result KPageTable::LockForTransferMemory(KPageGroup* out, KProcessAddress address, size_t size,
+ KMemoryPermission perm) {
+ R_RETURN(this->LockMemoryAndOpen(out, nullptr, address, size, KMemoryState::FlagCanTransfer,
+ KMemoryState::FlagCanTransfer, KMemoryPermission::All,
+ KMemoryPermission::UserReadWrite, KMemoryAttribute::All,
+ KMemoryAttribute::None, perm, KMemoryAttribute::Locked));
+}
+
+Result KPageTable::UnlockForTransferMemory(KProcessAddress address, size_t size,
+ const KPageGroup& pg) {
+ R_RETURN(this->UnlockMemory(address, size, KMemoryState::FlagCanTransfer,
+ KMemoryState::FlagCanTransfer, KMemoryPermission::None,
+ KMemoryPermission::None, KMemoryAttribute::All,
+ KMemoryAttribute::Locked, KMemoryPermission::UserReadWrite,
+ KMemoryAttribute::Locked, std::addressof(pg)));
+}
+
Result KPageTable::LockForCodeMemory(KPageGroup* out, KProcessAddress addr, size_t size) {
R_RETURN(this->LockMemoryAndOpen(
out, nullptr, addr, size, KMemoryState::FlagCanCodeMemory, KMemoryState::FlagCanCodeMemory,
@@ -3388,6 +3405,11 @@ Result KPageTable::LockMemoryAndOpen(KPageGroup* out_pg, KPhysicalAddress* out_K
new_attr, KMemoryBlockDisableMergeAttribute::Locked,
KMemoryBlockDisableMergeAttribute::None);
+ // If we have an output page group, open.
+ if (out_pg) {
+ out_pg->Open();
+ }
+
R_SUCCEED();
}
diff --git a/src/core/hle/kernel/k_page_table.h b/src/core/hle/kernel/k_page_table.h
index b9e8c6042..7da675f27 100644
--- a/src/core/hle/kernel/k_page_table.h
+++ b/src/core/hle/kernel/k_page_table.h
@@ -104,6 +104,9 @@ public:
Result CleanupForIpcServer(KProcessAddress address, size_t size, KMemoryState dst_state);
Result CleanupForIpcClient(KProcessAddress address, size_t size, KMemoryState dst_state);
+ Result LockForTransferMemory(KPageGroup* out, KProcessAddress address, size_t size,
+ KMemoryPermission perm);
+ Result UnlockForTransferMemory(KProcessAddress address, size_t size, const KPageGroup& pg);
Result LockForCodeMemory(KPageGroup* out, KProcessAddress addr, size_t size);
Result UnlockForCodeMemory(KProcessAddress addr, size_t size, const KPageGroup& pg);
Result MakeAndOpenPageGroup(KPageGroup* out, KProcessAddress address, size_t num_pages,
diff --git a/src/core/hle/kernel/k_transfer_memory.cpp b/src/core/hle/kernel/k_transfer_memory.cpp
index 13d34125c..0e2e11743 100644
--- a/src/core/hle/kernel/k_transfer_memory.cpp
+++ b/src/core/hle/kernel/k_transfer_memory.cpp
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include "common/scope_exit.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_resource_limit.h"
#include "core/hle/kernel/k_transfer_memory.h"
@@ -9,28 +10,50 @@
namespace Kernel {
KTransferMemory::KTransferMemory(KernelCore& kernel)
- : KAutoObjectWithSlabHeapAndContainer{kernel} {}
+ : KAutoObjectWithSlabHeapAndContainer{kernel}, m_lock{kernel} {}
KTransferMemory::~KTransferMemory() = default;
-Result KTransferMemory::Initialize(KProcessAddress address, std::size_t size,
- Svc::MemoryPermission owner_perm) {
+Result KTransferMemory::Initialize(KProcessAddress addr, std::size_t size,
+ Svc::MemoryPermission own_perm) {
// Set members.
m_owner = GetCurrentProcessPointer(m_kernel);
- // TODO(bunnei): Lock for transfer memory
+ // Get the owner page table.
+ auto& page_table = m_owner->GetPageTable();
+
+ // Construct the page group, guarding to make sure our state is valid on exit.
+ m_page_group.emplace(m_kernel, page_table.GetBlockInfoManager());
+ auto pg_guard = SCOPE_GUARD({ m_page_group.reset(); });
+
+ // Lock the memory.
+ R_TRY(page_table.LockForTransferMemory(std::addressof(*m_page_group), addr, size,
+ ConvertToKMemoryPermission(own_perm)));
// Set remaining tracking members.
m_owner->Open();
- m_owner_perm = owner_perm;
- m_address = address;
- m_size = size;
+ m_owner_perm = own_perm;
+ m_address = addr;
m_is_initialized = true;
+ m_is_mapped = false;
+ // We succeeded.
+ pg_guard.Cancel();
R_SUCCEED();
}
-void KTransferMemory::Finalize() {}
+void KTransferMemory::Finalize() {
+ // Unlock.
+ if (!m_is_mapped) {
+ const size_t size = m_page_group->GetNumPages() * PageSize;
+ ASSERT(R_SUCCEEDED(
+ m_owner->GetPageTable().UnlockForTransferMemory(m_address, size, *m_page_group)));
+ }
+
+ // Close the page group.
+ m_page_group->Close();
+ m_page_group->Finalize();
+}
void KTransferMemory::PostDestroy(uintptr_t arg) {
KProcess* owner = reinterpret_cast<KProcess*>(arg);
@@ -38,4 +61,54 @@ void KTransferMemory::PostDestroy(uintptr_t arg) {
owner->Close();
}
+Result KTransferMemory::Map(KProcessAddress address, size_t size, Svc::MemoryPermission map_perm) {
+ // Validate the size.
+ R_UNLESS(m_page_group->GetNumPages() == Common::DivideUp(size, PageSize), ResultInvalidSize);
+
+ // Validate the permission.
+ R_UNLESS(m_owner_perm == map_perm, ResultInvalidState);
+
+ // Lock ourselves.
+ KScopedLightLock lk(m_lock);
+
+ // Ensure we're not already mapped.
+ R_UNLESS(!m_is_mapped, ResultInvalidState);
+
+ // Map the memory.
+ const KMemoryState state = (m_owner_perm == Svc::MemoryPermission::None)
+ ? KMemoryState::Transfered
+ : KMemoryState::SharedTransfered;
+ R_TRY(GetCurrentProcess(m_kernel).GetPageTable().MapPageGroup(
+ address, *m_page_group, state, KMemoryPermission::UserReadWrite));
+
+ // Mark ourselves as mapped.
+ m_is_mapped = true;
+
+ R_SUCCEED();
+}
+
+Result KTransferMemory::Unmap(KProcessAddress address, size_t size) {
+ // Validate the size.
+ R_UNLESS(m_page_group->GetNumPages() == Common::DivideUp(size, PageSize), ResultInvalidSize);
+
+ // Lock ourselves.
+ KScopedLightLock lk(m_lock);
+
+ // Unmap the memory.
+ const KMemoryState state = (m_owner_perm == Svc::MemoryPermission::None)
+ ? KMemoryState::Transfered
+ : KMemoryState::SharedTransfered;
+ R_TRY(GetCurrentProcess(m_kernel).GetPageTable().UnmapPageGroup(address, *m_page_group, state));
+
+ // Mark ourselves as unmapped.
+ ASSERT(m_is_mapped);
+ m_is_mapped = false;
+
+ R_SUCCEED();
+}
+
+size_t KTransferMemory::GetSize() const {
+ return m_is_initialized ? m_page_group->GetNumPages() * PageSize : 0;
+}
+
} // namespace Kernel
diff --git a/src/core/hle/kernel/k_transfer_memory.h b/src/core/hle/kernel/k_transfer_memory.h
index 54f97ccb4..8a0b08761 100644
--- a/src/core/hle/kernel/k_transfer_memory.h
+++ b/src/core/hle/kernel/k_transfer_memory.h
@@ -3,6 +3,9 @@
#pragma once
+#include <optional>
+
+#include "core/hle/kernel/k_page_group.h"
#include "core/hle/kernel/slab_helpers.h"
#include "core/hle/kernel/svc_types.h"
#include "core/hle/result.h"
@@ -48,16 +51,19 @@ public:
return m_address;
}
- size_t GetSize() const {
- return m_is_initialized ? m_size : 0;
- }
+ size_t GetSize() const;
+
+ Result Map(KProcessAddress address, size_t size, Svc::MemoryPermission map_perm);
+ Result Unmap(KProcessAddress address, size_t size);
private:
+ std::optional<KPageGroup> m_page_group{};
KProcess* m_owner{};
KProcessAddress m_address{};
+ KLightLock m_lock;
Svc::MemoryPermission m_owner_perm{};
- size_t m_size{};
bool m_is_initialized{};
+ bool m_is_mapped{};
};
} // namespace Kernel
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index a1134b7e2..cb025c3d6 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -373,7 +373,7 @@ struct KernelCore::Impl {
static inline thread_local u8 host_thread_id = UINT8_MAX;
/// Sets the host thread ID for the caller.
- u32 SetHostThreadId(std::size_t core_id) {
+ LTO_NOINLINE u32 SetHostThreadId(std::size_t core_id) {
// This should only be called during core init.
ASSERT(host_thread_id == UINT8_MAX);
@@ -384,13 +384,13 @@ struct KernelCore::Impl {
}
/// Gets the host thread ID for the caller
- u32 GetHostThreadId() const {
+ LTO_NOINLINE u32 GetHostThreadId() const {
return host_thread_id;
}
// Gets the dummy KThread for the caller, allocating a new one if this is the first time
- KThread* GetHostDummyThread(KThread* existing_thread) {
- const auto initialize{[](KThread* thread) {
+ LTO_NOINLINE KThread* GetHostDummyThread(KThread* existing_thread) {
+ const auto initialize{[](KThread* thread) LTO_NOINLINE {
ASSERT(KThread::InitializeDummyThread(thread, nullptr).IsSuccess());
return thread;
}};
@@ -424,11 +424,11 @@ struct KernelCore::Impl {
static inline thread_local bool is_phantom_mode_for_singlecore{false};
- bool IsPhantomModeForSingleCore() const {
+ LTO_NOINLINE bool IsPhantomModeForSingleCore() const {
return is_phantom_mode_for_singlecore;
}
- void SetIsPhantomModeForSingleCore(bool value) {
+ LTO_NOINLINE void SetIsPhantomModeForSingleCore(bool value) {
ASSERT(!is_multicore);
is_phantom_mode_for_singlecore = value;
}
@@ -439,14 +439,14 @@ struct KernelCore::Impl {
static inline thread_local KThread* current_thread{nullptr};
- KThread* GetCurrentEmuThread() {
+ LTO_NOINLINE KThread* GetCurrentEmuThread() {
if (!current_thread) {
current_thread = GetHostDummyThread(nullptr);
}
return current_thread;
}
- void SetCurrentEmuThread(KThread* thread) {
+ LTO_NOINLINE void SetCurrentEmuThread(KThread* thread) {
current_thread = thread;
}
diff --git a/src/core/hle/kernel/svc/svc_transfer_memory.cpp b/src/core/hle/kernel/svc/svc_transfer_memory.cpp
index 7d94e7f09..1f97121b3 100644
--- a/src/core/hle/kernel/svc/svc_transfer_memory.cpp
+++ b/src/core/hle/kernel/svc/svc_transfer_memory.cpp
@@ -71,15 +71,59 @@ Result CreateTransferMemory(Core::System& system, Handle* out, u64 address, u64
}
Result MapTransferMemory(Core::System& system, Handle trmem_handle, uint64_t address, uint64_t size,
- MemoryPermission owner_perm) {
- UNIMPLEMENTED();
- R_THROW(ResultNotImplemented);
+ MemoryPermission map_perm) {
+ // Validate the address/size.
+ R_UNLESS(Common::IsAligned(address, PageSize), ResultInvalidAddress);
+ R_UNLESS(Common::IsAligned(size, PageSize), ResultInvalidSize);
+ R_UNLESS(size > 0, ResultInvalidSize);
+ R_UNLESS((address < address + size), ResultInvalidCurrentMemory);
+
+ // Validate the permission.
+ R_UNLESS(IsValidTransferMemoryPermission(map_perm), ResultInvalidState);
+
+ // Get the transfer memory.
+ KScopedAutoObject trmem = GetCurrentProcess(system.Kernel())
+ .GetHandleTable()
+ .GetObject<KTransferMemory>(trmem_handle);
+ R_UNLESS(trmem.IsNotNull(), ResultInvalidHandle);
+
+ // Verify that the mapping is in range.
+ R_UNLESS(GetCurrentProcess(system.Kernel())
+ .GetPageTable()
+ .CanContain(address, size, KMemoryState::Transfered),
+ ResultInvalidMemoryRegion);
+
+ // Map the transfer memory.
+ R_TRY(trmem->Map(address, size, map_perm));
+
+ // We succeeded.
+ R_SUCCEED();
}
Result UnmapTransferMemory(Core::System& system, Handle trmem_handle, uint64_t address,
uint64_t size) {
- UNIMPLEMENTED();
- R_THROW(ResultNotImplemented);
+ // Validate the address/size.
+ R_UNLESS(Common::IsAligned(address, PageSize), ResultInvalidAddress);
+ R_UNLESS(Common::IsAligned(size, PageSize), ResultInvalidSize);
+ R_UNLESS(size > 0, ResultInvalidSize);
+ R_UNLESS((address < address + size), ResultInvalidCurrentMemory);
+
+ // Get the transfer memory.
+ KScopedAutoObject trmem = GetCurrentProcess(system.Kernel())
+ .GetHandleTable()
+ .GetObject<KTransferMemory>(trmem_handle);
+ R_UNLESS(trmem.IsNotNull(), ResultInvalidHandle);
+
+ // Verify that the mapping is in range.
+ R_UNLESS(GetCurrentProcess(system.Kernel())
+ .GetPageTable()
+ .CanContain(address, size, KMemoryState::Transfered),
+ ResultInvalidMemoryRegion);
+
+ // Unmap the transfer memory.
+ R_TRY(trmem->Unmap(address, size));
+
+ R_SUCCEED();
}
Result MapTransferMemory64(Core::System& system, Handle trmem_handle, uint64_t address,
diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp
index b971401e6..1b1c8190e 100644
--- a/src/core/hle/service/acc/acc.cpp
+++ b/src/core/hle/service/acc/acc.cpp
@@ -49,7 +49,7 @@ public:
: ServiceFramework{system_, "IManagerForSystemService"} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "CheckAvailability"},
+ {0, &IManagerForSystemService::CheckAvailability, "CheckAvailability"},
{1, nullptr, "GetAccountId"},
{2, nullptr, "EnsureIdTokenCacheAsync"},
{3, nullptr, "LoadIdTokenCache"},
@@ -78,6 +78,13 @@ public:
RegisterHandlers(functions);
}
+
+private:
+ void CheckAvailability(HLERequestContext& ctx) {
+ LOG_WARNING(Service_ACC, "(STUBBED) called");
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
};
// 3.0.0+
@@ -400,13 +407,13 @@ protected:
IPC::RequestParser rp{ctx};
const auto base = rp.PopRaw<ProfileBase>();
- const auto user_data = ctx.ReadBuffer();
- const auto image_data = ctx.ReadBuffer(1);
+ const auto image_data = ctx.ReadBufferA(0);
+ const auto user_data = ctx.ReadBufferX(0);
- LOG_DEBUG(Service_ACC, "called, username='{}', timestamp={:016X}, uuid=0x{}",
- Common::StringFromFixedZeroTerminatedBuffer(
- reinterpret_cast<const char*>(base.username.data()), base.username.size()),
- base.timestamp, base.user_uuid.RawString());
+ LOG_INFO(Service_ACC, "called, username='{}', timestamp={:016X}, uuid=0x{}",
+ Common::StringFromFixedZeroTerminatedBuffer(
+ reinterpret_cast<const char*>(base.username.data()), base.username.size()),
+ base.timestamp, base.user_uuid.RawString());
if (user_data.size() < sizeof(UserData)) {
LOG_ERROR(Service_ACC, "UserData buffer too small!");
@@ -837,6 +844,29 @@ void Module::Interface::InitializeApplicationInfoV2(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
}
+void Module::Interface::BeginUserRegistration(HLERequestContext& ctx) {
+ const auto user_id = Common::UUID::MakeRandom();
+ profile_manager->CreateNewUser(user_id, "yuzu");
+
+ LOG_INFO(Service_ACC, "called, uuid={}", user_id.FormattedString());
+
+ IPC::ResponseBuilder rb{ctx, 6};
+ rb.Push(ResultSuccess);
+ rb.PushRaw(user_id);
+}
+
+void Module::Interface::CompleteUserRegistration(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ Common::UUID user_id = rp.PopRaw<Common::UUID>();
+
+ LOG_INFO(Service_ACC, "called, uuid={}", user_id.FormattedString());
+
+ profile_manager->WriteUserSaveFile();
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+}
+
void Module::Interface::GetProfileEditor(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
Common::UUID user_id = rp.PopRaw<Common::UUID>();
@@ -880,6 +910,17 @@ void Module::Interface::StoreSaveDataThumbnailApplication(HLERequestContext& ctx
StoreSaveDataThumbnail(ctx, uuid, tid);
}
+void Module::Interface::GetBaasAccountManagerForSystemService(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto uuid = rp.PopRaw<Common::UUID>();
+
+ LOG_INFO(Service_ACC, "called, uuid=0x{}", uuid.RawString());
+
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<IManagerForSystemService>(system, uuid);
+}
+
void Module::Interface::StoreSaveDataThumbnailSystem(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto uuid = rp.PopRaw<Common::UUID>();
diff --git a/src/core/hle/service/acc/acc.h b/src/core/hle/service/acc/acc.h
index 6b4735c2f..0395229b4 100644
--- a/src/core/hle/service/acc/acc.h
+++ b/src/core/hle/service/acc/acc.h
@@ -33,10 +33,13 @@ public:
void TrySelectUserWithoutInteraction(HLERequestContext& ctx);
void IsUserAccountSwitchLocked(HLERequestContext& ctx);
void InitializeApplicationInfoV2(HLERequestContext& ctx);
+ void BeginUserRegistration(HLERequestContext& ctx);
+ void CompleteUserRegistration(HLERequestContext& ctx);
void GetProfileEditor(HLERequestContext& ctx);
void ListQualifiedUsers(HLERequestContext& ctx);
void ListOpenContextStoredUsers(HLERequestContext& ctx);
void StoreSaveDataThumbnailApplication(HLERequestContext& ctx);
+ void GetBaasAccountManagerForSystemService(HLERequestContext& ctx);
void StoreSaveDataThumbnailSystem(HLERequestContext& ctx);
private:
diff --git a/src/core/hle/service/acc/acc_su.cpp b/src/core/hle/service/acc/acc_su.cpp
index d9882ecd3..770d13ec5 100644
--- a/src/core/hle/service/acc/acc_su.cpp
+++ b/src/core/hle/service/acc/acc_su.cpp
@@ -23,7 +23,7 @@ ACC_SU::ACC_SU(std::shared_ptr<Module> module_, std::shared_ptr<ProfileManager>
{99, nullptr, "DebugActivateOpenContextRetention"},
{100, nullptr, "GetUserRegistrationNotifier"},
{101, nullptr, "GetUserStateChangeNotifier"},
- {102, nullptr, "GetBaasAccountManagerForSystemService"},
+ {102, &ACC_SU::GetBaasAccountManagerForSystemService, "GetBaasAccountManagerForSystemService"},
{103, nullptr, "GetBaasUserAvailabilityChangeNotifier"},
{104, nullptr, "GetProfileUpdateNotifier"},
{105, nullptr, "CheckNetworkServiceAvailabilityAsync"},
@@ -40,8 +40,8 @@ ACC_SU::ACC_SU(std::shared_ptr<Module> module_, std::shared_ptr<ProfileManager>
{152, nullptr, "LoadSignedDeviceIdentifierCacheForNintendoAccount"},
{190, nullptr, "GetUserLastOpenedApplication"},
{191, nullptr, "ActivateOpenContextHolder"},
- {200, nullptr, "BeginUserRegistration"},
- {201, nullptr, "CompleteUserRegistration"},
+ {200, &ACC_SU::BeginUserRegistration, "BeginUserRegistration"},
+ {201, &ACC_SU::CompleteUserRegistration, "CompleteUserRegistration"},
{202, nullptr, "CancelUserRegistration"},
{203, nullptr, "DeleteUser"},
{204, nullptr, "SetUserPosition"},
diff --git a/src/core/hle/service/acc/profile_manager.h b/src/core/hle/service/acc/profile_manager.h
index 993a5a57a..900e32200 100644
--- a/src/core/hle/service/acc/profile_manager.h
+++ b/src/core/hle/service/acc/profile_manager.h
@@ -96,9 +96,10 @@ public:
bool SetProfileBaseAndData(Common::UUID uuid, const ProfileBase& profile_new,
const UserData& data_new);
+ void WriteUserSaveFile();
+
private:
void ParseUserSaveFile();
- void WriteUserSaveFile();
std::optional<std::size_t> AddToProfiles(const ProfileInfo& profile);
bool RemoveProfileAtIndex(std::size_t index);
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index 819dea6a7..98765b81a 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -31,7 +31,7 @@
#include "core/hle/service/apm/apm_controller.h"
#include "core/hle/service/apm/apm_interface.h"
#include "core/hle/service/bcat/backend/backend.h"
-#include "core/hle/service/caps/caps.h"
+#include "core/hle/service/caps/caps_types.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/ns/ns.h"
@@ -210,8 +210,8 @@ IDisplayController::IDisplayController(Core::System& system_)
{21, nullptr, "ClearAppletTransitionBuffer"},
{22, nullptr, "AcquireLastApplicationCaptureSharedBuffer"},
{23, nullptr, "ReleaseLastApplicationCaptureSharedBuffer"},
- {24, nullptr, "AcquireLastForegroundCaptureSharedBuffer"},
- {25, nullptr, "ReleaseLastForegroundCaptureSharedBuffer"},
+ {24, &IDisplayController::AcquireLastForegroundCaptureSharedBuffer, "AcquireLastForegroundCaptureSharedBuffer"},
+ {25, &IDisplayController::ReleaseLastForegroundCaptureSharedBuffer, "ReleaseLastForegroundCaptureSharedBuffer"},
{26, &IDisplayController::AcquireCallerAppletCaptureSharedBuffer, "AcquireCallerAppletCaptureSharedBuffer"},
{27, &IDisplayController::ReleaseCallerAppletCaptureSharedBuffer, "ReleaseCallerAppletCaptureSharedBuffer"},
{28, nullptr, "TakeScreenShotOfOwnLayerEx"},
@@ -239,6 +239,22 @@ void IDisplayController::TakeScreenShotOfOwnLayer(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
}
+void IDisplayController::AcquireLastForegroundCaptureSharedBuffer(HLERequestContext& ctx) {
+ LOG_WARNING(Service_AM, "(STUBBED) called");
+
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(ResultSuccess);
+ rb.Push(1U);
+ rb.Push(0);
+}
+
+void IDisplayController::ReleaseLastForegroundCaptureSharedBuffer(HLERequestContext& ctx) {
+ LOG_WARNING(Service_AM, "(STUBBED) called");
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+}
+
void IDisplayController::AcquireCallerAppletCaptureSharedBuffer(HLERequestContext& ctx) {
LOG_WARNING(Service_AM, "(STUBBED) called");
@@ -764,6 +780,66 @@ void AppletMessageQueue::OperationModeChanged() {
on_operation_mode_changed->Signal();
}
+ILockAccessor::ILockAccessor(Core::System& system_)
+ : ServiceFramework{system_, "ILockAccessor"}, service_context{system_, "ILockAccessor"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {1, &ILockAccessor::TryLock, "TryLock"},
+ {2, &ILockAccessor::Unlock, "Unlock"},
+ {3, &ILockAccessor::GetEvent, "GetEvent"},
+ {4,&ILockAccessor::IsLocked, "IsLocked"},
+ };
+ // clang-format on
+
+ RegisterHandlers(functions);
+
+ lock_event = service_context.CreateEvent("ILockAccessor::LockEvent");
+}
+
+ILockAccessor::~ILockAccessor() = default;
+
+void ILockAccessor::TryLock(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto return_handle = rp.Pop<bool>();
+
+ LOG_WARNING(Service_AM, "(STUBBED) called, return_handle={}", return_handle);
+
+ // TODO: When return_handle is true this function should return the lock handle
+
+ is_locked = true;
+
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push<u8>(is_locked);
+}
+
+void ILockAccessor::Unlock(HLERequestContext& ctx) {
+ LOG_INFO(Service_AM, "called");
+
+ is_locked = false;
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+}
+
+void ILockAccessor::GetEvent(HLERequestContext& ctx) {
+ LOG_INFO(Service_AM, "called");
+
+ lock_event->Signal();
+
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(lock_event->GetReadableEvent());
+}
+
+void ILockAccessor::IsLocked(HLERequestContext& ctx) {
+ LOG_INFO(Service_AM, "called");
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ rb.Push<u8>(is_locked);
+}
+
ICommonStateGetter::ICommonStateGetter(Core::System& system_,
std::shared_ptr<AppletMessageQueue> msg_queue_)
: ServiceFramework{system_, "ICommonStateGetter"}, msg_queue{std::move(msg_queue_)},
@@ -787,7 +863,7 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_,
{14, nullptr, "GetWakeupCount"},
{20, nullptr, "PushToGeneralChannel"},
{30, nullptr, "GetHomeButtonReaderLockAccessor"},
- {31, nullptr, "GetReaderLockAccessorEx"},
+ {31, &ICommonStateGetter::GetReaderLockAccessorEx, "GetReaderLockAccessorEx"},
{32, nullptr, "GetWriterLockAccessorEx"},
{40, nullptr, "GetCradleFwVersion"},
{50, &ICommonStateGetter::IsVrModeEnabled, "IsVrModeEnabled"},
@@ -805,7 +881,7 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_,
{65, nullptr, "GetApplicationIdByContentActionName"},
{66, &ICommonStateGetter::SetCpuBoostMode, "SetCpuBoostMode"},
{67, nullptr, "CancelCpuBoostMode"},
- {68, nullptr, "GetBuiltInDisplayType"},
+ {68, &ICommonStateGetter::GetBuiltInDisplayType, "GetBuiltInDisplayType"},
{80, &ICommonStateGetter::PerformSystemButtonPressingIfInFocus, "PerformSystemButtonPressingIfInFocus"},
{90, nullptr, "SetPerformanceConfigurationChangedNotification"},
{91, nullptr, "GetCurrentPerformanceConfiguration"},
@@ -886,6 +962,18 @@ void ICommonStateGetter::RequestToAcquireSleepLock(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
}
+void ICommonStateGetter::GetReaderLockAccessorEx(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto unknown = rp.Pop<u32>();
+
+ LOG_INFO(Service_AM, "called, unknown={}", unknown);
+
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<ILockAccessor>(system);
+}
+
void ICommonStateGetter::GetAcquiredSleepLockEvent(HLERequestContext& ctx) {
LOG_WARNING(Service_AM, "called");
@@ -970,6 +1058,14 @@ void ICommonStateGetter::SetCpuBoostMode(HLERequestContext& ctx) {
apm_sys->SetCpuBoostMode(ctx);
}
+void ICommonStateGetter::GetBuiltInDisplayType(HLERequestContext& ctx) {
+ LOG_WARNING(Service_AM, "(STUBBED) called");
+
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(0);
+}
+
void ICommonStateGetter::PerformSystemButtonPressingIfInFocus(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto system_button{rp.PopEnum<SystemButtonType>()};
@@ -1477,7 +1573,7 @@ ILibraryAppletSelfAccessor::ILibraryAppletSelfAccessor(Core::System& system_)
{100, nullptr, "CreateGameMovieTrimmer"},
{101, nullptr, "ReserveResourceForMovieOperation"},
{102, nullptr, "UnreserveResourceForMovieOperation"},
- {110, nullptr, "GetMainAppletAvailableUsers"},
+ {110, &ILibraryAppletSelfAccessor::GetMainAppletAvailableUsers, "GetMainAppletAvailableUsers"},
{120, nullptr, "GetLaunchStorageInfoForDebug"},
{130, nullptr, "GetGpuErrorDetectedSystemEvent"},
{140, nullptr, "SetApplicationMemoryReservation"},
@@ -1493,6 +1589,9 @@ ILibraryAppletSelfAccessor::ILibraryAppletSelfAccessor(Core::System& system_)
case Applets::AppletId::MiiEdit:
PushInShowMiiEditData();
break;
+ case Applets::AppletId::PhotoViewer:
+ PushInShowAlbum();
+ break;
default:
break;
}
@@ -1569,6 +1668,42 @@ void ILibraryAppletSelfAccessor::GetCallerAppletIdentityInfo(HLERequestContext&
rb.PushRaw(applet_info);
}
+void ILibraryAppletSelfAccessor::GetMainAppletAvailableUsers(HLERequestContext& ctx) {
+ const Service::Account::ProfileManager manager{};
+ bool is_empty{true};
+ s32 user_count{-1};
+
+ LOG_INFO(Service_AM, "called");
+
+ if (manager.GetUserCount() > 0) {
+ is_empty = false;
+ user_count = static_cast<s32>(manager.GetUserCount());
+ ctx.WriteBuffer(manager.GetAllUsers());
+ }
+
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(ResultSuccess);
+ rb.Push<u8>(is_empty);
+ rb.Push(user_count);
+}
+
+void ILibraryAppletSelfAccessor::PushInShowAlbum() {
+ const Applets::CommonArguments arguments{
+ .arguments_version = Applets::CommonArgumentVersion::Version3,
+ .size = Applets::CommonArgumentSize::Version3,
+ .library_version = 1,
+ .theme_color = Applets::ThemeColor::BasicBlack,
+ .play_startup_sound = true,
+ .system_tick = system.CoreTiming().GetClockTicks(),
+ };
+
+ std::vector<u8> argument_data(sizeof(arguments));
+ std::vector<u8> settings_data{2};
+ std::memcpy(argument_data.data(), &arguments, sizeof(arguments));
+ queue_data.emplace_back(std::move(argument_data));
+ queue_data.emplace_back(std::move(settings_data));
+}
+
void ILibraryAppletSelfAccessor::PushInShowCabinetData() {
const Applets::CommonArguments arguments{
.arguments_version = Applets::CommonArgumentVersion::Version3,
diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h
index 349482dcc..64b3f3fe2 100644
--- a/src/core/hle/service/am/am.h
+++ b/src/core/hle/service/am/am.h
@@ -124,6 +124,8 @@ public:
private:
void GetCallerAppletCaptureImageEx(HLERequestContext& ctx);
void TakeScreenShotOfOwnLayer(HLERequestContext& ctx);
+ void AcquireLastForegroundCaptureSharedBuffer(HLERequestContext& ctx);
+ void ReleaseLastForegroundCaptureSharedBuffer(HLERequestContext& ctx);
void AcquireCallerAppletCaptureSharedBuffer(HLERequestContext& ctx);
void ReleaseCallerAppletCaptureSharedBuffer(HLERequestContext& ctx);
};
@@ -195,6 +197,23 @@ private:
ScreenshotPermission screenshot_permission = ScreenshotPermission::Inherit;
};
+class ILockAccessor final : public ServiceFramework<ILockAccessor> {
+public:
+ explicit ILockAccessor(Core::System& system_);
+ ~ILockAccessor() override;
+
+private:
+ void TryLock(HLERequestContext& ctx);
+ void Unlock(HLERequestContext& ctx);
+ void GetEvent(HLERequestContext& ctx);
+ void IsLocked(HLERequestContext& ctx);
+
+ bool is_locked{};
+
+ Kernel::KEvent* lock_event;
+ KernelHelpers::ServiceContext service_context;
+};
+
class ICommonStateGetter final : public ServiceFramework<ICommonStateGetter> {
public:
explicit ICommonStateGetter(Core::System& system_,
@@ -237,6 +256,7 @@ private:
void GetCurrentFocusState(HLERequestContext& ctx);
void RequestToAcquireSleepLock(HLERequestContext& ctx);
void GetAcquiredSleepLockEvent(HLERequestContext& ctx);
+ void GetReaderLockAccessorEx(HLERequestContext& ctx);
void GetDefaultDisplayResolutionChangeEvent(HLERequestContext& ctx);
void GetOperationMode(HLERequestContext& ctx);
void GetPerformanceMode(HLERequestContext& ctx);
@@ -248,6 +268,7 @@ private:
void EndVrModeEx(HLERequestContext& ctx);
void GetDefaultDisplayResolution(HLERequestContext& ctx);
void SetCpuBoostMode(HLERequestContext& ctx);
+ void GetBuiltInDisplayType(HLERequestContext& ctx);
void PerformSystemButtonPressingIfInFocus(HLERequestContext& ctx);
void GetSettingsPlatformRegion(HLERequestContext& ctx);
void SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled(HLERequestContext& ctx);
@@ -326,7 +347,9 @@ private:
void GetLibraryAppletInfo(HLERequestContext& ctx);
void ExitProcessAndReturn(HLERequestContext& ctx);
void GetCallerAppletIdentityInfo(HLERequestContext& ctx);
+ void GetMainAppletAvailableUsers(HLERequestContext& ctx);
+ void PushInShowAlbum();
void PushInShowCabinetData();
void PushInShowMiiEditData();
diff --git a/src/core/hle/service/am/applet_ae.cpp b/src/core/hle/service/am/applet_ae.cpp
index eb12312cc..e30e6478a 100644
--- a/src/core/hle/service/am/applet_ae.cpp
+++ b/src/core/hle/service/am/applet_ae.cpp
@@ -28,8 +28,8 @@ public:
{11, &ILibraryAppletProxy::GetLibraryAppletCreator, "GetLibraryAppletCreator"},
{20, &ILibraryAppletProxy::OpenLibraryAppletSelfAccessor, "OpenLibraryAppletSelfAccessor"},
{21, &ILibraryAppletProxy::GetAppletCommonFunctions, "GetAppletCommonFunctions"},
- {22, nullptr, "GetHomeMenuFunctions"},
- {23, nullptr, "GetGlobalStateController"},
+ {22, &ILibraryAppletProxy::GetHomeMenuFunctions, "GetHomeMenuFunctions"},
+ {23, &ILibraryAppletProxy::GetGlobalStateController, "GetGlobalStateController"},
{1000, &ILibraryAppletProxy::GetDebugFunctions, "GetDebugFunctions"},
};
// clang-format on
@@ -110,6 +110,22 @@ private:
rb.PushIpcInterface<IAppletCommonFunctions>(system);
}
+ void GetHomeMenuFunctions(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_AM, "called");
+
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<IHomeMenuFunctions>(system);
+ }
+
+ void GetGlobalStateController(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_AM, "called");
+
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<IGlobalStateController>(system);
+ }
+
void GetDebugFunctions(HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "called");
diff --git a/src/core/hle/service/am/applets/applet_error.cpp b/src/core/hle/service/am/applets/applet_error.cpp
index b46ea840c..5d17c353f 100644
--- a/src/core/hle/service/am/applets/applet_error.cpp
+++ b/src/core/hle/service/am/applets/applet_error.cpp
@@ -138,6 +138,10 @@ void Error::Initialize() {
CopyArgumentData(data, args->application_error);
error_code = Result(args->application_error.error_code);
break;
+ case ErrorAppletMode::ShowErrorPctl:
+ CopyArgumentData(data, args->error_record);
+ error_code = Decode64BitError(args->error_record.error_code_64);
+ break;
case ErrorAppletMode::ShowErrorRecord:
CopyArgumentData(data, args->error_record);
error_code = Decode64BitError(args->error_record.error_code_64);
@@ -191,6 +195,7 @@ void Error::Execute() {
frontend.ShowCustomErrorText(error_code, main_text_string, detail_text_string, callback);
break;
}
+ case ErrorAppletMode::ShowErrorPctl:
case ErrorAppletMode::ShowErrorRecord:
reporter.SaveErrorReport(title_id, error_code,
fmt::format("{:016X}", args->error_record.posix_time));
diff --git a/src/core/hle/service/caps/caps.cpp b/src/core/hle/service/caps/caps.cpp
index 610fe9940..31dd98140 100644
--- a/src/core/hle/service/caps/caps.cpp
+++ b/src/core/hle/service/caps/caps.cpp
@@ -4,6 +4,7 @@
#include "core/hle/service/caps/caps.h"
#include "core/hle/service/caps/caps_a.h"
#include "core/hle/service/caps/caps_c.h"
+#include "core/hle/service/caps/caps_manager.h"
#include "core/hle/service/caps/caps_sc.h"
#include "core/hle/service/caps/caps_ss.h"
#include "core/hle/service/caps/caps_su.h"
@@ -15,13 +16,21 @@ namespace Service::Capture {
void LoopProcess(Core::System& system) {
auto server_manager = std::make_unique<ServerManager>(system);
+ auto album_manager = std::make_shared<AlbumManager>(system);
+
+ server_manager->RegisterNamedService(
+ "caps:a", std::make_shared<IAlbumAccessorService>(system, album_manager));
+ server_manager->RegisterNamedService(
+ "caps:c", std::make_shared<IAlbumControlService>(system, album_manager));
+ server_manager->RegisterNamedService(
+ "caps:u", std::make_shared<IAlbumApplicationService>(system, album_manager));
+
+ server_manager->RegisterNamedService("caps:ss", std::make_shared<IScreenShotService>(system));
+ server_manager->RegisterNamedService("caps:sc",
+ std::make_shared<IScreenShotControlService>(system));
+ server_manager->RegisterNamedService("caps:su",
+ std::make_shared<IScreenShotApplicationService>(system));
- server_manager->RegisterNamedService("caps:a", std::make_shared<CAPS_A>(system));
- server_manager->RegisterNamedService("caps:c", std::make_shared<CAPS_C>(system));
- server_manager->RegisterNamedService("caps:u", std::make_shared<CAPS_U>(system));
- server_manager->RegisterNamedService("caps:sc", std::make_shared<CAPS_SC>(system));
- server_manager->RegisterNamedService("caps:ss", std::make_shared<CAPS_SS>(system));
- server_manager->RegisterNamedService("caps:su", std::make_shared<CAPS_SU>(system));
ServerManager::RunServer(std::move(server_manager));
}
diff --git a/src/core/hle/service/caps/caps.h b/src/core/hle/service/caps/caps.h
index 15f0ecfaa..58e9725b8 100644
--- a/src/core/hle/service/caps/caps.h
+++ b/src/core/hle/service/caps/caps.h
@@ -3,93 +3,12 @@
#pragma once
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-
namespace Core {
class System;
}
-namespace Service::SM {
-class ServiceManager;
-}
-
namespace Service::Capture {
-enum class AlbumImageOrientation {
- Orientation0 = 0,
- Orientation1 = 1,
- Orientation2 = 2,
- Orientation3 = 3,
-};
-
-enum class AlbumReportOption : s32 {
- Disable = 0,
- Enable = 1,
-};
-
-enum class ContentType : u8 {
- Screenshot = 0,
- Movie = 1,
- ExtraMovie = 3,
-};
-
-enum class AlbumStorage : u8 {
- NAND = 0,
- SD = 1,
-};
-
-struct AlbumFileDateTime {
- s16 year{};
- s8 month{};
- s8 day{};
- s8 hour{};
- s8 minute{};
- s8 second{};
- s8 uid{};
-};
-static_assert(sizeof(AlbumFileDateTime) == 0x8, "AlbumFileDateTime has incorrect size.");
-
-struct AlbumEntry {
- u64 size{};
- u64 application_id{};
- AlbumFileDateTime datetime{};
- AlbumStorage storage{};
- ContentType content{};
- INSERT_PADDING_BYTES(6);
-};
-static_assert(sizeof(AlbumEntry) == 0x20, "AlbumEntry has incorrect size.");
-
-struct AlbumFileEntry {
- u64 size{}; // Size of the entry
- u64 hash{}; // AES256 with hardcoded key over AlbumEntry
- AlbumFileDateTime datetime{};
- AlbumStorage storage{};
- ContentType content{};
- INSERT_PADDING_BYTES(5);
- u8 unknown{1}; // Set to 1 on official SW
-};
-static_assert(sizeof(AlbumFileEntry) == 0x20, "AlbumFileEntry has incorrect size.");
-
-struct ApplicationAlbumEntry {
- u64 size{}; // Size of the entry
- u64 hash{}; // AES256 with hardcoded key over AlbumEntry
- AlbumFileDateTime datetime{};
- AlbumStorage storage{};
- ContentType content{};
- INSERT_PADDING_BYTES(5);
- u8 unknown{1}; // Set to 1 on official SW
-};
-static_assert(sizeof(ApplicationAlbumEntry) == 0x20, "ApplicationAlbumEntry has incorrect size.");
-
-struct ApplicationAlbumFileEntry {
- ApplicationAlbumEntry entry{};
- AlbumFileDateTime datetime{};
- u64 unknown{};
-};
-static_assert(sizeof(ApplicationAlbumFileEntry) == 0x30,
- "ApplicationAlbumFileEntry has incorrect size.");
-
void LoopProcess(Core::System& system);
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_a.cpp b/src/core/hle/service/caps/caps_a.cpp
index 44267b284..9925720a3 100644
--- a/src/core/hle/service/caps/caps_a.cpp
+++ b/src/core/hle/service/caps/caps_a.cpp
@@ -1,40 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include "common/logging/log.h"
#include "core/hle/service/caps/caps_a.h"
+#include "core/hle/service/caps/caps_manager.h"
+#include "core/hle/service/caps/caps_result.h"
+#include "core/hle/service/caps/caps_types.h"
+#include "core/hle/service/ipc_helpers.h"
namespace Service::Capture {
-class IAlbumAccessorSession final : public ServiceFramework<IAlbumAccessorSession> {
-public:
- explicit IAlbumAccessorSession(Core::System& system_)
- : ServiceFramework{system_, "IAlbumAccessorSession"} {
- // clang-format off
- static const FunctionInfo functions[] = {
- {2001, nullptr, "OpenAlbumMovieReadStream"},
- {2002, nullptr, "CloseAlbumMovieReadStream"},
- {2003, nullptr, "GetAlbumMovieReadStreamMovieDataSize"},
- {2004, nullptr, "ReadMovieDataFromAlbumMovieReadStream"},
- {2005, nullptr, "GetAlbumMovieReadStreamBrokenReason"},
- {2006, nullptr, "GetAlbumMovieReadStreamImageDataSize"},
- {2007, nullptr, "ReadImageDataFromAlbumMovieReadStream"},
- {2008, nullptr, "ReadFileAttributeFromAlbumMovieReadStream"},
- };
- // clang-format on
-
- RegisterHandlers(functions);
- }
-};
-
-CAPS_A::CAPS_A(Core::System& system_) : ServiceFramework{system_, "caps:a"} {
+IAlbumAccessorService::IAlbumAccessorService(Core::System& system_,
+ std::shared_ptr<AlbumManager> album_manager)
+ : ServiceFramework{system_, "caps:a"}, manager{album_manager} {
// clang-format off
static const FunctionInfo functions[] = {
{0, nullptr, "GetAlbumFileCount"},
{1, nullptr, "GetAlbumFileList"},
{2, nullptr, "LoadAlbumFile"},
- {3, nullptr, "DeleteAlbumFile"},
+ {3, &IAlbumAccessorService::DeleteAlbumFile, "DeleteAlbumFile"},
{4, nullptr, "StorageCopyAlbumFile"},
- {5, nullptr, "IsAlbumMounted"},
+ {5, &IAlbumAccessorService::IsAlbumMounted, "IsAlbumMounted"},
{6, nullptr, "GetAlbumUsage"},
{7, nullptr, "GetAlbumFileSize"},
{8, nullptr, "LoadAlbumFileThumbnail"},
@@ -47,18 +33,18 @@ CAPS_A::CAPS_A(Core::System& system_) : ServiceFramework{system_, "caps:a"} {
{15, nullptr, "GetAlbumUsage3"},
{16, nullptr, "GetAlbumMountResult"},
{17, nullptr, "GetAlbumUsage16"},
- {18, nullptr, "Unknown18"},
+ {18, &IAlbumAccessorService::Unknown18, "Unknown18"},
{19, nullptr, "Unknown19"},
{100, nullptr, "GetAlbumFileCountEx0"},
- {101, nullptr, "GetAlbumFileListEx0"},
+ {101, &IAlbumAccessorService::GetAlbumFileListEx0, "GetAlbumFileListEx0"},
{202, nullptr, "SaveEditedScreenShot"},
{301, nullptr, "GetLastThumbnail"},
{302, nullptr, "GetLastOverlayMovieThumbnail"},
- {401, nullptr, "GetAutoSavingStorage"},
+ {401, &IAlbumAccessorService::GetAutoSavingStorage, "GetAutoSavingStorage"},
{501, nullptr, "GetRequiredStorageSpaceSizeToCopyAll"},
{1001, nullptr, "LoadAlbumScreenShotThumbnailImageEx0"},
- {1002, nullptr, "LoadAlbumScreenShotImageEx1"},
- {1003, nullptr, "LoadAlbumScreenShotThumbnailImageEx1"},
+ {1002, &IAlbumAccessorService::LoadAlbumScreenShotImageEx1, "LoadAlbumScreenShotImageEx1"},
+ {1003, &IAlbumAccessorService::LoadAlbumScreenShotThumbnailImageEx1, "LoadAlbumScreenShotThumbnailImageEx1"},
{8001, nullptr, "ForceAlbumUnmounted"},
{8002, nullptr, "ResetAlbumMountStatus"},
{8011, nullptr, "RefreshAlbumCache"},
@@ -74,6 +60,199 @@ CAPS_A::CAPS_A(Core::System& system_) : ServiceFramework{system_, "caps:a"} {
RegisterHandlers(functions);
}
-CAPS_A::~CAPS_A() = default;
+IAlbumAccessorService::~IAlbumAccessorService() = default;
+
+void IAlbumAccessorService::DeleteAlbumFile(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto file_id{rp.PopRaw<AlbumFileId>()};
+
+ LOG_INFO(Service_Capture, "called, application_id=0x{:0x}, storage={}, type={}",
+ file_id.application_id, file_id.storage, file_id.type);
+
+ Result result = manager->DeleteAlbumFile(file_id);
+ result = TranslateResult(result);
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
+}
+
+void IAlbumAccessorService::IsAlbumMounted(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto storage{rp.PopEnum<AlbumStorage>()};
+
+ LOG_INFO(Service_Capture, "called, storage={}", storage);
+
+ Result result = manager->IsAlbumMounted(storage);
+ const bool is_mounted = result.IsSuccess();
+ result = TranslateResult(result);
+
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result);
+ rb.Push<u8>(is_mounted);
+}
+
+void IAlbumAccessorService::Unknown18(HLERequestContext& ctx) {
+ struct UnknownBuffer {
+ INSERT_PADDING_BYTES(0x10);
+ };
+ static_assert(sizeof(UnknownBuffer) == 0x10, "UnknownBuffer is an invalid size");
+
+ LOG_WARNING(Service_Capture, "(STUBBED) called");
+
+ std::vector<UnknownBuffer> buffer{};
+
+ if (!buffer.empty()) {
+ ctx.WriteBuffer(buffer);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(static_cast<u32>(buffer.size()));
+}
+
+void IAlbumAccessorService::GetAlbumFileListEx0(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto storage{rp.PopEnum<AlbumStorage>()};
+ const auto flags{rp.Pop<u8>()};
+ const auto album_entry_size{ctx.GetWriteBufferNumElements<AlbumEntry>()};
+
+ LOG_INFO(Service_Capture, "called, storage={}, flags={}", storage, flags);
+
+ std::vector<AlbumEntry> entries;
+ Result result = manager->GetAlbumFileList(entries, storage, flags);
+ result = TranslateResult(result);
+
+ entries.resize(std::min(album_entry_size, entries.size()));
+
+ if (!entries.empty()) {
+ ctx.WriteBuffer(entries);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result);
+ rb.Push<u64>(entries.size());
+}
+
+void IAlbumAccessorService::GetAutoSavingStorage(HLERequestContext& ctx) {
+ LOG_WARNING(Service_Capture, "(STUBBED) called");
+
+ bool is_autosaving{};
+ Result result = manager->GetAutoSavingStorage(is_autosaving);
+ result = TranslateResult(result);
+
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(result);
+ rb.Push<u8>(is_autosaving);
+}
+
+void IAlbumAccessorService::LoadAlbumScreenShotImageEx1(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto file_id{rp.PopRaw<AlbumFileId>()};
+ const auto decoder_options{rp.PopRaw<ScreenShotDecodeOption>()};
+ const auto image_buffer_size{ctx.GetWriteBufferSize(1)};
+
+ LOG_INFO(Service_Capture, "called, application_id=0x{:0x}, storage={}, type={}, flags={}",
+ file_id.application_id, file_id.storage, file_id.type, decoder_options.flags);
+
+ std::vector<u8> image;
+ LoadAlbumScreenShotImageOutput image_output;
+ Result result =
+ manager->LoadAlbumScreenShotImage(image_output, image, file_id, decoder_options);
+ result = TranslateResult(result);
+
+ if (image.size() > image_buffer_size) {
+ result = ResultWorkMemoryError;
+ }
+
+ if (result.IsSuccess()) {
+ ctx.WriteBuffer(image_output, 0);
+ ctx.WriteBuffer(image, 1);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
+}
+
+void IAlbumAccessorService::LoadAlbumScreenShotThumbnailImageEx1(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto file_id{rp.PopRaw<AlbumFileId>()};
+ const auto decoder_options{rp.PopRaw<ScreenShotDecodeOption>()};
+
+ LOG_INFO(Service_Capture, "called, application_id=0x{:0x}, storage={}, type={}, flags={}",
+ file_id.application_id, file_id.storage, file_id.type, decoder_options.flags);
+
+ std::vector<u8> image(ctx.GetWriteBufferSize(1));
+ LoadAlbumScreenShotImageOutput image_output;
+ Result result =
+ manager->LoadAlbumScreenShotThumbnail(image_output, image, file_id, decoder_options);
+ result = TranslateResult(result);
+
+ if (result.IsSuccess()) {
+ ctx.WriteBuffer(image_output, 0);
+ ctx.WriteBuffer(image, 1);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
+}
+
+Result IAlbumAccessorService::TranslateResult(Result in_result) {
+ if (in_result.IsSuccess()) {
+ return in_result;
+ }
+
+ if ((in_result.raw & 0x3801ff) == ResultUnknown1024.raw) {
+ if (in_result.description - 0x514 < 100) {
+ return ResultInvalidFileData;
+ }
+ if (in_result.description - 0x5dc < 100) {
+ return ResultInvalidFileData;
+ }
+
+ if (in_result.description - 0x578 < 100) {
+ if (in_result == ResultFileCountLimit) {
+ return ResultUnknown22;
+ }
+ return ResultUnknown25;
+ }
+
+ if (in_result.raw < ResultUnknown1801.raw) {
+ if (in_result == ResultUnknown1202) {
+ return ResultUnknown810;
+ }
+ if (in_result == ResultUnknown1203) {
+ return ResultUnknown810;
+ }
+ if (in_result == ResultUnknown1701) {
+ return ResultUnknown5;
+ }
+ } else if (in_result.raw < ResultUnknown1803.raw) {
+ if (in_result == ResultUnknown1801) {
+ return ResultUnknown5;
+ }
+ if (in_result == ResultUnknown1802) {
+ return ResultUnknown6;
+ }
+ } else {
+ if (in_result == ResultUnknown1803) {
+ return ResultUnknown7;
+ }
+ if (in_result == ResultUnknown1804) {
+ return ResultOutOfRange;
+ }
+ }
+ return ResultUnknown1024;
+ }
+
+ if (in_result.module == ErrorModule::FS) {
+ if ((in_result.description >> 0xc < 0x7d) || (in_result.description - 1000 < 2000) ||
+ (((in_result.description - 3000) >> 3) < 0x271)) {
+ // TODO: Translate FS error
+ return in_result;
+ }
+ }
+
+ return in_result;
+}
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_a.h b/src/core/hle/service/caps/caps_a.h
index 98a21a5ad..c90cff71e 100644
--- a/src/core/hle/service/caps/caps_a.h
+++ b/src/core/hle/service/caps/caps_a.h
@@ -10,11 +10,26 @@ class System;
}
namespace Service::Capture {
+class AlbumManager;
-class CAPS_A final : public ServiceFramework<CAPS_A> {
+class IAlbumAccessorService final : public ServiceFramework<IAlbumAccessorService> {
public:
- explicit CAPS_A(Core::System& system_);
- ~CAPS_A() override;
+ explicit IAlbumAccessorService(Core::System& system_,
+ std::shared_ptr<AlbumManager> album_manager);
+ ~IAlbumAccessorService() override;
+
+private:
+ void DeleteAlbumFile(HLERequestContext& ctx);
+ void IsAlbumMounted(HLERequestContext& ctx);
+ void Unknown18(HLERequestContext& ctx);
+ void GetAlbumFileListEx0(HLERequestContext& ctx);
+ void GetAutoSavingStorage(HLERequestContext& ctx);
+ void LoadAlbumScreenShotImageEx1(HLERequestContext& ctx);
+ void LoadAlbumScreenShotThumbnailImageEx1(HLERequestContext& ctx);
+
+ Result TranslateResult(Result in_result);
+
+ std::shared_ptr<AlbumManager> manager = nullptr;
};
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_c.cpp b/src/core/hle/service/caps/caps_c.cpp
index fc77e35cd..1e7fe6474 100644
--- a/src/core/hle/service/caps/caps_c.cpp
+++ b/src/core/hle/service/caps/caps_c.cpp
@@ -3,53 +3,21 @@
#include "common/logging/log.h"
#include "core/hle/service/caps/caps_c.h"
+#include "core/hle/service/caps/caps_manager.h"
+#include "core/hle/service/caps/caps_result.h"
+#include "core/hle/service/caps/caps_types.h"
#include "core/hle/service/ipc_helpers.h"
namespace Service::Capture {
-class IAlbumControlSession final : public ServiceFramework<IAlbumControlSession> {
-public:
- explicit IAlbumControlSession(Core::System& system_)
- : ServiceFramework{system_, "IAlbumControlSession"} {
- // clang-format off
- static const FunctionInfo functions[] = {
- {2001, nullptr, "OpenAlbumMovieReadStream"},
- {2002, nullptr, "CloseAlbumMovieReadStream"},
- {2003, nullptr, "GetAlbumMovieReadStreamMovieDataSize"},
- {2004, nullptr, "ReadMovieDataFromAlbumMovieReadStream"},
- {2005, nullptr, "GetAlbumMovieReadStreamBrokenReason"},
- {2006, nullptr, "GetAlbumMovieReadStreamImageDataSize"},
- {2007, nullptr, "ReadImageDataFromAlbumMovieReadStream"},
- {2008, nullptr, "ReadFileAttributeFromAlbumMovieReadStream"},
- {2401, nullptr, "OpenAlbumMovieWriteStream"},
- {2402, nullptr, "FinishAlbumMovieWriteStream"},
- {2403, nullptr, "CommitAlbumMovieWriteStream"},
- {2404, nullptr, "DiscardAlbumMovieWriteStream"},
- {2405, nullptr, "DiscardAlbumMovieWriteStreamNoDelete"},
- {2406, nullptr, "CommitAlbumMovieWriteStreamEx"},
- {2411, nullptr, "StartAlbumMovieWriteStreamDataSection"},
- {2412, nullptr, "EndAlbumMovieWriteStreamDataSection"},
- {2413, nullptr, "StartAlbumMovieWriteStreamMetaSection"},
- {2414, nullptr, "EndAlbumMovieWriteStreamMetaSection"},
- {2421, nullptr, "ReadDataFromAlbumMovieWriteStream"},
- {2422, nullptr, "WriteDataToAlbumMovieWriteStream"},
- {2424, nullptr, "WriteMetaToAlbumMovieWriteStream"},
- {2431, nullptr, "GetAlbumMovieWriteStreamBrokenReason"},
- {2433, nullptr, "GetAlbumMovieWriteStreamDataSize"},
- {2434, nullptr, "SetAlbumMovieWriteStreamDataSize"},
- };
- // clang-format on
-
- RegisterHandlers(functions);
- }
-};
-
-CAPS_C::CAPS_C(Core::System& system_) : ServiceFramework{system_, "caps:c"} {
+IAlbumControlService::IAlbumControlService(Core::System& system_,
+ std::shared_ptr<AlbumManager> album_manager)
+ : ServiceFramework{system_, "caps:c"}, manager{album_manager} {
// clang-format off
static const FunctionInfo functions[] = {
{1, nullptr, "CaptureRawImage"},
{2, nullptr, "CaptureRawImageWithTimeout"},
- {33, &CAPS_C::SetShimLibraryVersion, "SetShimLibraryVersion"},
+ {33, &IAlbumControlService::SetShimLibraryVersion, "SetShimLibraryVersion"},
{1001, nullptr, "RequestTakingScreenShot"},
{1002, nullptr, "RequestTakingScreenShotWithTimeout"},
{1011, nullptr, "NotifyTakingScreenShotRefused"},
@@ -72,9 +40,9 @@ CAPS_C::CAPS_C(Core::System& system_) : ServiceFramework{system_, "caps:c"} {
RegisterHandlers(functions);
}
-CAPS_C::~CAPS_C() = default;
+IAlbumControlService::~IAlbumControlService() = default;
-void CAPS_C::SetShimLibraryVersion(HLERequestContext& ctx) {
+void IAlbumControlService::SetShimLibraryVersion(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto library_version{rp.Pop<u64>()};
const auto applet_resource_user_id{rp.Pop<u64>()};
diff --git a/src/core/hle/service/caps/caps_c.h b/src/core/hle/service/caps/caps_c.h
index 537b3a2e3..92ba242db 100644
--- a/src/core/hle/service/caps/caps_c.h
+++ b/src/core/hle/service/caps/caps_c.h
@@ -10,14 +10,18 @@ class System;
}
namespace Service::Capture {
+class AlbumManager;
-class CAPS_C final : public ServiceFramework<CAPS_C> {
+class IAlbumControlService final : public ServiceFramework<IAlbumControlService> {
public:
- explicit CAPS_C(Core::System& system_);
- ~CAPS_C() override;
+ explicit IAlbumControlService(Core::System& system_,
+ std::shared_ptr<AlbumManager> album_manager);
+ ~IAlbumControlService() override;
private:
void SetShimLibraryVersion(HLERequestContext& ctx);
+
+ std::shared_ptr<AlbumManager> manager = nullptr;
};
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_manager.cpp b/src/core/hle/service/caps/caps_manager.cpp
new file mode 100644
index 000000000..2b4e3f076
--- /dev/null
+++ b/src/core/hle/service/caps/caps_manager.cpp
@@ -0,0 +1,386 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <sstream>
+#include <stb_image.h>
+#include <stb_image_resize.h>
+
+#include "common/fs/file.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+#include "core/core.h"
+#include "core/hle/service/caps/caps_manager.h"
+#include "core/hle/service/caps/caps_result.h"
+#include "core/hle/service/time/time_manager.h"
+#include "core/hle/service/time/time_zone_content_manager.h"
+
+namespace Service::Capture {
+
+AlbumManager::AlbumManager(Core::System& system_) : system{system_} {}
+
+AlbumManager::~AlbumManager() = default;
+
+Result AlbumManager::DeleteAlbumFile(const AlbumFileId& file_id) {
+ if (file_id.storage > AlbumStorage::Sd) {
+ return ResultInvalidStorage;
+ }
+
+ if (!is_mounted) {
+ return ResultIsNotMounted;
+ }
+
+ std::filesystem::path path;
+ const auto result = GetFile(path, file_id);
+
+ if (result.IsError()) {
+ return result;
+ }
+
+ if (!Common::FS::RemoveFile(path)) {
+ return ResultFileNotFound;
+ }
+
+ return ResultSuccess;
+}
+
+Result AlbumManager::IsAlbumMounted(AlbumStorage storage) {
+ if (storage > AlbumStorage::Sd) {
+ return ResultInvalidStorage;
+ }
+
+ is_mounted = true;
+
+ if (storage == AlbumStorage::Sd) {
+ FindScreenshots();
+ }
+
+ return is_mounted ? ResultSuccess : ResultIsNotMounted;
+}
+
+Result AlbumManager::GetAlbumFileList(std::vector<AlbumEntry>& out_entries, AlbumStorage storage,
+ u8 flags) const {
+ if (storage > AlbumStorage::Sd) {
+ return ResultInvalidStorage;
+ }
+
+ if (!is_mounted) {
+ return ResultIsNotMounted;
+ }
+
+ for (auto& [file_id, path] : album_files) {
+ if (file_id.storage != storage) {
+ continue;
+ }
+ if (out_entries.size() >= SdAlbumFileLimit) {
+ break;
+ }
+
+ const auto entry_size = Common::FS::GetSize(path);
+ out_entries.push_back({
+ .entry_size = entry_size,
+ .file_id = file_id,
+ });
+ }
+
+ return ResultSuccess;
+}
+
+Result AlbumManager::GetAlbumFileList(std::vector<ApplicationAlbumFileEntry>& out_entries,
+ ContentType contex_type, s64 start_posix_time,
+ s64 end_posix_time, u64 aruid) const {
+ if (!is_mounted) {
+ return ResultIsNotMounted;
+ }
+
+ std::vector<ApplicationAlbumEntry> album_entries;
+ const auto start_date = ConvertToAlbumDateTime(start_posix_time);
+ const auto end_date = ConvertToAlbumDateTime(end_posix_time);
+ const auto result = GetAlbumFileList(album_entries, contex_type, start_date, end_date, aruid);
+
+ if (result.IsError()) {
+ return result;
+ }
+
+ for (const auto& album_entry : album_entries) {
+ ApplicationAlbumFileEntry entry{
+ .entry = album_entry,
+ .datetime = album_entry.datetime,
+ .unknown = {},
+ };
+ out_entries.push_back(entry);
+ }
+
+ return ResultSuccess;
+}
+
+Result AlbumManager::GetAlbumFileList(std::vector<ApplicationAlbumEntry>& out_entries,
+ ContentType contex_type, AlbumFileDateTime start_date,
+ AlbumFileDateTime end_date, u64 aruid) const {
+ if (!is_mounted) {
+ return ResultIsNotMounted;
+ }
+
+ for (auto& [file_id, path] : album_files) {
+ if (file_id.type != contex_type) {
+ continue;
+ }
+ if (file_id.date > start_date) {
+ continue;
+ }
+ if (file_id.date < end_date) {
+ continue;
+ }
+ if (out_entries.size() >= SdAlbumFileLimit) {
+ break;
+ }
+
+ const auto entry_size = Common::FS::GetSize(path);
+ ApplicationAlbumEntry entry{
+ .size = entry_size,
+ .hash{},
+ .datetime = file_id.date,
+ .storage = file_id.storage,
+ .content = contex_type,
+ .unknown = 1,
+ };
+ out_entries.push_back(entry);
+ }
+
+ return ResultSuccess;
+}
+
+Result AlbumManager::GetAutoSavingStorage(bool& out_is_autosaving) const {
+ out_is_autosaving = false;
+ return ResultSuccess;
+}
+
+Result AlbumManager::LoadAlbumScreenShotImage(LoadAlbumScreenShotImageOutput& out_image_output,
+ std::vector<u8>& out_image,
+ const AlbumFileId& file_id,
+ const ScreenShotDecodeOption& decoder_options) const {
+ if (file_id.storage > AlbumStorage::Sd) {
+ return ResultInvalidStorage;
+ }
+
+ if (!is_mounted) {
+ return ResultIsNotMounted;
+ }
+
+ out_image_output = {
+ .width = 1280,
+ .height = 720,
+ .attribute =
+ {
+ .unknown_0{},
+ .orientation = AlbumImageOrientation::None,
+ .unknown_1{},
+ .unknown_2{},
+ },
+ };
+
+ std::filesystem::path path;
+ const auto result = GetFile(path, file_id);
+
+ if (result.IsError()) {
+ return result;
+ }
+
+ out_image.resize(out_image_output.height * out_image_output.width * STBI_rgb_alpha);
+
+ return LoadImage(out_image, path, static_cast<int>(out_image_output.width),
+ +static_cast<int>(out_image_output.height), decoder_options.flags);
+}
+
+Result AlbumManager::LoadAlbumScreenShotThumbnail(
+ LoadAlbumScreenShotImageOutput& out_image_output, std::vector<u8>& out_image,
+ const AlbumFileId& file_id, const ScreenShotDecodeOption& decoder_options) const {
+ if (file_id.storage > AlbumStorage::Sd) {
+ return ResultInvalidStorage;
+ }
+
+ if (!is_mounted) {
+ return ResultIsNotMounted;
+ }
+
+ out_image_output = {
+ .width = 320,
+ .height = 180,
+ .attribute =
+ {
+ .unknown_0{},
+ .orientation = AlbumImageOrientation::None,
+ .unknown_1{},
+ .unknown_2{},
+ },
+ };
+
+ std::filesystem::path path;
+ const auto result = GetFile(path, file_id);
+
+ if (result.IsError()) {
+ return result;
+ }
+
+ out_image.resize(out_image_output.height * out_image_output.width * STBI_rgb_alpha);
+
+ return LoadImage(out_image, path, static_cast<int>(out_image_output.width),
+ +static_cast<int>(out_image_output.height), decoder_options.flags);
+}
+
+Result AlbumManager::GetFile(std::filesystem::path& out_path, const AlbumFileId& file_id) const {
+ const auto file = album_files.find(file_id);
+
+ if (file == album_files.end()) {
+ return ResultFileNotFound;
+ }
+
+ out_path = file->second;
+ return ResultSuccess;
+}
+
+void AlbumManager::FindScreenshots() {
+ is_mounted = false;
+ album_files.clear();
+
+ // TODO: Swap this with a blocking operation.
+ const auto screenshots_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ScreenshotsDir);
+ Common::FS::IterateDirEntries(
+ screenshots_dir,
+ [this](const std::filesystem::path& full_path) {
+ AlbumEntry entry;
+ if (GetAlbumEntry(entry, full_path).IsError()) {
+ return true;
+ }
+ while (album_files.contains(entry.file_id)) {
+ if (++entry.file_id.date.unique_id == 0) {
+ break;
+ }
+ }
+ album_files[entry.file_id] = full_path;
+ return true;
+ },
+ Common::FS::DirEntryFilter::File);
+
+ is_mounted = true;
+}
+
+Result AlbumManager::GetAlbumEntry(AlbumEntry& out_entry, const std::filesystem::path& path) const {
+ std::istringstream line_stream(path.filename().string());
+ std::string date;
+ std::string application;
+ std::string time;
+
+ // Parse filename to obtain entry properties
+ std::getline(line_stream, application, '_');
+ std::getline(line_stream, date, '_');
+ std::getline(line_stream, time, '_');
+
+ std::istringstream date_stream(date);
+ std::istringstream time_stream(time);
+ std::string year;
+ std::string month;
+ std::string day;
+ std::string hour;
+ std::string minute;
+ std::string second;
+
+ std::getline(date_stream, year, '-');
+ std::getline(date_stream, month, '-');
+ std::getline(date_stream, day, '-');
+
+ std::getline(time_stream, hour, '-');
+ std::getline(time_stream, minute, '-');
+ std::getline(time_stream, second, '-');
+
+ try {
+ out_entry = {
+ .entry_size = 1,
+ .file_id{
+ .application_id = static_cast<u64>(std::stoll(application, 0, 16)),
+ .date =
+ {
+ .year = static_cast<s16>(std::stoi(year)),
+ .month = static_cast<s8>(std::stoi(month)),
+ .day = static_cast<s8>(std::stoi(day)),
+ .hour = static_cast<s8>(std::stoi(hour)),
+ .minute = static_cast<s8>(std::stoi(minute)),
+ .second = static_cast<s8>(std::stoi(second)),
+ .unique_id = 0,
+ },
+ .storage = AlbumStorage::Sd,
+ .type = ContentType::Screenshot,
+ .unknown = 1,
+ },
+ };
+ } catch (const std::invalid_argument&) {
+ return ResultUnknown;
+ } catch (const std::out_of_range&) {
+ return ResultUnknown;
+ } catch (const std::exception&) {
+ return ResultUnknown;
+ }
+
+ return ResultSuccess;
+}
+
+Result AlbumManager::LoadImage(std::span<u8> out_image, const std::filesystem::path& path,
+ int width, int height, ScreenShotDecoderFlag flag) const {
+ if (out_image.size() != static_cast<std::size_t>(width * height * STBI_rgb_alpha)) {
+ return ResultUnknown;
+ }
+
+ const Common::FS::IOFile db_file{path, Common::FS::FileAccessMode::Read,
+ Common::FS::FileType::BinaryFile};
+
+ std::vector<u8> raw_file(db_file.GetSize());
+ if (db_file.Read(raw_file) != raw_file.size()) {
+ return ResultUnknown;
+ }
+
+ int filter_flag = STBIR_FILTER_DEFAULT;
+ int original_width, original_height, color_channels;
+ const auto dbi_image =
+ stbi_load_from_memory(raw_file.data(), static_cast<int>(raw_file.size()), &original_width,
+ &original_height, &color_channels, STBI_rgb_alpha);
+
+ if (dbi_image == nullptr) {
+ return ResultUnknown;
+ }
+
+ switch (flag) {
+ case ScreenShotDecoderFlag::EnableFancyUpsampling:
+ filter_flag = STBIR_FILTER_TRIANGLE;
+ break;
+ case ScreenShotDecoderFlag::EnableBlockSmoothing:
+ filter_flag = STBIR_FILTER_BOX;
+ break;
+ default:
+ filter_flag = STBIR_FILTER_DEFAULT;
+ break;
+ }
+
+ stbir_resize_uint8_srgb(dbi_image, original_width, original_height, 0, out_image.data(), width,
+ height, 0, STBI_rgb_alpha, 3, filter_flag);
+
+ return ResultSuccess;
+}
+
+AlbumFileDateTime AlbumManager::ConvertToAlbumDateTime(u64 posix_time) const {
+ Time::TimeZone::CalendarInfo calendar_date{};
+ const auto& time_zone_manager =
+ system.GetTimeManager().GetTimeZoneContentManager().GetTimeZoneManager();
+
+ time_zone_manager.ToCalendarTimeWithMyRules(posix_time, calendar_date);
+
+ return {
+ .year = calendar_date.time.year,
+ .month = calendar_date.time.month,
+ .day = calendar_date.time.day,
+ .hour = calendar_date.time.hour,
+ .minute = calendar_date.time.minute,
+ .second = calendar_date.time.second,
+ .unique_id = 0,
+ };
+}
+
+} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_manager.h b/src/core/hle/service/caps/caps_manager.h
new file mode 100644
index 000000000..f65eb12c1
--- /dev/null
+++ b/src/core/hle/service/caps/caps_manager.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <unordered_map>
+
+#include "common/fs/fs.h"
+#include "core/hle/result.h"
+#include "core/hle/service/caps/caps_types.h"
+
+namespace Core {
+class System;
+}
+
+namespace std {
+// Hash used to create lists from AlbumFileId data
+template <>
+struct hash<Service::Capture::AlbumFileId> {
+ size_t operator()(const Service::Capture::AlbumFileId& pad_id) const noexcept {
+ u64 hash_value = (static_cast<u64>(pad_id.date.year) << 8);
+ hash_value ^= (static_cast<u64>(pad_id.date.month) << 7);
+ hash_value ^= (static_cast<u64>(pad_id.date.day) << 6);
+ hash_value ^= (static_cast<u64>(pad_id.date.hour) << 5);
+ hash_value ^= (static_cast<u64>(pad_id.date.minute) << 4);
+ hash_value ^= (static_cast<u64>(pad_id.date.second) << 3);
+ hash_value ^= (static_cast<u64>(pad_id.date.unique_id) << 2);
+ hash_value ^= (static_cast<u64>(pad_id.storage) << 1);
+ hash_value ^= static_cast<u64>(pad_id.type);
+ return static_cast<size_t>(hash_value);
+ }
+};
+
+} // namespace std
+
+namespace Service::Capture {
+
+class AlbumManager {
+public:
+ explicit AlbumManager(Core::System& system_);
+ ~AlbumManager();
+
+ Result DeleteAlbumFile(const AlbumFileId& file_id);
+ Result IsAlbumMounted(AlbumStorage storage);
+ Result GetAlbumFileList(std::vector<AlbumEntry>& out_entries, AlbumStorage storage,
+ u8 flags) const;
+ Result GetAlbumFileList(std::vector<ApplicationAlbumFileEntry>& out_entries,
+ ContentType contex_type, s64 start_posix_time, s64 end_posix_time,
+ u64 aruid) const;
+ Result GetAlbumFileList(std::vector<ApplicationAlbumEntry>& out_entries,
+ ContentType contex_type, AlbumFileDateTime start_date,
+ AlbumFileDateTime end_date, u64 aruid) const;
+ Result GetAutoSavingStorage(bool& out_is_autosaving) const;
+ Result LoadAlbumScreenShotImage(LoadAlbumScreenShotImageOutput& out_image_output,
+ std::vector<u8>& out_image, const AlbumFileId& file_id,
+ const ScreenShotDecodeOption& decoder_options) const;
+ Result LoadAlbumScreenShotThumbnail(LoadAlbumScreenShotImageOutput& out_image_output,
+ std::vector<u8>& out_image, const AlbumFileId& file_id,
+ const ScreenShotDecodeOption& decoder_options) const;
+
+private:
+ static constexpr std::size_t NandAlbumFileLimit = 1000;
+ static constexpr std::size_t SdAlbumFileLimit = 10000;
+
+ void FindScreenshots();
+ Result GetFile(std::filesystem::path& out_path, const AlbumFileId& file_id) const;
+ Result GetAlbumEntry(AlbumEntry& out_entry, const std::filesystem::path& path) const;
+ Result LoadImage(std::span<u8> out_image, const std::filesystem::path& path, int width,
+ int height, ScreenShotDecoderFlag flag) const;
+
+ AlbumFileDateTime ConvertToAlbumDateTime(u64 posix_time) const;
+
+ bool is_mounted{};
+ std::unordered_map<AlbumFileId, std::filesystem::path> album_files;
+
+ Core::System& system;
+};
+
+} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_result.h b/src/core/hle/service/caps/caps_result.h
new file mode 100644
index 000000000..c65e5fb9a
--- /dev/null
+++ b/src/core/hle/service/caps/caps_result.h
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "core/hle/result.h"
+
+namespace Service::Capture {
+
+constexpr Result ResultWorkMemoryError(ErrorModule::Capture, 3);
+constexpr Result ResultUnknown5(ErrorModule::Capture, 5);
+constexpr Result ResultUnknown6(ErrorModule::Capture, 6);
+constexpr Result ResultUnknown7(ErrorModule::Capture, 7);
+constexpr Result ResultOutOfRange(ErrorModule::Capture, 8);
+constexpr Result ResulInvalidTimestamp(ErrorModule::Capture, 12);
+constexpr Result ResultInvalidStorage(ErrorModule::Capture, 13);
+constexpr Result ResultInvalidFileContents(ErrorModule::Capture, 14);
+constexpr Result ResultIsNotMounted(ErrorModule::Capture, 21);
+constexpr Result ResultUnknown22(ErrorModule::Capture, 22);
+constexpr Result ResultFileNotFound(ErrorModule::Capture, 23);
+constexpr Result ResultInvalidFileData(ErrorModule::Capture, 24);
+constexpr Result ResultUnknown25(ErrorModule::Capture, 25);
+constexpr Result ResultReadBufferShortage(ErrorModule::Capture, 30);
+constexpr Result ResultUnknown810(ErrorModule::Capture, 810);
+constexpr Result ResultUnknown1024(ErrorModule::Capture, 1024);
+constexpr Result ResultUnknown1202(ErrorModule::Capture, 1202);
+constexpr Result ResultUnknown1203(ErrorModule::Capture, 1203);
+constexpr Result ResultFileCountLimit(ErrorModule::Capture, 1401);
+constexpr Result ResultUnknown1701(ErrorModule::Capture, 1701);
+constexpr Result ResultUnknown1801(ErrorModule::Capture, 1801);
+constexpr Result ResultUnknown1802(ErrorModule::Capture, 1802);
+constexpr Result ResultUnknown1803(ErrorModule::Capture, 1803);
+constexpr Result ResultUnknown1804(ErrorModule::Capture, 1804);
+
+} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_sc.cpp b/src/core/hle/service/caps/caps_sc.cpp
index 395b13da7..6117cb7c6 100644
--- a/src/core/hle/service/caps/caps_sc.cpp
+++ b/src/core/hle/service/caps/caps_sc.cpp
@@ -5,7 +5,8 @@
namespace Service::Capture {
-CAPS_SC::CAPS_SC(Core::System& system_) : ServiceFramework{system_, "caps:sc"} {
+IScreenShotControlService::IScreenShotControlService(Core::System& system_)
+ : ServiceFramework{system_, "caps:sc"} {
// clang-format off
static const FunctionInfo functions[] = {
{1, nullptr, "CaptureRawImage"},
@@ -34,6 +35,6 @@ CAPS_SC::CAPS_SC(Core::System& system_) : ServiceFramework{system_, "caps:sc"} {
RegisterHandlers(functions);
}
-CAPS_SC::~CAPS_SC() = default;
+IScreenShotControlService::~IScreenShotControlService() = default;
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_sc.h b/src/core/hle/service/caps/caps_sc.h
index e5600f6d7..d555f4979 100644
--- a/src/core/hle/service/caps/caps_sc.h
+++ b/src/core/hle/service/caps/caps_sc.h
@@ -11,10 +11,10 @@ class System;
namespace Service::Capture {
-class CAPS_SC final : public ServiceFramework<CAPS_SC> {
+class IScreenShotControlService final : public ServiceFramework<IScreenShotControlService> {
public:
- explicit CAPS_SC(Core::System& system_);
- ~CAPS_SC() override;
+ explicit IScreenShotControlService(Core::System& system_);
+ ~IScreenShotControlService() override;
};
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_ss.cpp b/src/core/hle/service/caps/caps_ss.cpp
index 62b9edd41..d0d1b5425 100644
--- a/src/core/hle/service/caps/caps_ss.cpp
+++ b/src/core/hle/service/caps/caps_ss.cpp
@@ -5,7 +5,8 @@
namespace Service::Capture {
-CAPS_SS::CAPS_SS(Core::System& system_) : ServiceFramework{system_, "caps:ss"} {
+IScreenShotService::IScreenShotService(Core::System& system_)
+ : ServiceFramework{system_, "caps:ss"} {
// clang-format off
static const FunctionInfo functions[] = {
{201, nullptr, "SaveScreenShot"},
@@ -21,6 +22,6 @@ CAPS_SS::CAPS_SS(Core::System& system_) : ServiceFramework{system_, "caps:ss"} {
RegisterHandlers(functions);
}
-CAPS_SS::~CAPS_SS() = default;
+IScreenShotService::~IScreenShotService() = default;
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_ss.h b/src/core/hle/service/caps/caps_ss.h
index 718ade485..381e44fd4 100644
--- a/src/core/hle/service/caps/caps_ss.h
+++ b/src/core/hle/service/caps/caps_ss.h
@@ -11,10 +11,10 @@ class System;
namespace Service::Capture {
-class CAPS_SS final : public ServiceFramework<CAPS_SS> {
+class IScreenShotService final : public ServiceFramework<IScreenShotService> {
public:
- explicit CAPS_SS(Core::System& system_);
- ~CAPS_SS() override;
+ explicit IScreenShotService(Core::System& system_);
+ ~IScreenShotService() override;
};
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_su.cpp b/src/core/hle/service/caps/caps_su.cpp
index 3b11cc95c..cad173dc7 100644
--- a/src/core/hle/service/caps/caps_su.cpp
+++ b/src/core/hle/service/caps/caps_su.cpp
@@ -7,10 +7,11 @@
namespace Service::Capture {
-CAPS_SU::CAPS_SU(Core::System& system_) : ServiceFramework{system_, "caps:su"} {
+IScreenShotApplicationService::IScreenShotApplicationService(Core::System& system_)
+ : ServiceFramework{system_, "caps:su"} {
// clang-format off
static const FunctionInfo functions[] = {
- {32, &CAPS_SU::SetShimLibraryVersion, "SetShimLibraryVersion"},
+ {32, &IScreenShotApplicationService::SetShimLibraryVersion, "SetShimLibraryVersion"},
{201, nullptr, "SaveScreenShot"},
{203, nullptr, "SaveScreenShotEx0"},
{205, nullptr, "SaveScreenShotEx1"},
@@ -21,9 +22,9 @@ CAPS_SU::CAPS_SU(Core::System& system_) : ServiceFramework{system_, "caps:su"} {
RegisterHandlers(functions);
}
-CAPS_SU::~CAPS_SU() = default;
+IScreenShotApplicationService::~IScreenShotApplicationService() = default;
-void CAPS_SU::SetShimLibraryVersion(HLERequestContext& ctx) {
+void IScreenShotApplicationService::SetShimLibraryVersion(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto library_version{rp.Pop<u64>()};
const auto applet_resource_user_id{rp.Pop<u64>()};
diff --git a/src/core/hle/service/caps/caps_su.h b/src/core/hle/service/caps/caps_su.h
index c6398858d..647e3059d 100644
--- a/src/core/hle/service/caps/caps_su.h
+++ b/src/core/hle/service/caps/caps_su.h
@@ -11,10 +11,10 @@ class System;
namespace Service::Capture {
-class CAPS_SU final : public ServiceFramework<CAPS_SU> {
+class IScreenShotApplicationService final : public ServiceFramework<IScreenShotApplicationService> {
public:
- explicit CAPS_SU(Core::System& system_);
- ~CAPS_SU() override;
+ explicit IScreenShotApplicationService(Core::System& system_);
+ ~IScreenShotApplicationService() override;
private:
void SetShimLibraryVersion(HLERequestContext& ctx);
diff --git a/src/core/hle/service/caps/caps_types.h b/src/core/hle/service/caps/caps_types.h
new file mode 100644
index 000000000..7fd357954
--- /dev/null
+++ b/src/core/hle/service/caps/caps_types.h
@@ -0,0 +1,184 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+
+namespace Service::Capture {
+
+// This is nn::album::ImageOrientation
+enum class AlbumImageOrientation {
+ None,
+ Rotate90,
+ Rotate180,
+ Rotate270,
+};
+
+// This is nn::album::AlbumReportOption
+enum class AlbumReportOption : s32 {
+ Disable,
+ Enable,
+};
+
+enum class ContentType : u8 {
+ Screenshot = 0,
+ Movie = 1,
+ ExtraMovie = 3,
+};
+
+enum class AlbumStorage : u8 {
+ Nand,
+ Sd,
+};
+
+enum class ScreenShotDecoderFlag : u64 {
+ None = 0,
+ EnableFancyUpsampling = 1 << 0,
+ EnableBlockSmoothing = 1 << 1,
+};
+
+// This is nn::capsrv::AlbumFileDateTime
+struct AlbumFileDateTime {
+ s16 year{};
+ s8 month{};
+ s8 day{};
+ s8 hour{};
+ s8 minute{};
+ s8 second{};
+ s8 unique_id{};
+
+ friend constexpr bool operator==(const AlbumFileDateTime&, const AlbumFileDateTime&) = default;
+ friend constexpr bool operator>(const AlbumFileDateTime& a, const AlbumFileDateTime& b) {
+ if (a.year > b.year) {
+ return true;
+ }
+ if (a.month > b.month) {
+ return true;
+ }
+ if (a.day > b.day) {
+ return true;
+ }
+ if (a.hour > b.hour) {
+ return true;
+ }
+ if (a.minute > b.minute) {
+ return true;
+ }
+ return a.second > b.second;
+ };
+ friend constexpr bool operator<(const AlbumFileDateTime& a, const AlbumFileDateTime& b) {
+ if (a.year < b.year) {
+ return true;
+ }
+ if (a.month < b.month) {
+ return true;
+ }
+ if (a.day < b.day) {
+ return true;
+ }
+ if (a.hour < b.hour) {
+ return true;
+ }
+ if (a.minute < b.minute) {
+ return true;
+ }
+ return a.second < b.second;
+ };
+};
+static_assert(sizeof(AlbumFileDateTime) == 0x8, "AlbumFileDateTime has incorrect size.");
+
+// This is nn::album::AlbumEntry
+struct AlbumFileEntry {
+ u64 size{}; // Size of the entry
+ u64 hash{}; // AES256 with hardcoded key over AlbumEntry
+ AlbumFileDateTime datetime{};
+ AlbumStorage storage{};
+ ContentType content{};
+ INSERT_PADDING_BYTES(5);
+ u8 unknown{}; // Set to 1 on official SW
+};
+static_assert(sizeof(AlbumFileEntry) == 0x20, "AlbumFileEntry has incorrect size.");
+
+struct AlbumFileId {
+ u64 application_id{};
+ AlbumFileDateTime date{};
+ AlbumStorage storage{};
+ ContentType type{};
+ INSERT_PADDING_BYTES(0x5);
+ u8 unknown{};
+
+ friend constexpr bool operator==(const AlbumFileId&, const AlbumFileId&) = default;
+};
+static_assert(sizeof(AlbumFileId) == 0x18, "AlbumFileId is an invalid size");
+
+// This is nn::capsrv::AlbumEntry
+struct AlbumEntry {
+ u64 entry_size{};
+ AlbumFileId file_id{};
+};
+static_assert(sizeof(AlbumEntry) == 0x20, "AlbumEntry has incorrect size.");
+
+// This is nn::capsrv::ApplicationAlbumEntry
+struct ApplicationAlbumEntry {
+ u64 size{}; // Size of the entry
+ u64 hash{}; // AES256 with hardcoded key over AlbumEntry
+ AlbumFileDateTime datetime{};
+ AlbumStorage storage{};
+ ContentType content{};
+ INSERT_PADDING_BYTES(5);
+ u8 unknown{1}; // Set to 1 on official SW
+};
+static_assert(sizeof(ApplicationAlbumEntry) == 0x20, "ApplicationAlbumEntry has incorrect size.");
+
+// This is nn::capsrv::ApplicationAlbumFileEntry
+struct ApplicationAlbumFileEntry {
+ ApplicationAlbumEntry entry{};
+ AlbumFileDateTime datetime{};
+ u64 unknown{};
+};
+static_assert(sizeof(ApplicationAlbumFileEntry) == 0x30,
+ "ApplicationAlbumFileEntry has incorrect size.");
+
+struct ApplicationData {
+ std::array<u8, 0x400> data{};
+ u32 data_size{};
+};
+static_assert(sizeof(ApplicationData) == 0x404, "ApplicationData is an invalid size");
+
+struct ScreenShotAttribute {
+ u32 unknown_0{};
+ AlbumImageOrientation orientation{};
+ u32 unknown_1{};
+ u32 unknown_2{};
+ INSERT_PADDING_BYTES(0x30);
+};
+static_assert(sizeof(ScreenShotAttribute) == 0x40, "ScreenShotAttribute is an invalid size");
+
+struct ScreenShotDecodeOption {
+ ScreenShotDecoderFlag flags{};
+ INSERT_PADDING_BYTES(0x18);
+};
+static_assert(sizeof(ScreenShotDecodeOption) == 0x20, "ScreenShotDecodeOption is an invalid size");
+
+struct LoadAlbumScreenShotImageOutput {
+ s64 width{};
+ s64 height{};
+ ScreenShotAttribute attribute{};
+ INSERT_PADDING_BYTES(0x400);
+};
+static_assert(sizeof(LoadAlbumScreenShotImageOutput) == 0x450,
+ "LoadAlbumScreenShotImageOutput is an invalid size");
+
+struct LoadAlbumScreenShotImageOutputForApplication {
+ s64 width{};
+ s64 height{};
+ ScreenShotAttribute attribute{};
+ ApplicationData data{};
+ INSERT_PADDING_BYTES(0xAC);
+};
+static_assert(sizeof(LoadAlbumScreenShotImageOutputForApplication) == 0x500,
+ "LoadAlbumScreenShotImageOutput is an invalid size");
+
+} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_u.cpp b/src/core/hle/service/caps/caps_u.cpp
index bffe0f8d0..b6b33fb2f 100644
--- a/src/core/hle/service/caps/caps_u.cpp
+++ b/src/core/hle/service/caps/caps_u.cpp
@@ -2,45 +2,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
-#include "core/hle/service/caps/caps.h"
+#include "core/hle/service/caps/caps_manager.h"
+#include "core/hle/service/caps/caps_types.h"
#include "core/hle/service/caps/caps_u.h"
#include "core/hle/service/ipc_helpers.h"
namespace Service::Capture {
-class IAlbumAccessorApplicationSession final
- : public ServiceFramework<IAlbumAccessorApplicationSession> {
-public:
- explicit IAlbumAccessorApplicationSession(Core::System& system_)
- : ServiceFramework{system_, "IAlbumAccessorApplicationSession"} {
- // clang-format off
- static const FunctionInfo functions[] = {
- {2001, nullptr, "OpenAlbumMovieReadStream"},
- {2002, nullptr, "CloseAlbumMovieReadStream"},
- {2003, nullptr, "GetAlbumMovieReadStreamMovieDataSize"},
- {2004, nullptr, "ReadMovieDataFromAlbumMovieReadStream"},
- {2005, nullptr, "GetAlbumMovieReadStreamBrokenReason"},
- };
- // clang-format on
-
- RegisterHandlers(functions);
- }
-};
-
-CAPS_U::CAPS_U(Core::System& system_) : ServiceFramework{system_, "caps:u"} {
+IAlbumApplicationService::IAlbumApplicationService(Core::System& system_,
+ std::shared_ptr<AlbumManager> album_manager)
+ : ServiceFramework{system_, "caps:u"}, manager{album_manager} {
// clang-format off
static const FunctionInfo functions[] = {
- {32, &CAPS_U::SetShimLibraryVersion, "SetShimLibraryVersion"},
- {102, &CAPS_U::GetAlbumContentsFileListForApplication, "GetAlbumContentsFileListForApplication"},
- {103, nullptr, "DeleteAlbumContentsFileForApplication"},
- {104, nullptr, "GetAlbumContentsFileSizeForApplication"},
+ {32, &IAlbumApplicationService::SetShimLibraryVersion, "SetShimLibraryVersion"},
+ {102, &IAlbumApplicationService::GetAlbumFileList0AafeAruidDeprecated, "GetAlbumFileList0AafeAruidDeprecated"},
+ {103, nullptr, "DeleteAlbumFileByAruid"},
+ {104, nullptr, "GetAlbumFileSizeByAruid"},
{105, nullptr, "DeleteAlbumFileByAruidForDebug"},
- {110, nullptr, "LoadAlbumContentsFileScreenShotImageForApplication"},
- {120, nullptr, "LoadAlbumContentsFileThumbnailImageForApplication"},
- {130, nullptr, "PrecheckToCreateContentsForApplication"},
+ {110, nullptr, "LoadAlbumScreenShotImageByAruid"},
+ {120, nullptr, "LoadAlbumScreenShotThumbnailImageByAruid"},
+ {130, nullptr, "PrecheckToCreateContentsByAruid"},
{140, nullptr, "GetAlbumFileList1AafeAruidDeprecated"},
{141, nullptr, "GetAlbumFileList2AafeUidAruidDeprecated"},
- {142, &CAPS_U::GetAlbumFileList3AaeAruid, "GetAlbumFileList3AaeAruid"},
+ {142, &IAlbumApplicationService::GetAlbumFileList3AaeAruid, "GetAlbumFileList3AaeAruid"},
{143, nullptr, "GetAlbumFileList4AaeUidAruid"},
{144, nullptr, "GetAllAlbumFileList3AaeAruid"},
{60002, nullptr, "OpenAccessorSessionForApplication"},
@@ -50,9 +34,9 @@ CAPS_U::CAPS_U(Core::System& system_) : ServiceFramework{system_, "caps:u"} {
RegisterHandlers(functions);
}
-CAPS_U::~CAPS_U() = default;
+IAlbumApplicationService::~IAlbumApplicationService() = default;
-void CAPS_U::SetShimLibraryVersion(HLERequestContext& ctx) {
+void IAlbumApplicationService::SetShimLibraryVersion(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto library_version{rp.Pop<u64>()};
const auto applet_resource_user_id{rp.Pop<u64>()};
@@ -64,37 +48,89 @@ void CAPS_U::SetShimLibraryVersion(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
}
-void CAPS_U::GetAlbumContentsFileListForApplication(HLERequestContext& ctx) {
- // Takes a type-0x6 output buffer containing an array of ApplicationAlbumFileEntry, a PID, an
- // u8 ContentType, two s64s, and an u64 AppletResourceUserId. Returns an output u64 for total
- // output entries (which is copied to a s32 by official SW).
+void IAlbumApplicationService::GetAlbumFileList0AafeAruidDeprecated(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
- const auto pid{rp.Pop<s32>()};
- const auto content_type{rp.PopEnum<ContentType>()};
- const auto start_posix_time{rp.Pop<s64>()};
- const auto end_posix_time{rp.Pop<s64>()};
- const auto applet_resource_user_id{rp.Pop<u64>()};
+ struct Parameters {
+ ContentType content_type;
+ INSERT_PADDING_BYTES(7);
+ s64 start_posix_time;
+ s64 end_posix_time;
+ u64 applet_resource_user_id;
+ };
+ static_assert(sizeof(Parameters) == 0x20, "Parameters has incorrect size.");
- // TODO: Update this when we implement the album.
- // Currently we do not have a method of accessing album entries, set this to 0 for now.
- constexpr u32 total_entries_1{};
- constexpr u32 total_entries_2{};
+ const auto parameters{rp.PopRaw<Parameters>()};
- LOG_WARNING(
- Service_Capture,
- "(STUBBED) called. pid={}, content_type={}, start_posix_time={}, "
- "end_posix_time={}, applet_resource_user_id={}, total_entries_1={}, total_entries_2={}",
- pid, content_type, start_posix_time, end_posix_time, applet_resource_user_id,
- total_entries_1, total_entries_2);
+ LOG_WARNING(Service_Capture,
+ "(STUBBED) called. content_type={}, start_posix_time={}, end_posix_time={}, "
+ "applet_resource_user_id={}",
+ parameters.content_type, parameters.start_posix_time, parameters.end_posix_time,
+ parameters.applet_resource_user_id);
+
+ Result result = ResultSuccess;
+
+ if (result.IsSuccess()) {
+ result = manager->IsAlbumMounted(AlbumStorage::Sd);
+ }
+
+ std::vector<ApplicationAlbumFileEntry> entries;
+ if (result.IsSuccess()) {
+ result = manager->GetAlbumFileList(entries, parameters.content_type,
+ parameters.start_posix_time, parameters.end_posix_time,
+ parameters.applet_resource_user_id);
+ }
+
+ if (!entries.empty()) {
+ ctx.WriteBuffer(entries);
+ }
IPC::ResponseBuilder rb{ctx, 4};
- rb.Push(ResultSuccess);
- rb.Push(total_entries_1);
- rb.Push(total_entries_2);
+ rb.Push(result);
+ rb.Push<u64>(entries.size());
}
-void CAPS_U::GetAlbumFileList3AaeAruid(HLERequestContext& ctx) {
- GetAlbumContentsFileListForApplication(ctx);
+void IAlbumApplicationService::GetAlbumFileList3AaeAruid(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ struct Parameters {
+ ContentType content_type;
+ INSERT_PADDING_BYTES(1);
+ AlbumFileDateTime start_date_time;
+ AlbumFileDateTime end_date_time;
+ INSERT_PADDING_BYTES(6);
+ u64 applet_resource_user_id;
+ };
+ static_assert(sizeof(Parameters) == 0x20, "Parameters has incorrect size.");
+
+ const auto parameters{rp.PopRaw<Parameters>()};
+
+ LOG_WARNING(Service_Capture,
+ "(STUBBED) called. content_type={}, start_date={}/{}/{}, "
+ "end_date={}/{}/{}, applet_resource_user_id={}",
+ parameters.content_type, parameters.start_date_time.year,
+ parameters.start_date_time.month, parameters.start_date_time.day,
+ parameters.end_date_time.year, parameters.end_date_time.month,
+ parameters.end_date_time.day, parameters.applet_resource_user_id);
+
+ Result result = ResultSuccess;
+
+ if (result.IsSuccess()) {
+ result = manager->IsAlbumMounted(AlbumStorage::Sd);
+ }
+
+ std::vector<ApplicationAlbumEntry> entries;
+ if (result.IsSuccess()) {
+ result =
+ manager->GetAlbumFileList(entries, parameters.content_type, parameters.start_date_time,
+ parameters.end_date_time, parameters.applet_resource_user_id);
+ }
+
+ if (!entries.empty()) {
+ ctx.WriteBuffer(entries);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result);
+ rb.Push<u64>(entries.size());
}
} // namespace Service::Capture
diff --git a/src/core/hle/service/caps/caps_u.h b/src/core/hle/service/caps/caps_u.h
index e8dd037d7..9458c128e 100644
--- a/src/core/hle/service/caps/caps_u.h
+++ b/src/core/hle/service/caps/caps_u.h
@@ -10,16 +10,20 @@ class System;
}
namespace Service::Capture {
+class AlbumManager;
-class CAPS_U final : public ServiceFramework<CAPS_U> {
+class IAlbumApplicationService final : public ServiceFramework<IAlbumApplicationService> {
public:
- explicit CAPS_U(Core::System& system_);
- ~CAPS_U() override;
+ explicit IAlbumApplicationService(Core::System& system_,
+ std::shared_ptr<AlbumManager> album_manager);
+ ~IAlbumApplicationService() override;
private:
void SetShimLibraryVersion(HLERequestContext& ctx);
- void GetAlbumContentsFileListForApplication(HLERequestContext& ctx);
+ void GetAlbumFileList0AafeAruidDeprecated(HLERequestContext& ctx);
void GetAlbumFileList3AaeAruid(HLERequestContext& ctx);
+
+ std::shared_ptr<AlbumManager> manager = nullptr;
};
} // namespace Service::Capture
diff --git a/src/core/hle/service/hle_ipc.cpp b/src/core/hle/service/hle_ipc.cpp
index f6a1e54f2..6f3ae3cc4 100644
--- a/src/core/hle/service/hle_ipc.cpp
+++ b/src/core/hle/service/hle_ipc.cpp
@@ -23,6 +23,17 @@
#include "core/hle/service/ipc_helpers.h"
#include "core/memory.h"
+namespace {
+static thread_local std::array read_buffer_data_a{
+ Common::ScratchBuffer<u8>(),
+ Common::ScratchBuffer<u8>(),
+};
+static thread_local std::array read_buffer_data_x{
+ Common::ScratchBuffer<u8>(),
+ Common::ScratchBuffer<u8>(),
+};
+} // Anonymous namespace
+
namespace Service {
SessionRequestHandler::SessionRequestHandler(Kernel::KernelCore& kernel_, const char* service_name_)
@@ -328,26 +339,57 @@ std::vector<u8> HLERequestContext::ReadBufferCopy(std::size_t buffer_index) cons
}
}
-std::span<const u8> HLERequestContext::ReadBuffer(std::size_t buffer_index) const {
+std::span<const u8> HLERequestContext::ReadBufferA(std::size_t buffer_index) const {
static thread_local std::array read_buffer_a{
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
};
- static thread_local std::array read_buffer_data_a{
- Common::ScratchBuffer<u8>(),
- Common::ScratchBuffer<u8>(),
- };
+
+ ASSERT_OR_EXECUTE_MSG(
+ BufferDescriptorA().size() > buffer_index, { return {}; },
+ "BufferDescriptorA invalid buffer_index {}", buffer_index);
+ auto& read_buffer = read_buffer_a[buffer_index];
+ return read_buffer.Read(BufferDescriptorA()[buffer_index].Address(),
+ BufferDescriptorA()[buffer_index].Size(),
+ &read_buffer_data_a[buffer_index]);
+}
+
+std::span<const u8> HLERequestContext::ReadBufferX(std::size_t buffer_index) const {
static thread_local std::array read_buffer_x{
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
};
- static thread_local std::array read_buffer_data_x{
- Common::ScratchBuffer<u8>(),
- Common::ScratchBuffer<u8>(),
+
+ ASSERT_OR_EXECUTE_MSG(
+ BufferDescriptorX().size() > buffer_index, { return {}; },
+ "BufferDescriptorX invalid buffer_index {}", buffer_index);
+ auto& read_buffer = read_buffer_x[buffer_index];
+ return read_buffer.Read(BufferDescriptorX()[buffer_index].Address(),
+ BufferDescriptorX()[buffer_index].Size(),
+ &read_buffer_data_x[buffer_index]);
+}
+
+std::span<const u8> HLERequestContext::ReadBuffer(std::size_t buffer_index) const {
+ static thread_local std::array read_buffer_a{
+ Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
+ Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
+ };
+ static thread_local std::array read_buffer_x{
+ Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
+ Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::SafeRead>(memory, 0, 0),
};
const bool is_buffer_a{BufferDescriptorA().size() > buffer_index &&
BufferDescriptorA()[buffer_index].Size()};
+ const bool is_buffer_x{BufferDescriptorX().size() > buffer_index &&
+ BufferDescriptorX()[buffer_index].Size()};
+
+ if (is_buffer_a && is_buffer_x) {
+ LOG_WARNING(Input, "Both buffer descriptors are available a.size={}, x.size={}",
+ BufferDescriptorA()[buffer_index].Size(),
+ BufferDescriptorX()[buffer_index].Size());
+ }
+
if (is_buffer_a) {
ASSERT_OR_EXECUTE_MSG(
BufferDescriptorA().size() > buffer_index, { return {}; },
diff --git a/src/core/hle/service/hle_ipc.h b/src/core/hle/service/hle_ipc.h
index 4bd24c899..ad5259a5c 100644
--- a/src/core/hle/service/hle_ipc.h
+++ b/src/core/hle/service/hle_ipc.h
@@ -253,6 +253,12 @@ public:
return domain_message_header.has_value();
}
+ /// Helper function to get a span of a buffer using the buffer descriptor A
+ [[nodiscard]] std::span<const u8> ReadBufferA(std::size_t buffer_index = 0) const;
+
+ /// Helper function to get a span of a buffer using the buffer descriptor X
+ [[nodiscard]] std::span<const u8> ReadBufferX(std::size_t buffer_index = 0) const;
+
/// Helper function to get a span of a buffer using the appropriate buffer descriptor
[[nodiscard]] std::span<const u8> ReadBuffer(std::size_t buffer_index = 0) const;
diff --git a/src/core/hle/service/jit/jit_context.cpp b/src/core/hle/service/jit/jit_context.cpp
index 4ed3f02e2..0090e8568 100644
--- a/src/core/hle/service/jit/jit_context.cpp
+++ b/src/core/hle/service/jit/jit_context.cpp
@@ -156,6 +156,8 @@ public:
bool LoadNRO(std::span<const u8> data) {
local_memory.clear();
+
+ relocbase = local_memory.size();
local_memory.insert(local_memory.end(), data.begin(), data.end());
if (FixupRelocations()) {
@@ -181,8 +183,8 @@ public:
// https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html
// https://refspecs.linuxbase.org/elf/gabi4+/ch4.reloc.html
VAddr dynamic_offset{mod_offset + callbacks->MemoryRead32(mod_offset + 4)};
- VAddr rela_dyn = 0;
- size_t num_rela = 0;
+ VAddr rela_dyn = 0, relr_dyn = 0;
+ size_t num_rela = 0, num_relr = 0;
while (true) {
const auto dyn{callbacks->ReadMemory<Elf64_Dyn>(dynamic_offset)};
dynamic_offset += sizeof(Elf64_Dyn);
@@ -196,6 +198,12 @@ public:
if (dyn.d_tag == ElfDtRelasz) {
num_rela = dyn.d_un.d_val / sizeof(Elf64_Rela);
}
+ if (dyn.d_tag == ElfDtRelr) {
+ relr_dyn = dyn.d_un.d_ptr;
+ }
+ if (dyn.d_tag == ElfDtRelrsz) {
+ num_relr = dyn.d_un.d_val / sizeof(Elf64_Relr);
+ }
}
for (size_t i = 0; i < num_rela; i++) {
@@ -207,6 +215,29 @@ public:
callbacks->MemoryWrite64(rela.r_offset, contents + rela.r_addend);
}
+ VAddr relr_where = 0;
+ for (size_t i = 0; i < num_relr; i++) {
+ const auto relr{callbacks->ReadMemory<Elf64_Relr>(relr_dyn + i * sizeof(Elf64_Relr))};
+ const auto incr{[&](VAddr where) {
+ callbacks->MemoryWrite64(where, callbacks->MemoryRead64(where) + relocbase);
+ }};
+
+ if ((relr & 1) == 0) {
+ // where pointer
+ relr_where = relocbase + relr;
+ incr(relr_where);
+ relr_where += sizeof(Elf64_Addr);
+ } else {
+ // bitmap
+ for (int bit = 1; bit < 64; bit++) {
+ if ((relr & (1ULL << bit)) != 0) {
+ incr(relr_where + i * sizeof(Elf64_Addr));
+ }
+ }
+ relr_where += 63 * sizeof(Elf64_Addr);
+ }
+ }
+
return true;
}
@@ -313,6 +344,7 @@ public:
Core::Memory::Memory& memory;
VAddr top_of_stack;
VAddr heap_pointer;
+ VAddr relocbase;
};
void DynarmicCallbacks64::CallSVC(u32 swi) {
diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp
index 21b06d10b..22dc55a6d 100644
--- a/src/core/hle/service/nifm/nifm.cpp
+++ b/src/core/hle/service/nifm/nifm.cpp
@@ -545,6 +545,16 @@ void IGeneralService::IsAnyInternetRequestAccepted(HLERequestContext& ctx) {
}
}
+void IGeneralService::IsAnyForegroundRequestAccepted(HLERequestContext& ctx) {
+ const bool is_accepted{};
+
+ LOG_WARNING(Service_NIFM, "(STUBBED) called, is_accepted={}", is_accepted);
+
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push<u8>(is_accepted);
+}
+
IGeneralService::IGeneralService(Core::System& system_)
: ServiceFramework{system_, "IGeneralService"}, network{system_.GetRoomNetwork()} {
// clang-format off
@@ -569,7 +579,7 @@ IGeneralService::IGeneralService(Core::System& system_)
{19, nullptr, "SetEthernetCommunicationEnabled"},
{20, &IGeneralService::IsEthernetCommunicationEnabled, "IsEthernetCommunicationEnabled"},
{21, &IGeneralService::IsAnyInternetRequestAccepted, "IsAnyInternetRequestAccepted"},
- {22, nullptr, "IsAnyForegroundRequestAccepted"},
+ {22, &IGeneralService::IsAnyForegroundRequestAccepted, "IsAnyForegroundRequestAccepted"},
{23, nullptr, "PutToSleep"},
{24, nullptr, "WakeUp"},
{25, nullptr, "GetSsidListVersion"},
diff --git a/src/core/hle/service/nifm/nifm.h b/src/core/hle/service/nifm/nifm.h
index ae99c4695..b74b66438 100644
--- a/src/core/hle/service/nifm/nifm.h
+++ b/src/core/hle/service/nifm/nifm.h
@@ -35,6 +35,7 @@ private:
void GetInternetConnectionStatus(HLERequestContext& ctx);
void IsEthernetCommunicationEnabled(HLERequestContext& ctx);
void IsAnyInternetRequestAccepted(HLERequestContext& ctx);
+ void IsAnyForegroundRequestAccepted(HLERequestContext& ctx);
Network::RoomNetwork& network;
};
diff --git a/src/core/hle/service/ns/ns.cpp b/src/core/hle/service/ns/ns.cpp
index 6e0baf0be..f9e0e272d 100644
--- a/src/core/hle/service/ns/ns.cpp
+++ b/src/core/hle/service/ns/ns.cpp
@@ -7,6 +7,7 @@
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/vfs.h"
+#include "core/hle/service/filesystem/filesystem.h"
#include "core/hle/service/glue/glue_manager.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/ns/errors.h"
@@ -502,8 +503,8 @@ IContentManagementInterface::IContentManagementInterface(Core::System& system_)
static const FunctionInfo functions[] = {
{11, nullptr, "CalculateApplicationOccupiedSize"},
{43, nullptr, "CheckSdCardMountStatus"},
- {47, nullptr, "GetTotalSpaceSize"},
- {48, nullptr, "GetFreeSpaceSize"},
+ {47, &IContentManagementInterface::GetTotalSpaceSize, "GetTotalSpaceSize"},
+ {48, &IContentManagementInterface::GetFreeSpaceSize, "GetFreeSpaceSize"},
{600, nullptr, "CountApplicationContentMeta"},
{601, nullptr, "ListApplicationContentMetaStatus"},
{605, nullptr, "ListApplicationContentMetaStatusWithRightsCheck"},
@@ -516,6 +517,28 @@ IContentManagementInterface::IContentManagementInterface(Core::System& system_)
IContentManagementInterface::~IContentManagementInterface() = default;
+void IContentManagementInterface::GetTotalSpaceSize(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto storage{rp.PopEnum<FileSys::StorageId>()};
+
+ LOG_INFO(Service_Capture, "called, storage={}", storage);
+
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(ResultSuccess);
+ rb.Push<u64>(system.GetFileSystemController().GetTotalSpaceSize(storage));
+}
+
+void IContentManagementInterface::GetFreeSpaceSize(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto storage{rp.PopEnum<FileSys::StorageId>()};
+
+ LOG_INFO(Service_Capture, "called, storage={}", storage);
+
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(ResultSuccess);
+ rb.Push<u64>(system.GetFileSystemController().GetFreeSpaceSize(storage));
+}
+
IDocumentInterface::IDocumentInterface(Core::System& system_)
: ServiceFramework{system_, "IDocumentInterface"} {
// clang-format off
diff --git a/src/core/hle/service/ns/ns.h b/src/core/hle/service/ns/ns.h
index 175dad780..34d2a45dc 100644
--- a/src/core/hle/service/ns/ns.h
+++ b/src/core/hle/service/ns/ns.h
@@ -48,6 +48,10 @@ class IContentManagementInterface final : public ServiceFramework<IContentManage
public:
explicit IContentManagementInterface(Core::System& system_);
~IContentManagementInterface() override;
+
+private:
+ void GetTotalSpaceSize(HLERequestContext& ctx);
+ void GetFreeSpaceSize(HLERequestContext& ctx);
};
class IDocumentInterface final : public ServiceFramework<IDocumentInterface> {
diff --git a/src/core/hle/service/pctl/pctl_module.cpp b/src/core/hle/service/pctl/pctl_module.cpp
index 5db1703d1..938330dd0 100644
--- a/src/core/hle/service/pctl/pctl_module.cpp
+++ b/src/core/hle/service/pctl/pctl_module.cpp
@@ -33,7 +33,7 @@ public:
{1001, &IParentalControlService::CheckFreeCommunicationPermission, "CheckFreeCommunicationPermission"},
{1002, nullptr, "ConfirmLaunchApplicationPermission"},
{1003, nullptr, "ConfirmResumeApplicationPermission"},
- {1004, nullptr, "ConfirmSnsPostPermission"},
+ {1004, &IParentalControlService::ConfirmSnsPostPermission, "ConfirmSnsPostPermission"},
{1005, nullptr, "ConfirmSystemSettingsPermission"},
{1006, &IParentalControlService::IsRestrictionTemporaryUnlocked, "IsRestrictionTemporaryUnlocked"},
{1007, nullptr, "RevertRestrictionTemporaryUnlocked"},
@@ -236,6 +236,13 @@ private:
states.free_communication = true;
}
+ void ConfirmSnsPostPermission(HLERequestContext& ctx) {
+ LOG_WARNING(Service_PCTL, "(STUBBED) called");
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(Error::ResultNoFreeCommunication);
+ }
+
void IsRestrictionTemporaryUnlocked(HLERequestContext& ctx) {
const bool is_temporary_unlocked = false;
diff --git a/src/core/hle/service/prepo/prepo.cpp b/src/core/hle/service/prepo/prepo.cpp
index ec4a84989..14e8df63a 100644
--- a/src/core/hle/service/prepo/prepo.cpp
+++ b/src/core/hle/service/prepo/prepo.cpp
@@ -58,14 +58,8 @@ private:
IPC::RequestParser rp{ctx};
const auto process_id = rp.PopRaw<u64>();
- const auto data1 = ctx.ReadBuffer(0);
- const auto data2 = [&ctx] {
- if (ctx.CanReadBuffer(1)) {
- return ctx.ReadBuffer(1);
- }
-
- return std::span<const u8>{};
- }();
+ const auto data1 = ctx.ReadBufferA(0);
+ const auto data2 = ctx.ReadBufferX(0);
LOG_DEBUG(Service_PREPO,
"called, type={:02X}, process_id={:016X}, data1_size={:016X}, data2_size={:016X}",
@@ -85,14 +79,8 @@ private:
const auto user_id = rp.PopRaw<u128>();
const auto process_id = rp.PopRaw<u64>();
- const auto data1 = ctx.ReadBuffer(0);
- const auto data2 = [&ctx] {
- if (ctx.CanReadBuffer(1)) {
- return ctx.ReadBuffer(1);
- }
-
- return std::span<const u8>{};
- }();
+ const auto data1 = ctx.ReadBufferA(0);
+ const auto data2 = ctx.ReadBufferX(0);
LOG_DEBUG(Service_PREPO,
"called, type={:02X}, user_id={:016X}{:016X}, process_id={:016X}, "
@@ -137,14 +125,8 @@ private:
IPC::RequestParser rp{ctx};
const auto title_id = rp.PopRaw<u64>();
- const auto data1 = ctx.ReadBuffer(0);
- const auto data2 = [&ctx] {
- if (ctx.CanReadBuffer(1)) {
- return ctx.ReadBuffer(1);
- }
-
- return std::span<const u8>{};
- }();
+ const auto data1 = ctx.ReadBufferA(0);
+ const auto data2 = ctx.ReadBufferX(0);
LOG_DEBUG(Service_PREPO, "called, title_id={:016X}, data1_size={:016X}, data2_size={:016X}",
title_id, data1.size(), data2.size());
@@ -161,14 +143,8 @@ private:
const auto user_id = rp.PopRaw<u128>();
const auto title_id = rp.PopRaw<u64>();
- const auto data1 = ctx.ReadBuffer(0);
- const auto data2 = [&ctx] {
- if (ctx.CanReadBuffer(1)) {
- return ctx.ReadBuffer(1);
- }
-
- return std::span<const u8>{};
- }();
+ const auto data1 = ctx.ReadBufferA(0);
+ const auto data2 = ctx.ReadBufferX(0);
LOG_DEBUG(Service_PREPO,
"called, user_id={:016X}{:016X}, title_id={:016X}, data1_size={:016X}, "
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 5a42dea48..5c36b71e5 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -118,7 +118,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
return {ResultStatus::ErrorMissingNPDM, {}};
}
- const ResultStatus result2 = metadata.Load(npdm);
+ const ResultStatus result2 = metadata.Reload(npdm);
if (result2 != ResultStatus::Success) {
return {result2, {}};
}
diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp
index 808b21069..77db60e92 100644
--- a/src/input_common/drivers/udp_client.cpp
+++ b/src/input_common/drivers/udp_client.cpp
@@ -338,6 +338,7 @@ void UDPClient::StartCommunication(std::size_t client, const std::string& host,
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
const PadIdentifier identifier = GetPadIdentifier(client * PADS_PER_CLIENT + index);
PreSetController(identifier);
+ PreSetMotion(identifier, 0);
}
}
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index 9e90c587c..9b2698fad 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -544,7 +544,7 @@ void BufferCache<P>::CommitAsyncFlushesHigh() {
it++;
}
- boost::container::small_vector<std::pair<BufferCopy, BufferId>, 1> downloads;
+ boost::container::small_vector<std::pair<BufferCopy, BufferId>, 16> downloads;
u64 total_size_bytes = 0;
u64 largest_copy = 0;
for (const IntervalSet& intervals : committed_ranges) {
@@ -914,6 +914,11 @@ void BufferCache<P>::BindHostGraphicsStorageBuffers(size_t stage) {
const u32 offset = buffer.Offset(binding.cpu_addr);
const bool is_written = ((channel_state->written_storage_buffers[stage] >> index) & 1) != 0;
+
+ if (is_written) {
+ MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, size);
+ }
+
if constexpr (NEEDS_BIND_STORAGE_INDEX) {
runtime.BindStorageBuffer(stage, binding_index, buffer, offset, size, is_written);
++binding_index;
@@ -931,6 +936,11 @@ void BufferCache<P>::BindHostGraphicsTextureBuffers(size_t stage) {
const u32 size = binding.size;
SynchronizeBuffer(buffer, binding.cpu_addr, size);
+ const bool is_written = ((channel_state->written_texture_buffers[stage] >> index) & 1) != 0;
+ if (is_written) {
+ MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, size);
+ }
+
const u32 offset = buffer.Offset(binding.cpu_addr);
const PixelFormat format = binding.format;
if constexpr (SEPARATE_IMAGE_BUFFERS_BINDINGS) {
@@ -962,6 +972,8 @@ void BufferCache<P>::BindHostTransformFeedbackBuffers() {
const u32 size = binding.size;
SynchronizeBuffer(buffer, binding.cpu_addr, size);
+ MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, size);
+
const u32 offset = buffer.Offset(binding.cpu_addr);
host_bindings.buffers.push_back(&buffer);
host_bindings.offsets.push_back(offset);
@@ -1011,6 +1023,11 @@ void BufferCache<P>::BindHostComputeStorageBuffers() {
const u32 offset = buffer.Offset(binding.cpu_addr);
const bool is_written =
((channel_state->written_compute_storage_buffers >> index) & 1) != 0;
+
+ if (is_written) {
+ MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, size);
+ }
+
if constexpr (NEEDS_BIND_STORAGE_INDEX) {
runtime.BindComputeStorageBuffer(binding_index, buffer, offset, size, is_written);
++binding_index;
@@ -1028,6 +1045,12 @@ void BufferCache<P>::BindHostComputeTextureBuffers() {
const u32 size = binding.size;
SynchronizeBuffer(buffer, binding.cpu_addr, size);
+ const bool is_written =
+ ((channel_state->written_compute_texture_buffers >> index) & 1) != 0;
+ if (is_written) {
+ MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, size);
+ }
+
const u32 offset = buffer.Offset(binding.cpu_addr);
const PixelFormat format = binding.format;
if constexpr (SEPARATE_IMAGE_BUFFERS_BINDINGS) {
@@ -1201,16 +1224,11 @@ void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
- const u32 written_mask = channel_state->written_storage_buffers[stage];
ForEachEnabledBit(channel_state->enabled_storage_buffers[stage], [&](u32 index) {
// Resolve buffer
Binding& binding = channel_state->storage_buffers[stage][index];
const BufferId buffer_id = FindBuffer(binding.cpu_addr, binding.size);
binding.buffer_id = buffer_id;
- // Mark buffer as written if needed
- if (((written_mask >> index) & 1) != 0) {
- MarkWrittenBuffer(buffer_id, binding.cpu_addr, binding.size);
- }
});
}
@@ -1219,10 +1237,6 @@ void BufferCache<P>::UpdateTextureBuffers(size_t stage) {
ForEachEnabledBit(channel_state->enabled_texture_buffers[stage], [&](u32 index) {
Binding& binding = channel_state->texture_buffers[stage][index];
binding.buffer_id = FindBuffer(binding.cpu_addr, binding.size);
- // Mark buffer as written if needed
- if (((channel_state->written_texture_buffers[stage] >> index) & 1) != 0) {
- MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, binding.size);
- }
});
}
@@ -1252,7 +1266,6 @@ void BufferCache<P>::UpdateTransformFeedbackBuffer(u32 index) {
.size = size,
.buffer_id = buffer_id,
};
- MarkWrittenBuffer(buffer_id, *cpu_addr, size);
}
template <class P>
@@ -1279,10 +1292,6 @@ void BufferCache<P>::UpdateComputeStorageBuffers() {
// Resolve buffer
Binding& binding = channel_state->compute_storage_buffers[index];
binding.buffer_id = FindBuffer(binding.cpu_addr, binding.size);
- // Mark as written if needed
- if (((channel_state->written_compute_storage_buffers >> index) & 1) != 0) {
- MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, binding.size);
- }
});
}
@@ -1291,18 +1300,11 @@ void BufferCache<P>::UpdateComputeTextureBuffers() {
ForEachEnabledBit(channel_state->enabled_compute_texture_buffers, [&](u32 index) {
Binding& binding = channel_state->compute_texture_buffers[index];
binding.buffer_id = FindBuffer(binding.cpu_addr, binding.size);
- // Mark as written if needed
- if (((channel_state->written_compute_texture_buffers >> index) & 1) != 0) {
- MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, binding.size);
- }
});
}
template <class P>
void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, VAddr cpu_addr, u32 size) {
- if (memory_tracker.IsRegionCpuModified(cpu_addr, size)) {
- SynchronizeBuffer(slot_buffers[buffer_id], cpu_addr, size);
- }
memory_tracker.MarkRegionAsGpuModified(cpu_addr, size);
const IntervalType base_interval{cpu_addr, cpu_addr + size};
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index c4f6e8d12..eed267361 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -62,7 +62,11 @@ using BufferId = SlotId;
using VideoCore::Surface::PixelFormat;
using namespace Common::Literals;
+#ifdef __APPLE__
+constexpr u32 NUM_VERTEX_BUFFERS = 16;
+#else
constexpr u32 NUM_VERTEX_BUFFERS = 32;
+#endif
constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4;
constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18;
constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8;
diff --git a/src/video_core/engines/draw_manager.cpp b/src/video_core/engines/draw_manager.cpp
index f34090791..d77ff455b 100644
--- a/src/video_core/engines/draw_manager.cpp
+++ b/src/video_core/engines/draw_manager.cpp
@@ -48,8 +48,14 @@ void DrawManager::ProcessMethodCall(u32 method, u32 argument) {
SetInlineIndexBuffer(regs.inline_index_4x8.index3);
break;
case MAXWELL3D_REG_INDEX(vertex_array_instance_first):
+ DrawArrayInstanced(regs.vertex_array_instance_first.topology.Value(),
+ regs.vertex_array_instance_first.start.Value(),
+ regs.vertex_array_instance_first.count.Value(), false);
+ break;
case MAXWELL3D_REG_INDEX(vertex_array_instance_subsequent): {
- LOG_WARNING(HW_GPU, "(STUBBED) called");
+ DrawArrayInstanced(regs.vertex_array_instance_subsequent.topology.Value(),
+ regs.vertex_array_instance_subsequent.start.Value(),
+ regs.vertex_array_instance_subsequent.count.Value(), true);
break;
}
case MAXWELL3D_REG_INDEX(draw_texture.src_y0): {
@@ -84,6 +90,22 @@ void DrawManager::DrawArray(PrimitiveTopology topology, u32 vertex_first, u32 ve
ProcessDraw(false, num_instances);
}
+void DrawManager::DrawArrayInstanced(PrimitiveTopology topology, u32 vertex_first, u32 vertex_count,
+ bool subsequent) {
+ draw_state.topology = topology;
+ draw_state.vertex_buffer.first = vertex_first;
+ draw_state.vertex_buffer.count = vertex_count;
+
+ if (!subsequent) {
+ draw_state.instance_count = 1;
+ }
+
+ draw_state.base_instance = draw_state.instance_count - 1;
+ draw_state.draw_mode = DrawMode::Instance;
+ draw_state.instance_count++;
+ ProcessDraw(false, 1);
+}
+
void DrawManager::DrawIndex(PrimitiveTopology topology, u32 index_first, u32 index_count,
u32 base_index, u32 base_instance, u32 num_instances) {
const auto& regs{maxwell3d->regs};
diff --git a/src/video_core/engines/draw_manager.h b/src/video_core/engines/draw_manager.h
index 18d959143..cfc8127fc 100644
--- a/src/video_core/engines/draw_manager.h
+++ b/src/video_core/engines/draw_manager.h
@@ -66,6 +66,8 @@ public:
void DrawArray(PrimitiveTopology topology, u32 vertex_first, u32 vertex_count,
u32 base_instance, u32 num_instances);
+ void DrawArrayInstanced(PrimitiveTopology topology, u32 vertex_first, u32 vertex_count,
+ bool subsequent);
void DrawIndex(PrimitiveTopology topology, u32 index_first, u32 index_count, u32 base_index,
u32 base_instance, u32 num_instances);
diff --git a/src/video_core/host1x/codecs/codec.cpp b/src/video_core/host1x/codecs/codec.cpp
index 8d7da50fc..dbcf508e5 100644
--- a/src/video_core/host1x/codecs/codec.cpp
+++ b/src/video_core/host1x/codecs/codec.cpp
@@ -137,16 +137,6 @@ bool Codec::CreateGpuAvDevice() {
break;
}
if ((config->methods & HW_CONFIG_METHOD) != 0 && config->device_type == type) {
-#if defined(__unix__)
- // Some linux decoding backends are reported to crash with this config method
- // TODO(ameerj): Properly support this method
- if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX) != 0) {
- // skip zero-copy decoders, we don't currently support them
- LOG_DEBUG(Service_NVDRV, "Skipping decoder {} with unsupported capability {}.",
- av_hwdevice_get_type_name(type), config->methods);
- continue;
- }
-#endif
LOG_INFO(Service_NVDRV, "Using {} GPU decoder", av_hwdevice_get_type_name(type));
av_codec_ctx->pix_fmt = config->pix_fmt;
return true;
diff --git a/src/video_core/host_shaders/convert_d24s8_to_abgr8.frag b/src/video_core/host_shaders/convert_d24s8_to_abgr8.frag
index d33131d7c..b81a54056 100644
--- a/src/video_core/host_shaders/convert_d24s8_to_abgr8.frag
+++ b/src/video_core/host_shaders/convert_d24s8_to_abgr8.frag
@@ -3,16 +3,16 @@
#version 450
+precision mediump int;
+precision highp float;
+
layout(binding = 0) uniform sampler2D depth_tex;
-layout(binding = 1) uniform isampler2D stencil_tex;
+layout(binding = 1) uniform usampler2D stencil_tex;
layout(location = 0) out vec4 color;
void main() {
ivec2 coord = ivec2(gl_FragCoord.xy);
- uint depth = uint(textureLod(depth_tex, coord, 0).r * (exp2(24.0) - 1.0f));
- uint stencil = uint(textureLod(stencil_tex, coord, 0).r);
-
highp uint depth_val =
uint(textureLod(depth_tex, coord, 0).r * (exp2(32.0) - 1.0));
lowp uint stencil_val = textureLod(stencil_tex, coord, 0).r;
diff --git a/src/video_core/host_shaders/convert_s8d24_to_abgr8.frag b/src/video_core/host_shaders/convert_s8d24_to_abgr8.frag
index 31db7d426..6a457981d 100644
--- a/src/video_core/host_shaders/convert_s8d24_to_abgr8.frag
+++ b/src/video_core/host_shaders/convert_s8d24_to_abgr8.frag
@@ -3,16 +3,16 @@
#version 450
+precision mediump int;
+precision highp float;
+
layout(binding = 0) uniform sampler2D depth_tex;
-layout(binding = 1) uniform isampler2D stencil_tex;
+layout(binding = 1) uniform usampler2D stencil_tex;
layout(location = 0) out vec4 color;
void main() {
ivec2 coord = ivec2(gl_FragCoord.xy);
- uint depth = uint(textureLod(depth_tex, coord, 0).r * (exp2(24.0) - 1.0f));
- uint stencil = uint(textureLod(stencil_tex, coord, 0).r);
-
highp uint depth_val =
uint(textureLod(depth_tex, coord, 0).r * (exp2(32.0) - 1.0));
lowp uint stencil_val = textureLod(stencil_tex, coord, 0).r;
diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h
index 3e12a8813..78ea5208b 100644
--- a/src/video_core/renderer_base.h
+++ b/src/video_core/renderer_base.h
@@ -89,9 +89,6 @@ public:
void RequestScreenshot(void* data, std::function<void(bool)> callback,
const Layout::FramebufferLayout& layout);
- /// This is called to notify the rendering backend of a surface change
- virtual void NotifySurfaceChanged() {}
-
protected:
Core::Frontend::EmuWindow& render_window; ///< Reference to the render window handle.
std::unique_ptr<Core::Frontend::GraphicsContext> context;
diff --git a/src/video_core/renderer_opengl/gl_texture_cache.cpp b/src/video_core/renderer_opengl/gl_texture_cache.cpp
index 9cafd2983..512eef575 100644
--- a/src/video_core/renderer_opengl/gl_texture_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_texture_cache.cpp
@@ -1048,6 +1048,10 @@ void Image::Scale(bool up_scale) {
}
bool Image::ScaleUp(bool ignore) {
+ const auto& resolution = runtime->resolution;
+ if (!resolution.active) {
+ return false;
+ }
if (True(flags & ImageFlagBits::Rescaled)) {
return false;
}
@@ -1060,9 +1064,6 @@ bool Image::ScaleUp(bool ignore) {
return false;
}
flags |= ImageFlagBits::Rescaled;
- if (!runtime->resolution.active) {
- return false;
- }
has_scaled = true;
if (ignore) {
current_texture = upscaled_backup.handle;
@@ -1073,13 +1074,14 @@ bool Image::ScaleUp(bool ignore) {
}
bool Image::ScaleDown(bool ignore) {
- if (False(flags & ImageFlagBits::Rescaled)) {
+ const auto& resolution = runtime->resolution;
+ if (!resolution.active) {
return false;
}
- flags &= ~ImageFlagBits::Rescaled;
- if (!runtime->resolution.active) {
+ if (False(flags & ImageFlagBits::Rescaled)) {
return false;
}
+ flags &= ~ImageFlagBits::Rescaled;
if (ignore) {
current_texture = texture.handle;
return true;
diff --git a/src/video_core/renderer_opengl/gl_texture_cache.h b/src/video_core/renderer_opengl/gl_texture_cache.h
index 3676eaaa9..e71b87e99 100644
--- a/src/video_core/renderer_opengl/gl_texture_cache.h
+++ b/src/video_core/renderer_opengl/gl_texture_cache.h
@@ -118,6 +118,8 @@ public:
void InsertUploadMemoryBarrier();
+ void TransitionImageLayout(Image& image) {}
+
FormatProperties FormatInfo(VideoCommon::ImageType type, GLenum internal_format) const;
bool HasNativeBgr() const noexcept {
diff --git a/src/video_core/renderer_opengl/maxwell_to_gl.h b/src/video_core/renderer_opengl/maxwell_to_gl.h
index c7dc7e0a1..5ea9e2378 100644
--- a/src/video_core/renderer_opengl/maxwell_to_gl.h
+++ b/src/video_core/renderer_opengl/maxwell_to_gl.h
@@ -116,6 +116,7 @@ constexpr std::array<FormatTuple, VideoCore::Surface::MaxPixelFormat> FORMAT_TAB
{GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV}, // E5B9G9R9_FLOAT
{GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT}, // D32_FLOAT
{GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16_UNORM
+ {GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT_24_8}, // X8_D24_UNORM
{GL_STENCIL_INDEX8, GL_STENCIL, GL_UNSIGNED_BYTE}, // S8_UINT
{GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // D24_UNORM_S8_UINT
{GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // S8_UINT_D24_UNORM
diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index 1a40a4d05..c3db09424 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -627,6 +627,8 @@ void BlitImageHelper::ClearDepthStencil(const Framebuffer* dst_framebuffer, bool
const VkPipelineLayout layout = *clear_color_pipeline_layout;
scheduler.RequestRenderpass(dst_framebuffer);
scheduler.Record([pipeline, layout, clear_depth, dst_region](vk::CommandBuffer cmdbuf) {
+ constexpr std::array blend_constants{0.0f, 0.0f, 0.0f, 0.0f};
+ cmdbuf.SetBlendConstants(blend_constants.data());
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
BindBlitState(cmdbuf, dst_region);
cmdbuf.PushConstants(layout, VK_SHADER_STAGE_FRAGMENT_BIT, clear_depth);
@@ -883,7 +885,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceClearStencilPipeline(
.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
- .depthTestEnable = VK_FALSE,
+ .depthTestEnable = key.depth_clear,
.depthWriteEnable = key.depth_clear,
.depthCompareOp = VK_COMPARE_OP_ALWAYS,
.depthBoundsTestEnable = VK_FALSE,
diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
index 208e88533..a08f2f67f 100644
--- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
+++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
@@ -214,8 +214,9 @@ struct FormatTuple {
{VK_FORMAT_E5B9G9R9_UFLOAT_PACK32}, // E5B9G9R9_FLOAT
// Depth formats
- {VK_FORMAT_D32_SFLOAT, Attachable}, // D32_FLOAT
- {VK_FORMAT_D16_UNORM, Attachable}, // D16_UNORM
+ {VK_FORMAT_D32_SFLOAT, Attachable}, // D32_FLOAT
+ {VK_FORMAT_D16_UNORM, Attachable}, // D16_UNORM
+ {VK_FORMAT_X8_D24_UNORM_PACK32, Attachable}, // X8_D24_UNORM
// Stencil formats
{VK_FORMAT_S8_UINT, Attachable}, // S8_UINT
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h
index 590bc1c64..14e257cf7 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.h
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.h
@@ -56,10 +56,6 @@ public:
return device.GetDriverName();
}
- void NotifySurfaceChanged() override {
- present_manager.NotifySurfaceChanged();
- }
-
private:
void Report() const;
diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp
index 31928bb94..52fc142d1 100644
--- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp
+++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp
@@ -96,6 +96,7 @@ std::size_t GetSizeInBytes(const Tegra::FramebufferConfig& framebuffer) {
VkFormat GetFormat(const Tegra::FramebufferConfig& framebuffer) {
switch (framebuffer.pixel_format) {
case Service::android::PixelFormat::Rgba8888:
+ case Service::android::PixelFormat::Rgbx8888:
return VK_FORMAT_A8B8G8R8_UNORM_PACK32;
case Service::android::PixelFormat::Rgb565:
return VK_FORMAT_R5G6B5_UNORM_PACK16;
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp
index d681bd22a..2ef36583b 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.cpp
+++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp
@@ -103,8 +103,7 @@ PresentManager::PresentManager(const vk::Instance& instance_,
surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(),
swapchain.GetImageViewFormat())},
use_present_thread{Settings::values.async_presentation.GetValue()},
- image_count{swapchain.GetImageCount()}, last_render_surface{
- render_window_.GetWindowInfo().render_surface} {
+ image_count{swapchain.GetImageCount()} {
auto& dld = device.GetLogical();
cmdpool = dld.CreateCommandPool({
@@ -289,44 +288,36 @@ void PresentManager::PresentThread(std::stop_token token) {
}
}
-void PresentManager::NotifySurfaceChanged() {
-#ifdef ANDROID
- std::scoped_lock lock{recreate_surface_mutex};
- recreate_surface_cv.notify_one();
-#endif
+void PresentManager::RecreateSwapchain(Frame* frame) {
+ swapchain.Create(*surface, frame->width, frame->height, frame->is_srgb);
+ image_count = swapchain.GetImageCount();
}
void PresentManager::CopyToSwapchain(Frame* frame) {
- MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain);
-
- const auto recreate_swapchain = [&] {
- swapchain.Create(*surface, frame->width, frame->height, frame->is_srgb);
- image_count = swapchain.GetImageCount();
- };
-
-#ifdef ANDROID
- std::unique_lock lock{recreate_surface_mutex};
-
- const auto needs_recreation = [&] {
- if (last_render_surface != render_window.GetWindowInfo().render_surface) {
- return true;
- }
- if (swapchain.NeedsRecreation(frame->is_srgb)) {
- return true;
+ bool requires_recreation = false;
+
+ while (true) {
+ try {
+ // Recreate surface and swapchain if needed.
+ if (requires_recreation) {
+ surface = CreateSurface(instance, render_window.GetWindowInfo());
+ RecreateSwapchain(frame);
+ }
+
+ // Draw to swapchain.
+ return CopyToSwapchainImpl(frame);
+ } catch (const vk::Exception& except) {
+ if (except.GetResult() != VK_ERROR_SURFACE_LOST_KHR) {
+ throw;
+ }
+
+ requires_recreation = true;
}
- return false;
- };
-
- recreate_surface_cv.wait_for(lock, std::chrono::milliseconds(400),
- [&]() { return !needs_recreation(); });
-
- // If the frontend recreated the surface, recreate the renderer surface and swapchain.
- if (last_render_surface != render_window.GetWindowInfo().render_surface) {
- last_render_surface = render_window.GetWindowInfo().render_surface;
- surface = CreateSurface(instance, render_window.GetWindowInfo());
- recreate_swapchain();
}
-#endif
+}
+
+void PresentManager::CopyToSwapchainImpl(Frame* frame) {
+ MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain);
// If the size or colorspace of the incoming frames has changed, recreate the swapchain
// to account for that.
@@ -334,11 +325,11 @@ void PresentManager::CopyToSwapchain(Frame* frame) {
const bool size_changed =
swapchain.GetWidth() != frame->width || swapchain.GetHeight() != frame->height;
if (srgb_changed || size_changed) {
- recreate_swapchain();
+ RecreateSwapchain(frame);
}
while (swapchain.AcquireNextImage()) {
- recreate_swapchain();
+ RecreateSwapchain(frame);
}
const vk::CommandBuffer cmdbuf{frame->cmdbuf};
@@ -488,4 +479,4 @@ void PresentManager::CopyToSwapchain(Frame* frame) {
swapchain.Present(render_semaphore);
}
-} // namespace Vulkan \ No newline at end of file
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h
index 83e859416..a3d825fe6 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.h
+++ b/src/video_core/renderer_vulkan/vk_present_manager.h
@@ -54,14 +54,15 @@ public:
/// Waits for the present thread to finish presenting all queued frames.
void WaitPresent();
- /// This is called to notify the rendering backend of a surface change
- void NotifySurfaceChanged();
-
private:
void PresentThread(std::stop_token token);
void CopyToSwapchain(Frame* frame);
+ void CopyToSwapchainImpl(Frame* frame);
+
+ void RecreateSwapchain(Frame* frame);
+
private:
const vk::Instance& instance;
Core::Frontend::EmuWindow& render_window;
@@ -76,16 +77,13 @@ private:
std::queue<Frame*> free_queue;
std::condition_variable_any frame_cv;
std::condition_variable free_cv;
- std::condition_variable recreate_surface_cv;
std::mutex swapchain_mutex;
- std::mutex recreate_surface_mutex;
std::mutex queue_mutex;
std::mutex free_mutex;
std::jthread present_thread;
bool blit_supported;
bool use_present_thread;
std::size_t image_count{};
- void* last_render_surface{};
};
} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_query_cache.cpp b/src/video_core/renderer_vulkan/vk_query_cache.cpp
index 2edaafa7e..66c03bf17 100644
--- a/src/video_core/renderer_vulkan/vk_query_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_query_cache.cpp
@@ -1436,6 +1436,7 @@ void QueryCacheRuntime::Barriers(bool is_prebarrier) {
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
};
+ impl->scheduler.RequestOutsideRenderPassOperationContext();
if (is_prebarrier) {
impl->scheduler.Record([](vk::CommandBuffer cmdbuf) {
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 1628d76d6..61d03daae 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -422,7 +422,8 @@ void RasterizerVulkan::Clear(u32 layer_count) {
return;
}
- if (use_stencil && regs.stencil_front_mask != 0xFF && regs.stencil_front_mask != 0) {
+ if (use_stencil && framebuffer->HasAspectStencilBit() && regs.stencil_front_mask != 0xFF &&
+ regs.stencil_front_mask != 0) {
Region2D dst_region = {
Offset2D{.x = clear_rect.rect.offset.x, .y = clear_rect.rect.offset.y},
Offset2D{.x = clear_rect.rect.offset.x + static_cast<s32>(clear_rect.rect.extent.width),
@@ -974,6 +975,19 @@ void RasterizerVulkan::UpdateScissorsState(Tegra::Engines::Maxwell3D::Regs& regs
if (!state_tracker.TouchScissors()) {
return;
}
+ if (!regs.viewport_scale_offset_enabled) {
+ const auto x = static_cast<float>(regs.surface_clip.x);
+ const auto y = static_cast<float>(regs.surface_clip.y);
+ const auto width = static_cast<float>(regs.surface_clip.width);
+ const auto height = static_cast<float>(regs.surface_clip.height);
+ VkRect2D scissor;
+ scissor.offset.x = static_cast<u32>(x);
+ scissor.offset.y = static_cast<u32>(y);
+ scissor.extent.width = static_cast<u32>(width != 0.0f ? width : 1.0f);
+ scissor.extent.height = static_cast<u32>(height != 0.0f ? height : 1.0f);
+ scheduler.Record([scissor](vk::CommandBuffer cmdbuf) { cmdbuf.SetScissor(0, scissor); });
+ return;
+ }
u32 up_scale = 1;
u32 down_shift = 0;
if (texture_cache.IsRescaling()) {
diff --git a/src/video_core/renderer_vulkan/vk_render_pass_cache.cpp b/src/video_core/renderer_vulkan/vk_render_pass_cache.cpp
index ae9f1de64..7746a88d3 100644
--- a/src/video_core/renderer_vulkan/vk_render_pass_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_render_pass_cache.cpp
@@ -19,7 +19,7 @@ VkAttachmentDescription AttachmentDescription(const Device& device, PixelFormat
VkSampleCountFlagBits samples) {
using MaxwellToVK::SurfaceFormat;
return {
- .flags = VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT,
+ .flags = {},
.format = SurfaceFormat(device, FormatType::Optimal, true, format).format,
.samples = samples,
.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
index ce92f66ab..b278614e6 100644
--- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
+++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
@@ -24,25 +24,38 @@ using namespace Common::Literals;
// Maximum potential alignment of a Vulkan buffer
constexpr VkDeviceSize MAX_ALIGNMENT = 256;
-// Maximum size to put elements in the stream buffer
-constexpr VkDeviceSize MAX_STREAM_BUFFER_REQUEST_SIZE = 8_MiB;
// Stream buffer size in bytes
-constexpr VkDeviceSize STREAM_BUFFER_SIZE = 128_MiB;
-constexpr VkDeviceSize REGION_SIZE = STREAM_BUFFER_SIZE / StagingBufferPool::NUM_SYNCS;
+constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB;
-size_t Region(size_t iterator) noexcept {
- return iterator / REGION_SIZE;
+size_t GetStreamBufferSize(const Device& device) {
+ VkDeviceSize size{0};
+ if (device.HasDebuggingToolAttached()) {
+ ForEachDeviceLocalHostVisibleHeap(device, [&size](size_t index, VkMemoryHeap& heap) {
+ size = std::max(size, heap.size);
+ });
+ // If rebar is not supported, cut the max heap size to 40%. This will allow 2 captures to be
+ // loaded at the same time in RenderDoc. If rebar is supported, this shouldn't be an issue
+ // as the heap will be much larger.
+ if (size <= 256_MiB) {
+ size = size * 40 / 100;
+ }
+ } else {
+ size = MAX_STREAM_BUFFER_SIZE;
+ }
+ return std::min(Common::AlignUp(size, MAX_ALIGNMENT), MAX_STREAM_BUFFER_SIZE);
}
} // Anonymous namespace
StagingBufferPool::StagingBufferPool(const Device& device_, MemoryAllocator& memory_allocator_,
Scheduler& scheduler_)
- : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_} {
+ : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_},
+ stream_buffer_size{GetStreamBufferSize(device)}, region_size{stream_buffer_size /
+ StagingBufferPool::NUM_SYNCS} {
VkBufferCreateInfo stream_ci = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
- .size = STREAM_BUFFER_SIZE,
+ .size = stream_buffer_size,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT |
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
@@ -63,7 +76,7 @@ StagingBufferPool::StagingBufferPool(const Device& device_, MemoryAllocator& mem
StagingBufferPool::~StagingBufferPool() = default;
StagingBufferRef StagingBufferPool::Request(size_t size, MemoryUsage usage, bool deferred) {
- if (!deferred && usage == MemoryUsage::Upload && size <= MAX_STREAM_BUFFER_REQUEST_SIZE) {
+ if (!deferred && usage == MemoryUsage::Upload && size <= region_size) {
return GetStreamBuffer(size);
}
return GetStagingBuffer(size, usage, deferred);
@@ -101,7 +114,7 @@ StagingBufferRef StagingBufferPool::GetStreamBuffer(size_t size) {
used_iterator = iterator;
free_iterator = std::max(free_iterator, iterator + size);
- if (iterator + size >= STREAM_BUFFER_SIZE) {
+ if (iterator + size >= stream_buffer_size) {
std::fill(sync_ticks.begin() + Region(used_iterator), sync_ticks.begin() + NUM_SYNCS,
current_tick);
used_iterator = 0;
diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
index 5f69f08b1..d3deb9072 100644
--- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
+++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
@@ -90,6 +90,9 @@ private:
void ReleaseCache(MemoryUsage usage);
void ReleaseLevel(StagingBuffersCache& cache, size_t log2);
+ size_t Region(size_t iter) const noexcept {
+ return iter / region_size;
+ }
const Device& device;
MemoryAllocator& memory_allocator;
@@ -97,6 +100,8 @@ private:
vk::Buffer stream_buffer;
std::span<u8> stream_pointer;
+ VkDeviceSize stream_buffer_size;
+ VkDeviceSize region_size;
size_t iterator = 0;
size_t used_iterator = 0;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index cdc41816f..80efd9517 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -238,6 +238,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
return any_r ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT;
case PixelFormat::D16_UNORM:
case PixelFormat::D32_FLOAT:
+ case PixelFormat::X8_D24_UNORM:
return VK_IMAGE_ASPECT_DEPTH_BIT;
case PixelFormat::S8_UINT:
return VK_IMAGE_ASPECT_STENCIL_BIT;
@@ -1550,15 +1551,15 @@ bool Image::IsRescaled() const noexcept {
}
bool Image::ScaleUp(bool ignore) {
+ const auto& resolution = runtime->resolution;
+ if (!resolution.active) {
+ return false;
+ }
if (True(flags & ImageFlagBits::Rescaled)) {
return false;
}
ASSERT(info.type != ImageType::Linear);
flags |= ImageFlagBits::Rescaled;
- const auto& resolution = runtime->resolution;
- if (!resolution.active) {
- return false;
- }
has_scaled = true;
if (!scaled_image) {
const bool is_2d = info.type == ImageType::e2D;
@@ -1587,15 +1588,15 @@ bool Image::ScaleUp(bool ignore) {
}
bool Image::ScaleDown(bool ignore) {
+ const auto& resolution = runtime->resolution;
+ if (!resolution.active) {
+ return false;
+ }
if (False(flags & ImageFlagBits::Rescaled)) {
return false;
}
ASSERT(info.type != ImageType::Linear);
flags &= ~ImageFlagBits::Rescaled;
- const auto& resolution = runtime->resolution;
- if (!resolution.active) {
- return false;
- }
current_image = *original_image;
if (ignore) {
return true;
@@ -2033,4 +2034,32 @@ void TextureCacheRuntime::AccelerateImageUpload(
ASSERT(false);
}
+void TextureCacheRuntime::TransitionImageLayout(Image& image) {
+ if (!image.ExchangeInitialization()) {
+ VkImageMemoryBarrier barrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = VK_ACCESS_NONE,
+ .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = image.Handle(),
+ .subresourceRange{
+ .aspectMask = image.AspectMask(),
+ .baseMipLevel = 0,
+ .levelCount = VK_REMAINING_MIP_LEVELS,
+ .baseArrayLayer = 0,
+ .layerCount = VK_REMAINING_ARRAY_LAYERS,
+ },
+ };
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([barrier = barrier](vk::CommandBuffer cmdbuf) {
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, barrier);
+ });
+ }
+}
+
} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index d6c5a15cc..7a0807709 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -92,6 +92,8 @@ public:
void InsertUploadMemoryBarrier() {}
+ void TransitionImageLayout(Image& image);
+
bool HasBrokenTextureViewFormats() const noexcept {
// No known Vulkan driver has broken image views
return false;
diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp
index e16cd5e73..5b3c7aa5a 100644
--- a/src/video_core/surface.cpp
+++ b/src/video_core/surface.cpp
@@ -85,6 +85,8 @@ PixelFormat PixelFormatFromDepthFormat(Tegra::DepthFormat format) {
return PixelFormat::S8_UINT;
case Tegra::DepthFormat::Z32_FLOAT_X24S8_UINT:
return PixelFormat::D32_FLOAT_S8_UINT;
+ case Tegra::DepthFormat::X8Z24_UNORM:
+ return PixelFormat::X8_D24_UNORM;
default:
UNIMPLEMENTED_MSG("Unimplemented format={}", format);
return PixelFormat::S8_UINT_D24_UNORM;
@@ -202,6 +204,7 @@ PixelFormat PixelFormatFromRenderTargetFormat(Tegra::RenderTargetFormat format)
PixelFormat PixelFormatFromGPUPixelFormat(Service::android::PixelFormat format) {
switch (format) {
case Service::android::PixelFormat::Rgba8888:
+ case Service::android::PixelFormat::Rgbx8888:
return PixelFormat::A8B8G8R8_UNORM;
case Service::android::PixelFormat::Rgb565:
return PixelFormat::R5G6B5_UNORM;
diff --git a/src/video_core/surface.h b/src/video_core/surface.h
index 9b9c4d9bc..a5e8e2f62 100644
--- a/src/video_core/surface.h
+++ b/src/video_core/surface.h
@@ -115,6 +115,7 @@ enum class PixelFormat {
// Depth formats
D32_FLOAT = MaxColorFormat,
D16_UNORM,
+ X8_D24_UNORM,
MaxDepthFormat,
@@ -251,6 +252,7 @@ constexpr std::array<u8, MaxPixelFormat> BLOCK_WIDTH_TABLE = {{
1, // E5B9G9R9_FLOAT
1, // D32_FLOAT
1, // D16_UNORM
+ 1, // X8_D24_UNORM
1, // S8_UINT
1, // D24_UNORM_S8_UINT
1, // S8_UINT_D24_UNORM
@@ -360,6 +362,7 @@ constexpr std::array<u8, MaxPixelFormat> BLOCK_HEIGHT_TABLE = {{
1, // E5B9G9R9_FLOAT
1, // D32_FLOAT
1, // D16_UNORM
+ 1, // X8_D24_UNORM
1, // S8_UINT
1, // D24_UNORM_S8_UINT
1, // S8_UINT_D24_UNORM
@@ -469,6 +472,7 @@ constexpr std::array<u8, MaxPixelFormat> BITS_PER_BLOCK_TABLE = {{
32, // E5B9G9R9_FLOAT
32, // D32_FLOAT
16, // D16_UNORM
+ 32, // X8_D24_UNORM
8, // S8_UINT
32, // D24_UNORM_S8_UINT
32, // S8_UINT_D24_UNORM
diff --git a/src/video_core/texture_cache/format_lookup_table.cpp b/src/video_core/texture_cache/format_lookup_table.cpp
index 56307d030..8c774f512 100644
--- a/src/video_core/texture_cache/format_lookup_table.cpp
+++ b/src/video_core/texture_cache/format_lookup_table.cpp
@@ -138,10 +138,16 @@ PixelFormat PixelFormatFromTextureInfo(TextureFormat format, ComponentType red,
return PixelFormat::E5B9G9R9_FLOAT;
case Hash(TextureFormat::Z32, FLOAT):
return PixelFormat::D32_FLOAT;
+ case Hash(TextureFormat::Z32, FLOAT, UINT, UINT, UINT, LINEAR):
+ return PixelFormat::D32_FLOAT;
case Hash(TextureFormat::Z16, UNORM):
return PixelFormat::D16_UNORM;
case Hash(TextureFormat::Z16, UNORM, UINT, UINT, UINT, LINEAR):
return PixelFormat::D16_UNORM;
+ case Hash(TextureFormat::X8Z24, UNORM):
+ return PixelFormat::X8_D24_UNORM;
+ case Hash(TextureFormat::X8Z24, UNORM, UINT, UINT, UINT, LINEAR):
+ return PixelFormat::X8_D24_UNORM;
case Hash(TextureFormat::Z24S8, UINT, UNORM, UNORM, UNORM, LINEAR):
return PixelFormat::S8_UINT_D24_UNORM;
case Hash(TextureFormat::Z24S8, UINT, UNORM, UINT, UINT, LINEAR):
diff --git a/src/video_core/texture_cache/formatter.cpp b/src/video_core/texture_cache/formatter.cpp
index 6279d8e9e..2b7e0df72 100644
--- a/src/video_core/texture_cache/formatter.cpp
+++ b/src/video_core/texture_cache/formatter.cpp
@@ -10,19 +10,23 @@
#include "video_core/texture_cache/image_info.h"
#include "video_core/texture_cache/image_view_base.h"
#include "video_core/texture_cache/render_targets.h"
+#include "video_core/texture_cache/samples_helper.h"
namespace VideoCommon {
std::string Name(const ImageBase& image) {
const GPUVAddr gpu_addr = image.gpu_addr;
const ImageInfo& info = image.info;
- const u32 width = info.size.width;
- const u32 height = info.size.height;
+ u32 width = info.size.width;
+ u32 height = info.size.height;
const u32 depth = info.size.depth;
const u32 num_layers = image.info.resources.layers;
const u32 num_levels = image.info.resources.levels;
std::string resource;
if (image.info.num_samples > 1) {
+ const auto [samples_x, samples_y] = VideoCommon::SamplesLog2(image.info.num_samples);
+ width >>= samples_x;
+ height >>= samples_y;
resource += fmt::format(":{}xMSAA", image.info.num_samples);
}
if (num_layers > 1) {
diff --git a/src/video_core/texture_cache/formatter.h b/src/video_core/texture_cache/formatter.h
index 9ee57a076..cabbfcb2d 100644
--- a/src/video_core/texture_cache/formatter.h
+++ b/src/video_core/texture_cache/formatter.h
@@ -211,6 +211,8 @@ struct fmt::formatter<VideoCore::Surface::PixelFormat> : fmt::formatter<fmt::str
return "D32_FLOAT";
case PixelFormat::D16_UNORM:
return "D16_UNORM";
+ case PixelFormat::X8_D24_UNORM:
+ return "X8_D24_UNORM";
case PixelFormat::S8_UINT:
return "S8_UINT";
case PixelFormat::D24_UNORM_S8_UINT:
diff --git a/src/video_core/texture_cache/image_view_base.cpp b/src/video_core/texture_cache/image_view_base.cpp
index 0c5f4450d..18b9250f9 100644
--- a/src/video_core/texture_cache/image_view_base.cpp
+++ b/src/video_core/texture_cache/image_view_base.cpp
@@ -85,6 +85,7 @@ bool ImageViewBase::SupportsAnisotropy() const noexcept {
// Depth formats
case PixelFormat::D32_FLOAT:
case PixelFormat::D16_UNORM:
+ case PixelFormat::X8_D24_UNORM:
// Stencil formats
case PixelFormat::S8_UINT:
// DepthStencil formats
diff --git a/src/video_core/texture_cache/samples_helper.h b/src/video_core/texture_cache/samples_helper.h
index 203ac1b11..2ee2f8312 100644
--- a/src/video_core/texture_cache/samples_helper.h
+++ b/src/video_core/texture_cache/samples_helper.h
@@ -24,7 +24,7 @@ namespace VideoCommon {
return {2, 2};
}
ASSERT_MSG(false, "Invalid number of samples={}", num_samples);
- return {1, 1};
+ return {0, 0};
}
[[nodiscard]] inline int NumSamples(Tegra::Texture::MsaaMode msaa_mode) {
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 1bdb0def5..d575c57ca 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -1016,6 +1016,7 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
if (image.info.num_samples > 1 && !runtime.CanUploadMSAA()) {
LOG_WARNING(HW_GPU, "MSAA image uploads are not implemented");
+ runtime.TransitionImageLayout(image);
return;
}
if (True(image.flags & ImageFlagBits::AsynchronousDecode)) {
diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp
index 0a86ce139..15596c925 100644
--- a/src/video_core/texture_cache/util.cpp
+++ b/src/video_core/texture_cache/util.cpp
@@ -68,6 +68,7 @@ struct LevelInfo {
Extent2D tile_size;
u32 bpp_log2;
u32 tile_width_spacing;
+ u32 num_levels;
};
[[nodiscard]] constexpr u32 AdjustTileSize(u32 shift, u32 unit_factor, u32 dimension) {
@@ -118,11 +119,11 @@ template <u32 GOB_EXTENT>
}
[[nodiscard]] constexpr Extent3D AdjustMipBlockSize(Extent3D num_tiles, Extent3D block_size,
- u32 level) {
+ u32 level, u32 num_levels) {
return {
.width = AdjustMipBlockSize<GOB_SIZE_X>(num_tiles.width, block_size.width, level),
.height = AdjustMipBlockSize<GOB_SIZE_Y>(num_tiles.height, block_size.height, level),
- .depth = level == 0
+ .depth = level == 0 && num_levels == 1
? block_size.depth
: AdjustMipBlockSize<GOB_SIZE_Z>(num_tiles.depth, block_size.depth, level),
};
@@ -166,7 +167,7 @@ template <u32 GOB_EXTENT>
}
[[nodiscard]] constexpr Extent3D TileShift(const LevelInfo& info, u32 level) {
- if (level == 0) {
+ if (level == 0 && info.num_levels == 1) {
return Extent3D{
.width = info.block.width,
.height = info.block.height,
@@ -257,7 +258,7 @@ template <u32 GOB_EXTENT>
}
[[nodiscard]] constexpr LevelInfo MakeLevelInfo(PixelFormat format, Extent3D size, Extent3D block,
- u32 tile_width_spacing) {
+ u32 tile_width_spacing, u32 num_levels) {
const u32 bytes_per_block = BytesPerBlock(format);
return {
.size =
@@ -270,16 +271,18 @@ template <u32 GOB_EXTENT>
.tile_size = DefaultBlockSize(format),
.bpp_log2 = BytesPerBlockLog2(bytes_per_block),
.tile_width_spacing = tile_width_spacing,
+ .num_levels = num_levels,
};
}
[[nodiscard]] constexpr LevelInfo MakeLevelInfo(const ImageInfo& info) {
- return MakeLevelInfo(info.format, info.size, info.block, info.tile_width_spacing);
+ return MakeLevelInfo(info.format, info.size, info.block, info.tile_width_spacing,
+ info.resources.levels);
}
[[nodiscard]] constexpr u32 CalculateLevelOffset(PixelFormat format, Extent3D size, Extent3D block,
u32 tile_width_spacing, u32 level) {
- const LevelInfo info = MakeLevelInfo(format, size, block, tile_width_spacing);
+ const LevelInfo info = MakeLevelInfo(format, size, block, tile_width_spacing, level);
u32 offset = 0;
for (u32 current_level = 0; current_level < level; ++current_level) {
offset += CalculateLevelSize(info, current_level);
@@ -466,7 +469,7 @@ template <u32 GOB_EXTENT>
};
const u32 bpp_log2 = BytesPerBlockLog2(info.format);
const u32 alignment = StrideAlignment(num_tiles, info.block, bpp_log2, info.tile_width_spacing);
- const Extent3D mip_block = AdjustMipBlockSize(num_tiles, info.block, 0);
+ const Extent3D mip_block = AdjustMipBlockSize(num_tiles, info.block, 0, info.resources.levels);
return Extent3D{
.width = Common::AlignUpLog2(num_tiles.width, alignment),
.height = Common::AlignUpLog2(num_tiles.height, GOB_SIZE_Y_SHIFT + mip_block.height),
@@ -533,7 +536,8 @@ void SwizzleBlockLinearImage(Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr
UNIMPLEMENTED_IF(copy.image_extent != level_size);
const Extent3D num_tiles = AdjustTileSize(level_size, tile_size);
- const Extent3D block = AdjustMipBlockSize(num_tiles, level_info.block, level);
+ const Extent3D block =
+ AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels);
size_t host_offset = copy.buffer_offset;
@@ -698,7 +702,7 @@ u32 CalculateLevelStrideAlignment(const ImageInfo& info, u32 level) {
const Extent2D tile_size = DefaultBlockSize(info.format);
const Extent3D level_size = AdjustMipSize(info.size, level);
const Extent3D num_tiles = AdjustTileSize(level_size, tile_size);
- const Extent3D block = AdjustMipBlockSize(num_tiles, info.block, level);
+ const Extent3D block = AdjustMipBlockSize(num_tiles, info.block, level, info.resources.levels);
const u32 bpp_log2 = BytesPerBlockLog2(info.format);
return StrideAlignment(num_tiles, block, bpp_log2, info.tile_width_spacing);
}
@@ -887,7 +891,8 @@ boost::container::small_vector<BufferImageCopy, 16> UnswizzleImage(Tegra::Memory
.image_extent = level_size,
};
const Extent3D num_tiles = AdjustTileSize(level_size, tile_size);
- const Extent3D block = AdjustMipBlockSize(num_tiles, level_info.block, level);
+ const Extent3D block =
+ AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels);
const u32 stride_alignment = StrideAlignment(num_tiles, info.block, gob, bpp_log2);
size_t guest_layer_offset = 0;
@@ -1041,7 +1046,7 @@ Extent3D MipBlockSize(const ImageInfo& info, u32 level) {
const Extent2D tile_size = DefaultBlockSize(info.format);
const Extent3D level_size = AdjustMipSize(info.size, level);
const Extent3D num_tiles = AdjustTileSize(level_size, tile_size);
- return AdjustMipBlockSize(num_tiles, level_info.block, level);
+ return AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels);
}
boost::container::small_vector<SwizzleParameters, 16> FullUploadSwizzles(const ImageInfo& info) {
@@ -1063,7 +1068,8 @@ boost::container::small_vector<SwizzleParameters, 16> FullUploadSwizzles(const I
for (s32 level = 0; level < num_levels; ++level) {
const Extent3D level_size = AdjustMipSize(size, level);
const Extent3D num_tiles = AdjustTileSize(level_size, tile_size);
- const Extent3D block = AdjustMipBlockSize(num_tiles, level_info.block, level);
+ const Extent3D block =
+ AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels);
params[level] = SwizzleParameters{
.num_tiles = num_tiles,
.block = block,
@@ -1292,11 +1298,11 @@ u32 MapSizeBytes(const ImageBase& image) {
}
}
-static_assert(CalculateLevelSize(LevelInfo{{1920, 1080, 1}, {0, 2, 0}, {1, 1}, 2, 0}, 0) ==
+static_assert(CalculateLevelSize(LevelInfo{{1920, 1080, 1}, {0, 2, 0}, {1, 1}, 2, 0, 1}, 0) ==
0x7f8000);
-static_assert(CalculateLevelSize(LevelInfo{{32, 32, 1}, {0, 0, 4}, {1, 1}, 4, 0}, 0) == 0x40000);
+static_assert(CalculateLevelSize(LevelInfo{{32, 32, 1}, {0, 0, 4}, {1, 1}, 4, 0, 1}, 0) == 0x40000);
-static_assert(CalculateLevelSize(LevelInfo{{128, 8, 1}, {0, 4, 0}, {1, 1}, 4, 0}, 0) == 0x40000);
+static_assert(CalculateLevelSize(LevelInfo{{128, 8, 1}, {0, 4, 0}, {1, 1}, 4, 0, 1}, 0) == 0x40000);
static_assert(CalculateLevelOffset(PixelFormat::R8_SINT, {1920, 1080, 1}, {0, 2, 0}, 0, 7) ==
0x2afc00);
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 3960b135a..876cec2e8 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -84,9 +84,12 @@ constexpr std::array VK_FORMAT_A4B4G4R4_UNORM_PACK16{
} // namespace Alternatives
enum class NvidiaArchitecture {
- AmpereOrNewer,
+ KeplerOrOlder,
+ Maxwell,
+ Pascal,
+ Volta,
Turing,
- VoltaOrOlder,
+ AmpereOrNewer,
};
template <typename T>
@@ -200,6 +203,7 @@ std::unordered_map<VkFormat, VkFormatProperties> GetFormatProperties(vk::Physica
VK_FORMAT_BC7_UNORM_BLOCK,
VK_FORMAT_D16_UNORM,
VK_FORMAT_D16_UNORM_S8_UINT,
+ VK_FORMAT_X8_D24_UNORM_PACK32,
VK_FORMAT_D24_UNORM_S8_UINT,
VK_FORMAT_D32_SFLOAT,
VK_FORMAT_D32_SFLOAT_S8_UINT,
@@ -321,13 +325,38 @@ NvidiaArchitecture GetNvidiaArchitecture(vk::PhysicalDevice physical,
physical.GetProperties2(physical_properties);
if (shading_rate_props.primitiveFragmentShadingRateWithMultipleViewports) {
// Only Ampere and newer support this feature
+ // TODO: Find a way to differentiate Ampere and Ada
return NvidiaArchitecture::AmpereOrNewer;
}
- }
- if (exts.contains(VK_NV_SHADING_RATE_IMAGE_EXTENSION_NAME)) {
return NvidiaArchitecture::Turing;
}
- return NvidiaArchitecture::VoltaOrOlder;
+
+ if (exts.contains(VK_EXT_BLEND_OPERATION_ADVANCED_EXTENSION_NAME)) {
+ VkPhysicalDeviceBlendOperationAdvancedPropertiesEXT advanced_blending_props{};
+ advanced_blending_props.sType =
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BLEND_OPERATION_ADVANCED_PROPERTIES_EXT;
+ VkPhysicalDeviceProperties2 physical_properties{};
+ physical_properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
+ physical_properties.pNext = &advanced_blending_props;
+ physical.GetProperties2(physical_properties);
+ if (advanced_blending_props.advancedBlendMaxColorAttachments == 1) {
+ return NvidiaArchitecture::Maxwell;
+ }
+
+ if (exts.contains(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME)) {
+ VkPhysicalDeviceConservativeRasterizationPropertiesEXT conservative_raster_props{};
+ conservative_raster_props.sType =
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONSERVATIVE_RASTERIZATION_PROPERTIES_EXT;
+ physical_properties.pNext = &conservative_raster_props;
+ physical.GetProperties2(physical_properties);
+ if (conservative_raster_props.degenerateLinesRasterized) {
+ return NvidiaArchitecture::Volta;
+ }
+ return NvidiaArchitecture::Pascal;
+ }
+ }
+
+ return NvidiaArchitecture::KeplerOrOlder;
}
std::vector<const char*> ExtensionListForVulkan(
@@ -504,19 +533,14 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
if (is_nvidia) {
const u32 nv_major_version = (properties.properties.driverVersion >> 22) & 0x3ff;
const auto arch = GetNvidiaArchitecture(physical, supported_extensions);
- switch (arch) {
- case NvidiaArchitecture::AmpereOrNewer:
+ if (arch >= NvidiaArchitecture::AmpereOrNewer) {
LOG_WARNING(Render_Vulkan, "Ampere and newer have broken float16 math");
features.shader_float16_int8.shaderFloat16 = false;
- break;
- case NvidiaArchitecture::Turing:
- break;
- case NvidiaArchitecture::VoltaOrOlder:
+ } else if (arch <= NvidiaArchitecture::Volta) {
if (nv_major_version < 527) {
LOG_WARNING(Render_Vulkan, "Volta and older have broken VK_KHR_push_descriptor");
RemoveExtension(extensions.push_descriptor, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME);
}
- break;
}
if (nv_major_version >= 510) {
LOG_WARNING(Render_Vulkan, "NVIDIA Drivers >= 510 do not support MSAA image blits");
@@ -661,7 +685,15 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
"ANV drivers 22.3.0 to 23.1.0 have broken VK_KHR_push_descriptor");
RemoveExtension(extensions.push_descriptor, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME);
}
+ } else if (extensions.push_descriptor && is_nvidia) {
+ const auto arch = GetNvidiaArchitecture(physical, supported_extensions);
+ if (arch <= NvidiaArchitecture::Pascal) {
+ LOG_WARNING(Render_Vulkan,
+ "Pascal and older architectures have broken VK_KHR_push_descriptor");
+ RemoveExtension(extensions.push_descriptor, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME);
+ }
}
+
if (is_mvk) {
LOG_WARNING(Render_Vulkan,
"MVK driver breaks when using more than 16 vertex attributes/bindings");
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
index 3ef381a38..8dd1667f3 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
@@ -9,6 +9,7 @@
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common_types.h"
+#include "common/literals.h"
#include "common/logging/log.h"
#include "common/polyfill_ranges.h"
#include "video_core/vulkan_common/vma.h"
@@ -65,12 +66,12 @@ struct Range {
switch (usage) {
case MemoryUsage::Upload:
case MemoryUsage::Stream:
- return VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
+ return VMA_ALLOCATION_CREATE_MAPPED_BIT |
+ VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
case MemoryUsage::Download:
- return VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT;
+ return VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT;
case MemoryUsage::DeviceLocal:
- return VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
- VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT;
+ return {};
}
return {};
}
@@ -212,7 +213,20 @@ MemoryAllocator::MemoryAllocator(const Device& device_)
: device{device_}, allocator{device.GetAllocator()},
properties{device_.GetPhysical().GetMemoryProperties().memoryProperties},
buffer_image_granularity{
- device_.GetPhysical().GetProperties().limits.bufferImageGranularity} {}
+ device_.GetPhysical().GetProperties().limits.bufferImageGranularity} {
+ // GPUs not supporting rebar may only have a region with less than 256MB host visible/device
+ // local memory. In that case, opening 2 RenderDoc captures side-by-side is not possible due to
+ // the heap running out of memory. With RenderDoc attached and only a small host/device region,
+ // only allow the stream buffer in this memory heap.
+ if (device.HasDebuggingToolAttached()) {
+ using namespace Common::Literals;
+ ForEachDeviceLocalHostVisibleHeap(device, [this](size_t index, VkMemoryHeap& heap) {
+ if (heap.size <= 256_MiB) {
+ valid_memory_types &= ~(1u << index);
+ }
+ });
+ }
+}
MemoryAllocator::~MemoryAllocator() = default;
@@ -239,12 +253,11 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo& ci) const {
vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo& ci, MemoryUsage usage) const {
const VmaAllocationCreateInfo alloc_ci = {
- .flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT |
- MemoryUsageVmaFlags(usage),
+ .flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | MemoryUsageVmaFlags(usage),
.usage = MemoryUsageVma(usage),
.requiredFlags = 0,
.preferredFlags = MemoryUsagePreferedVmaFlags(usage),
- .memoryTypeBits = 0,
+ .memoryTypeBits = usage == MemoryUsage::Stream ? 0u : valid_memory_types,
.pool = VK_NULL_HANDLE,
.pUserData = nullptr,
.priority = 0.f,
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h
index f449bc8d0..38a182bcb 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.h
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h
@@ -7,6 +7,7 @@
#include <span>
#include <vector>
#include "common/common_types.h"
+#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
VK_DEFINE_HANDLE(VmaAllocator)
@@ -26,6 +27,18 @@ enum class MemoryUsage {
Stream, ///< Requests device local host visible buffer, falling back host memory.
};
+template <typename F>
+void ForEachDeviceLocalHostVisibleHeap(const Device& device, F&& f) {
+ auto memory_props = device.GetPhysical().GetMemoryProperties().memoryProperties;
+ for (size_t i = 0; i < memory_props.memoryTypeCount; i++) {
+ auto& memory_type = memory_props.memoryTypes[i];
+ if ((memory_type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) &&
+ (memory_type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) {
+ f(memory_type.heapIndex, memory_props.memoryHeaps[memory_type.heapIndex]);
+ }
+ }
+}
+
/// Ownership handle of a memory commitment.
/// Points to a subregion of a memory allocation.
class MemoryCommit {
@@ -124,6 +137,7 @@ private:
std::vector<std::unique_ptr<MemoryAllocation>> allocations; ///< Current allocations.
VkDeviceSize buffer_image_granularity; // The granularity for adjacent offsets between buffers
// and optimal images
+ u32 valid_memory_types{~0u};
};
} // namespace Vulkan
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h
index 1e3c0fa64..0487cd3b6 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.h
+++ b/src/video_core/vulkan_common/vulkan_wrapper.h
@@ -117,6 +117,9 @@ public:
virtual ~Exception() = default;
const char* what() const noexcept override;
+ VkResult GetResult() const noexcept {
+ return result;
+ }
private:
VkResult result;
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 8f86a1553..34208ed74 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -195,6 +195,8 @@ add_executable(yuzu
multiplayer/state.cpp
multiplayer/state.h
multiplayer/validation.h
+ play_time_manager.cpp
+ play_time_manager.h
precompiled_headers.h
qt_common.cpp
qt_common.h
@@ -382,7 +384,7 @@ if (USE_DISCORD_PRESENCE)
discord_impl.cpp
discord_impl.h
)
- target_link_libraries(yuzu PRIVATE DiscordRPC::discord-rpc httplib::httplib)
+ target_link_libraries(yuzu PRIVATE DiscordRPC::discord-rpc httplib::httplib Qt${QT_MAJOR_VERSION}::Network)
target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE)
endif()
diff --git a/src/yuzu/applets/qt_controller.cpp b/src/yuzu/applets/qt_controller.cpp
index d15559518..ca0e14fad 100644
--- a/src/yuzu/applets/qt_controller.cpp
+++ b/src/yuzu/applets/qt_controller.cpp
@@ -23,6 +23,7 @@
#include "yuzu/configuration/configure_vibration.h"
#include "yuzu/configuration/input_profiles.h"
#include "yuzu/main.h"
+#include "yuzu/util/controller_navigation.h"
namespace {
@@ -132,6 +133,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
ui->checkboxPlayer7Connected, ui->checkboxPlayer8Connected,
};
+ ui->labelError->setVisible(false);
+
// Setup/load everything prior to setting up connections.
// This avoids unintentionally changing the states of elements while loading them in.
SetSupportedControllers();
@@ -143,6 +146,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
LoadConfiguration();
+ controller_navigation = new ControllerNavigation(system.HIDCore(), this);
+
for (std::size_t i = 0; i < NUM_PLAYERS; ++i) {
SetExplainText(i);
UpdateControllerIcon(i);
@@ -151,6 +156,8 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
connect(player_groupboxes[i], &QGroupBox::toggled, [this, i](bool checked) {
if (checked) {
+ // Hide eventual error message about number of controllers
+ ui->labelError->setVisible(false);
for (std::size_t index = 0; index <= i; ++index) {
connected_controller_checkboxes[index]->setChecked(checked);
}
@@ -199,6 +206,12 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
&QtControllerSelectorDialog::ApplyConfiguration);
+ connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
+ [this](Qt::Key key) {
+ QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
+ QCoreApplication::postEvent(this, event);
+ });
+
// Enhancement: Check if the parameters have already been met before disconnecting controllers.
// If all the parameters are met AND only allows a single player,
// stop the constructor here as we do not need to continue.
@@ -217,6 +230,7 @@ QtControllerSelectorDialog::QtControllerSelectorDialog(
}
QtControllerSelectorDialog::~QtControllerSelectorDialog() {
+ controller_navigation->UnloadController();
system.HIDCore().DisableAllControllerConfiguration();
}
@@ -291,6 +305,31 @@ void QtControllerSelectorDialog::CallConfigureInputProfileDialog() {
dialog.exec();
}
+void QtControllerSelectorDialog::keyPressEvent(QKeyEvent* evt) {
+ const auto num_connected_players = static_cast<int>(
+ std::count_if(player_groupboxes.begin(), player_groupboxes.end(),
+ [](const QGroupBox* player) { return player->isChecked(); }));
+
+ const auto min_supported_players = parameters.enable_single_mode ? 1 : parameters.min_players;
+ const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players;
+
+ if ((evt->key() == Qt::Key_Enter || evt->key() == Qt::Key_Return) && !parameters_met) {
+ // Display error message when trying to validate using "Enter" and "OK" button is disabled
+ ui->labelError->setVisible(true);
+ return;
+ } else if (evt->key() == Qt::Key_Left && num_connected_players > min_supported_players) {
+ // Remove a player if possible
+ connected_controller_checkboxes[num_connected_players - 1]->setChecked(false);
+ return;
+ } else if (evt->key() == Qt::Key_Right && num_connected_players < max_supported_players) {
+ // Add a player, if possible
+ ui->labelError->setVisible(false);
+ connected_controller_checkboxes[num_connected_players]->setChecked(true);
+ return;
+ }
+ QDialog::keyPressEvent(evt);
+}
+
bool QtControllerSelectorDialog::CheckIfParametersMet() {
// Here, we check and validate the current configuration against all applicable parameters.
const auto num_connected_players = static_cast<int>(
diff --git a/src/yuzu/applets/qt_controller.h b/src/yuzu/applets/qt_controller.h
index 2fdc35857..7f0673d06 100644
--- a/src/yuzu/applets/qt_controller.h
+++ b/src/yuzu/applets/qt_controller.h
@@ -34,6 +34,8 @@ class HIDCore;
enum class NpadStyleIndex : u8;
} // namespace Core::HID
+class ControllerNavigation;
+
class QtControllerSelectorDialog final : public QDialog {
Q_OBJECT
@@ -46,6 +48,8 @@ public:
int exec() override;
+ void keyPressEvent(QKeyEvent* evt) override;
+
private:
// Applies the current configuration.
void ApplyConfiguration();
@@ -110,6 +114,8 @@ private:
Core::System& system;
+ ControllerNavigation* controller_navigation = nullptr;
+
// This is true if and only if all parameters are met. Otherwise, this is false.
// This determines whether the "OK" button can be clicked to exit the applet.
bool parameters_met{false};
diff --git a/src/yuzu/applets/qt_controller.ui b/src/yuzu/applets/qt_controller.ui
index 729e921ee..6f7cb3c13 100644
--- a/src/yuzu/applets/qt_controller.ui
+++ b/src/yuzu/applets/qt_controller.ui
@@ -2624,13 +2624,53 @@
</spacer>
</item>
<item alignment="Qt::AlignBottom">
- <widget class="QDialogButtonBox" name="buttonBox">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="standardButtons">
- <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
- </property>
+ <widget class="QWidget" name="closeButtons" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_46">
+ <property name="spacing">
+ <number>7</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="labelError">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QLabel { color : red; }</string>
+ </property>
+ <property name="text">
+ <string>Not enough controllers</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
</layout>
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 1de093447..d5157c502 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -128,8 +128,8 @@ const std::array<UISettings::Shortcut, 22> Config::default_hotkeys{{
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Fullscreen")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F11"), QStringLiteral("Home+B"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load File")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+O"), QStringLiteral(""), Qt::WidgetWithChildrenShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load/Remove Amiibo")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F2"), QStringLiteral("Home+A"), Qt::WidgetWithChildrenShortcut, false}},
- {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Restart Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F6"), QStringLiteral(""), Qt::WindowShortcut, false}},
- {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Stop Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F5"), QStringLiteral(""), Qt::WindowShortcut, false}},
+ {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Restart Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F6"), QStringLiteral("R+Plus+Minus"), Qt::WindowShortcut, false}},
+ {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Stop Emulation")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("F5"), QStringLiteral("L+Plus+Minus"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Record")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+F7"), QStringLiteral(""), Qt::ApplicationShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Reset")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+F6"), QStringLiteral(""), Qt::ApplicationShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Start/Stop")), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")), {QStringLiteral("Ctrl+F5"), QStringLiteral(""), Qt::ApplicationShortcut, false}},
diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp
index 9ccfb2435..81dd51ad3 100644
--- a/src/yuzu/configuration/configure_audio.cpp
+++ b/src/yuzu/configuration/configure_audio.cpp
@@ -42,6 +42,9 @@ void ConfigureAudio::Setup(const ConfigurationShared::Builder& builder) {
for (auto* setting : Settings::values.linkage.by_category[category]) {
settings.push_back(setting);
}
+ for (auto* setting : UISettings::values.linkage.by_category[category]) {
+ settings.push_back(setting);
+ }
};
push(Settings::Category::Audio);
diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp
index e8f9ebfd8..5a48e388b 100644
--- a/src/yuzu/configuration/configure_input.cpp
+++ b/src/yuzu/configuration/configure_input.cpp
@@ -115,17 +115,9 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
for (std::size_t i = 0; i < player_tabs.size(); ++i) {
player_tabs[i]->setLayout(new QHBoxLayout(player_tabs[i]));
player_tabs[i]->layout()->addWidget(player_controllers[i]);
- connect(player_controllers[i], &ConfigureInputPlayer::Connected, [&, i](bool is_connected) {
+ connect(player_connected[i], &QCheckBox::clicked, [this, i](int checked) {
// Ensures that the controllers are always connected in sequential order
- if (is_connected) {
- for (std::size_t index = 0; index <= i; ++index) {
- player_connected[index]->setChecked(is_connected);
- }
- } else {
- for (std::size_t index = i; index < player_tabs.size(); ++index) {
- player_connected[index]->setChecked(is_connected);
- }
- }
+ this->propagateMouseClickOnPlayers(i, checked, true);
});
connect(player_controllers[i], &ConfigureInputPlayer::RefreshInputDevices, this,
&ConfigureInput::UpdateAllInputDevices);
@@ -183,6 +175,30 @@ void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem,
LoadConfiguration();
}
+void ConfigureInput::propagateMouseClickOnPlayers(size_t player_index, bool checked, bool origin) {
+ // Origin has already been toggled
+ if (!origin) {
+ player_connected[player_index]->setChecked(checked);
+ }
+
+ if (checked) {
+ // Check all previous buttons when checked
+ if (player_index > 0) {
+ propagateMouseClickOnPlayers(player_index - 1, checked, false);
+ }
+ } else {
+ // Unchecked all following buttons when unchecked
+ if (player_index < player_tabs.size() - 1) {
+ // Reconnect current player if it was the last one checked
+ // (player number was reduced by more than one)
+ if (origin && player_connected[player_index + 1]->checkState() == Qt::Checked) {
+ player_connected[player_index]->setCheckState(Qt::Checked);
+ }
+ propagateMouseClickOnPlayers(player_index + 1, checked, false);
+ }
+ }
+}
+
QList<QWidget*> ConfigureInput::GetSubTabs() const {
return {
ui->tabPlayer1, ui->tabPlayer2, ui->tabPlayer3, ui->tabPlayer4, ui->tabPlayer5,
diff --git a/src/yuzu/configuration/configure_input.h b/src/yuzu/configuration/configure_input.h
index c89189c36..abb7f7089 100644
--- a/src/yuzu/configuration/configure_input.h
+++ b/src/yuzu/configuration/configure_input.h
@@ -56,6 +56,7 @@ private:
void UpdateDockedState(bool is_handheld);
void UpdateAllInputDevices();
void UpdateAllInputProfiles(std::size_t player_index);
+ void propagateMouseClickOnPlayers(size_t player_index, bool origin, bool checked);
/// Load configuration settings.
void LoadConfiguration();
diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp
index a9fde9f4f..82f3b6e78 100644
--- a/src/yuzu/configuration/configure_ui.cpp
+++ b/src/yuzu/configuration/configure_ui.cpp
@@ -123,6 +123,8 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent)
connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate);
+ connect(ui->show_play_time, &QCheckBox::stateChanged, this,
+ &ConfigureUi::RequestGameListUpdate);
connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&ConfigureUi::RequestGameListUpdate);
connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged),
@@ -167,6 +169,7 @@ void ConfigureUi::ApplyConfiguration() {
UISettings::values.show_compat = ui->show_compat->isChecked();
UISettings::values.show_size = ui->show_size->isChecked();
UISettings::values.show_types = ui->show_types->isChecked();
+ UISettings::values.show_play_time = ui->show_play_time->isChecked();
UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt();
UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt();
UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt();
@@ -179,6 +182,7 @@ void ConfigureUi::ApplyConfiguration() {
const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText());
UISettings::values.screenshot_height.SetValue(height);
+ RequestGameListUpdate();
system.ApplySettings();
}
@@ -194,6 +198,7 @@ void ConfigureUi::SetConfiguration() {
ui->show_compat->setChecked(UISettings::values.show_compat.GetValue());
ui->show_size->setChecked(UISettings::values.show_size.GetValue());
ui->show_types->setChecked(UISettings::values.show_types.GetValue());
+ ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue());
ui->game_icon_size_combobox->setCurrentIndex(
ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue()));
ui->folder_icon_size_combobox->setCurrentIndex(
diff --git a/src/yuzu/configuration/configure_ui.ui b/src/yuzu/configuration/configure_ui.ui
index cb66ef104..b8e648381 100644
--- a/src/yuzu/configuration/configure_ui.ui
+++ b/src/yuzu/configuration/configure_ui.ui
@@ -105,6 +105,13 @@
</widget>
</item>
<item>
+ <widget class="QCheckBox" name="show_play_time">
+ <property name="text">
+ <string>Show Play Time Column</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2">
<item>
<widget class="QLabel" name="game_icon_size_label">
diff --git a/src/yuzu/configuration/shared_translation.cpp b/src/yuzu/configuration/shared_translation.cpp
index 276bdbaba..3fe448f27 100644
--- a/src/yuzu/configuration/shared_translation.cpp
+++ b/src/yuzu/configuration/shared_translation.cpp
@@ -29,9 +29,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(Settings, sink_id, "Output Engine:", "");
INSERT(Settings, audio_output_device_id, "Output Device:", "");
INSERT(Settings, audio_input_device_id, "Input Device:", "");
- INSERT(Settings, audio_muted, "Mute audio when in background", "");
+ INSERT(Settings, audio_muted, "Mute audio", "");
INSERT(Settings, volume, "Volume:", "");
INSERT(Settings, dump_audio_commands, "", "");
+ INSERT(UISettings, mute_when_in_background, "Mute audio when in background", "");
// Core
INSERT(Settings, use_multi_core, "Multicore CPU Emulation", "");
@@ -156,6 +157,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(UISettings, select_user_on_boot, "Prompt for user on game boot", "");
INSERT(UISettings, pause_when_in_background, "Pause emulation when in background", "");
INSERT(UISettings, confirm_before_closing, "Confirm exit while emulation is running", "");
+ INSERT(UISettings, confirm_before_stopping, "Confirm before stopping emulation", "");
INSERT(UISettings, hide_mouse, "Hide mouse on inactivity", "");
INSERT(UISettings, controller_applet_disabled, "Disable controller applet", "");
@@ -382,6 +384,13 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
translations->insert(
{Settings::EnumMetadata<Settings::ConsoleMode>::Index(),
{PAIR(ConsoleMode, Docked, "Docked"), PAIR(ConsoleMode, Handheld, "Handheld")}});
+ translations->insert(
+ {Settings::EnumMetadata<Settings::ConfirmStop>::Index(),
+ {
+ PAIR(ConfirmStop, Ask_Always, "Always ask (Default)"),
+ PAIR(ConfirmStop, Ask_Based_On_Game, "Only if game specifies not to stop"),
+ PAIR(ConfirmStop, Ask_Never, "Never ask"),
+ }});
#undef PAIR
#undef CTX_PAIR
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index f254c1e1c..2bb1a0239 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -312,8 +312,10 @@ void GameList::OnFilterCloseClicked() {
}
GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_,
- Core::System& system_, GMainWindow* parent)
- : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, system{system_} {
+ PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
+ GMainWindow* parent)
+ : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_},
+ play_time_manager{play_time_manager_}, system{system_} {
watcher = new QFileSystemWatcher(this);
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
@@ -340,6 +342,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
+ tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
item_model->setSortRole(GameListItemPath::SortRole);
connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons);
@@ -548,6 +551,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update"));
QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC"));
QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration"));
+ QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data"));
QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage"));
QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache"));
QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache"));
@@ -560,9 +564,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
-#ifndef WIN32
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
+#ifndef WIN32
QAction* create_applications_menu_shortcut =
shortcut_menu->addAction(tr("Add to Applications Menu"));
#endif
@@ -622,6 +626,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() {
emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path);
});
+ connect(remove_play_time_data, &QAction::triggered,
+ [this, program_id]() { emit RemovePlayTimeRequested(program_id); });
connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] {
emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path);
});
@@ -638,10 +644,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
});
-#ifndef WIN32
connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop);
});
+#ifndef WIN32
connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications);
});
@@ -790,6 +796,7 @@ void GameList::RetranslateUI() {
item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons"));
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
+ item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
}
void GameListSearchField::changeEvent(QEvent* event) {
@@ -817,15 +824,17 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size);
+ tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time);
+
+ // Before deleting rows, cancel the worker so that it is not using them
+ emit ShouldCancelWorker();
// Delete any rows that might already exist if we're repopulating
item_model->removeRows(0, item_model->rowCount());
search_field->clear();
- emit ShouldCancelWorker();
-
GameListWorker* worker =
- new GameListWorker(vfs, provider, game_dirs, compatibility_list, system);
+ new GameListWorker(vfs, provider, game_dirs, compatibility_list, play_time_manager, system);
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index 1fcbbf0ba..712570cea 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -18,6 +18,7 @@
#include "core/core.h"
#include "uisettings.h"
#include "yuzu/compatibility_list.h"
+#include "yuzu/play_time_manager.h"
namespace Core {
class System;
@@ -75,11 +76,13 @@ public:
COLUMN_ADD_ONS,
COLUMN_FILE_TYPE,
COLUMN_SIZE,
+ COLUMN_PLAY_TIME,
COLUMN_COUNT, // Number of columns
};
explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
- FileSys::ManualContentProvider* provider_, Core::System& system_,
+ FileSys::ManualContentProvider* provider_,
+ PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_,
GMainWindow* parent = nullptr);
~GameList() override;
@@ -113,6 +116,7 @@ signals:
void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type);
void RemoveFileRequested(u64 program_id, GameListRemoveTarget target,
const std::string& game_path);
+ void RemovePlayTimeRequested(u64 program_id);
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void VerifyIntegrityRequested(const std::string& game_path);
void CopyTIDRequested(u64 program_id);
@@ -168,6 +172,7 @@ private:
friend class GameListSearchField;
+ const PlayTime::PlayTimeManager& play_time_manager;
Core::System& system;
};
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index 1800f090f..86a0c41d9 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -18,6 +18,7 @@
#include "common/common_types.h"
#include "common/logging/log.h"
#include "common/string_util.h"
+#include "yuzu/play_time_manager.h"
#include "yuzu/uisettings.h"
#include "yuzu/util/util.h"
@@ -221,6 +222,31 @@ public:
}
};
+/**
+ * GameListItem for Play Time values.
+ * This object stores the play time of a game in seconds, and its readable
+ * representation in minutes/hours
+ */
+class GameListItemPlayTime : public GameListItem {
+public:
+ static constexpr int PlayTimeRole = SortRole;
+
+ GameListItemPlayTime() = default;
+ explicit GameListItemPlayTime(const qulonglong time_seconds) {
+ setData(time_seconds, PlayTimeRole);
+ }
+
+ void setData(const QVariant& value, int role) override {
+ qulonglong time_seconds = value.toULongLong();
+ GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole);
+ GameListItem::setData(value, PlayTimeRole);
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong();
+ }
+};
+
class GameListDir : public GameListItem {
public:
static constexpr int GameDirRole = Qt::UserRole + 2;
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index e7fb8a282..077ced12b 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -194,6 +194,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
const std::size_t size, const std::vector<u8>& icon,
Loader::AppLoader& loader, u64 program_id,
const CompatibilityList& compatibility_list,
+ const PlayTime::PlayTimeManager& play_time_manager,
const FileSys::PatchManager& patch) {
const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
@@ -212,6 +213,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
new GameListItemCompat(compatibility),
new GameListItem(file_type_string),
new GameListItemSize(size),
+ new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
};
const auto patch_versions = GetGameListCachedObject(
@@ -227,9 +229,12 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
FileSys::ManualContentProvider* provider_,
QVector<UISettings::GameDir>& game_dirs_,
- const CompatibilityList& compatibility_list_, Core::System& system_)
+ const CompatibilityList& compatibility_list_,
+ const PlayTime::PlayTimeManager& play_time_manager_,
+ Core::System& system_)
: vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
- compatibility_list{compatibility_list_}, system{system_} {}
+ compatibility_list{compatibility_list_},
+ play_time_manager{play_time_manager_}, system{system_} {}
GameListWorker::~GameListWorker() = default;
@@ -280,7 +285,7 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
}
emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
- program_id, compatibility_list, patch),
+ program_id, compatibility_list, play_time_manager, patch),
parent_dir);
}
}
@@ -288,7 +293,7 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
GameListDir* parent_dir) {
const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool {
- if (stop_processing) {
+ if (stop_requested) {
// Breaks the callback loop.
return false;
}
@@ -357,7 +362,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
emit EntryReady(MakeGameListEntry(physical_name, name,
Common::FS::GetSize(physical_name), icon,
- *loader, id, compatibility_list, patch),
+ *loader, id, compatibility_list,
+ play_time_manager, patch),
parent_dir);
}
} else {
@@ -370,10 +376,11 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
system.GetContentProvider()};
- emit EntryReady(
- MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name),
- icon, *loader, program_id, compatibility_list, patch),
- parent_dir);
+ emit EntryReady(MakeGameListEntry(physical_name, name,
+ Common::FS::GetSize(physical_name), icon,
+ *loader, program_id, compatibility_list,
+ play_time_manager, patch),
+ parent_dir);
}
}
} else if (is_dir) {
@@ -392,7 +399,6 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
}
void GameListWorker::run() {
- stop_processing = false;
provider->ClearAllEntries();
for (UISettings::GameDir& game_dir : game_dirs) {
@@ -420,9 +426,11 @@ void GameListWorker::run() {
}
emit Finished(watch_list);
+ processing_completed.Set();
}
void GameListWorker::Cancel() {
this->disconnect();
- stop_processing = true;
+ stop_requested.store(true);
+ processing_completed.Wait();
}
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h
index 24a4e92c3..54dc05e30 100644
--- a/src/yuzu/game_list_worker.h
+++ b/src/yuzu/game_list_worker.h
@@ -12,7 +12,9 @@
#include <QRunnable>
#include <QString>
+#include "common/thread.h"
#include "yuzu/compatibility_list.h"
+#include "yuzu/play_time_manager.h"
namespace Core {
class System;
@@ -36,7 +38,9 @@ public:
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
FileSys::ManualContentProvider* provider_,
QVector<UISettings::GameDir>& game_dirs_,
- const CompatibilityList& compatibility_list_, Core::System& system_);
+ const CompatibilityList& compatibility_list_,
+ const PlayTime::PlayTimeManager& play_time_manager_,
+ Core::System& system_);
~GameListWorker() override;
/// Starts the processing of directory tree information.
@@ -76,9 +80,12 @@ private:
FileSys::ManualContentProvider* provider;
QVector<UISettings::GameDir>& game_dirs;
const CompatibilityList& compatibility_list;
+ const PlayTime::PlayTimeManager& play_time_manager;
QStringList watch_list;
- std::atomic_bool stop_processing;
+
+ Common::Event processing_completed;
+ std::atomic_bool stop_requested = false;
Core::System& system;
};
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 16fa92e2c..1431cf2fe 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -67,6 +67,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#define QT_NO_OPENGL
#include <QClipboard>
#include <QDesktopServices>
+#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QInputDialog>
@@ -76,6 +77,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QPushButton>
#include <QScreen>
#include <QShortcut>
+#include <QStandardPaths>
#include <QStatusBar>
#include <QString>
#include <QSysInfo>
@@ -98,6 +100,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#ifdef _WIN32
+#include <shlobj.h>
#include "common/windows/timer_resolution.h"
#endif
#ifdef ARCHITECTURE_x86_64
@@ -150,6 +153,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/install_dialog.h"
#include "yuzu/loading_screen.h"
#include "yuzu/main.h"
+#include "yuzu/play_time_manager.h"
#include "yuzu/startup_checks.h"
#include "yuzu/uisettings.h"
#include "yuzu/util/clickable_label.h"
@@ -207,7 +211,7 @@ void GMainWindow::ShowTelemetryCallout() {
tr("<a href='https://yuzu-emu.org/help/feature/telemetry/'>Anonymous "
"data is collected</a> to help improve yuzu. "
"<br/><br/>Would you like to share your usage data with us?");
- if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) != QMessageBox::Yes) {
+ if (!question(this, tr("Telemetry"), telemetry_message)) {
Settings::values.enable_telemetry = false;
system->ApplySettings();
}
@@ -338,6 +342,8 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
discord_rpc->Update();
+ play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
+
system->GetRoomNetwork().Init();
RegisterMetaTypes();
@@ -986,7 +992,7 @@ void GMainWindow::InitializeWidgets() {
render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system);
render_window->hide();
- game_list = new GameList(vfs, provider.get(), *system, this);
+ game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this);
ui->horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this);
@@ -1447,6 +1453,7 @@ void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) {
Settings::values.audio_muted = false;
auto_muted = false;
}
+ UpdateVolumeUI();
}
}
@@ -1460,6 +1467,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::RemoveInstalledEntryRequested, this,
&GMainWindow::OnGameListRemoveInstalledEntry);
connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
+ connect(game_list, &GameList::RemovePlayTimeRequested, this,
+ &GMainWindow::OnGameListRemovePlayTimeData);
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
connect(game_list, &GameList::VerifyIntegrityRequested, this,
&GMainWindow::OnGameListVerifyIntegrity);
@@ -1553,6 +1562,7 @@ void GMainWindow::ConnectMenuEvents() {
// Tools
connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this,
ReinitializeKeyBehavior::Warning));
+ connect_menu(ui->action_Load_Album, &GMainWindow::OnAlbum);
connect_menu(ui->action_Load_Cabinet_Nickname_Owner,
[this]() { OnCabinet(Service::NFP::CabinetMode::StartNicknameAndOwnerSettings); });
connect_menu(ui->action_Load_Cabinet_Eraser,
@@ -1590,6 +1600,7 @@ void GMainWindow::UpdateMenuState() {
};
const std::array applet_actions{
+ ui->action_Load_Album,
ui->action_Load_Cabinet_Nickname_Owner,
ui->action_Load_Cabinet_Eraser,
ui->action_Load_Cabinet_Restorer,
@@ -2409,9 +2420,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
}
}();
- if (QMessageBox::question(this, tr("Remove Entry"), entry_question,
- QMessageBox::Yes | QMessageBox::No,
- QMessageBox::No) != QMessageBox::Yes) {
+ if (!question(this, tr("Remove Entry"), entry_question, QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No)) {
return;
}
@@ -2510,8 +2520,8 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ
}
}();
- if (QMessageBox::question(this, tr("Remove File"), question, QMessageBox::Yes | QMessageBox::No,
- QMessageBox::No) != QMessageBox::Yes) {
+ if (!GMainWindow::question(this, tr("Remove File"), question,
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No)) {
return;
}
@@ -2534,6 +2544,17 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ
}
}
+void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
+ if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+
+ play_time_manager->ResetProgramPlayTime(program_id);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+}
+
void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) {
const auto target_file_name = [target] {
switch (target) {
@@ -2825,7 +2846,6 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
const QStringList args = QApplication::arguments();
std::filesystem::path yuzu_command = args[0].toStdString();
-#if defined(__linux__) || defined(__FreeBSD__)
// If relative path, make it an absolute path
if (yuzu_command.c_str()[0] == '.') {
yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;
@@ -2848,44 +2868,52 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
UISettings::values.shortcut_already_warned = true;
}
#endif // __linux__
-#endif // __linux__ || __FreeBSD__
std::filesystem::path target_directory{};
- // Determine target directory for shortcut
-#if defined(__linux__) || defined(__FreeBSD__)
- const char* home = std::getenv("HOME");
- const std::filesystem::path home_path = (home == nullptr ? "~" : home);
- const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
- if (target == GameListShortcutTarget::Desktop) {
- target_directory = home_path / "Desktop";
- if (!Common::FS::IsDir(target_directory)) {
- QMessageBox::critical(
- this, tr("Create Shortcut"),
- tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.")
- .arg(QString::fromStdString(target_directory)),
- QMessageBox::StandardButton::Ok);
- return;
- }
- } else if (target == GameListShortcutTarget::Applications) {
- target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) /
- "applications";
- if (!Common::FS::CreateDirs(target_directory)) {
- QMessageBox::critical(this, tr("Create Shortcut"),
- tr("Cannot create shortcut in applications menu. Path \"%1\" "
- "does not exist and cannot be created.")
- .arg(QString::fromStdString(target_directory)),
- QMessageBox::StandardButton::Ok);
- return;
+ switch (target) {
+ case GameListShortcutTarget::Desktop: {
+ const QString desktop_path =
+ QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+ target_directory = desktop_path.toUtf8().toStdString();
+ break;
+ }
+ case GameListShortcutTarget::Applications: {
+ const QString applications_path =
+ QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
+ if (applications_path.isEmpty()) {
+ const char* home = std::getenv("HOME");
+ if (home != nullptr) {
+ target_directory = std::filesystem::path(home) / ".local/share/applications";
+ }
+ } else {
+ target_directory = applications_path.toUtf8().toStdString();
}
+ break;
+ }
+ default:
+ return;
+ }
+
+ const QDir dir(QString::fromStdString(target_directory.generic_string()));
+ if (!dir.exists()) {
+ QMessageBox::critical(this, tr("Create Shortcut"),
+ tr("Cannot create shortcut. Path \"%1\" does not exist.")
+ .arg(QString::fromStdString(target_directory.generic_string())),
+ QMessageBox::StandardButton::Ok);
+ return;
}
-#endif
const std::string game_file_name = std::filesystem::path(game_path).filename().string();
// Determine full paths for icon and shortcut
#if defined(__linux__) || defined(__FreeBSD__)
+ const char* home = std::getenv("HOME");
+ const std::filesystem::path home_path = (home == nullptr ? "~" : home);
+ const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
+
std::filesystem::path system_icons_path =
- (xdg_data_home == nullptr ? home_path / ".local/share/" : xdg_data_home) /
+ (xdg_data_home == nullptr ? home_path / ".local/share/"
+ : std::filesystem::path(xdg_data_home)) /
"icons/hicolor/256x256";
if (!Common::FS::CreateDirs(system_icons_path)) {
QMessageBox::critical(
@@ -2901,9 +2929,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
const std::filesystem::path shortcut_path =
target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name)
: fmt::format("yuzu-{:016X}.desktop", program_id));
+#elif defined(WIN32)
+ std::filesystem::path icons_path =
+ Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir);
+ std::filesystem::path icon_path =
+ icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name)
+ : fmt::format("yuzu-{:016X}.ico", program_id)));
#else
- const std::filesystem::path icon_path{};
- const std::filesystem::path shortcut_path{};
+ std::string icon_extension;
#endif
// Get title from game file
@@ -2928,29 +2961,37 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path);
}
- QImage icon_jpeg =
+ QImage icon_data =
QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size()));
#if defined(__linux__) || defined(__FreeBSD__)
// Convert and write the icon as a PNG
- if (!icon_jpeg.save(QString::fromStdString(icon_path.string()))) {
+ if (!icon_data.save(QString::fromStdString(icon_path.string()))) {
LOG_ERROR(Frontend, "Could not write icon as PNG to file");
} else {
LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string());
}
+#elif defined(WIN32)
+ if (!SaveIconToFile(icon_path.string(), icon_data)) {
+ LOG_ERROR(Frontend, "Could not write icon to file");
+ return;
+ }
#endif // __linux__
-#if defined(__linux__) || defined(__FreeBSD__)
+#ifdef _WIN32
+ // Replace characters that are illegal in Windows filenames by a dash
+ const std::string illegal_chars = "<>:\"/\\|?*";
+ for (char c : illegal_chars) {
+ std::replace(title.begin(), title.end(), c, '_');
+ }
+ const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str();
+#endif
+
const std::string comment =
tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString();
const std::string arguments = fmt::format("-g \"{:s}\"", game_path);
const std::string categories = "Game;Emulator;Qt;";
const std::string keywords = "Switch;Nintendo;";
-#else
- const std::string comment{};
- const std::string arguments{};
- const std::string categories{};
- const std::string keywords{};
-#endif
+
if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(),
yuzu_command.string(), arguments, categories, keywords)) {
QMessageBox::critical(this, tr("Create Shortcut"),
@@ -3357,6 +3398,9 @@ void GMainWindow::OnStartGame() {
UpdateMenuState();
OnTasStateChanged();
+ play_time_manager->SetProgramId(system->GetApplicationProcessProgramID());
+ play_time_manager->Start();
+
discord_rpc->Update();
}
@@ -3364,14 +3408,18 @@ void GMainWindow::OnRestartGame() {
if (!system->IsPoweredOn()) {
return;
}
- // Make a copy since ShutdownGame edits game_path
- const auto current_game = QString(current_game_path);
- ShutdownGame();
- BootGame(current_game);
+
+ if (ConfirmShutdownGame()) {
+ // Make a copy since ShutdownGame edits game_path
+ const auto current_game = QString(current_game_path);
+ ShutdownGame();
+ BootGame(current_game);
+ }
}
void GMainWindow::OnPauseGame() {
emu_thread->SetRunning(false);
+ play_time_manager->Stop();
UpdateMenuState();
AllowOSSleep();
}
@@ -3388,15 +3436,39 @@ void GMainWindow::OnPauseContinueGame() {
}
void GMainWindow::OnStopGame() {
- if (system->GetExitLocked() && !ConfirmForceLockedExit()) {
- return;
+ if (ConfirmShutdownGame()) {
+ play_time_manager->Stop();
+ // Update game list to show new play time
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+ if (OnShutdownBegin()) {
+ OnShutdownBeginDialog();
+ } else {
+ OnEmulationStopped();
+ }
}
+}
- if (OnShutdownBegin()) {
- OnShutdownBeginDialog();
+bool GMainWindow::ConfirmShutdownGame() {
+ if (UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Always) {
+ if (system->GetExitLocked()) {
+ if (!ConfirmForceLockedExit()) {
+ return false;
+ }
+ } else {
+ if (!ConfirmChangeGame()) {
+ return false;
+ }
+ }
} else {
- OnEmulationStopped();
+ if (UISettings::values.confirm_before_stopping.GetValue() ==
+ ConfirmStop::Ask_Based_On_Game &&
+ system->GetExitLocked()) {
+ if (!ConfirmForceLockedExit()) {
+ return false;
+ }
+ }
}
+ return true;
}
void GMainWindow::OnLoadComplete() {
@@ -3776,22 +3848,11 @@ void GMainWindow::OnTasRecord() {
const bool is_recording = input_subsystem->GetTas()->Record();
if (!is_recording) {
is_tas_recording_dialog_active = true;
- ControllerNavigation* controller_navigation =
- new ControllerNavigation(system->HIDCore(), this);
- // Use QMessageBox instead of question so we can link controller navigation
- QMessageBox* box_dialog = new QMessageBox();
- box_dialog->setWindowTitle(tr("TAS Recording"));
- box_dialog->setText(tr("Overwrite file of player 1?"));
- box_dialog->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
- box_dialog->setDefaultButton(QMessageBox::Yes);
- connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
- [box_dialog](Qt::Key key) {
- QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
- QCoreApplication::postEvent(box_dialog, event);
- });
- int res = box_dialog->exec();
- controller_navigation->UnloadController();
- input_subsystem->GetTas()->SaveRecording(res == QMessageBox::Yes);
+
+ bool answer = question(this, tr("TAS Recording"), tr("Overwrite file of player 1?"),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ input_subsystem->GetTas()->SaveRecording(answer);
is_tas_recording_dialog_active = false;
}
OnTasStateChanged();
@@ -3965,6 +4026,34 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st
shortcut_stream.close();
return true;
+#elif defined(WIN32)
+ IShellLinkW* shell_link;
+ auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
+ (void**)&shell_link);
+ if (FAILED(hres)) {
+ return false;
+ }
+ shell_link->SetPath(
+ Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to
+ shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data());
+ shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data());
+ shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0);
+
+ IPersistFile* persist_file;
+ hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file);
+ if (FAILED(hres)) {
+ return false;
+ }
+
+ hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE);
+ if (FAILED(hres)) {
+ return false;
+ }
+
+ persist_file->Release();
+ shell_link->Release();
+
+ return true;
#endif
return false;
}
@@ -4004,6 +4093,29 @@ void GMainWindow::OnLoadAmiibo() {
LoadAmiibo(filename);
}
+bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text,
+ QMessageBox::StandardButtons buttons,
+ QMessageBox::StandardButton defaultButton) {
+
+ QMessageBox* box_dialog = new QMessageBox(parent);
+ box_dialog->setWindowTitle(title);
+ box_dialog->setText(text);
+ box_dialog->setStandardButtons(buttons);
+ box_dialog->setDefaultButton(defaultButton);
+
+ ControllerNavigation* controller_navigation =
+ new ControllerNavigation(system->HIDCore(), box_dialog);
+ connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
+ [box_dialog](Qt::Key key) {
+ QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
+ QCoreApplication::postEvent(box_dialog, event);
+ });
+ int res = box_dialog->exec();
+
+ controller_navigation->UnloadController();
+ return res == QMessageBox::Yes;
+}
+
void GMainWindow::LoadAmiibo(const QString& filename) {
auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo();
const QString title = tr("Error loading Amiibo data");
@@ -4157,6 +4269,29 @@ void GMainWindow::OnToggleStatusBar() {
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
}
+void GMainWindow::OnAlbum() {
+ constexpr u64 AlbumId = 0x010000000000100Dull;
+ auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
+ if (!bis_system) {
+ QMessageBox::warning(this, tr("No firmware available"),
+ tr("Please install the firmware to use the Album applet."));
+ return;
+ }
+
+ auto album_nca = bis_system->GetEntry(AlbumId, FileSys::ContentRecordType::Program);
+ if (!album_nca) {
+ QMessageBox::warning(this, tr("Album Applet"),
+ tr("Album applet is not available. Please reinstall firmware."));
+ return;
+ }
+
+ system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::PhotoViewer);
+
+ const auto filename = QString::fromStdString(album_nca->GetFullPath());
+ UISettings::values.roms_path = QFileInfo(filename).path();
+ BootGame(filename);
+}
+
void GMainWindow::OnCabinet(Service::NFP::CabinetMode mode) {
constexpr u64 CabinetId = 0x0100000000001002ull;
auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
@@ -4714,8 +4849,7 @@ bool GMainWindow::ConfirmClose() {
return true;
}
const auto text = tr("Are you sure you want to close yuzu?");
- const auto answer = QMessageBox::question(this, tr("yuzu"), text);
- return answer != QMessageBox::No;
+ return question(this, tr("yuzu"), text);
}
void GMainWindow::closeEvent(QCloseEvent* event) {
@@ -4808,11 +4942,11 @@ bool GMainWindow::ConfirmChangeGame() {
if (emu_thread == nullptr)
return true;
- const auto answer = QMessageBox::question(
+ // Use custom question to link controller navigation
+ return question(
this, tr("yuzu"),
tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."),
- QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
- return answer != QMessageBox::No;
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
}
bool GMainWindow::ConfirmForceLockedExit() {
@@ -4822,8 +4956,7 @@ bool GMainWindow::ConfirmForceLockedExit() {
const auto text = tr("The currently running application has requested yuzu to not exit.\n\n"
"Would you like to bypass this and exit anyway?");
- const auto answer = QMessageBox::question(this, tr("yuzu"), text);
- return answer != QMessageBox::No;
+ return question(this, tr("yuzu"), text);
}
void GMainWindow::RequestGameExit() {
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 52028234c..270a40c5f 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -7,6 +7,7 @@
#include <optional>
#include <QMainWindow>
+#include <QMessageBox>
#include <QTimer>
#include <QTranslator>
@@ -15,6 +16,7 @@
#include "input_common/drivers/tas_input.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/hotkeys.h"
+#include "yuzu/util/controller_navigation.h"
#ifdef __unix__
#include <QVariant>
@@ -81,6 +83,10 @@ namespace DiscordRPC {
class DiscordInterface;
}
+namespace PlayTime {
+class PlayTimeManager;
+}
+
namespace FileSys {
class ContentProvider;
class ManualContentProvider;
@@ -323,6 +329,7 @@ private slots:
void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type);
void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target,
const std::string& game_path);
+ void OnGameListRemovePlayTimeData(u64 program_id);
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void OnGameListVerifyIntegrity(const std::string& game_path);
void OnGameListCopyTID(u64 program_id);
@@ -369,6 +376,7 @@ private slots:
void ResetWindowSize720();
void ResetWindowSize900();
void ResetWindowSize1080();
+ void OnAlbum();
void OnCabinet(Service::NFP::CabinetMode mode);
void OnMiiEdit();
void OnCaptureScreenshot();
@@ -389,6 +397,7 @@ private:
void RemoveVulkanDriverPipelineCache(u64 program_id);
void RemoveAllTransferableShaderCaches(u64 program_id);
void RemoveCustomConfiguration(u64 program_id, const std::string& game_path);
+ void RemovePlayTimeData(u64 program_id);
void RemoveCacheStorage(u64 program_id);
bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
u64* selected_title_id, u8* selected_content_record_type);
@@ -417,6 +426,11 @@ private:
bool CheckSystemArchiveDecryption();
bool CheckFirmwarePresence();
void ConfigureFilesystemProvider(const std::string& filepath);
+ /**
+ * Open (or not) the right confirm dialog based on current setting and game exit lock
+ * @returns true if the player confirmed or the settings do no require it
+ */
+ bool ConfirmShutdownGame();
QString GetTasStateDescription() const;
bool CreateShortcut(const std::string& shortcut_path, const std::string& title,
@@ -424,10 +438,22 @@ private:
const std::string& command, const std::string& arguments,
const std::string& categories, const std::string& keywords);
+ /**
+ * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog
+ * The only difference is that it returns a boolean.
+ *
+ * @returns true if buttons contains QMessageBox::Yes and the user clicks on the "Yes" button.
+ */
+ bool question(QWidget* parent, const QString& title, const QString& text,
+ QMessageBox::StandardButtons buttons =
+ QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No),
+ QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
+
std::unique_ptr<Ui::MainWindow> ui;
std::unique_ptr<Core::System> system;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
+ std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
MultiplayerState* multiplayer_state = nullptr;
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui
index 31c3de9ef..88684ffb5 100644
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -160,6 +160,7 @@
<addaction name="action_Verify_installed_contents"/>
<addaction name="separator"/>
<addaction name="menu_cabinet_applet"/>
+ <addaction name="action_Load_Album"/>
<addaction name="action_Load_Mii_Edit"/>
<addaction name="separator"/>
<addaction name="action_Capture_Screenshot"/>
@@ -380,6 +381,11 @@
<string>&amp;Capture Screenshot</string>
</property>
</action>
+ <action name="action_Load_Album">
+ <property name="text">
+ <string>Open &amp;Album</string>
+ </property>
+ </action>
<action name="action_Load_Cabinet_Nickname_Owner">
<property name="text">
<string>&amp;Set Nickname and Owner</string>
diff --git a/src/yuzu/play_time_manager.cpp b/src/yuzu/play_time_manager.cpp
new file mode 100644
index 000000000..155c36b7d
--- /dev/null
+++ b/src/yuzu/play_time_manager.cpp
@@ -0,0 +1,179 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/alignment.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+#include "common/thread.h"
+#include "core/hle/service/acc/profile_manager.h"
+#include "yuzu/play_time_manager.h"
+
+namespace PlayTime {
+
+namespace {
+
+struct PlayTimeElement {
+ ProgramId program_id;
+ PlayTime play_time;
+};
+
+std::optional<std::filesystem::path> GetCurrentUserPlayTimePath() {
+ const Service::Account::ProfileManager manager;
+ const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user));
+ if (!uuid.has_value()) {
+ return std::nullopt;
+ }
+ return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) /
+ uuid->RawString().append(".bin");
+}
+
+[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) {
+ const auto filename = GetCurrentUserPlayTimePath();
+
+ if (!filename.has_value()) {
+ LOG_ERROR(Frontend, "Failed to get current user path");
+ return false;
+ }
+
+ out_play_time_db.clear();
+
+ if (Common::FS::Exists(filename.value())) {
+ Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read,
+ Common::FS::FileType::BinaryFile};
+ if (!file.IsOpen()) {
+ LOG_ERROR(Frontend, "Failed to open play time file: {}",
+ Common::FS::PathToUTF8String(filename.value()));
+ return false;
+ }
+
+ const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement);
+ std::vector<PlayTimeElement> elements(num_elements);
+
+ if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) {
+ return false;
+ }
+
+ for (const auto& [program_id, play_time] : elements) {
+ if (program_id != 0) {
+ out_play_time_db[program_id] = play_time;
+ }
+ }
+ }
+
+ return true;
+}
+
+[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) {
+ const auto filename = GetCurrentUserPlayTimePath();
+
+ if (!filename.has_value()) {
+ LOG_ERROR(Frontend, "Failed to get current user path");
+ return false;
+ }
+
+ Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write,
+ Common::FS::FileType::BinaryFile};
+ if (!file.IsOpen()) {
+ LOG_ERROR(Frontend, "Failed to open play time file: {}",
+ Common::FS::PathToUTF8String(filename.value()));
+ return false;
+ }
+
+ std::vector<PlayTimeElement> elements;
+ elements.reserve(play_time_db.size());
+
+ for (auto& [program_id, play_time] : play_time_db) {
+ if (program_id != 0) {
+ elements.push_back(PlayTimeElement{program_id, play_time});
+ }
+ }
+
+ return file.WriteSpan<PlayTimeElement>(elements) == elements.size();
+}
+
+} // namespace
+
+PlayTimeManager::PlayTimeManager() {
+ if (!ReadPlayTimeFile(database)) {
+ LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default.");
+ }
+}
+
+PlayTimeManager::~PlayTimeManager() {
+ Save();
+}
+
+void PlayTimeManager::SetProgramId(u64 program_id) {
+ running_program_id = program_id;
+}
+
+void PlayTimeManager::Start() {
+ play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); });
+}
+
+void PlayTimeManager::Stop() {
+ play_time_thread = {};
+}
+
+void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
+ Common::SetCurrentThreadName("PlayTimeReport");
+
+ using namespace std::literals::chrono_literals;
+ using std::chrono::seconds;
+ using std::chrono::steady_clock;
+
+ auto timestamp = steady_clock::now();
+
+ const auto GetDuration = [&]() -> u64 {
+ const auto last_timestamp = std::exchange(timestamp, steady_clock::now());
+ const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp);
+ return static_cast<u64>(duration.count());
+ };
+
+ while (!stop_token.stop_requested()) {
+ Common::StoppableTimedWait(stop_token, 30s);
+
+ database[running_program_id] += GetDuration();
+ Save();
+ }
+}
+
+void PlayTimeManager::Save() {
+ if (!WritePlayTimeFile(database)) {
+ LOG_ERROR(Frontend, "Failed to update play time database!");
+ }
+}
+
+u64 PlayTimeManager::GetPlayTime(u64 program_id) const {
+ auto it = database.find(program_id);
+ if (it != database.end()) {
+ return it->second;
+ } else {
+ return 0;
+ }
+}
+
+void PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
+ database.erase(program_id);
+ Save();
+}
+
+QString ReadablePlayTime(qulonglong time_seconds) {
+ if (time_seconds == 0) {
+ return {};
+ }
+ const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0);
+ const auto time_hours = static_cast<double>(time_seconds) / 3600;
+ const bool is_minutes = time_minutes < 60;
+ const char* unit = is_minutes ? "m" : "h";
+ const auto value = is_minutes ? time_minutes : time_hours;
+
+ return QStringLiteral("%L1 %2")
+ .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0)
+ .arg(QString::fromUtf8(unit));
+}
+
+} // namespace PlayTime
diff --git a/src/yuzu/play_time_manager.h b/src/yuzu/play_time_manager.h
new file mode 100644
index 000000000..5f96f3447
--- /dev/null
+++ b/src/yuzu/play_time_manager.h
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QString>
+
+#include <map>
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+#include "common/polyfill_thread.h"
+
+namespace PlayTime {
+
+using ProgramId = u64;
+using PlayTime = u64;
+using PlayTimeDatabase = std::map<ProgramId, PlayTime>;
+
+class PlayTimeManager {
+public:
+ explicit PlayTimeManager();
+ ~PlayTimeManager();
+
+ YUZU_NON_COPYABLE(PlayTimeManager);
+ YUZU_NON_MOVEABLE(PlayTimeManager);
+
+ u64 GetPlayTime(u64 program_id) const;
+ void ResetProgramPlayTime(u64 program_id);
+ void SetProgramId(u64 program_id);
+ void Start();
+ void Stop();
+
+private:
+ PlayTimeDatabase database;
+ u64 running_program_id;
+ std::jthread play_time_thread;
+ void AutoTimestamp(std::stop_token stop_token);
+ void Save();
+};
+
+QString ReadablePlayTime(qulonglong time_seconds);
+
+} // namespace PlayTime
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index 8efd63f31..b62ff620c 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -16,7 +16,9 @@
#include "common/settings_enums.h"
using Settings::Category;
+using Settings::ConfirmStop;
using Settings::Setting;
+using Settings::SwitchableSetting;
#ifndef CANNOT_EXPLICITLY_INSTANTIATE
namespace Settings {
@@ -94,6 +96,15 @@ struct Values {
Setting<bool> confirm_before_closing{
linkage, true, "confirmClose", Category::UiGeneral, Settings::Specialization::Default,
true, true};
+
+ SwitchableSetting<ConfirmStop> confirm_before_stopping{linkage,
+ ConfirmStop::Ask_Always,
+ "confirmStop",
+ Category::UiGeneral,
+ Settings::Specialization::Default,
+ true,
+ true};
+
Setting<bool> first_start{linkage, true, "firstStart", Category::Ui};
Setting<bool> pause_when_in_background{linkage,
false,
@@ -103,7 +114,7 @@ struct Values {
true,
true};
Setting<bool> mute_when_in_background{
- linkage, false, "muteWhenInBackground", Category::Ui, Settings::Specialization::Default,
+ linkage, false, "muteWhenInBackground", Category::Audio, Settings::Specialization::Default,
true, true};
Setting<bool> hide_mouse{
linkage, true, "hideInactiveMouse", Category::UiGeneral, Settings::Specialization::Default,
@@ -183,6 +194,9 @@ struct Values {
Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList};
Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList};
+ // Play time
+ Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList};
+
bool configuration_applied;
bool reset_to_defaults;
bool shortcut_already_warned{false};
diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp
index 5c3e4589e..f2854c8ec 100644
--- a/src/yuzu/util/util.cpp
+++ b/src/yuzu/util/util.cpp
@@ -5,6 +5,10 @@
#include <cmath>
#include <QPainter>
#include "yuzu/util/util.h"
+#ifdef _WIN32
+#include <windows.h>
+#include "common/fs/file.h"
+#endif
QFont GetMonospaceFont() {
QFont font(QStringLiteral("monospace"));
@@ -37,3 +41,101 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) {
painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0);
return circle_pixmap;
}
+
+bool SaveIconToFile(const std::string_view path, const QImage& image) {
+#if defined(WIN32)
+#pragma pack(push, 2)
+ struct IconDir {
+ WORD id_reserved;
+ WORD id_type;
+ WORD id_count;
+ };
+
+ struct IconDirEntry {
+ BYTE width;
+ BYTE height;
+ BYTE color_count;
+ BYTE reserved;
+ WORD planes;
+ WORD bit_count;
+ DWORD bytes_in_res;
+ DWORD image_offset;
+ };
+#pragma pack(pop)
+
+ const QImage source_image = image.convertToFormat(QImage::Format_RGB32);
+ constexpr std::array<int, 7> scale_sizes{256, 128, 64, 48, 32, 24, 16};
+ constexpr int bytes_per_pixel = 4;
+
+ const IconDir icon_dir{
+ .id_reserved = 0,
+ .id_type = 1,
+ .id_count = static_cast<WORD>(scale_sizes.size()),
+ };
+
+ Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write,
+ Common::FS::FileType::BinaryFile);
+ if (!icon_file.IsOpen()) {
+ return false;
+ }
+
+ if (!icon_file.Write(icon_dir)) {
+ return false;
+ }
+
+ std::size_t image_offset = sizeof(IconDir) + (sizeof(IconDirEntry) * scale_sizes.size());
+ for (std::size_t i = 0; i < scale_sizes.size(); i++) {
+ const int image_size = scale_sizes[i] * scale_sizes[i] * bytes_per_pixel;
+ const IconDirEntry icon_entry{
+ .width = static_cast<BYTE>(scale_sizes[i]),
+ .height = static_cast<BYTE>(scale_sizes[i]),
+ .color_count = 0,
+ .reserved = 0,
+ .planes = 1,
+ .bit_count = bytes_per_pixel * 8,
+ .bytes_in_res = static_cast<DWORD>(sizeof(BITMAPINFOHEADER) + image_size),
+ .image_offset = static_cast<DWORD>(image_offset),
+ };
+ image_offset += icon_entry.bytes_in_res;
+ if (!icon_file.Write(icon_entry)) {
+ return false;
+ }
+ }
+
+ for (std::size_t i = 0; i < scale_sizes.size(); i++) {
+ const QImage scaled_image = source_image.scaled(
+ scale_sizes[i], scale_sizes[i], Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ const BITMAPINFOHEADER info_header{
+ .biSize = sizeof(BITMAPINFOHEADER),
+ .biWidth = scaled_image.width(),
+ .biHeight = scaled_image.height() * 2,
+ .biPlanes = 1,
+ .biBitCount = bytes_per_pixel * 8,
+ .biCompression = BI_RGB,
+ .biSizeImage{},
+ .biXPelsPerMeter{},
+ .biYPelsPerMeter{},
+ .biClrUsed{},
+ .biClrImportant{},
+ };
+
+ if (!icon_file.Write(info_header)) {
+ return false;
+ }
+
+ for (int y = 0; y < scaled_image.height(); y++) {
+ const auto* line = scaled_image.scanLine(scaled_image.height() - 1 - y);
+ std::vector<u8> line_data(scaled_image.width() * bytes_per_pixel);
+ std::memcpy(line_data.data(), line, line_data.size());
+ if (!icon_file.Write(line_data)) {
+ return false;
+ }
+ }
+ }
+ icon_file.Close();
+
+ return true;
+#else
+ return false;
+#endif
+}
diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h
index 39dd2d895..09c14ce3f 100644
--- a/src/yuzu/util/util.h
+++ b/src/yuzu/util/util.h
@@ -7,14 +7,22 @@
#include <QString>
/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc.
-QFont GetMonospaceFont();
+[[nodiscard]] QFont GetMonospaceFont();
/// Convert a size in bytes into a readable format (KiB, MiB, etc.)
-QString ReadableByteSize(qulonglong size);
+[[nodiscard]] QString ReadableByteSize(qulonglong size);
/**
* Creates a circle pixmap from a specified color
* @param color The color the pixmap shall have
* @return QPixmap circle pixmap
*/
-QPixmap CreateCirclePixmapFromColor(const QColor& color);
+[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);
+
+/**
+ * Saves a windows icon to a file
+ * @param path The icons path
+ * @param image The image to save
+ * @return bool If the operation succeeded
+ */
+[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image);