diff options
Diffstat (limited to 'src')
246 files changed, 10611 insertions, 1751 deletions
diff --git a/src/android/.gitignore b/src/android/.gitignore index 121cc8484..ff7121acd 100644 --- a/src/android/.gitignore +++ b/src/android/.gitignore @@ -63,3 +63,6 @@ fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md + +# Autogenerated library for vulkan validation layers +libVkLayer_khronos_validation.so diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 431f899b3..84a3308b7 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -214,7 +214,7 @@ dependencies { implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") implementation("io.coil-kt:coil:2.2.2") implementation("androidx.core:core-splashscreen:1.0.1") - implementation("androidx.window:window:1.1.0") + 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") 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 21f67f32a..6e39e542b 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 @@ -247,7 +247,12 @@ object NativeLibrary { external fun setAppDirectory(directory: String) - external fun installFileToNand(filename: String): Int + /** + * Installs a nsp or xci file to nand + * @param filename String representation of file uri + * @param extension Lowercase string representation of file extension without "." + */ + external fun installFileToNand(filename: String, extension: String): Int external fun initializeGpuDriver( hookLibDir: String?, @@ -512,6 +517,11 @@ object NativeLibrary { external fun submitInlineKeyboardInput(key_code: Int) /** + * Creates a generic user directory if it doesn't exist already + */ + external fun initializeEmptyUserDirectory() + + /** * Button type for use in onTouchEvent */ object ButtonType { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index d4ae39661..e96a2059b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.activities +import android.annotation.SuppressLint import android.app.Activity import android.app.PendingIntent import android.app.PictureInPictureParams @@ -397,6 +398,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } } + @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration @@ -409,7 +411,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { addAction(actionMute) addAction(actionUnmute) }.also { - registerReceiver(pictureInPictureReceiver, it) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(pictureInPictureReceiver, it, RECEIVER_EXPORTED) + } else { + registerReceiver(pictureInPictureReceiver, it) + } } } else { try { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index 1675627a1..58ce343f4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -49,6 +49,7 @@ class HomeSettingAdapter( holder.option.onClick.invoke() } else { MessageDialogFragment.newInstance( + activity, titleId = holder.option.disabledTitleId, descriptionId = holder.option.disabledMessageId ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt new file mode 100644 index 000000000..e960fbaab --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.CardInstallableBinding +import org.yuzu.yuzu_emu.model.Installable + +class InstallableAdapter(private val installables: List<Installable>) : + RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): InstallableAdapter.InstallableViewHolder { + val binding = + CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return InstallableViewHolder(binding) + } + + override fun getItemCount(): Int = installables.size + + override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = + holder.bind(installables[position]) + + inner class InstallableViewHolder(val binding: CardInstallableBinding) : + RecyclerView.ViewHolder(binding.root) { + lateinit var installable: Installable + + fun bind(installable: Installable) { + this.installable = installable + + binding.title.setText(installable.titleId) + binding.description.setText(installable.descriptionId) + + if (installable.install != null) { + binding.buttonInstall.visibility = View.VISIBLE + binding.buttonInstall.setOnClickListener { installable.install.invoke() } + } + if (installable.export != null) { + binding.buttonExport.visibility = View.VISIBLE + binding.buttonExport.setOnClickListener { installable.export.invoke() } + } + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 4d2f2f604..c73edd50e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -21,6 +21,7 @@ import androidx.navigation.navArgs import com.google.android.material.color.MaterialColors import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.NativeLibrary import java.io.IOException import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding @@ -168,7 +169,7 @@ class SettingsActivity : AppCompatActivity() { if (!settingsFile.delete()) { throw IOException("Failed to delete $settingsFile") } - Settings.settingsList.forEach { it.reset() } + NativeLibrary.reloadSettings() Toast.makeText( applicationContext, @@ -181,12 +182,14 @@ class SettingsActivity : AppCompatActivity() { private fun setInsets() { ViewCompat.setOnApplyWindowInsetsListener( binding.navigationBarShade - ) { view: View, windowInsets: WindowInsetsCompat -> + ) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - val mlpShade = view.layoutParams as MarginLayoutParams - mlpShade.height = barInsets.bottom - view.layoutParams = mlpShade + // The only situation where we care to have a nav bar shade is when it's at the bottom + // of the screen where scrolling list elements can go behind it. + val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams + mlpNavShade.height = barInsets.bottom + binding.navigationBarShade.layoutParams = mlpNavShade windowInsets } 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 3e6c157c7..e6ad2aa77 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 @@ -15,9 +15,9 @@ import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.Rational import android.view.* import android.widget.TextView +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.PopupMenu import androidx.core.content.res.ResourcesCompat @@ -54,6 +54,7 @@ import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.overlay.InputOverlay import org.yuzu.yuzu_emu.utils.* +import java.lang.NullPointerException class EmulationFragment : Fragment(), SurfaceHolder.Callback { private lateinit var preferences: SharedPreferences @@ -105,10 +106,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { null } } - game = if (args.game != null) { - args.game!! - } else { - intentGame ?: error("[EmulationFragment] No bootable game present!") + + try { + game = if (args.game != null) { + args.game!! + } else { + intentGame!! + } + } catch (e: NullPointerException) { + Toast.makeText( + requireContext(), + R.string.no_game_present, + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + return } // So this fragment doesn't restart on configuration changes; i.e. rotation. @@ -132,6 +144,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // This is using the correct scope, lint is just acting up @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (requireActivity().isFinishing) { + return + } + binding.surfaceEmulation.holder.addCallback(this) binding.showFpsText.setTextColor(Color.YELLOW) binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } @@ -287,24 +304,23 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) + if (_binding == null) { + return + } + + updateScreenLayout() if (emulationActivity?.isInPictureInPictureMode == true) { if (binding.drawerLayout.isOpen) { binding.drawerLayout.close() } if (EmulationMenuSettings.showOverlay) { - binding.surfaceInputOverlay.post { - binding.surfaceInputOverlay.visibility = View.VISIBLE - } + binding.surfaceInputOverlay.visibility = View.INVISIBLE } } else { if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) { - binding.surfaceInputOverlay.post { - binding.surfaceInputOverlay.visibility = View.VISIBLE - } + binding.surfaceInputOverlay.visibility = View.VISIBLE } else { - binding.surfaceInputOverlay.post { - binding.surfaceInputOverlay.visibility = View.INVISIBLE - } + binding.surfaceInputOverlay.visibility = View.INVISIBLE } if (!isInFoldableLayout) { if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { @@ -328,7 +344,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } override fun onPause() { - if (emulationState.isRunning) { + if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { emulationState.pause() } super.onPause() @@ -394,16 +410,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun updateScreenLayout() { - binding.surfaceEmulation.setAspectRatio( - when (IntSetting.RENDERER_ASPECT_RATIO.int) { - 0 -> Rational(16, 9) - 1 -> Rational(4, 3) - 2 -> Rational(21, 9) - 3 -> Rational(16, 10) - 4 -> null // Stretch - else -> Rational(16, 9) - } - ) + binding.surfaceEmulation.setAspectRatio(null) emulationActivity?.buildPictureInPictureParams() updateOrientation() } @@ -693,7 +700,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private class EmulationState(private val gamePath: String) { private var state: State private var surface: Surface? = null - private var runWhenSurfaceIsValid = false init { // Starting state is stopped. @@ -751,8 +757,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // If the surface is set, run now. Otherwise, wait for it to get set. if (surface != null) { runWithValidSurface() - } else { - runWhenSurfaceIsValid = true } } @@ -760,7 +764,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { @Synchronized fun newSurface(surface: Surface?) { this.surface = surface - if (runWhenSurfaceIsValid) { + if (this.surface != null) { runWithValidSurface() } } @@ -788,10 +792,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun runWithValidSurface() { - runWhenSurfaceIsValid = false + NativeLibrary.surfaceChanged(surface) when (state) { State.STOPPED -> { - NativeLibrary.surfaceChanged(surface) val emulationThread = Thread({ Log.debug("[EmulationFragment] Starting emulation thread.") NativeLibrary.run(gamePath) @@ -801,7 +804,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { State.PAUSED -> { Log.debug("[EmulationFragment] Resuming emulation.") - NativeLibrary.surfaceChanged(surface) NativeLibrary.unpauseEmulation() } 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 c119e69c9..8923c0ea2 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 @@ -118,18 +118,13 @@ class HomeSettingsFragment : Fragment() { ) add( HomeSetting( - R.string.install_amiibo_keys, - R.string.install_amiibo_keys_description, - R.drawable.ic_nfc, - { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } - ) - ) - add( - HomeSetting( - R.string.install_game_content, - R.string.install_game_content_description, - R.drawable.ic_system_update_alt, - { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } + R.string.manage_yuzu_data, + R.string.manage_yuzu_data_description, + R.drawable.ic_install, + { + binding.root.findNavController() + .navigate(R.id.action_homeSettingsFragment_to_installableFragment) + } ) ) add( @@ -150,35 +145,6 @@ class HomeSettingsFragment : Fragment() { ) add( HomeSetting( - R.string.manage_save_data, - R.string.import_export_saves_description, - R.drawable.ic_save, - { - ImportExportSavesFragment().show( - parentFragmentManager, - ImportExportSavesFragment.TAG - ) - } - ) - ) - add( - HomeSetting( - R.string.install_prod_keys, - R.string.install_prod_keys_description, - R.drawable.ic_unlock, - { mainActivity.getProdKey.launch(arrayOf("*/*")) } - ) - ) - add( - HomeSetting( - R.string.install_firmware, - R.string.install_firmware_description, - R.drawable.ic_firmware, - { mainActivity.getFirmware.launch(arrayOf("application/zip")) } - ) - ) - add( - HomeSetting( R.string.share_log, R.string.share_log_description, R.drawable.ic_log, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt deleted file mode 100644 index f38aeea53..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt +++ /dev/null @@ -1,213 +0,0 @@ -// 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.content.Intent -import android.net.Uri -import android.os.Bundle -import android.provider.DocumentsContract -import android.widget.Toast -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.DialogFragment -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileOutputStream -import java.io.FilenameFilter -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -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.features.DocumentProvider -import org.yuzu.yuzu_emu.getPublicFilesDir -import org.yuzu.yuzu_emu.utils.FileUtil - -class ImportExportSavesFragment : DialogFragment() { - private val context = YuzuApplication.appContext - private val savesFolder = - "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" - - // Get first subfolder in saves folder (should be the user folder) - private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" - private var lastZipCreated: File? = null - - private lateinit var startForResultExportSave: ActivityResultLauncher<Intent> - private lateinit var documentPicker: ActivityResultLauncher<Array<String>> - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val activity = requireActivity() as AppCompatActivity - - val activityResultRegistry = requireActivity().activityResultRegistry - startForResultExportSave = activityResultRegistry.register( - "startForResultExportSaveKey", - ActivityResultContracts.StartActivityForResult() - ) { - File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively() - } - documentPicker = activityResultRegistry.register( - "documentPickerKey", - ActivityResultContracts.OpenDocument() - ) { - it?.let { uri -> importSave(uri, activity) } - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return if (savesFolderRoot == "") { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.manage_save_data) - .setMessage(R.string.import_export_saves_no_profile) - .setPositiveButton(android.R.string.ok, null) - .show() - } else { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.manage_save_data) - .setMessage(R.string.manage_save_data_description) - .setNegativeButton(R.string.export_saves) { _, _ -> - exportSave() - } - .setPositiveButton(R.string.import_saves) { _, _ -> - documentPicker.launch(arrayOf("application/zip")) - } - .setNeutralButton(android.R.string.cancel, null) - .show() - } - } - - /** - * Zips the save files located in the given folder path and creates a new zip file with the current date and time. - * @return true if the zip file is successfully created, false otherwise. - */ - private fun zipSave(): Boolean { - try { - val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp") - tempFolder.mkdirs() - val saveFolder = File(savesFolderRoot) - val outputZipFile = File( - tempFolder, - "yuzu saves - ${ - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) - }.zip" - ) - outputZipFile.createNewFile() - ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> - saveFolder.walkTopDown().forEach { file -> - val zipFileName = - file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") - if (zipFileName == "") { - return@forEach - } - val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") - zos.putNextEntry(entry) - if (file.isFile) { - file.inputStream().use { fis -> fis.copyTo(zos) } - } - } - } - lastZipCreated = outputZipFile - } catch (e: Exception) { - return false - } - return true - } - - /** - * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. - */ - private fun exportSave() { - CoroutineScope(Dispatchers.IO).launch { - val wasZipCreated = zipSave() - val lastZipFile = lastZipCreated - if (!wasZipCreated || lastZipFile == null) { - withContext(Dispatchers.Main) { - Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show() - } - return@launch - } - - withContext(Dispatchers.Main) { - val file = DocumentFile.fromSingleUri( - context, - DocumentsContract.buildDocumentUri( - DocumentProvider.AUTHORITY, - "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" - ) - )!! - val intent = Intent(Intent.ACTION_SEND) - .setDataAndType(file.uri, "application/zip") - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, file.uri) - startForResultExportSave.launch(Intent.createChooser(intent, "Share save file")) - } - } - } - - /** - * Imports the save files contained in the zip file, and replaces any existing ones with the new save file. - * @param zipUri The Uri of the zip file containing the save file(s) to import. - */ - private fun importSave(zipUri: Uri, activity: AppCompatActivity) { - val inputZip = context.contentResolver.openInputStream(zipUri) - // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. - var validZip = false - val savesFolder = File(savesFolderRoot) - val cacheSaveDir = File("${context.cacheDir.path}/saves/") - cacheSaveDir.mkdir() - - if (inputZip == null) { - Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) - .show() - return - } - - val filterTitleId = - FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } - - try { - CoroutineScope(Dispatchers.IO).launch { - FileUtil.unzip(inputZip, cacheSaveDir) - cacheSaveDir.list(filterTitleId)?.forEach { savePath -> - File(savesFolder, savePath).deleteRecursively() - File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) - validZip = true - } - - withContext(Dispatchers.Main) { - if (!validZip) { - MessageDialogFragment.newInstance( - titleId = R.string.save_file_invalid_zip_structure, - descriptionId = R.string.save_file_invalid_zip_structure_description - ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) - return@withContext - } - Toast.makeText( - context, - context.getString(R.string.save_file_imported_success), - Toast.LENGTH_LONG - ).show() - } - - cacheSaveDir.deleteRecursively() - } - } catch (e: Exception) { - Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) - .show() - } - } - - companion object { - const val TAG = "ImportExportSavesFragment" - } -} 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 18bc34b9f..f128deda8 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 @@ -9,6 +9,7 @@ import android.view.LayoutInflater 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.activityViewModels @@ -18,6 +19,7 @@ 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.TaskViewModel @@ -28,19 +30,25 @@ class IndeterminateProgressDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val titleId = requireArguments().getInt(TITLE) + val cancellable = requireArguments().getBoolean(CANCELLABLE) binding = DialogProgressBarBinding.inflate(layoutInflater) binding.progressBar.isIndeterminate = true val dialog = MaterialAlertDialogBuilder(requireContext()) .setTitle(titleId) .setView(binding.root) - .create() - dialog.setCanceledOnTouchOutside(false) + + if (cancellable) { + dialog.setNegativeButton(android.R.string.cancel, null) + } + + val alertDialog = dialog.create() + alertDialog.setCanceledOnTouchOutside(false) if (!taskViewModel.isRunning.value) { taskViewModel.runTask() } - return dialog + return alertDialog } override fun onCreateView( @@ -53,24 +61,50 @@ class IndeterminateProgressDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - taskViewModel.isComplete.collect { - if (it) { - dismiss() - when (val result = taskViewModel.result.value) { - is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG) - .show() - - is MessageDialogFragment -> result.show( - requireActivity().supportFragmentManager, - MessageDialogFragment.TAG - ) + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.isComplete.collect { + if (it) { + dismiss() + when (val result = taskViewModel.result.value) { + is String -> Toast.makeText( + requireContext(), + result, + Toast.LENGTH_LONG + ).show() + + is MessageDialogFragment -> result.show( + requireActivity().supportFragmentManager, + MessageDialogFragment.TAG + ) + } + taskViewModel.clear() } - taskViewModel.clear() } } } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.cancelled.collect { + if (it) { + dialog?.setTitle(R.string.cancelling) + } + } + } + } + } + } + + // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed. + // Setting the OnClickListener again after the dialog is shown overrides this behavior. + override fun onResume() { + super.onResume() + val alertDialog = dialog as AlertDialog + val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) + negativeButton.setOnClickListener { + alertDialog.setTitle(getString(R.string.cancelling)) + taskViewModel.setCancelled(true) } } @@ -78,16 +112,19 @@ class IndeterminateProgressDialogFragment : DialogFragment() { const val TAG = "IndeterminateProgressDialogFragment" private const val TITLE = "Title" + private const val CANCELLABLE = "Cancellable" fun newInstance( activity: AppCompatActivity, titleId: Int, + cancellable: Boolean = false, task: () -> Any ): IndeterminateProgressDialogFragment { val dialog = IndeterminateProgressDialogFragment() val args = Bundle() ViewModelProvider(activity)[TaskViewModel::class.java].task = task args.putInt(TITLE, titleId) + args.putBoolean(CANCELLABLE, cancellable) dialog.arguments = args return dialog } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt new file mode 100644 index 000000000..ec116ab62 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -0,0 +1,138 @@ +// 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.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.navigation.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.InstallableAdapter +import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.ui.main.MainActivity + +class InstallableFragment : Fragment() { + private var _binding: FragmentInstallablesBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel 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 = FragmentInstallablesBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val mainActivity = requireActivity() as MainActivity + + homeViewModel.setNavigationVisibility(visible = false, animated = true) + homeViewModel.setStatusBarShadeVisibility(visible = false) + + binding.toolbarInstallables.setNavigationOnClickListener { + binding.root.findNavController().popBackStack() + } + + val installables = listOf( + Installable( + R.string.user_data, + R.string.user_data_description, + install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, + export = { mainActivity.exportUserData.launch("export.zip") } + ), + Installable( + R.string.install_game_content, + R.string.install_game_content_description, + install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } + ), + Installable( + R.string.install_firmware, + R.string.install_firmware_description, + install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } + ), + if (mainActivity.savesFolderRoot != "") { + Installable( + R.string.manage_save_data, + R.string.import_export_saves_description, + install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, + export = { mainActivity.exportSave() } + ) + } else { + Installable( + R.string.manage_save_data, + R.string.import_export_saves_description, + install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } + ) + }, + Installable( + R.string.install_prod_keys, + R.string.install_prod_keys_description, + install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } + ), + Installable( + R.string.install_amiibo_keys, + R.string.install_amiibo_keys_description, + install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } + ) + ) + + binding.listInstallables.apply { + layoutManager = GridLayoutManager( + requireContext(), + resources.getInteger(R.integer.grid_columns) + ) + adapter = InstallableAdapter(installables) + } + + setInsets() + } + + 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.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams + mlpAppBar.leftMargin = leftInsets + mlpAppBar.rightMargin = rightInsets + binding.toolbarInstallables.layoutParams = mlpAppBar + + val mlpScrollAbout = + binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams + mlpScrollAbout.leftMargin = leftInsets + mlpScrollAbout.rightMargin = rightInsets + binding.listInstallables.layoutParams = mlpScrollAbout + + binding.listInstallables.updatePadding(bottom = barInsets.bottom) + + windowInsets + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt index 7d1c2c8dd..541b22f47 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt @@ -4,14 +4,21 @@ package org.yuzu.yuzu_emu.fragments import android.app.Dialog +import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.model.MessageDialogViewModel class MessageDialogFragment : DialogFragment() { + private val messageDialogViewModel: MessageDialogViewModel by activityViewModels() + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val titleId = requireArguments().getInt(TITLE_ID) val titleString = requireArguments().getString(TITLE_STRING)!! @@ -37,6 +44,12 @@ class MessageDialogFragment : DialogFragment() { return dialog.show() } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + messageDialogViewModel.dismissAction.invoke() + messageDialogViewModel.clear() + } + private fun openLink(link: String) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) startActivity(intent) @@ -52,11 +65,13 @@ class MessageDialogFragment : DialogFragment() { private const val HELP_LINK = "Link" fun newInstance( + activity: FragmentActivity, titleId: Int = 0, titleString: String = "", descriptionId: Int = 0, descriptionString: String = "", - helpLinkId: Int = 0 + helpLinkId: Int = 0, + dismissAction: () -> Unit = {} ): MessageDialogFragment { val dialog = MessageDialogFragment() val bundle = Bundle() @@ -67,6 +82,8 @@ class MessageDialogFragment : DialogFragment() { putString(DESCRIPTION_STRING, descriptionString) putInt(HELP_LINK, helpLinkId) } + ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction = + dismissAction dialog.arguments = bundle return dialog } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index fbb2f6e18..c66bb635a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -295,8 +295,10 @@ class SetupFragment : Fragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible) - outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible) + if (_binding != null) { + outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible) + outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible) + } outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned) } @@ -353,11 +355,15 @@ class SetupFragment : Fragment() { } fun pageForward() { - binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 + if (_binding != null) { + binding.viewPager2.currentItem += 1 + } } fun pageBackward() { - binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1 + if (_binding != null) { + binding.viewPager2.currentItem -= 1 + } } fun setPageWarned(page: Int) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt new file mode 100644 index 000000000..36a7c97b8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.annotation.StringRes + +data class Installable( + @StringRes val titleId: Int, + @StringRes val descriptionId: Int, + val install: (() -> Unit)? = null, + val export: (() -> Unit)? = null +) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt new file mode 100644 index 000000000..36ffd08d2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.ViewModel + +class MessageDialogViewModel : ViewModel() { + var dismissAction: () -> Unit = {} + + fun clear() { + dismissAction = {} + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt index 531c2aaf0..16a794dee 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt @@ -20,12 +20,20 @@ class TaskViewModel : ViewModel() { val isRunning: StateFlow<Boolean> get() = _isRunning private val _isRunning = MutableStateFlow(false) + val cancelled: StateFlow<Boolean> get() = _cancelled + private val _cancelled = MutableStateFlow(false) + lateinit var task: () -> Any fun clear() { _result.value = Any() _isComplete.value = false _isRunning.value = false + _cancelled.value = false + } + + fun setCancelled(value: Boolean) { + _cancelled.value = value } fun runTask() { @@ -42,3 +50,9 @@ class TaskViewModel : ViewModel() { } } } + +enum class TaskState { + Completed, + Failed, + Cancelled +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt index c055c2e35..a13faf3c7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -352,7 +352,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : } private fun addOverlayControls(layout: String) { - val windowSize = getSafeScreenSize(context) + val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight)) if (preferences.getBoolean(Settings.PREF_BUTTON_A, true)) { overlayButtons.add( initializeOverlayButton( @@ -593,7 +593,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : } private fun saveControlPosition(prefId: String, x: Int, y: Int, layout: String) { - val windowSize = getSafeScreenSize(context) + val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight)) val min = windowSize.first val max = windowSize.second PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() @@ -968,14 +968,17 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : * @return A pair of points, the first being the top left corner of the safe area, * the second being the bottom right corner of the safe area */ - private fun getSafeScreenSize(context: Context): Pair<Point, Point> { + private fun getSafeScreenSize( + context: Context, + screenSize: Pair<Int, Int> + ): Pair<Point, Point> { // Get screen size val windowMetrics = WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(context as Activity) - var maxY = windowMetrics.bounds.height().toFloat() - var maxX = windowMetrics.bounds.width().toFloat() - var minY = 0 + var maxX = screenSize.first.toFloat() + var maxY = screenSize.second.toFloat() var minX = 0 + var minY = 0 // If we have API access, calculate the safe area to draw the overlay var cutoutLeft = 0 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 b6b6c6c17..0fa5df5e5 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 @@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.ui.main import android.content.Intent import android.net.Uri import android.os.Bundle +import android.provider.DocumentsContract import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager @@ -19,6 +20,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -29,6 +31,7 @@ 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 @@ -41,21 +44,40 @@ 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 import org.yuzu.yuzu_emu.fragments.MessageDialogFragment +import org.yuzu.yuzu_emu.getPublicFilesDir import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.TaskState +import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.FileOutputStream +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding private val homeViewModel: HomeViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels() + private val taskViewModel: TaskViewModel by viewModels() override var themeId: Int = 0 + private val savesFolder + get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" + + // Get first subfolder in saves folder (should be the user folder) + val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" + private var lastZipCreated: File? = null + override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } @@ -307,6 +329,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { fun processKey(result: Uri): Boolean { if (FileUtil.getExtension(result) != "keys") { MessageDialogFragment.newInstance( + this, titleId = R.string.reading_keys_failure, descriptionId = R.string.install_prod_keys_failure_extension_description ).show(supportFragmentManager, MessageDialogFragment.TAG) @@ -336,6 +359,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return true } else { MessageDialogFragment.newInstance( + this, titleId = R.string.invalid_keys_error, descriptionId = R.string.install_keys_failure_description, helpLinkId = R.string.dumping_keys_quickstart_link @@ -371,11 +395,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val task: () -> Any = { var messageToShow: Any try { - FileUtil.unzip(inputZip, cacheFirmwareDir) + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { MessageDialogFragment.newInstance( + this, titleId = R.string.firmware_installed_failure, descriptionId = R.string.firmware_installed_failure_description ) @@ -395,7 +420,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { IndeterminateProgressDialogFragment.newInstance( this, R.string.firmware_installing, - task + task = task ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } @@ -407,6 +432,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (FileUtil.getExtension(result) != "bin") { MessageDialogFragment.newInstance( + this, titleId = R.string.reading_keys_failure, descriptionId = R.string.install_amiibo_keys_failure_extension_description ).show(supportFragmentManager, MessageDialogFragment.TAG) @@ -434,6 +460,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ).show() } else { MessageDialogFragment.newInstance( + this, titleId = R.string.invalid_keys_error, descriptionId = R.string.install_keys_failure_description, helpLinkId = R.string.dumping_keys_quickstart_link @@ -501,7 +528,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (documents.isNotEmpty()) { IndeterminateProgressDialogFragment.newInstance( this@MainActivity, - R.string.install_game_content + R.string.installing_game_content ) { var installSuccess = 0 var installOverwrite = 0 @@ -509,7 +536,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider { var errorExtension = 0 var errorOther = 0 documents.forEach { - when (NativeLibrary.installFileToNand(it.toString())) { + when ( + NativeLibrary.installFileToNand( + it.toString(), + FileUtil.getExtension(it) + ) + ) { NativeLibrary.InstallFileToNandResult.Success -> { installSuccess += 1 } @@ -583,12 +615,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider { installResult.append(separator) } return@newInstance MessageDialogFragment.newInstance( + this, titleId = R.string.install_game_content_failure, descriptionString = installResult.toString().trim(), helpLinkId = R.string.install_game_content_help_link ) } else { return@newInstance MessageDialogFragment.newInstance( + this, titleId = R.string.install_game_content_success, descriptionString = installResult.toString().trim() ) @@ -596,4 +630,228 @@ class MainActivity : AppCompatActivity(), ThemeProvider { }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } } + + val exportUserData = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/zip") + ) { result -> + if (result == null) { + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + this, + R.string.exporting_user_data, + true + ) { + val zipResult = FileUtil.zipFromInternalStorage( + File(DirectoryInitialization.userDirectory!!), + DirectoryInitialization.userDirectory!!, + BufferedOutputStream(contentResolver.openOutputStream(result)), + taskViewModel.cancelled + ) + return@newInstance when (zipResult) { + TaskState.Completed -> getString(R.string.user_data_export_success) + TaskState.Failed -> R.string.export_failed + TaskState.Cancelled -> R.string.user_data_export_cancelled + } + }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + + val importUserData = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + this, + R.string.importing_user_data + ) { + val checkStream = + ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) + var isYuzuBackup = false + checkStream.use { stream -> + var ze: ZipEntry? = null + while (stream.nextEntry?.also { ze = it } != null) { + val itemName = ze!!.name.trim() + if (itemName == "/config/config.ini" || itemName == "config/config.ini") { + isYuzuBackup = true + return@use + } + } + } + if (!isYuzuBackup) { + return@newInstance MessageDialogFragment.newInstance( + this, + titleId = R.string.invalid_yuzu_backup, + descriptionId = R.string.user_data_import_failed_description + ) + } + + // Clear existing user data + File(DirectoryInitialization.userDirectory!!).deleteRecursively() + + // Copy archive to internal storage + try { + FileUtil.unzipToInternalStorage( + BufferedInputStream(contentResolver.openInputStream(result)), + File(DirectoryInitialization.userDirectory!!) + ) + } catch (e: Exception) { + return@newInstance MessageDialogFragment.newInstance( + this, + titleId = R.string.import_failed, + descriptionId = R.string.user_data_import_failed_description + ) + } + + // Reinitialize relevant data + NativeLibrary.initializeEmulation() + gamesViewModel.reloadGames(false) + + return@newInstance getString(R.string.user_data_import_success) + }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + + /** + * Zips the save files located in the given folder path and creates a new zip file with the current date and time. + * @return true if the zip file is successfully created, false otherwise. + */ + private fun zipSave(): Boolean { + try { + val tempFolder = File(getPublicFilesDir().canonicalPath, "temp") + tempFolder.mkdirs() + val saveFolder = File(savesFolderRoot) + val outputZipFile = File( + tempFolder, + "yuzu saves - ${ + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + }.zip" + ) + outputZipFile.createNewFile() + val result = FileUtil.zipFromInternalStorage( + saveFolder, + savesFolderRoot, + BufferedOutputStream(FileOutputStream(outputZipFile)) + ) + if (result == TaskState.Failed) { + return false + } + lastZipCreated = outputZipFile + } catch (e: Exception) { + return false + } + return true + } + + /** + * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. + */ + fun exportSave() { + CoroutineScope(Dispatchers.IO).launch { + val wasZipCreated = zipSave() + val lastZipFile = lastZipCreated + if (!wasZipCreated || lastZipFile == null) { + withContext(Dispatchers.Main) { + Toast.makeText( + this@MainActivity, + getString(R.string.export_save_failed), + Toast.LENGTH_LONG + ).show() + } + return@launch + } + + withContext(Dispatchers.Main) { + val file = DocumentFile.fromSingleUri( + this@MainActivity, + DocumentsContract.buildDocumentUri( + DocumentProvider.AUTHORITY, + "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" + ) + )!! + val intent = Intent(Intent.ACTION_SEND) + .setDataAndType(file.uri, "application/zip") + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, file.uri) + startForResultExportSave.launch( + Intent.createChooser( + intent, + getString(R.string.share_save_file) + ) + ) + } + } + } + + private val startForResultExportSave = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> + File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively() + } + + val importSaves = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + NativeLibrary.initializeEmptyUserDirectory() + + val inputZip = contentResolver.openInputStream(result) + // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. + var validZip = false + val savesFolder = File(savesFolderRoot) + val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/") + cacheSaveDir.mkdir() + + if (inputZip == null) { + Toast.makeText( + applicationContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + return@registerForActivityResult + } + + val filterTitleId = + FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } + + try { + CoroutineScope(Dispatchers.IO).launch { + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) + cacheSaveDir.list(filterTitleId)?.forEach { savePath -> + File(savesFolder, savePath).deleteRecursively() + File(cacheSaveDir, savePath).copyRecursively( + File(savesFolder, savePath), + true + ) + validZip = true + } + + withContext(Dispatchers.Main) { + if (!validZip) { + MessageDialogFragment.newInstance( + this@MainActivity, + titleId = R.string.save_file_invalid_zip_structure, + descriptionId = R.string.save_file_invalid_zip_structure_description + ).show(supportFragmentManager, MessageDialogFragment.TAG) + return@withContext + } + Toast.makeText( + applicationContext, + getString(R.string.save_file_imported_success), + Toast.LENGTH_LONG + ).show() + } + + cacheSaveDir.deleteRecursively() + } + } catch (e: Exception) { + Toast.makeText( + applicationContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + } + } } 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 142af5f26..c3f53f1c5 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 @@ -8,6 +8,7 @@ import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile +import kotlinx.coroutines.flow.StateFlow import java.io.BufferedInputStream import java.io.File import java.io.FileOutputStream @@ -18,6 +19,9 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream 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.util.zip.ZipOutputStream object FileUtil { const val PATH_TREE = "tree" @@ -282,30 +286,65 @@ object FileUtil { /** * Extracts the given zip file into the given directory. - * @exception IOException if the file was being created outside of the target directory */ @Throws(SecurityException::class) - fun unzip(zipStream: InputStream, destDir: File): Boolean { - ZipInputStream(BufferedInputStream(zipStream)).use { zis -> + fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { + ZipInputStream(zipStream).use { zis -> var entry: ZipEntry? = zis.nextEntry while (entry != null) { - val entryName = entry.name - val entryFile = File(destDir, entryName) - if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { - throw SecurityException("Entry is outside of the target dir: " + entryFile.name) + val newFile = File(destDir, entry.name) + val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile + + if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { + throw SecurityException("Zip file attempted path traversal! ${entry.name}") + } + + if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) { + throw IOException("Failed to create directory $destinationDirectory") } - if (entry.isDirectory) { - entryFile.mkdirs() - } else { - entryFile.parentFile?.mkdirs() - entryFile.createNewFile() - entryFile.outputStream().use { fos -> zis.copyTo(fos) } + + if (!entry.isDirectory) { + newFile.outputStream().use { fos -> zis.copyTo(fos) } } entry = zis.nextEntry } } + } - return true + /** + * Creates a zip file from a directory within internal storage + * @param inputFile File representation of the item that will be zipped + * @param rootDir Directory containing the inputFile + * @param outputStream Stream where the zip file will be output + */ + fun zipFromInternalStorage( + inputFile: File, + rootDir: String, + outputStream: BufferedOutputStream, + cancelled: StateFlow<Boolean>? = null + ): TaskState { + try { + ZipOutputStream(outputStream).use { zos -> + inputFile.walkTopDown().forEach { file -> + if (cancelled?.value == true) { + return TaskState.Cancelled + } + + if (!file.isDirectory) { + val entryName = + file.absolutePath.removePrefix(rootDir).removePrefix("/") + val entry = ZipEntry(entryName) + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { fis -> fis.copyTo(zos) } + } + } + } + } + } catch (e: Exception) { + return TaskState.Failed + } + return TaskState.Completed } fun isRootTreeUri(uri: Uri): Boolean { diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index a890c6604..a7e414b81 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -11,6 +11,12 @@ #include "jni/emu_window/emu_window.h" void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { + m_window_width = ANativeWindow_getWidth(surface); + m_window_height = ANativeWindow_getHeight(surface); + + // Ensures that we emulate with the correct aspect ratio. + UpdateCurrentFramebufferLayout(m_window_width, m_window_height); + window_info.render_surface = reinterpret_cast<void*>(surface); } @@ -62,14 +68,8 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste return; } - m_window_width = ANativeWindow_getWidth(surface); - m_window_height = ANativeWindow_getHeight(surface); - - // Ensures that we emulate with the correct aspect ratio. - UpdateCurrentFramebufferLayout(m_window_width, m_window_height); - + OnSurfaceChanged(surface); window_info.type = Core::Frontend::WindowSystemType::Android; - window_info.render_surface = reinterpret_cast<void*>(surface); m_input_subsystem->Initialize(); } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f31fe054b..598f4e8bf 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -13,6 +13,8 @@ #include <android/api-level.h> #include <android/native_window_jni.h> +#include <common/fs/fs.h> +#include <core/file_sys/savedata_factory.h> #include <core/loader/nro.h> #include <jni.h> @@ -102,7 +104,7 @@ public: m_native_window = native_window; } - int InstallFileToNand(std::string filename) { + int InstallFileToNand(std::string filename, std::string file_extension) { jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, std::size_t block_size) { if (src == nullptr || dest == nullptr) { @@ -134,15 +136,11 @@ public: m_system.GetFileSystemController().CreateFactories(*m_vfs); [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp; - if (filename.ends_with("nsp")) { + if (file_extension == "nsp") { nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); if (nsp->IsExtractedType()) { return InstallError; } - } else if (filename.ends_with("xci")) { - jconst xci = - std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); - nsp = xci->GetSecurePartitionNSP(); } else { return ErrorFilenameExtension; } @@ -220,7 +218,6 @@ public: return; } m_window->OnSurfaceChanged(m_native_window); - m_system.Renderer().NotifySurfaceChanged(); } void ConfigureFilesystemProvider(const std::string& filepath) { @@ -607,8 +604,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject } int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, - [[maybe_unused]] jstring j_file) { - return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file)); + jstring j_file, + jstring j_file_extension) { + return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file), + GetJString(env, j_file_extension)); } void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, @@ -879,4 +878,24 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env, + jobject instance) { + const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); + auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory( + Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read); + + Service::Account::ProfileManager manager; + const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); + ASSERT(user_id); + + const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( + EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, + FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0); + + const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); + if (!Common::FS::CreateParentDirs(full_path)) { + LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory"); + } +} + } // extern "C" diff --git a/src/android/app/src/main/res/drawable/ic_export.xml b/src/android/app/src/main/res/drawable/ic_export.xml new file mode 100644 index 000000000..463d2f41c --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_export.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="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_import.xml b/src/android/app/src/main/res/drawable/ic_import.xml new file mode 100644 index 000000000..3a99dd5e6 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_import.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="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" /> +</vector> diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml index 8a026a30a..a187665f2 100644 --- a/src/android/app/src/main/res/layout/activity_settings.xml +++ b/src/android/app/src/main/res/layout/activity_settings.xml @@ -22,7 +22,7 @@ <View android:id="@+id/navigation_bar_shade" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="1px" android:background="@android:color/transparent" android:clickable="false" diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml new file mode 100644 index 000000000..f5b0e3741 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_installable.xml @@ -0,0 +1,71 @@ +<?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"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:orientation="horizontal" + android:layout_gravity="center"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:layout_weight="1" + android:orientation="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:text="@string/user_data" + android:textAlignment="viewStart" /> + + <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:text="@string/user_data_description" + android:textAlignment="viewStart" /> + + </LinearLayout> + + <Button + android:id="@+id/button_export" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:contentDescription="@string/export" + android:tooltipText="@string/export" + android:visibility="gone" + app:icon="@drawable/ic_export" + tools:visibility="visible" /> + + <Button + android:id="@+id/button_install" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="12dp" + android:contentDescription="@string/string_import" + android:tooltipText="@string/string_import" + android:visibility="gone" + app:icon="@drawable/ic_import" + tools:visibility="visible" /> + + </LinearLayout> + +</com.google.android.material.card.MaterialCardView> diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml index d17711a65..0209ea082 100644 --- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml +++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml @@ -1,24 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" +<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:orientation="vertical"> - - <com.google.android.material.progressindicator.LinearProgressIndicator - android:id="@+id/progress_bar" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="24dp" - app:trackCornerRadius="4dp" /> - - <TextView - android:id="@+id/progress_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginLeft="24dp" - android:layout_marginRight="24dp" - android:layout_marginBottom="24dp" - android:gravity="end" /> - -</LinearLayout> + android:id="@+id/progress_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="24dp" + app:trackCornerRadius="4dp" /> diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml index da97d85c1..750ce094a 100644 --- a/src/android/app/src/main/res/layout/fragment_emulation.xml +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml @@ -32,7 +32,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:focusable="false"> + android:focusable="false" + android:clickable="false"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/loading_layout" @@ -155,7 +156,7 @@ android:id="@+id/in_game_menu" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_gravity="start|bottom" + android:layout_gravity="start" app:headerLayout="@layout/header_in_game" app:menu="@menu/menu_in_game" tools:visibility="gone" /> diff --git a/src/android/app/src/main/res/layout/fragment_installables.xml b/src/android/app/src/main/res/layout/fragment_installables.xml new file mode 100644 index 000000000..3a4df81a6 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_installables.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appbar_installables" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar_installables" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:title="@string/manage_yuzu_data" + app:navigationIcon="@drawable/ic_back" /> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/list_installables" + 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> 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 2e0ce7a3d..2356b802b 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -19,6 +19,9 @@ <action android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment" app:destination="@id/earlyAccessFragment" /> + <action + android:id="@+id/action_homeSettingsFragment_to_installableFragment" + app:destination="@id/installableFragment" /> </fragment> <fragment @@ -88,5 +91,9 @@ <action android:id="@+id/action_global_settingsActivity" app:destination="@id/settingsActivity" /> + <fragment + android:id="@+id/installableFragment" + android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" + android:label="InstallableFragment" /> </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 daaa7ffde..dd0f36392 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -79,7 +79,6 @@ <string name="manage_save_data">Speicherdaten verwalten</string> <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string> <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string> - <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string> <string name="save_file_imported_success">Erfolgreich importiert</string> <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string> <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</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 e9129cb00..d398f862f 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Administrar datos de guardado</string> <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string> <string name="import_export_saves_description">Importar o exportar archivos de guardado</string> - <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string> <string name="save_file_imported_success">Importado correctamente</string> <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string> <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</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 2d99d618e..a7abd9077 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Gérer les données de sauvegarde</string> <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string> <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string> - <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string> <string name="save_file_imported_success">Importé avec succès</string> <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string> <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</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 d9c3de385..b18161801 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Gestisci i salvataggi</string> <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string> <string name="import_export_saves_description">Importa o esporta i salvataggi</string> - <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string> <string name="save_file_imported_success">Importato con successo</string> <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string> <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</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 7a226cd5c..88fa5a0bb 100644 --- a/src/android/app/src/main/res/values-ja/strings.xml +++ b/src/android/app/src/main/res/values-ja/strings.xml @@ -80,7 +80,6 @@ <string name="manage_save_data">セーブデータを管理</string> <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string> <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string> - <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string> <string name="save_file_imported_success">インポートが完了しました</string> <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string> <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</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 427b6e5a0..4b658255c 100644 --- a/src/android/app/src/main/res/values-ko/strings.xml +++ b/src/android/app/src/main/res/values-ko/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">저장 데이터 관리</string> <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string> <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string> - <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string> <string name="save_file_imported_success">가져오기 성공</string> <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string> <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</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 ce8d7a9e4..dd602a389 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Administrere lagringsdata</string> <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string> <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string> - <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string> <string name="save_file_imported_success">Vellykket import</string> <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string> <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</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 c2c24b48f..2fdd1f952 100644 --- a/src/android/app/src/main/res/values-pl/strings.xml +++ b/src/android/app/src/main/res/values-pl/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Zarządzaj plikami zapisów gier</string> <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string> <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string> - <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string> <string name="save_file_imported_success">Zaimportowano pomyślnie</string> <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string> <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</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 04f276108..2f26367fe 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 @@ -81,7 +81,6 @@ <string name="manage_save_data">Gerir dados guardados</string> <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> <string name="import_export_saves_description">Importa ou exporta dados guardados</string> - <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string> <string name="save_file_imported_success">Importado com sucesso</string> <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</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 66a3a1a2e..4e1eb4cd7 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 @@ -81,7 +81,6 @@ <string name="manage_save_data">Gerir dados guardados</string> <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> <string name="import_export_saves_description">Importa ou exporta dados guardados</string> - <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string> <string name="save_file_imported_success">Importado com sucesso</string> <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</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 f770e954f..f5695dc93 100644 --- a/src/android/app/src/main/res/values-ru/strings.xml +++ b/src/android/app/src/main/res/values-ru/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Управление данными сохранений</string> <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string> <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string> - <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string> <string name="save_file_imported_success">Успешно импортировано</string> <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string> <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</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 ea3ab1b15..061bc6f04 100644 --- a/src/android/app/src/main/res/values-uk/strings.xml +++ b/src/android/app/src/main/res/values-uk/strings.xml @@ -81,7 +81,6 @@ <string name="manage_save_data">Керування даними збережень</string> <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string> <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string> - <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string> <string name="save_file_imported_success">Успішно імпортовано</string> <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string> <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string> diff --git a/src/android/app/src/main/res/values-w600dp/integers.xml b/src/android/app/src/main/res/values-w600dp/integers.xml new file mode 100644 index 000000000..9975db801 --- /dev/null +++ b/src/android/app/src/main/res/values-w600dp/integers.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <integer name="grid_columns">2</integer> + +</resources> 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 b45a5a528..fe6dd5eaa 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 @@ -81,7 +81,6 @@ <string name="manage_save_data">管理存档数据</string> <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string> <string name="import_export_saves_description">导入或导出存档</string> - <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string> <string name="save_file_imported_success">已成功导入存档</string> <string name="save_file_invalid_zip_structure">无效的存档目录</string> <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</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 3aab889e4..9b3e54224 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 @@ -81,7 +81,6 @@ <string name="manage_save_data">管理儲存資料</string> <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string> <string name="import_export_saves_description">匯入或匯出儲存檔案</string> - <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string> <string name="save_file_imported_success">已成功匯入</string> <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string> <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string> diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml index 5e39bc7d9..dc527965c 100644 --- a/src/android/app/src/main/res/values/integers.xml +++ b/src/android/app/src/main/res/values/integers.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <integer name="game_title_lines">2</integer> + <integer name="grid_columns">1</integer> <!-- Default SWITCH landscape layout --> <integer name="SWITCH_BUTTON_A_X">760</integer> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index b163e6fc1..e51edf872 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -90,7 +90,6 @@ <string name="manage_save_data">Manage save data</string> <string name="manage_save_data_description">Save data found. Please select an option below.</string> <string name="import_export_saves_description">Import or export save files</string> - <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string> <string name="save_file_imported_success">Imported successfully</string> <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> @@ -101,12 +100,13 @@ <string name="firmware_installing">Installing firmware</string> <string name="firmware_installed_success">Firmware installed successfully</string> <string name="firmware_installed_failure">Firmware installation failed</string> - <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string> + <string name="firmware_installed_failure_description">Make sure the firmware nca files are at the root of the zip and try again.</string> <string name="share_log">Share debug logs</string> <string name="share_log_description">Share yuzu\'s log file to debug issues</string> <string name="share_log_missing">No log file found</string> <string name="install_game_content">Install game content</string> <string name="install_game_content_description">Install game updates or DLC</string> + <string name="installing_game_content">Installing content…</string> <string name="install_game_content_failure">Error installing file(s) to NAND</string> <string name="install_game_content_failure_description">Please ensure content(s) are valid and that the prod.keys file is installed.</string> <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts.</string> @@ -118,6 +118,10 @@ <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> <string name="custom_driver_not_supported">Custom drivers not supported</string> <string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string> + <string name="manage_yuzu_data">Manage yuzu data</string> + <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string> + <string name="share_save_file">Share save file</string> + <string name="export_save_failed">Failed to export save</string> <!-- About screen strings --> <string name="gaia_is_not_real">Gaia isn\'t real</string> @@ -128,6 +132,16 @@ <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string> <string name="licenses_description">Projects that make yuzu for Android possible</string> <string name="build">Build</string> + <string name="user_data">User data</string> + <string name="user_data_description">Import/export all app data.\n\nWhen importing user data, all existing user data will be deleted!</string> + <string name="exporting_user_data">Exporting user data…</string> + <string name="importing_user_data">Importing user data…</string> + <string name="import_user_data">Import user data</string> + <string name="invalid_yuzu_backup">Invalid yuzu backup</string> + <string name="user_data_export_success">User data exported successfully</string> + <string name="user_data_import_success">User data imported successfully</string> + <string name="user_data_export_cancelled">Export cancelled</string> + <string name="user_data_import_failed_description">Make sure the user data folders are at the root of the zip folder and contain a config file at config/config.ini and try again.</string> <string name="support_link">https://discord.gg/u77vRWY</string> <string name="website_link">https://yuzu-emu.org/</string> <string name="github_link">https://github.com/yuzu-emu</string> @@ -215,6 +229,11 @@ <string name="auto">Auto</string> <string name="submit">Submit</string> <string name="string_null">Null</string> + <string name="string_import">Import</string> + <string name="export">Export</string> + <string name="export_failed">Export failed</string> + <string name="import_failed">Import failed</string> + <string name="cancelling">Cancelling</string> <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> @@ -281,6 +300,7 @@ <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string> <string name="device_memory_inadequate">Device RAM: %1$s\nRecommended: %2$s</string> <string name="memory_formatted">%1$s %2$s</string> + <string name="no_game_present">No bootable game present!</string> <!-- Region Names --> <string name="region_japan">Japan</string> diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.cpp b/src/audio_core/renderer/command/command_processing_time_estimator.cpp index a48a016b1..0f7aff1b4 100644 --- a/src/audio_core/renderer/command/command_processing_time_estimator.cpp +++ b/src/audio_core/renderer/command/command_processing_time_estimator.cpp @@ -27,12 +27,12 @@ u32 CommandProcessingTimeEstimatorVersion1::Estimate( u32 CommandProcessingTimeEstimatorVersion1::Estimate( const AdpcmDataSourceVersion1Command& command) const { - return static_cast<u32>(command.pitch * 0.25f * 1.2f); + return static_cast<u32>(command.pitch * 0.46f * 1.2f); } u32 CommandProcessingTimeEstimatorVersion1::Estimate( const AdpcmDataSourceVersion2Command& command) const { - return static_cast<u32>(command.pitch * 0.25f * 1.2f); + return static_cast<u32>(command.pitch * 0.46f * 1.2f); } u32 CommandProcessingTimeEstimatorVersion1::Estimate( diff --git a/src/audio_core/renderer/command/mix/depop_prepare.h b/src/audio_core/renderer/command/mix/depop_prepare.h index 161a94461..a0cc228f7 100644 --- a/src/audio_core/renderer/command/mix/depop_prepare.h +++ b/src/audio_core/renderer/command/mix/depop_prepare.h @@ -16,7 +16,7 @@ namespace AudioCore::Renderer { /** * AudioRenderer command for preparing depop. - * Adds the previusly output last samples to the depop buffer. + * Adds the previously output last samples to the depop buffer. */ struct DepopPrepareCommand : ICommand { /** diff --git a/src/audio_core/renderer/system.cpp b/src/audio_core/renderer/system.cpp index d29754634..31f92087c 100644 --- a/src/audio_core/renderer/system.cpp +++ b/src/audio_core/renderer/system.cpp @@ -684,11 +684,11 @@ u64 System::GenerateCommand(std::span<u8> in_command_buffer, sink_context, splitter_context, perf_manager}; voice_context.SortInfo(); + command_generator.GenerateVoiceCommands(); const auto start_estimated_time{drop_voice_param * static_cast<f32>(command_buffer.estimated_process_time)}; - command_generator.GenerateVoiceCommands(); command_generator.GenerateSubMixCommands(); command_generator.GenerateFinalMixCommands(); command_generator.GenerateSinkCommands(); @@ -708,11 +708,13 @@ u64 System::GenerateCommand(std::span<u8> in_command_buffer, const auto end_estimated_time{drop_voice_param * static_cast<f32>(command_buffer.estimated_process_time)}; + + const auto dsp_time_limit{((time_limit_percent / 100.0f) * 2'880'000.0f) * + (static_cast<f32>(render_time_limit_percent) / 100.0f)}; + const auto estimated_time{start_estimated_time - end_estimated_time}; - const auto time_limit{static_cast<u32>( - estimated_time + (((time_limit_percent / 100.0f) * 2'880'000.0) * - (static_cast<f32>(render_time_limit_percent) / 100.0f)))}; + const auto time_limit{static_cast<u32>(std::max(dsp_time_limit + estimated_time, 0.0f))}; num_voices_dropped = DropVoices(command_buffer, static_cast<u32>(start_estimated_time), time_limit); } 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/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/settings.cpp b/src/common/settings.cpp index 4ecaf550b..3fde3cae6 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -130,13 +130,17 @@ void LogSettings() { log_path("DataStorage_SDMCDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir)); } +void UpdateGPUAccuracy() { + values.current_gpu_accuracy = values.gpu_accuracy.GetValue(); +} + bool IsGPULevelExtreme() { - return values.gpu_accuracy.GetValue() == GpuAccuracy::Extreme; + return values.current_gpu_accuracy == GpuAccuracy::Extreme; } bool IsGPULevelHigh() { - return values.gpu_accuracy.GetValue() == GpuAccuracy::Extreme || - values.gpu_accuracy.GetValue() == GpuAccuracy::High; + return values.current_gpu_accuracy == GpuAccuracy::Extreme || + values.current_gpu_accuracy == GpuAccuracy::High; } bool IsFastmemEnabled() { diff --git a/src/common/settings.h b/src/common/settings.h index 82ec9077e..98ab0ec2e 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -307,6 +307,7 @@ struct Values { Specialization::Default, true, true}; + GpuAccuracy current_gpu_accuracy{GpuAccuracy::High}; SwitchableSetting<AnisotropyMode, true> max_anisotropy{ linkage, AnisotropyMode::Automatic, AnisotropyMode::Automatic, AnisotropyMode::X16, "max_anisotropy", Category::RendererAdvanced}; @@ -350,6 +351,8 @@ struct Values { linkage, false, "disable_shader_loop_safety_checks", Category::RendererDebug}; Setting<bool> enable_renderdoc_hotkey{linkage, false, "renderdoc_hotkey", Category::RendererDebug}; + // TODO: remove this once AMDVLK supports VK_EXT_depth_bias_control + bool renderer_amdvlk_depth_bias_workaround{}; // System SwitchableSetting<Language, true> language_index{linkage, @@ -522,6 +525,7 @@ struct Values { extern Values values; +void UpdateGPUAccuracy(); bool IsGPULevelExtreme(); bool IsGPULevelHigh(); diff --git a/src/common/settings_setting.h b/src/common/settings_setting.h index 7be6f26f7..3175ab07d 100644 --- a/src/common/settings_setting.h +++ b/src/common/settings_setting.h @@ -187,6 +187,8 @@ public: this->SetValue(input == "true"); } else if constexpr (std::is_same_v<Type, float>) { this->SetValue(std::stof(input)); + } else if constexpr (std::is_same_v<Type, AudioEngine>) { + this->SetValue(ToEnum<AudioEngine>(input)); } else { this->SetValue(static_cast<Type>(std::stoll(input))); } 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 b2dc71d4c..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 @@ -596,6 +600,10 @@ add_library(core STATIC hle/service/mii/types/ver3_store_data.h hle/service/mii/mii.cpp hle/service/mii/mii.h + hle/service/mii/mii_database.cpp + hle/service/mii/mii_database.h + hle/service/mii/mii_database_manager.cpp + hle/service/mii/mii_database_manager.h hle/service/mii/mii_manager.cpp hle/service/mii/mii_manager.h hle/service/mii/mii_result.h @@ -694,6 +702,8 @@ add_library(core STATIC hle/service/nvnflinger/consumer_base.cpp hle/service/nvnflinger/consumer_base.h hle/service/nvnflinger/consumer_listener.h + hle/service/nvnflinger/fb_share_buffer_manager.cpp + hle/service/nvnflinger/fb_share_buffer_manager.h hle/service/nvnflinger/graphic_buffer_producer.cpp hle/service/nvnflinger/graphic_buffer_producer.h hle/service/nvnflinger/hos_binder_driver_server.cpp @@ -890,7 +900,7 @@ endif() create_target_directory_groups(core) target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core nx_tzdb) -target_link_libraries(core PUBLIC Boost::headers PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls renderdoc) +target_link_libraries(core PUBLIC Boost::headers PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls RenderDoc::API) if (MINGW) target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) endif() diff --git a/src/core/core.cpp b/src/core/core.cpp index e8300cd05..0ab2e3b76 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -381,6 +381,10 @@ struct System::Impl { room_member->SendGameInfo(game_info); } + // Workarounds: + // Activate this in Super Smash Brothers Ultimate, it only affects AMD cards using AMDVLK + Settings::values.renderer_amdvlk_depth_bias_workaround = program_id == 0x1006A800016E000ULL; + status = SystemResultStatus::Success; return status; } @@ -440,6 +444,9 @@ struct System::Impl { room_member->SendGameInfo(game_info); } + // Workarounds + Settings::values.renderer_amdvlk_depth_bias_workaround = false; + LOG_DEBUG(Core, "Shutdown OK"); } @@ -1071,6 +1078,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/core_timing.cpp b/src/core/core_timing.cpp index b98a0cb33..e671b270f 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -32,6 +32,7 @@ struct CoreTiming::Event { std::uintptr_t user_data; std::weak_ptr<EventType> type; s64 reschedule_time; + heap_t::handle_type handle{}; // Sort by time, unless the times are the same, in which case sort by // the order added to the queue @@ -122,9 +123,9 @@ void CoreTiming::ScheduleEvent(std::chrono::nanoseconds ns_into_future, std::scoped_lock scope{basic_lock}; const auto next_time{absolute_time ? ns_into_future : GetGlobalTimeNs() + ns_into_future}; - event_queue.emplace_back( - Event{next_time.count(), event_fifo_id++, user_data, event_type, 0}); - std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + auto h{event_queue.emplace( + Event{next_time.count(), event_fifo_id++, user_data, event_type, 0})}; + (*h).handle = h; } event.Set(); @@ -138,10 +139,9 @@ void CoreTiming::ScheduleLoopingEvent(std::chrono::nanoseconds start_time, std::scoped_lock scope{basic_lock}; const auto next_time{absolute_time ? start_time : GetGlobalTimeNs() + start_time}; - event_queue.emplace_back( - Event{next_time.count(), event_fifo_id++, user_data, event_type, resched_time.count()}); - - std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + auto h{event_queue.emplace(Event{next_time.count(), event_fifo_id++, user_data, event_type, + resched_time.count()})}; + (*h).handle = h; } event.Set(); @@ -151,15 +151,17 @@ void CoreTiming::UnscheduleEvent(const std::shared_ptr<EventType>& event_type, std::uintptr_t user_data, bool wait) { { std::scoped_lock lk{basic_lock}; - const auto itr = - std::remove_if(event_queue.begin(), event_queue.end(), [&](const Event& e) { - return e.type.lock().get() == event_type.get() && e.user_data == user_data; - }); - - // Removing random items breaks the invariant so we have to re-establish it. - if (itr != event_queue.end()) { - event_queue.erase(itr, event_queue.end()); - std::make_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + + std::vector<heap_t::handle_type> to_remove; + for (auto itr = event_queue.begin(); itr != event_queue.end(); itr++) { + const Event& e = *itr; + if (e.type.lock().get() == event_type.get() && e.user_data == user_data) { + to_remove.push_back(itr->handle); + } + } + + for (auto h : to_remove) { + event_queue.erase(h); } } @@ -200,35 +202,45 @@ std::optional<s64> CoreTiming::Advance() { std::scoped_lock lock{advance_lock, basic_lock}; global_timer = GetGlobalTimeNs().count(); - while (!event_queue.empty() && event_queue.front().time <= global_timer) { - Event evt = std::move(event_queue.front()); - std::pop_heap(event_queue.begin(), event_queue.end(), std::greater<>()); - event_queue.pop_back(); + while (!event_queue.empty() && event_queue.top().time <= global_timer) { + const Event& evt = event_queue.top(); if (const auto event_type{evt.type.lock()}) { - basic_lock.unlock(); + if (evt.reschedule_time == 0) { + const auto evt_user_data = evt.user_data; + const auto evt_time = evt.time; + + event_queue.pop(); + + basic_lock.unlock(); + + event_type->callback( + evt_user_data, evt_time, + std::chrono::nanoseconds{GetGlobalTimeNs().count() - evt_time}); + + basic_lock.lock(); + } else { + basic_lock.unlock(); - const auto new_schedule_time{event_type->callback( - evt.user_data, evt.time, - std::chrono::nanoseconds{GetGlobalTimeNs().count() - evt.time})}; + const auto new_schedule_time{event_type->callback( + evt.user_data, evt.time, + std::chrono::nanoseconds{GetGlobalTimeNs().count() - evt.time})}; - basic_lock.lock(); + basic_lock.lock(); - if (evt.reschedule_time != 0) { const auto next_schedule_time{new_schedule_time.has_value() ? new_schedule_time.value().count() : evt.reschedule_time}; - // If this event was scheduled into a pause, its time now is going to be way behind. - // Re-set this event to continue from the end of the pause. + // If this event was scheduled into a pause, its time now is going to be way + // behind. Re-set this event to continue from the end of the pause. auto next_time{evt.time + next_schedule_time}; if (evt.time < pause_end_time) { next_time = pause_end_time + next_schedule_time; } - event_queue.emplace_back( - Event{next_time, event_fifo_id++, evt.user_data, evt.type, next_schedule_time}); - std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + event_queue.update(evt.handle, Event{next_time, event_fifo_id++, evt.user_data, + evt.type, next_schedule_time, evt.handle}); } } @@ -236,7 +248,7 @@ std::optional<s64> CoreTiming::Advance() { } if (!event_queue.empty()) { - return event_queue.front().time; + return event_queue.top().time; } else { return std::nullopt; } @@ -274,7 +286,8 @@ void CoreTiming::ThreadLoop() { #endif } } else { - // Queue is empty, wait until another event is scheduled and signals us to continue. + // Queue is empty, wait until another event is scheduled and signals us to + // continue. wait_set = true; event.Wait(); } diff --git a/src/core/core_timing.h b/src/core/core_timing.h index c20e906fb..26a8b93a7 100644 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -11,7 +11,8 @@ #include <optional> #include <string> #include <thread> -#include <vector> + +#include <boost/heap/fibonacci_heap.hpp> #include "common/common_types.h" #include "common/thread.h" @@ -151,11 +152,10 @@ private: s64 timer_resolution_ns; #endif - // The queue is a min-heap using std::make_heap/push_heap/pop_heap. - // We don't use std::priority_queue because we need to be able to serialize, unserialize and - // erase arbitrary events (RemoveEvent()) regardless of the queue order. These aren't - // accommodated by the standard adaptor class. - std::vector<Event> event_queue; + using heap_t = + boost::heap::fibonacci_heap<CoreTiming::Event, boost::heap::compare<std::greater<>>>; + + heap_t event_queue; u64 event_fifo_id = 0; std::shared_ptr<EventType> ev_lost; 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 += "&"; @@ -86,7 +95,11 @@ static std::string EscapeXML(std::string_view data) { escaped += ">"; 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/partition_filesystem.cpp b/src/core/file_sys/partition_filesystem.cpp index 2527ae606..2422cb51b 100644 --- a/src/core/file_sys/partition_filesystem.cpp +++ b/src/core/file_sys/partition_filesystem.cpp @@ -47,6 +47,7 @@ PartitionFilesystem::PartitionFilesystem(VirtualFile file) { // Actually read in now... std::vector<u8> file_data = file->ReadBytes(metadata_size); const std::size_t total_size = file_data.size(); + file_data.push_back(0); if (total_size != metadata_size) { status = Loader::ResultStatus::ErrorIncorrectPFSFileSize; 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/vfs.h b/src/core/file_sys/vfs.h index a93e21f67..a7cd1cae3 100644 --- a/src/core/file_sys/vfs.h +++ b/src/core/file_sys/vfs.h @@ -175,7 +175,7 @@ public: return Write(reinterpret_cast<const u8*>(&data), sizeof(T), offset); } - // Renames the file to name. Returns whether or not the operation was successsful. + // Renames the file to name. Returns whether or not the operation was successful. virtual bool Rename(std::string_view name) = 0; // Returns the full path of this file as a string, recursively diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp index 94bd656fe..2af3f06fc 100644 --- a/src/core/hid/emulated_controller.cpp +++ b/src/core/hid/emulated_controller.cpp @@ -542,6 +542,7 @@ void EmulatedController::UnloadInput() { } void EmulatedController::EnableConfiguration() { + std::scoped_lock lock{connect_mutex, npad_mutex}; is_configuring = true; tmp_is_connected = is_connected; tmp_npad_type = npad_type; @@ -1556,7 +1557,7 @@ void EmulatedController::Connect(bool use_temporary_value) { auto trigger_guard = SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Connected, !is_configuring); }); - std::scoped_lock lock{mutex}; + std::scoped_lock lock{connect_mutex, mutex}; if (is_configuring) { tmp_is_connected = true; return; @@ -1572,7 +1573,7 @@ void EmulatedController::Connect(bool use_temporary_value) { void EmulatedController::Disconnect() { auto trigger_guard = SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Disconnected, !is_configuring); }); - std::scoped_lock lock{mutex}; + std::scoped_lock lock{connect_mutex, mutex}; if (is_configuring) { tmp_is_connected = false; return; @@ -1586,7 +1587,7 @@ void EmulatedController::Disconnect() { } bool EmulatedController::IsConnected(bool get_temporary_value) const { - std::scoped_lock lock{mutex}; + std::scoped_lock lock{connect_mutex}; if (get_temporary_value && is_configuring) { return tmp_is_connected; } @@ -1599,7 +1600,7 @@ NpadIdType EmulatedController::GetNpadIdType() const { } NpadStyleIndex EmulatedController::GetNpadStyleIndex(bool get_temporary_value) const { - std::scoped_lock lock{mutex}; + std::scoped_lock lock{npad_mutex}; if (get_temporary_value && is_configuring) { return tmp_npad_type; } @@ -1609,7 +1610,7 @@ NpadStyleIndex EmulatedController::GetNpadStyleIndex(bool get_temporary_value) c void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) { auto trigger_guard = SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Type, !is_configuring); }); - std::scoped_lock lock{mutex}; + std::scoped_lock lock{mutex, npad_mutex}; if (is_configuring) { if (tmp_npad_type == npad_type_) { diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h index 88d77db8d..d4500583e 100644 --- a/src/core/hid/emulated_controller.h +++ b/src/core/hid/emulated_controller.h @@ -603,6 +603,8 @@ private: mutable std::mutex mutex; mutable std::mutex callback_mutex; + mutable std::mutex npad_mutex; + mutable std::mutex connect_mutex; std::unordered_map<int, ControllerUpdateCallback> callback_list; int last_callback_key = 0; diff --git a/src/core/hle/kernel/k_hardware_timer.cpp b/src/core/hle/kernel/k_hardware_timer.cpp index 4dcd53821..8e2e40307 100644 --- a/src/core/hle/kernel/k_hardware_timer.cpp +++ b/src/core/hle/kernel/k_hardware_timer.cpp @@ -35,7 +35,9 @@ void KHardwareTimer::DoTask() { } // Disable the timer interrupt while we handle this. - this->DisableInterrupt(); + // Not necessary due to core timing already having popped this event to call it. + // this->DisableInterrupt(); + m_wakeup_time = std::numeric_limits<s64>::max(); if (const s64 next_time = this->DoInterruptTaskImpl(GetTick()); 0 < next_time && next_time <= m_wakeup_time) { diff --git a/src/core/hle/kernel/k_memory_layout.cpp b/src/core/hle/kernel/k_memory_layout.cpp index af40092c0..bec714668 100644 --- a/src/core/hle/kernel/k_memory_layout.cpp +++ b/src/core/hle/kernel/k_memory_layout.cpp @@ -61,7 +61,7 @@ bool KMemoryRegionTree::Insert(u64 address, size_t size, u32 type_id, u32 new_at found->Reset(address, inserted_region_last, old_pair, new_attr, type_id); this->insert(*found); } else { - // If we can't re-use, adjust the old region. + // If we can't reuse, adjust the old region. found->Reset(old_address, address - 1, old_pair, old_attr, old_type); this->insert(*found); diff --git a/src/core/hle/kernel/k_page_table.cpp b/src/core/hle/kernel/k_page_table.cpp index 9bfc85b34..1fbfbf31f 100644 --- a/src/core/hle/kernel/k_page_table.cpp +++ b/src/core/hle/kernel/k_page_table.cpp @@ -5,6 +5,7 @@ #include "common/assert.h" #include "common/literals.h" #include "common/scope_exit.h" +#include "common/settings.h" #include "core/core.h" #include "core/hle/kernel/k_address_space_info.h" #include "core/hle/kernel/k_memory_block.h" @@ -337,11 +338,14 @@ Result KPageTable::InitializeForProcess(FileSys::ProgramAddressSpaceType as_type } void KPageTable::Finalize() { + auto HostUnmapCallback = [&](KProcessAddress addr, u64 size) { + if (Settings::IsFastmemEnabled()) { + m_system.DeviceMemory().buffer.Unmap(GetInteger(addr), size); + } + }; + // Finalize memory blocks. - m_memory_block_manager.Finalize(m_memory_block_slab_manager, - [&](KProcessAddress addr, u64 size) { - m_memory->UnmapRegion(*m_page_table_impl, addr, size); - }); + m_memory_block_manager.Finalize(m_memory_block_slab_manager, std::move(HostUnmapCallback)); // Release any insecure mapped memory. if (m_mapped_insecure_memory) { @@ -2945,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, 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/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/am/am.cpp b/src/core/hle/service/am/am.cpp index 8ffdd19e7..ac376b55a 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -8,6 +8,7 @@ #include "common/settings.h" #include "common/settings_enums.h" #include "core/core.h" +#include "core/core_timing.h" #include "core/file_sys/control_metadata.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" @@ -19,6 +20,8 @@ #include "core/hle/service/am/am.h" #include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_oe.h" +#include "core/hle/service/am/applets/applet_cabinet.h" +#include "core/hle/service/am/applets/applet_mii_edit_types.h" #include "core/hle/service/am/applets/applet_profile_select.h" #include "core/hle/service/am/applets/applet_web_browser.h" #include "core/hle/service/am/applets/applets.h" @@ -28,15 +31,17 @@ #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" +#include "core/hle/service/nvnflinger/fb_share_buffer_manager.h" #include "core/hle/service/nvnflinger/nvnflinger.h" #include "core/hle/service/pm/pm.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/sm/sm.h" #include "core/hle/service/vi/vi.h" +#include "core/hle/service/vi/vi_results.h" #include "core/memory.h" namespace Service::AM { @@ -189,8 +194,8 @@ IDisplayController::IDisplayController(Core::System& system_) {4, nullptr, "UpdateCallerAppletCaptureImage"}, {5, nullptr, "GetLastForegroundCaptureImageEx"}, {6, nullptr, "GetLastApplicationCaptureImageEx"}, - {7, nullptr, "GetCallerAppletCaptureImageEx"}, - {8, nullptr, "TakeScreenShotOfOwnLayer"}, + {7, &IDisplayController::GetCallerAppletCaptureImageEx, "GetCallerAppletCaptureImageEx"}, + {8, &IDisplayController::TakeScreenShotOfOwnLayer, "TakeScreenShotOfOwnLayer"}, {9, nullptr, "CopyBetweenCaptureBuffers"}, {10, nullptr, "AcquireLastApplicationCaptureBuffer"}, {11, nullptr, "ReleaseLastApplicationCaptureBuffer"}, @@ -207,8 +212,8 @@ IDisplayController::IDisplayController(Core::System& system_) {23, nullptr, "ReleaseLastApplicationCaptureSharedBuffer"}, {24, nullptr, "AcquireLastForegroundCaptureSharedBuffer"}, {25, nullptr, "ReleaseLastForegroundCaptureSharedBuffer"}, - {26, nullptr, "AcquireCallerAppletCaptureSharedBuffer"}, - {27, nullptr, "ReleaseCallerAppletCaptureSharedBuffer"}, + {26, &IDisplayController::AcquireCallerAppletCaptureSharedBuffer, "AcquireCallerAppletCaptureSharedBuffer"}, + {27, &IDisplayController::ReleaseCallerAppletCaptureSharedBuffer, "ReleaseCallerAppletCaptureSharedBuffer"}, {28, nullptr, "TakeScreenShotOfOwnLayerEx"}, }; // clang-format on @@ -218,6 +223,38 @@ IDisplayController::IDisplayController(Core::System& system_) IDisplayController::~IDisplayController() = default; +void IDisplayController::GetCallerAppletCaptureImageEx(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(ResultSuccess); + rb.Push(1u); + rb.Push(0); +} + +void IDisplayController::TakeScreenShotOfOwnLayer(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"); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(ResultSuccess); + rb.Push(1U); + rb.Push(0); +} + +void IDisplayController::ReleaseCallerAppletCaptureSharedBuffer(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + IDebugFunctions::IDebugFunctions(Core::System& system_) : ServiceFramework{system_, "IDebugFunctions"} { // clang-format off @@ -277,14 +314,14 @@ ISelfController::ISelfController(Core::System& system_, Nvnflinger::Nvnflinger& {20, nullptr, "SetDesirableKeyboardLayout"}, {21, nullptr, "GetScreenShotProgramId"}, {40, &ISelfController::CreateManagedDisplayLayer, "CreateManagedDisplayLayer"}, - {41, nullptr, "IsSystemBufferSharingEnabled"}, - {42, nullptr, "GetSystemSharedLayerHandle"}, - {43, nullptr, "GetSystemSharedBufferHandle"}, + {41, &ISelfController::IsSystemBufferSharingEnabled, "IsSystemBufferSharingEnabled"}, + {42, &ISelfController::GetSystemSharedLayerHandle, "GetSystemSharedLayerHandle"}, + {43, &ISelfController::GetSystemSharedBufferHandle, "GetSystemSharedBufferHandle"}, {44, &ISelfController::CreateManagedDisplaySeparableLayer, "CreateManagedDisplaySeparableLayer"}, {45, nullptr, "SetManagedDisplayLayerSeparationMode"}, {46, nullptr, "SetRecordingLayerCompositionEnabled"}, {50, &ISelfController::SetHandlesRequestToDisplay, "SetHandlesRequestToDisplay"}, - {51, nullptr, "ApproveToDisplay"}, + {51, &ISelfController::ApproveToDisplay, "ApproveToDisplay"}, {60, nullptr, "OverrideAutoSleepTimeAndDimmingTime"}, {61, nullptr, "SetMediaPlaybackState"}, {62, &ISelfController::SetIdleTimeDetectionExtension, "SetIdleTimeDetectionExtension"}, @@ -483,6 +520,50 @@ void ISelfController::CreateManagedDisplayLayer(HLERequestContext& ctx) { rb.Push(*layer_id); } +void ISelfController::IsSystemBufferSharingEnabled(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(this->EnsureBufferSharingEnabled()); +} + +void ISelfController::GetSystemSharedLayerHandle(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 6}; + rb.Push(this->EnsureBufferSharingEnabled()); + rb.Push<s64>(system_shared_buffer_id); + rb.Push<s64>(system_shared_layer_id); +} + +void ISelfController::GetSystemSharedBufferHandle(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(this->EnsureBufferSharingEnabled()); + rb.Push<s64>(system_shared_buffer_id); +} + +Result ISelfController::EnsureBufferSharingEnabled() { + if (buffer_sharing_enabled) { + return ResultSuccess; + } + + if (system.GetAppletManager().GetCurrentAppletId() <= Applets::AppletId::Application) { + return VI::ResultOperationFailed; + } + + const auto display_id = nvnflinger.OpenDisplay("Default"); + const auto result = nvnflinger.GetSystemBufferManager().Initialize( + &system_shared_buffer_id, &system_shared_layer_id, *display_id); + + if (result.IsSuccess()) { + buffer_sharing_enabled = true; + } + + return result; +} + void ISelfController::CreateManagedDisplaySeparableLayer(HLERequestContext& ctx) { LOG_WARNING(Service_AM, "(STUBBED) called"); @@ -508,6 +589,13 @@ void ISelfController::SetHandlesRequestToDisplay(HLERequestContext& ctx) { rb.Push(ResultSuccess); } +void ISelfController::ApproveToDisplay(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void ISelfController::SetIdleTimeDetectionExtension(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; idle_time_detection_extension = rp.Pop<u32>(); @@ -676,9 +764,70 @@ 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_)} { + : ServiceFramework{system_, "ICommonStateGetter"}, msg_queue{std::move(msg_queue_)}, + service_context{system_, "ICommonStateGetter"} { // clang-format off static const FunctionInfo functions[] = { {0, &ICommonStateGetter::GetEventHandle, "GetEventHandle"}, @@ -691,14 +840,14 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, {7, nullptr, "GetCradleStatus"}, {8, &ICommonStateGetter::GetBootMode, "GetBootMode"}, {9, &ICommonStateGetter::GetCurrentFocusState, "GetCurrentFocusState"}, - {10, nullptr, "RequestToAcquireSleepLock"}, + {10, &ICommonStateGetter::RequestToAcquireSleepLock, "RequestToAcquireSleepLock"}, {11, nullptr, "ReleaseSleepLock"}, {12, nullptr, "ReleaseSleepLockTransiently"}, - {13, nullptr, "GetAcquiredSleepLockEvent"}, + {13, &ICommonStateGetter::GetAcquiredSleepLockEvent, "GetAcquiredSleepLockEvent"}, {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"}, @@ -716,7 +865,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"}, @@ -724,7 +873,7 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, {110, nullptr, "OpenMyGpuErrorHandler"}, {120, nullptr, "GetAppletLaunchedHistory"}, {200, nullptr, "GetOperationModeSystemInfo"}, - {300, nullptr, "GetSettingsPlatformRegion"}, + {300, &ICommonStateGetter::GetSettingsPlatformRegion, "GetSettingsPlatformRegion"}, {400, nullptr, "ActivateMigrationService"}, {401, nullptr, "DeactivateMigrationService"}, {500, nullptr, "DisableSleepTillShutdown"}, @@ -736,6 +885,12 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, // clang-format on RegisterHandlers(functions); + + sleep_lock_event = service_context.CreateEvent("ICommonStateGetter::SleepLockEvent"); + + // Configure applets to be in foreground state + msg_queue->PushMessage(AppletMessageQueue::AppletMessage::FocusStateChanged); + msg_queue->PushMessage(AppletMessageQueue::AppletMessage::ChangeIntoForeground); } ICommonStateGetter::~ICommonStateGetter() = default; @@ -781,6 +936,36 @@ void ICommonStateGetter::GetCurrentFocusState(HLERequestContext& ctx) { rb.Push(static_cast<u8>(FocusState::InFocus)); } +void ICommonStateGetter::RequestToAcquireSleepLock(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + // Sleep lock is acquired immediately. + sleep_lock_event->Signal(); + + IPC::ResponseBuilder rb{ctx, 2}; + 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"); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(sleep_lock_event->GetReadableEvent()); +} + void ICommonStateGetter::IsVrModeEnabled(HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "called"); @@ -857,6 +1042,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>()}; @@ -867,6 +1060,14 @@ void ICommonStateGetter::PerformSystemButtonPressingIfInFocus(HLERequestContext& rb.Push(ResultSuccess); } +void ICommonStateGetter::GetSettingsPlatformRegion(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.PushEnum(SysPlatformRegion::Global); +} + void ICommonStateGetter::SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled( HLERequestContext& ctx) { LOG_WARNING(Service_AM, "(STUBBED) called"); @@ -1324,18 +1525,19 @@ void ILibraryAppletCreator::CreateHandleStorage(HLERequestContext& ctx) { ILibraryAppletSelfAccessor::ILibraryAppletSelfAccessor(Core::System& system_) : ServiceFramework{system_, "ILibraryAppletSelfAccessor"} { + // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "PopInData"}, - {1, nullptr, "PushOutData"}, + {0, &ILibraryAppletSelfAccessor::PopInData, "PopInData"}, + {1, &ILibraryAppletSelfAccessor::PushOutData, "PushOutData"}, {2, nullptr, "PopInteractiveInData"}, {3, nullptr, "PushInteractiveOutData"}, {5, nullptr, "GetPopInDataEvent"}, {6, nullptr, "GetPopInteractiveInDataEvent"}, - {10, nullptr, "ExitProcessAndReturn"}, - {11, nullptr, "GetLibraryAppletInfo"}, + {10, &ILibraryAppletSelfAccessor::ExitProcessAndReturn, "ExitProcessAndReturn"}, + {11, &ILibraryAppletSelfAccessor::GetLibraryAppletInfo, "GetLibraryAppletInfo"}, {12, nullptr, "GetMainAppletIdentityInfo"}, {13, nullptr, "CanUseApplicationCore"}, - {14, nullptr, "GetCallerAppletIdentityInfo"}, + {14, &ILibraryAppletSelfAccessor::GetCallerAppletIdentityInfo, "GetCallerAppletIdentityInfo"}, {15, nullptr, "GetMainAppletApplicationControlProperty"}, {16, nullptr, "GetMainAppletStorageId"}, {17, nullptr, "GetCallerAppletIdentityInfoStack"}, @@ -1361,10 +1563,200 @@ ILibraryAppletSelfAccessor::ILibraryAppletSelfAccessor(Core::System& system_) {140, nullptr, "SetApplicationMemoryReservation"}, {150, nullptr, "ShouldSetGpuTimeSliceManually"}, }; + // clang-format on RegisterHandlers(functions); + + switch (system.GetAppletManager().GetCurrentAppletId()) { + case Applets::AppletId::Cabinet: + PushInShowCabinetData(); + break; + case Applets::AppletId::MiiEdit: + PushInShowMiiEditData(); + break; + case Applets::AppletId::PhotoViewer: + PushInShowAlbum(); + break; + default: + break; + } } ILibraryAppletSelfAccessor::~ILibraryAppletSelfAccessor() = default; +void ILibraryAppletSelfAccessor::PopInData(HLERequestContext& ctx) { + LOG_INFO(Service_AM, "called"); + + if (queue_data.empty()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultNoDataInChannel); + return; + } + + auto data = queue_data.front(); + queue_data.pop_front(); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IStorage>(system, std::move(data)); +} + +void ILibraryAppletSelfAccessor::PushOutData(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + +void ILibraryAppletSelfAccessor::ExitProcessAndReturn(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + system.Exit(); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + +void ILibraryAppletSelfAccessor::GetLibraryAppletInfo(HLERequestContext& ctx) { + struct LibraryAppletInfo { + Applets::AppletId applet_id; + Applets::LibraryAppletMode library_applet_mode; + }; + + LOG_WARNING(Service_AM, "(STUBBED) called"); + + const LibraryAppletInfo applet_info{ + .applet_id = system.GetAppletManager().GetCurrentAppletId(), + .library_applet_mode = Applets::LibraryAppletMode::AllForeground, + }; + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(ResultSuccess); + rb.PushRaw(applet_info); +} + +void ILibraryAppletSelfAccessor::GetCallerAppletIdentityInfo(HLERequestContext& ctx) { + struct AppletIdentityInfo { + Applets::AppletId applet_id; + INSERT_PADDING_BYTES(0x4); + u64 application_id; + }; + + LOG_WARNING(Service_AM, "(STUBBED) called"); + + const AppletIdentityInfo applet_info{ + .applet_id = Applets::AppletId::QLaunch, + .application_id = 0x0100000000001000ull, + }; + + IPC::ResponseBuilder rb{ctx, 6}; + rb.Push(ResultSuccess); + rb.PushRaw(applet_info); +} + +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, + .size = Applets::CommonArgumentSize::Version3, + .library_version = static_cast<u32>(Applets::CabinetAppletVersion::Version1), + .theme_color = Applets::ThemeColor::BasicBlack, + .play_startup_sound = true, + .system_tick = system.CoreTiming().GetClockTicks(), + }; + + const Applets::StartParamForAmiiboSettings amiibo_settings{ + .param_1 = 0, + .applet_mode = system.GetAppletManager().GetCabinetMode(), + .flags = Applets::CabinetFlags::None, + .amiibo_settings_1 = 0, + .device_handle = 0, + .tag_info{}, + .register_info{}, + .amiibo_settings_3{}, + }; + + std::vector<u8> argument_data(sizeof(arguments)); + std::vector<u8> settings_data(sizeof(amiibo_settings)); + std::memcpy(argument_data.data(), &arguments, sizeof(arguments)); + std::memcpy(settings_data.data(), &amiibo_settings, sizeof(amiibo_settings)); + queue_data.emplace_back(std::move(argument_data)); + queue_data.emplace_back(std::move(settings_data)); +} + +void ILibraryAppletSelfAccessor::PushInShowMiiEditData() { + struct MiiEditV3 { + Applets::MiiEditAppletInputCommon common; + Applets::MiiEditAppletInputV3 input; + }; + static_assert(sizeof(MiiEditV3) == 0x100, "MiiEditV3 has incorrect size."); + + MiiEditV3 mii_arguments{ + .common = + { + .version = Applets::MiiEditAppletVersion::Version3, + .applet_mode = Applets::MiiEditAppletMode::ShowMiiEdit, + }, + .input{}, + }; + + std::vector<u8> argument_data(sizeof(mii_arguments)); + std::memcpy(argument_data.data(), &mii_arguments, sizeof(mii_arguments)); + + queue_data.emplace_back(std::move(argument_data)); +} + +IAppletCommonFunctions::IAppletCommonFunctions(Core::System& system_) + : ServiceFramework{system_, "IAppletCommonFunctions"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "SetTerminateResult"}, + {10, nullptr, "ReadThemeStorage"}, + {11, nullptr, "WriteThemeStorage"}, + {20, nullptr, "PushToAppletBoundChannel"}, + {21, nullptr, "TryPopFromAppletBoundChannel"}, + {40, nullptr, "GetDisplayLogicalResolution"}, + {42, nullptr, "SetDisplayMagnification"}, + {50, nullptr, "SetHomeButtonDoubleClickEnabled"}, + {51, nullptr, "GetHomeButtonDoubleClickEnabled"}, + {52, nullptr, "IsHomeButtonShortPressedBlocked"}, + {60, nullptr, "IsVrModeCurtainRequired"}, + {61, nullptr, "IsSleepRequiredByHighTemperature"}, + {62, nullptr, "IsSleepRequiredByLowBattery"}, + {70, &IAppletCommonFunctions::SetCpuBoostRequestPriority, "SetCpuBoostRequestPriority"}, + {80, nullptr, "SetHandlingCaptureButtonShortPressedMessageEnabledForApplet"}, + {81, nullptr, "SetHandlingCaptureButtonLongPressedMessageEnabledForApplet"}, + {90, nullptr, "OpenNamedChannelAsParent"}, + {91, nullptr, "OpenNamedChannelAsChild"}, + {100, nullptr, "SetApplicationCoreUsageMode"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IAppletCommonFunctions::~IAppletCommonFunctions() = default; + +void IAppletCommonFunctions::SetCpuBoostRequestPriority(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} IApplicationFunctions::IApplicationFunctions(Core::System& system_) : ServiceFramework{system_, "IApplicationFunctions"}, service_context{system, @@ -1941,9 +2333,6 @@ void IApplicationFunctions::PrepareForJit(HLERequestContext& ctx) { void LoopProcess(Nvnflinger::Nvnflinger& nvnflinger, Core::System& system) { auto message_queue = std::make_shared<AppletMessageQueue>(system); - // Needed on game boot - message_queue->PushMessage(AppletMessageQueue::AppletMessage::FocusStateChanged); - auto server_manager = std::make_unique<ServerManager>(system); server_manager->RegisterNamedService( @@ -2049,8 +2438,8 @@ IProcessWindingController::IProcessWindingController(Core::System& system_) : ServiceFramework{system_, "IProcessWindingController"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "GetLaunchReason"}, - {11, nullptr, "OpenCallingLibraryApplet"}, + {0, &IProcessWindingController::GetLaunchReason, "GetLaunchReason"}, + {11, &IProcessWindingController::OpenCallingLibraryApplet, "OpenCallingLibraryApplet"}, {21, nullptr, "PushContext"}, {22, nullptr, "PopContext"}, {23, nullptr, "CancelWindingReservation"}, @@ -2064,4 +2453,47 @@ IProcessWindingController::IProcessWindingController(Core::System& system_) } IProcessWindingController::~IProcessWindingController() = default; + +void IProcessWindingController::GetLaunchReason(HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + struct AppletProcessLaunchReason { + u8 flag; + INSERT_PADDING_BYTES(3); + }; + static_assert(sizeof(AppletProcessLaunchReason) == 0x4, + "AppletProcessLaunchReason is an invalid size"); + + AppletProcessLaunchReason reason{ + .flag = 0, + }; + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.PushRaw(reason); +} + +void IProcessWindingController::OpenCallingLibraryApplet(HLERequestContext& ctx) { + const auto applet_id = system.GetAppletManager().GetCurrentAppletId(); + const auto applet_mode = Applets::LibraryAppletMode::AllForeground; + + LOG_WARNING(Service_AM, "(STUBBED) called with applet_id={:08X}, applet_mode={:08X}", applet_id, + applet_mode); + + const auto& applet_manager{system.GetAppletManager()}; + const auto applet = applet_manager.GetApplet(applet_id, applet_mode); + + if (applet == nullptr) { + LOG_ERROR(Service_AM, "Applet doesn't exist! applet_id={}", applet_id); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultUnknown); + return; + } + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<ILibraryAppletAccessor>(system, applet); +} + } // namespace Service::AM diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index f86841c60..4a045cfd4 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -120,6 +120,12 @@ class IDisplayController final : public ServiceFramework<IDisplayController> { public: explicit IDisplayController(Core::System& system_); ~IDisplayController() override; + +private: + void GetCallerAppletCaptureImageEx(HLERequestContext& ctx); + void TakeScreenShotOfOwnLayer(HLERequestContext& ctx); + void AcquireCallerAppletCaptureSharedBuffer(HLERequestContext& ctx); + void ReleaseCallerAppletCaptureSharedBuffer(HLERequestContext& ctx); }; class IDebugFunctions final : public ServiceFramework<IDebugFunctions> { @@ -147,9 +153,13 @@ private: void SetRestartMessageEnabled(HLERequestContext& ctx); void SetOutOfFocusSuspendingEnabled(HLERequestContext& ctx); void SetAlbumImageOrientation(HLERequestContext& ctx); + void IsSystemBufferSharingEnabled(HLERequestContext& ctx); + void GetSystemSharedBufferHandle(HLERequestContext& ctx); + void GetSystemSharedLayerHandle(HLERequestContext& ctx); void CreateManagedDisplayLayer(HLERequestContext& ctx); void CreateManagedDisplaySeparableLayer(HLERequestContext& ctx); void SetHandlesRequestToDisplay(HLERequestContext& ctx); + void ApproveToDisplay(HLERequestContext& ctx); void SetIdleTimeDetectionExtension(HLERequestContext& ctx); void GetIdleTimeDetectionExtension(HLERequestContext& ctx); void ReportUserIsActive(HLERequestContext& ctx); @@ -161,6 +171,8 @@ private: void SaveCurrentScreenshot(HLERequestContext& ctx); void SetRecordVolumeMuted(HLERequestContext& ctx); + Result EnsureBufferSharingEnabled(); + enum class ScreenshotPermission : u32 { Inherit = 0, Enable = 1, @@ -176,10 +188,30 @@ private: u32 idle_time_detection_extension = 0; u64 num_fatal_sections_entered = 0; + u64 system_shared_buffer_id = 0; + u64 system_shared_layer_id = 0; bool is_auto_sleep_disabled = false; + bool buffer_sharing_enabled = false; 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_, @@ -212,9 +244,17 @@ private: CaptureButtonLongPressing, }; + enum class SysPlatformRegion : s32 { + Global = 1, + Terra = 2, + }; + void GetEventHandle(HLERequestContext& ctx); void ReceiveMessage(HLERequestContext& ctx); 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); @@ -226,11 +266,15 @@ 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); std::shared_ptr<AppletMessageQueue> msg_queue; bool vr_mode_state{}; + Kernel::KEvent* sleep_lock_event; + KernelHelpers::ServiceContext service_context; }; class IStorageImpl { @@ -294,6 +338,28 @@ class ILibraryAppletSelfAccessor final : public ServiceFramework<ILibraryAppletS public: explicit ILibraryAppletSelfAccessor(Core::System& system_); ~ILibraryAppletSelfAccessor() override; + +private: + void PopInData(HLERequestContext& ctx); + void PushOutData(HLERequestContext& ctx); + void GetLibraryAppletInfo(HLERequestContext& ctx); + void ExitProcessAndReturn(HLERequestContext& ctx); + void GetCallerAppletIdentityInfo(HLERequestContext& ctx); + + void PushInShowAlbum(); + void PushInShowCabinetData(); + void PushInShowMiiEditData(); + + std::deque<std::vector<u8>> queue_data; +}; + +class IAppletCommonFunctions final : public ServiceFramework<IAppletCommonFunctions> { +public: + explicit IAppletCommonFunctions(Core::System& system_); + ~IAppletCommonFunctions() override; + +private: + void SetCpuBoostRequestPriority(HLERequestContext& ctx); }; class IApplicationFunctions final : public ServiceFramework<IApplicationFunctions> { @@ -378,6 +444,10 @@ class IProcessWindingController final : public ServiceFramework<IProcessWindingC public: explicit IProcessWindingController(Core::System& system_); ~IProcessWindingController() override; + +private: + void GetLaunchReason(HLERequestContext& ctx); + void OpenCallingLibraryApplet(HLERequestContext& ctx); }; void LoopProcess(Nvnflinger::Nvnflinger& nvnflinger, Core::System& system); diff --git a/src/core/hle/service/am/applet_ae.cpp b/src/core/hle/service/am/applet_ae.cpp index ee9d99a54..e30e6478a 100644 --- a/src/core/hle/service/am/applet_ae.cpp +++ b/src/core/hle/service/am/applet_ae.cpp @@ -27,9 +27,9 @@ public: {10, &ILibraryAppletProxy::GetProcessWindingController, "GetProcessWindingController"}, {11, &ILibraryAppletProxy::GetLibraryAppletCreator, "GetLibraryAppletCreator"}, {20, &ILibraryAppletProxy::OpenLibraryAppletSelfAccessor, "OpenLibraryAppletSelfAccessor"}, - {21, nullptr, "GetAppletCommonFunctions"}, - {22, nullptr, "GetHomeMenuFunctions"}, - {23, nullptr, "GetGlobalStateController"}, + {21, &ILibraryAppletProxy::GetAppletCommonFunctions, "GetAppletCommonFunctions"}, + {22, &ILibraryAppletProxy::GetHomeMenuFunctions, "GetHomeMenuFunctions"}, + {23, &ILibraryAppletProxy::GetGlobalStateController, "GetGlobalStateController"}, {1000, &ILibraryAppletProxy::GetDebugFunctions, "GetDebugFunctions"}, }; // clang-format on @@ -86,28 +86,52 @@ private: rb.PushIpcInterface<IProcessWindingController>(system); } - void GetDebugFunctions(HLERequestContext& ctx) { + void GetLibraryAppletCreator(HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "called"); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); - rb.PushIpcInterface<IDebugFunctions>(system); + rb.PushIpcInterface<ILibraryAppletCreator>(system); } - void GetLibraryAppletCreator(HLERequestContext& ctx) { + void OpenLibraryAppletSelfAccessor(HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "called"); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); - rb.PushIpcInterface<ILibraryAppletCreator>(system); + rb.PushIpcInterface<ILibraryAppletSelfAccessor>(system); } - void OpenLibraryAppletSelfAccessor(HLERequestContext& ctx) { + void GetAppletCommonFunctions(HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "called"); IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); - rb.PushIpcInterface<ILibraryAppletSelfAccessor>(system); + 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"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IDebugFunctions>(system); } Nvnflinger::Nvnflinger& nvnflinger; @@ -133,7 +157,7 @@ public: {20, &ISystemAppletProxy::GetHomeMenuFunctions, "GetHomeMenuFunctions"}, {21, &ISystemAppletProxy::GetGlobalStateController, "GetGlobalStateController"}, {22, &ISystemAppletProxy::GetApplicationCreator, "GetApplicationCreator"}, - {23, nullptr, "GetAppletCommonFunctions"}, + {23, &ISystemAppletProxy::GetAppletCommonFunctions, "GetAppletCommonFunctions"}, {1000, &ISystemAppletProxy::GetDebugFunctions, "GetDebugFunctions"}, }; // clang-format on @@ -182,14 +206,6 @@ private: rb.PushIpcInterface<IDisplayController>(system); } - void GetDebugFunctions(HLERequestContext& ctx) { - LOG_DEBUG(Service_AM, "called"); - - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface<IDebugFunctions>(system); - } - void GetLibraryAppletCreator(HLERequestContext& ctx) { LOG_DEBUG(Service_AM, "called"); @@ -222,6 +238,22 @@ private: rb.PushIpcInterface<IApplicationCreator>(system); } + void GetAppletCommonFunctions(HLERequestContext& ctx) { + LOG_DEBUG(Service_AM, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IAppletCommonFunctions>(system); + } + + void GetDebugFunctions(HLERequestContext& ctx) { + LOG_DEBUG(Service_AM, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IDebugFunctions>(system); + } + Nvnflinger::Nvnflinger& nvnflinger; std::shared_ptr<AppletMessageQueue> msg_queue; }; diff --git a/src/core/hle/service/am/applets/applet_cabinet.h b/src/core/hle/service/am/applets/applet_cabinet.h index b56427021..f498796f7 100644 --- a/src/core/hle/service/am/applets/applet_cabinet.h +++ b/src/core/hle/service/am/applets/applet_cabinet.h @@ -29,6 +29,15 @@ enum class CabinetAppletVersion : u32 { Version1 = 0x1, }; +enum class CabinetFlags : u8 { + None = 0, + DeviceHandle = 1 << 0, + TagInfo = 1 << 1, + RegisterInfo = 1 << 2, + All = DeviceHandle | TagInfo | RegisterInfo, +}; +DECLARE_ENUM_FLAG_OPERATORS(CabinetFlags) + enum class CabinetResult : u8 { Cancel = 0, TagInfo = 1 << 1, @@ -51,7 +60,7 @@ static_assert(sizeof(AmiiboSettingsStartParam) == 0x30, struct StartParamForAmiiboSettings { u8 param_1; Service::NFP::CabinetMode applet_mode; - u8 flags; + CabinetFlags flags; u8 amiibo_settings_1; u64 device_handle; Service::NFP::TagInfo tag_info; 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/am/applets/applet_general_backend.cpp b/src/core/hle/service/am/applets/applet_general_backend.cpp index 8b352020e..c0032f652 100644 --- a/src/core/hle/service/am/applets/applet_general_backend.cpp +++ b/src/core/hle/service/am/applets/applet_general_backend.cpp @@ -223,9 +223,9 @@ void StubApplet::Initialize() { const auto data = broker.PeekDataToAppletForDebug(); system.GetReporter().SaveUnimplementedAppletReport( - static_cast<u32>(id), common_args.arguments_version, common_args.library_version, - common_args.theme_color, common_args.play_startup_sound, common_args.system_tick, - data.normal, data.interactive); + static_cast<u32>(id), static_cast<u32>(common_args.arguments_version), + common_args.library_version, static_cast<u32>(common_args.theme_color), + common_args.play_startup_sound, common_args.system_tick, data.normal, data.interactive); LogCurrentStorage(broker, "Initialize"); } diff --git a/src/core/hle/service/am/applets/applet_mii_edit.cpp b/src/core/hle/service/am/applets/applet_mii_edit.cpp index 350a90818..50adc7c02 100644 --- a/src/core/hle/service/am/applets/applet_mii_edit.cpp +++ b/src/core/hle/service/am/applets/applet_mii_edit.cpp @@ -7,7 +7,9 @@ #include "core/frontend/applets/mii_edit.h" #include "core/hle/service/am/am.h" #include "core/hle/service/am/applets/applet_mii_edit.h" +#include "core/hle/service/mii/mii.h" #include "core/hle/service/mii/mii_manager.h" +#include "core/hle/service/sm/sm.h" namespace Service::AM::Applets { @@ -56,6 +58,12 @@ void MiiEdit::Initialize() { sizeof(MiiEditAppletInputV4)); break; } + + manager = system.ServiceManager().GetService<Mii::MiiDBModule>("mii:e")->GetMiiManager(); + if (manager == nullptr) { + manager = std::make_shared<Mii::MiiManager>(); + } + manager->Initialize(metadata); } bool MiiEdit::TransactionComplete() const { @@ -78,22 +86,46 @@ void MiiEdit::Execute() { // This is a default stub for each of the MiiEdit applet modes. switch (applet_input_common.applet_mode) { case MiiEditAppletMode::ShowMiiEdit: - case MiiEditAppletMode::AppendMii: case MiiEditAppletMode::AppendMiiImage: case MiiEditAppletMode::UpdateMiiImage: MiiEditOutput(MiiEditResult::Success, 0); break; - case MiiEditAppletMode::CreateMii: - case MiiEditAppletMode::EditMii: { - Mii::CharInfo char_info{}; + case MiiEditAppletMode::AppendMii: { Mii::StoreData store_data{}; - store_data.BuildBase(Mii::Gender::Male); - char_info.SetFromStoreData(store_data); + store_data.BuildRandom(Mii::Age::All, Mii::Gender::All, Mii::Race::All); + store_data.SetNickname({u'y', u'u', u'z', u'u'}); + store_data.SetChecksum(); + const auto result = manager->AddOrReplace(metadata, store_data); + + if (result.IsError()) { + MiiEditOutput(MiiEditResult::Cancel, 0); + break; + } + + s32 index = manager->FindIndex(store_data.GetCreateId(), false); + + if (index == -1) { + MiiEditOutput(MiiEditResult::Cancel, 0); + break; + } + + MiiEditOutput(MiiEditResult::Success, index); + break; + } + case MiiEditAppletMode::CreateMii: { + Mii::CharInfo char_info{}; + manager->BuildRandom(char_info, Mii::Age::All, Mii::Gender::All, Mii::Race::All); const MiiEditCharInfo edit_char_info{ - .mii_info{applet_input_common.applet_mode == MiiEditAppletMode::EditMii - ? applet_input_v4.char_info.mii_info - : char_info}, + .mii_info{char_info}, + }; + + MiiEditOutputForCharInfoEditing(MiiEditResult::Success, edit_char_info); + break; + } + case MiiEditAppletMode::EditMii: { + const MiiEditCharInfo edit_char_info{ + .mii_info{applet_input_v4.char_info.mii_info}, }; MiiEditOutputForCharInfoEditing(MiiEditResult::Success, edit_char_info); @@ -113,6 +145,8 @@ void MiiEdit::MiiEditOutput(MiiEditResult result, s32 index) { .index{index}, }; + LOG_INFO(Input, "called, result={}, index={}", result, index); + std::vector<u8> out_data(sizeof(MiiEditAppletOutput)); std::memcpy(out_data.data(), &applet_output, sizeof(MiiEditAppletOutput)); diff --git a/src/core/hle/service/am/applets/applet_mii_edit.h b/src/core/hle/service/am/applets/applet_mii_edit.h index 3f46fae1b..7ff34af49 100644 --- a/src/core/hle/service/am/applets/applet_mii_edit.h +++ b/src/core/hle/service/am/applets/applet_mii_edit.h @@ -11,6 +11,11 @@ namespace Core { class System; } // namespace Core +namespace Service::Mii { +struct DatabaseSessionMetadata; +class MiiManager; +} // namespace Service::Mii + namespace Service::AM::Applets { class MiiEdit final : public Applet { @@ -40,6 +45,8 @@ private: MiiEditAppletInputV4 applet_input_v4{}; bool is_complete{false}; + std::shared_ptr<Mii::MiiManager> manager = nullptr; + Mii::DatabaseSessionMetadata metadata{}; }; } // namespace Service::AM::Applets diff --git a/src/core/hle/service/am/applets/applets.cpp b/src/core/hle/service/am/applets/applets.cpp index 10afbc2da..89d5434af 100644 --- a/src/core/hle/service/am/applets/applets.cpp +++ b/src/core/hle/service/am/applets/applets.cpp @@ -199,6 +199,14 @@ const AppletFrontendSet& AppletManager::GetAppletFrontendSet() const { return frontend; } +NFP::CabinetMode AppletManager::GetCabinetMode() const { + return cabinet_mode; +} + +AppletId AppletManager::GetCurrentAppletId() const { + return current_applet_id; +} + void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) { if (set.cabinet != nullptr) { frontend.cabinet = std::move(set.cabinet); @@ -237,6 +245,14 @@ void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) { } } +void AppletManager::SetCabinetMode(NFP::CabinetMode mode) { + cabinet_mode = mode; +} + +void AppletManager::SetCurrentAppletId(AppletId applet_id) { + current_applet_id = applet_id; +} + void AppletManager::SetDefaultAppletFrontendSet() { ClearAll(); SetDefaultAppletsIfMissing(); diff --git a/src/core/hle/service/am/applets/applets.h b/src/core/hle/service/am/applets/applets.h index 12f374199..f02bbc450 100644 --- a/src/core/hle/service/am/applets/applets.h +++ b/src/core/hle/service/am/applets/applets.h @@ -34,6 +34,10 @@ class KEvent; class KReadableEvent; } // namespace Kernel +namespace Service::NFP { +enum class CabinetMode : u8; +} // namespace Service::NFP + namespace Service::AM { class IStorage; @@ -41,6 +45,8 @@ class IStorage; namespace Applets { enum class AppletId : u32 { + None = 0x00, + Application = 0x01, OverlayDisplay = 0x02, QLaunch = 0x03, Starter = 0x04, @@ -71,6 +77,32 @@ enum class LibraryAppletMode : u32 { AllForegroundInitiallyHidden = 4, }; +enum class CommonArgumentVersion : u32 { + Version0, + Version1, + Version2, + Version3, +}; + +enum class CommonArgumentSize : u32 { + Version3 = 0x20, +}; + +enum class ThemeColor : u32 { + BasicWhite = 0, + BasicBlack = 3, +}; + +struct CommonArguments { + CommonArgumentVersion arguments_version; + CommonArgumentSize size; + u32 library_version; + ThemeColor theme_color; + bool play_startup_sound; + u64_le system_tick; +}; +static_assert(sizeof(CommonArguments) == 0x20, "CommonArguments has incorrect size."); + class AppletDataBroker final { public: explicit AppletDataBroker(Core::System& system_, LibraryAppletMode applet_mode_); @@ -161,16 +193,6 @@ public: } protected: - struct CommonArguments { - u32_le arguments_version; - u32_le size; - u32_le library_version; - u32_le theme_color; - bool play_startup_sound; - u64_le system_tick; - }; - static_assert(sizeof(CommonArguments) == 0x20, "CommonArguments has incorrect size."); - CommonArguments common_args{}; AppletDataBroker broker; LibraryAppletMode applet_mode; @@ -219,8 +241,12 @@ public: ~AppletManager(); const AppletFrontendSet& GetAppletFrontendSet() const; + NFP::CabinetMode GetCabinetMode() const; + AppletId GetCurrentAppletId() const; void SetAppletFrontendSet(AppletFrontendSet set); + void SetCabinetMode(NFP::CabinetMode mode); + void SetCurrentAppletId(AppletId applet_id); void SetDefaultAppletFrontendSet(); void SetDefaultAppletsIfMissing(); void ClearAll(); @@ -228,6 +254,9 @@ public: std::shared_ptr<Applet> GetApplet(AppletId id, LibraryAppletMode mode) const; private: + AppletId current_applet_id{}; + NFP::CabinetMode cabinet_mode{}; + AppletFrontendSet frontend; Core::System& system; }; diff --git a/src/core/hle/service/aoc/aoc_u.cpp b/src/core/hle/service/aoc/aoc_u.cpp index 38c2138e8..7075ab800 100644 --- a/src/core/hle/service/aoc/aoc_u.cpp +++ b/src/core/hle/service/aoc/aoc_u.cpp @@ -22,6 +22,8 @@ namespace Service::AOC { +constexpr Result ResultNoPurchasedProductInfoAvailable{ErrorModule::NIMShop, 400}; + static bool CheckAOCTitleIDMatchesBase(u64 title_id, u64 base) { return FileSys::GetBaseTitleID(title_id) == base; } @@ -54,8 +56,8 @@ public: {0, &IPurchaseEventManager::SetDefaultDeliveryTarget, "SetDefaultDeliveryTarget"}, {1, &IPurchaseEventManager::SetDeliveryTarget, "SetDeliveryTarget"}, {2, &IPurchaseEventManager::GetPurchasedEventReadableHandle, "GetPurchasedEventReadableHandle"}, - {3, nullptr, "PopPurchasedProductInfo"}, - {4, nullptr, "PopPurchasedProductInfoWithUid"}, + {3, &IPurchaseEventManager::PopPurchasedProductInfo, "PopPurchasedProductInfo"}, + {4, &IPurchaseEventManager::PopPurchasedProductInfoWithUid, "PopPurchasedProductInfoWithUid"}, }; // clang-format on @@ -101,6 +103,20 @@ private: rb.PushCopyObjects(purchased_event->GetReadableEvent()); } + void PopPurchasedProductInfo(HLERequestContext& ctx) { + LOG_DEBUG(Service_AOC, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultNoPurchasedProductInfoAvailable); + } + + void PopPurchasedProductInfoWithUid(HLERequestContext& ctx) { + LOG_DEBUG(Service_AOC, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultNoPurchasedProductInfoAvailable); + } + KernelHelpers::ServiceContext service_context; Kernel::KEvent* purchased_event; diff --git a/src/core/hle/service/caps/caps.cpp b/src/core/hle/service/caps/caps.cpp index 610fe9940..286f9fd10 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>(); + + 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..e22f72bf6 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, 3}; + rb.Push(result); + rb.Push(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..2df6a930a --- /dev/null +++ b/src/core/hle/service/caps/caps_manager.cpp @@ -0,0 +1,342 @@ +// 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/hle/service/caps/caps_manager.h" +#include "core/hle/service/caps/caps_result.h" + +namespace Service::Capture { + +AlbumManager::AlbumManager() {} + +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, 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); + ApplicationAlbumFileEntry entry{.entry = + { + .size = entry_size, + .hash{}, + .datetime = file_id.date, + .storage = file_id.storage, + .content = contex_type, + .unknown = 1, + }, + .datetime = file_id.date, + .unknown = {}}; + 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<u16>(std::stoi(year)), + .month = static_cast<u8>(std::stoi(month)), + .day = static_cast<u8>(std::stoi(day)), + .hour = static_cast<u8>(std::stoi(hour)), + .minute = static_cast<u8>(std::stoi(minute)), + .second = static_cast<u8>(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; +} +} // 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..8337c655c --- /dev/null +++ b/src/core/hle/service/caps/caps_manager.h @@ -0,0 +1,72 @@ +// 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(); + ~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, 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; + + bool is_mounted{}; + std::unordered_map<AlbumFileId, std::filesystem::path> album_files; +}; + +} // 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..bf6061273 --- /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 { + u16 year{}; + u8 month{}; + u8 day{}; + u8 hour{}; + u8 minute{}; + u8 second{}; + u8 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..260f25490 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,10 +48,7 @@ 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>()}; @@ -75,26 +56,49 @@ void CAPS_U::GetAlbumContentsFileListForApplication(HLERequestContext& ctx) { const auto end_posix_time{rp.Pop<s64>()}; const auto applet_resource_user_id{rp.Pop<u64>()}; - // 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{}; + LOG_WARNING(Service_Capture, + "(STUBBED) called. pid={}, content_type={}, start_posix_time={}, " + "end_posix_time={}, applet_resource_user_id={}", + pid, content_type, start_posix_time, end_posix_time, applet_resource_user_id); + + // TODO: Translate posix to DateTime + + std::vector<ApplicationAlbumFileEntry> entries; + const Result result = + manager->GetAlbumFileList(entries, content_type, {}, {}, applet_resource_user_id); - 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); + 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}; + const auto pid{rp.Pop<s32>()}; + const auto content_type{rp.PopEnum<ContentType>()}; + const auto start_date_time{rp.PopRaw<AlbumFileDateTime>()}; + const auto end_date_time{rp.PopRaw<AlbumFileDateTime>()}; + const auto applet_resource_user_id{rp.Pop<u64>()}; + + LOG_WARNING(Service_Capture, + "(STUBBED) called. pid={}, content_type={}, applet_resource_user_id={}", pid, + content_type, applet_resource_user_id); + + std::vector<ApplicationAlbumFileEntry> entries; + const Result result = manager->GetAlbumFileList(entries, content_type, start_date_time, + end_date_time, 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/filesystem/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp_srv.cpp index 6e4d26b1e..126cd6ffd 100644 --- a/src/core/hle/service/filesystem/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp_srv.cpp @@ -329,6 +329,7 @@ public: {13, &IFileSystem::CleanDirectoryRecursively, "CleanDirectoryRecursively"}, {14, &IFileSystem::GetFileTimeStampRaw, "GetFileTimeStampRaw"}, {15, nullptr, "QueryEntry"}, + {16, &IFileSystem::GetFileSystemAttribute, "GetFileSystemAttribute"}, }; RegisterHandlers(functions); } @@ -521,6 +522,46 @@ public: rb.PushRaw(vfs_timestamp); } + void GetFileSystemAttribute(HLERequestContext& ctx) { + LOG_WARNING(Service_FS, "(STUBBED) called"); + + struct FileSystemAttribute { + u8 dir_entry_name_length_max_defined; + u8 file_entry_name_length_max_defined; + u8 dir_path_name_length_max_defined; + u8 file_path_name_length_max_defined; + INSERT_PADDING_BYTES_NOINIT(0x5); + u8 utf16_dir_entry_name_length_max_defined; + u8 utf16_file_entry_name_length_max_defined; + u8 utf16_dir_path_name_length_max_defined; + u8 utf16_file_path_name_length_max_defined; + INSERT_PADDING_BYTES_NOINIT(0x18); + s32 dir_entry_name_length_max; + s32 file_entry_name_length_max; + s32 dir_path_name_length_max; + s32 file_path_name_length_max; + INSERT_PADDING_WORDS_NOINIT(0x5); + s32 utf16_dir_entry_name_length_max; + s32 utf16_file_entry_name_length_max; + s32 utf16_dir_path_name_length_max; + s32 utf16_file_path_name_length_max; + INSERT_PADDING_WORDS_NOINIT(0x18); + INSERT_PADDING_WORDS_NOINIT(0x1); + }; + static_assert(sizeof(FileSystemAttribute) == 0xc0, + "FileSystemAttribute has incorrect size"); + + FileSystemAttribute savedata_attribute{}; + savedata_attribute.dir_entry_name_length_max_defined = true; + savedata_attribute.file_entry_name_length_max_defined = true; + savedata_attribute.dir_entry_name_length_max = 0x40; + savedata_attribute.file_entry_name_length_max = 0x40; + + IPC::ResponseBuilder rb{ctx, 50}; + rb.Push(ResultSuccess); + rb.PushRaw(savedata_attribute); + } + private: VfsDirectoryServiceWrapper backend; SizeGetter size; @@ -698,7 +739,7 @@ FSP_SRV::FSP_SRV(Core::System& system_) {19, nullptr, "FormatSdCardFileSystem"}, {21, nullptr, "DeleteSaveDataFileSystem"}, {22, &FSP_SRV::CreateSaveDataFileSystem, "CreateSaveDataFileSystem"}, - {23, nullptr, "CreateSaveDataFileSystemBySystemSaveDataId"}, + {23, &FSP_SRV::CreateSaveDataFileSystemBySystemSaveDataId, "CreateSaveDataFileSystemBySystemSaveDataId"}, {24, nullptr, "RegisterSaveDataFileSystemAtomicDeletion"}, {25, nullptr, "DeleteSaveDataFileSystemBySaveDataSpaceId"}, {26, nullptr, "FormatSdCardDryRun"}, @@ -712,7 +753,7 @@ FSP_SRV::FSP_SRV(Core::System& system_) {35, nullptr, "CreateSaveDataFileSystemByHashSalt"}, {36, nullptr, "OpenHostFileSystemWithOption"}, {51, &FSP_SRV::OpenSaveDataFileSystem, "OpenSaveDataFileSystem"}, - {52, nullptr, "OpenSaveDataFileSystemBySystemSaveDataId"}, + {52, &FSP_SRV::OpenSaveDataFileSystemBySystemSaveDataId, "OpenSaveDataFileSystemBySystemSaveDataId"}, {53, &FSP_SRV::OpenReadOnlySaveDataFileSystem, "OpenReadOnlySaveDataFileSystem"}, {57, nullptr, "ReadSaveDataFileSystemExtraDataBySaveDataSpaceId"}, {58, nullptr, "ReadSaveDataFileSystemExtraData"}, @@ -814,6 +855,9 @@ FSP_SRV::FSP_SRV(Core::System& system_) if (Settings::values.enable_fs_access_log) { access_log_mode = AccessLogMode::SdCard; } + + // This should be true on creation + fsc.SetAutoSaveDataCreation(true); } FSP_SRV::~FSP_SRV() = default; @@ -870,6 +914,21 @@ void FSP_SRV::CreateSaveDataFileSystem(HLERequestContext& ctx) { rb.Push(ResultSuccess); } +void FSP_SRV::CreateSaveDataFileSystemBySystemSaveDataId(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + auto save_struct = rp.PopRaw<FileSys::SaveDataAttribute>(); + [[maybe_unused]] auto save_create_struct = rp.PopRaw<std::array<u8, 0x40>>(); + + LOG_DEBUG(Service_FS, "called save_struct = {}", save_struct.DebugInfo()); + + FileSys::VirtualDir save_data_dir{}; + fsc.CreateSaveData(&save_data_dir, FileSys::SaveDataSpaceId::NandSystem, save_struct); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void FSP_SRV::OpenSaveDataFileSystem(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; @@ -916,6 +975,11 @@ void FSP_SRV::OpenSaveDataFileSystem(HLERequestContext& ctx) { rb.PushIpcInterface<IFileSystem>(std::move(filesystem)); } +void FSP_SRV::OpenSaveDataFileSystemBySystemSaveDataId(HLERequestContext& ctx) { + LOG_WARNING(Service_FS, "(STUBBED) called, delegating to 51 OpenSaveDataFilesystem"); + OpenSaveDataFileSystem(ctx); +} + void FSP_SRV::OpenReadOnlySaveDataFileSystem(HLERequestContext& ctx) { LOG_WARNING(Service_FS, "(STUBBED) called, delegating to 51 OpenSaveDataFilesystem"); OpenSaveDataFileSystem(ctx); diff --git a/src/core/hle/service/filesystem/fsp_srv.h b/src/core/hle/service/filesystem/fsp_srv.h index 4f3c2f6de..280bc9867 100644 --- a/src/core/hle/service/filesystem/fsp_srv.h +++ b/src/core/hle/service/filesystem/fsp_srv.h @@ -39,7 +39,9 @@ private: void OpenFileSystemWithPatch(HLERequestContext& ctx); void OpenSdCardFileSystem(HLERequestContext& ctx); void CreateSaveDataFileSystem(HLERequestContext& ctx); + void CreateSaveDataFileSystemBySystemSaveDataId(HLERequestContext& ctx); void OpenSaveDataFileSystem(HLERequestContext& ctx); + void OpenSaveDataFileSystemBySystemSaveDataId(HLERequestContext& ctx); void OpenReadOnlySaveDataFileSystem(HLERequestContext& ctx); void OpenSaveDataInfoReaderBySaveDataSpaceId(HLERequestContext& ctx); void OpenSaveDataInfoReaderOnlyCacheStorage(HLERequestContext& ctx); diff --git a/src/core/hle/service/hid/controllers/npad.cpp b/src/core/hle/service/hid/controllers/npad.cpp index 146bb486d..bc822f19e 100644 --- a/src/core/hle/service/hid/controllers/npad.cpp +++ b/src/core/hle/service/hid/controllers/npad.cpp @@ -346,6 +346,7 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) { } SignalStyleSetChangedEvent(npad_id); WriteEmptyEntry(controller.shared_memory); + hid_core.SetLastActiveController(npad_id); } void Controller_NPad::OnInit() { diff --git a/src/core/hle/service/ldn/ldn.cpp b/src/core/hle/service/ldn/ldn.cpp index 9d149a7cd..7927f8264 100644 --- a/src/core/hle/service/ldn/ldn.cpp +++ b/src/core/hle/service/ldn/ldn.cpp @@ -23,19 +23,39 @@ public: explicit IMonitorService(Core::System& system_) : ServiceFramework{system_, "IMonitorService"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "GetStateForMonitor"}, + {0, &IMonitorService::GetStateForMonitor, "GetStateForMonitor"}, {1, nullptr, "GetNetworkInfoForMonitor"}, {2, nullptr, "GetIpv4AddressForMonitor"}, {3, nullptr, "GetDisconnectReasonForMonitor"}, {4, nullptr, "GetSecurityParameterForMonitor"}, {5, nullptr, "GetNetworkConfigForMonitor"}, - {100, nullptr, "InitializeMonitor"}, + {100, &IMonitorService::InitializeMonitor, "InitializeMonitor"}, {101, nullptr, "FinalizeMonitor"}, }; // clang-format on RegisterHandlers(functions); } + +private: + void GetStateForMonitor(HLERequestContext& ctx) { + LOG_INFO(Service_LDN, "called"); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.PushEnum(state); + } + + void InitializeMonitor(HLERequestContext& ctx) { + LOG_INFO(Service_LDN, "called"); + + state = State::Initialized; + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + State state{State::None}; }; class LDNM final : public ServiceFramework<LDNM> { @@ -731,14 +751,81 @@ public: } }; +class ISfMonitorService final : public ServiceFramework<ISfMonitorService> { +public: + explicit ISfMonitorService(Core::System& system_) + : ServiceFramework{system_, "ISfMonitorService"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, &ISfMonitorService::Initialize, "Initialize"}, + {288, &ISfMonitorService::GetGroupInfo, "GetGroupInfo"}, + {320, nullptr, "GetLinkLevel"}, + }; + // clang-format on + + RegisterHandlers(functions); + } + +private: + void Initialize(HLERequestContext& ctx) { + LOG_WARNING(Service_LDN, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(0); + } + + void GetGroupInfo(HLERequestContext& ctx) { + LOG_WARNING(Service_LDN, "(STUBBED) called"); + + struct GroupInfo { + std::array<u8, 0x200> info; + }; + + GroupInfo group_info{}; + + ctx.WriteBuffer(group_info); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } +}; + +class LP2PM final : public ServiceFramework<LP2PM> { +public: + explicit LP2PM(Core::System& system_) : ServiceFramework{system_, "lp2p:m"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, &LP2PM::CreateMonitorService, "CreateMonitorService"}, + }; + // clang-format on + + RegisterHandlers(functions); + } + +private: + void CreateMonitorService(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u64 reserved_input = rp.Pop<u64>(); + + LOG_INFO(Service_LDN, "called, reserved_input={}", reserved_input); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<ISfMonitorService>(system); + } +}; + void LoopProcess(Core::System& system) { auto server_manager = std::make_unique<ServerManager>(system); server_manager->RegisterNamedService("ldn:m", std::make_shared<LDNM>(system)); server_manager->RegisterNamedService("ldn:s", std::make_shared<LDNS>(system)); server_manager->RegisterNamedService("ldn:u", std::make_shared<LDNU>(system)); + server_manager->RegisterNamedService("lp2p:app", std::make_shared<LP2PAPP>(system)); server_manager->RegisterNamedService("lp2p:sys", std::make_shared<LP2PSYS>(system)); + server_manager->RegisterNamedService("lp2p:m", std::make_shared<LP2PM>(system)); + ServerManager::RunServer(std::move(server_manager)); } diff --git a/src/core/hle/service/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp index 3b83c5ed7..c28eed926 100644 --- a/src/core/hle/service/mii/mii.cpp +++ b/src/core/hle/service/mii/mii.cpp @@ -8,6 +8,9 @@ #include "core/hle/service/mii/mii.h" #include "core/hle/service/mii/mii_manager.h" #include "core/hle/service/mii/mii_result.h" +#include "core/hle/service/mii/types/char_info.h" +#include "core/hle/service/mii/types/store_data.h" +#include "core/hle/service/mii/types/ver3_store_data.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/service.h" @@ -15,8 +18,10 @@ namespace Service::Mii { class IDatabaseService final : public ServiceFramework<IDatabaseService> { public: - explicit IDatabaseService(Core::System& system_, bool is_system_) - : ServiceFramework{system_, "IDatabaseService"}, is_system{is_system_} { + explicit IDatabaseService(Core::System& system_, std::shared_ptr<MiiManager> mii_manager, + bool is_system_) + : ServiceFramework{system_, "IDatabaseService"}, manager{mii_manager}, is_system{ + is_system_} { // clang-format off static const FunctionInfo functions[] = { {0, &IDatabaseService::IsUpdated, "IsUpdated"}, @@ -27,29 +32,31 @@ public: {5, &IDatabaseService::UpdateLatest, "UpdateLatest"}, {6, &IDatabaseService::BuildRandom, "BuildRandom"}, {7, &IDatabaseService::BuildDefault, "BuildDefault"}, - {8, nullptr, "Get2"}, - {9, nullptr, "Get3"}, - {10, nullptr, "UpdateLatest1"}, - {11, nullptr, "FindIndex"}, - {12, nullptr, "Move"}, - {13, nullptr, "AddOrReplace"}, - {14, nullptr, "Delete"}, - {15, nullptr, "DestroyFile"}, - {16, nullptr, "DeleteFile"}, - {17, nullptr, "Format"}, + {8, &IDatabaseService::Get2, "Get2"}, + {9, &IDatabaseService::Get3, "Get3"}, + {10, &IDatabaseService::UpdateLatest1, "UpdateLatest1"}, + {11, &IDatabaseService::FindIndex, "FindIndex"}, + {12, &IDatabaseService::Move, "Move"}, + {13, &IDatabaseService::AddOrReplace, "AddOrReplace"}, + {14, &IDatabaseService::Delete, "Delete"}, + {15, &IDatabaseService::DestroyFile, "DestroyFile"}, + {16, &IDatabaseService::DeleteFile, "DeleteFile"}, + {17, &IDatabaseService::Format, "Format"}, {18, nullptr, "Import"}, {19, nullptr, "Export"}, - {20, nullptr, "IsBrokenDatabaseWithClearFlag"}, + {20, &IDatabaseService::IsBrokenDatabaseWithClearFlag, "IsBrokenDatabaseWithClearFlag"}, {21, &IDatabaseService::GetIndex, "GetIndex"}, {22, &IDatabaseService::SetInterfaceVersion, "SetInterfaceVersion"}, {23, &IDatabaseService::Convert, "Convert"}, - {24, nullptr, "ConvertCoreDataToCharInfo"}, - {25, nullptr, "ConvertCharInfoToCoreData"}, - {26, nullptr, "Append"}, + {24, &IDatabaseService::ConvertCoreDataToCharInfo, "ConvertCoreDataToCharInfo"}, + {25, &IDatabaseService::ConvertCharInfoToCoreData, "ConvertCharInfoToCoreData"}, + {26, &IDatabaseService::Append, "Append"}, }; // clang-format on RegisterHandlers(functions); + + manager->Initialize(metadata); } private: @@ -59,7 +66,7 @@ private: LOG_DEBUG(Service_Mii, "called with source_flag={}", source_flag); - const bool is_updated = manager.IsUpdated(metadata, source_flag); + const bool is_updated = manager->IsUpdated(metadata, source_flag); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); @@ -69,7 +76,7 @@ private: void IsFullDatabase(HLERequestContext& ctx) { LOG_DEBUG(Service_Mii, "called"); - const bool is_full_database = manager.IsFullDatabase(); + const bool is_full_database = manager->IsFullDatabase(); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); @@ -80,9 +87,9 @@ private: IPC::RequestParser rp{ctx}; const auto source_flag{rp.PopRaw<SourceFlag>()}; - LOG_DEBUG(Service_Mii, "called with source_flag={}", source_flag); + const u32 mii_count = manager->GetCount(metadata, source_flag); - const u32 mii_count = manager.GetCount(metadata, source_flag); + LOG_DEBUG(Service_Mii, "called with source_flag={}, mii_count={}", source_flag, mii_count); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); @@ -94,16 +101,17 @@ private: const auto source_flag{rp.PopRaw<SourceFlag>()}; const auto output_size{ctx.GetWriteBufferNumElements<CharInfoElement>()}; - LOG_DEBUG(Service_Mii, "called with source_flag={}, out_size={}", source_flag, output_size); - u32 mii_count{}; std::vector<CharInfoElement> char_info_elements(output_size); - Result result = manager.Get(metadata, char_info_elements, mii_count, source_flag); + const auto result = manager->Get(metadata, char_info_elements, mii_count, source_flag); if (mii_count != 0) { ctx.WriteBuffer(char_info_elements); } + LOG_INFO(Service_Mii, "called with source_flag={}, out_size={}, mii_count={}", source_flag, + output_size, mii_count); + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(result); rb.Push(mii_count); @@ -114,16 +122,17 @@ private: const auto source_flag{rp.PopRaw<SourceFlag>()}; const auto output_size{ctx.GetWriteBufferNumElements<CharInfo>()}; - LOG_DEBUG(Service_Mii, "called with source_flag={}, out_size={}", source_flag, output_size); - u32 mii_count{}; std::vector<CharInfo> char_info(output_size); - Result result = manager.Get(metadata, char_info, mii_count, source_flag); + const auto result = manager->Get(metadata, char_info, mii_count, source_flag); if (mii_count != 0) { ctx.WriteBuffer(char_info); } + LOG_INFO(Service_Mii, "called with source_flag={}, out_size={}, mii_count={}", source_flag, + output_size, mii_count); + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(result); rb.Push(mii_count); @@ -134,10 +143,10 @@ private: const auto char_info{rp.PopRaw<CharInfo>()}; const auto source_flag{rp.PopRaw<SourceFlag>()}; - LOG_DEBUG(Service_Mii, "called with source_flag={}", source_flag); + LOG_INFO(Service_Mii, "called with source_flag={}", source_flag); CharInfo new_char_info{}; - const auto result = manager.UpdateLatest(metadata, new_char_info, char_info, source_flag); + const auto result = manager->UpdateLatest(metadata, new_char_info, char_info, source_flag); if (result.IsFailure()) { IPC::ResponseBuilder rb{ctx, 2}; rb.Push(result); @@ -146,7 +155,7 @@ private: IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(ResultSuccess); - rb.PushRaw<CharInfo>(new_char_info); + rb.PushRaw(new_char_info); } void BuildRandom(HLERequestContext& ctx) { @@ -176,18 +185,18 @@ private: } CharInfo char_info{}; - manager.BuildRandom(char_info, age, gender, race); + manager->BuildRandom(char_info, age, gender, race); IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(ResultSuccess); - rb.PushRaw<CharInfo>(char_info); + rb.PushRaw(char_info); } void BuildDefault(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto index{rp.Pop<u32>()}; - LOG_INFO(Service_Mii, "called with index={}", index); + LOG_DEBUG(Service_Mii, "called with index={}", index); if (index > 5) { IPC::ResponseBuilder rb{ctx, 2}; @@ -196,11 +205,243 @@ private: } CharInfo char_info{}; - manager.BuildDefault(char_info, index); + manager->BuildDefault(char_info, index); IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(ResultSuccess); - rb.PushRaw<CharInfo>(char_info); + rb.PushRaw(char_info); + } + + void Get2(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto source_flag{rp.PopRaw<SourceFlag>()}; + const auto output_size{ctx.GetWriteBufferNumElements<StoreDataElement>()}; + + u32 mii_count{}; + std::vector<StoreDataElement> store_data_elements(output_size); + const auto result = manager->Get(metadata, store_data_elements, mii_count, source_flag); + + if (mii_count != 0) { + ctx.WriteBuffer(store_data_elements); + } + + LOG_INFO(Service_Mii, "called with source_flag={}, out_size={}, mii_count={}", source_flag, + output_size, mii_count); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(result); + rb.Push(mii_count); + } + + void Get3(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto source_flag{rp.PopRaw<SourceFlag>()}; + const auto output_size{ctx.GetWriteBufferNumElements<StoreData>()}; + + u32 mii_count{}; + std::vector<StoreData> store_data(output_size); + const auto result = manager->Get(metadata, store_data, mii_count, source_flag); + + if (mii_count != 0) { + ctx.WriteBuffer(store_data); + } + + LOG_INFO(Service_Mii, "called with source_flag={}, out_size={}, mii_count={}", source_flag, + output_size, mii_count); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(result); + rb.Push(mii_count); + } + + void UpdateLatest1(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto store_data{rp.PopRaw<StoreData>()}; + const auto source_flag{rp.PopRaw<SourceFlag>()}; + + LOG_INFO(Service_Mii, "called with source_flag={}", source_flag); + + Result result = ResultSuccess; + if (!is_system) { + result = ResultPermissionDenied; + } + + StoreData new_store_data{}; + if (result.IsSuccess()) { + result = manager->UpdateLatest(metadata, new_store_data, store_data, source_flag); + } + + if (result.IsFailure()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + IPC::ResponseBuilder rb{ctx, 2 + sizeof(StoreData) / sizeof(u32)}; + rb.Push(ResultSuccess); + rb.PushRaw<StoreData>(new_store_data); + } + + void FindIndex(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto create_id{rp.PopRaw<Common::UUID>()}; + const auto is_special{rp.PopRaw<bool>()}; + + LOG_INFO(Service_Mii, "called with create_id={}, is_special={}", + create_id.FormattedString(), is_special); + + const s32 index = manager->FindIndex(create_id, is_special); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(index); + } + + void Move(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto create_id{rp.PopRaw<Common::UUID>()}; + const auto new_index{rp.PopRaw<s32>()}; + + LOG_INFO(Service_Mii, "called with create_id={}, new_index={}", create_id.FormattedString(), + new_index); + + Result result = ResultSuccess; + if (!is_system) { + result = ResultPermissionDenied; + } + + if (result.IsSuccess()) { + const u32 count = manager->GetCount(metadata, SourceFlag::Database); + if (new_index < 0 || new_index >= static_cast<s32>(count)) { + result = ResultInvalidArgument; + } + } + + if (result.IsSuccess()) { + result = manager->Move(metadata, new_index, create_id); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void AddOrReplace(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto store_data{rp.PopRaw<StoreData>()}; + + LOG_INFO(Service_Mii, "called"); + + Result result = ResultSuccess; + + if (!is_system) { + result = ResultPermissionDenied; + } + + if (result.IsSuccess()) { + result = manager->AddOrReplace(metadata, store_data); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void Delete(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto create_id{rp.PopRaw<Common::UUID>()}; + + LOG_INFO(Service_Mii, "called, create_id={}", create_id.FormattedString()); + + Result result = ResultSuccess; + + if (!is_system) { + result = ResultPermissionDenied; + } + + if (result.IsSuccess()) { + result = manager->Delete(metadata, create_id); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void DestroyFile(HLERequestContext& ctx) { + // This calls nn::settings::fwdbg::GetSettingsItemValue("is_db_test_mode_enabled"); + const bool is_db_test_mode_enabled = false; + + LOG_INFO(Service_Mii, "called is_db_test_mode_enabled={}", is_db_test_mode_enabled); + + Result result = ResultSuccess; + + if (!is_db_test_mode_enabled) { + result = ResultTestModeOnly; + } + + if (result.IsSuccess()) { + result = manager->DestroyFile(metadata); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void DeleteFile(HLERequestContext& ctx) { + // This calls nn::settings::fwdbg::GetSettingsItemValue("is_db_test_mode_enabled"); + const bool is_db_test_mode_enabled = false; + + LOG_INFO(Service_Mii, "called is_db_test_mode_enabled={}", is_db_test_mode_enabled); + + Result result = ResultSuccess; + + if (!is_db_test_mode_enabled) { + result = ResultTestModeOnly; + } + + if (result.IsSuccess()) { + result = manager->DeleteFile(); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void Format(HLERequestContext& ctx) { + // This calls nn::settings::fwdbg::GetSettingsItemValue("is_db_test_mode_enabled"); + const bool is_db_test_mode_enabled = false; + + LOG_INFO(Service_Mii, "called is_db_test_mode_enabled={}", is_db_test_mode_enabled); + + Result result = ResultSuccess; + + if (!is_db_test_mode_enabled) { + result = ResultTestModeOnly; + } + + if (result.IsSuccess()) { + result = manager->Format(metadata); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + + void IsBrokenDatabaseWithClearFlag(HLERequestContext& ctx) { + LOG_DEBUG(Service_Mii, "called"); + + bool is_broken_with_clear_flag = false; + Result result = ResultSuccess; + + if (!is_system) { + result = ResultPermissionDenied; + } + + if (result.IsSuccess()) { + is_broken_with_clear_flag = manager->IsBrokenWithClearFlag(metadata); + } + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(result); + rb.Push<u8>(is_broken_with_clear_flag); } void GetIndex(HLERequestContext& ctx) { @@ -210,7 +451,7 @@ private: LOG_DEBUG(Service_Mii, "called"); s32 index{}; - const auto result = manager.GetIndex(metadata, info, index); + const auto result = manager->GetIndex(metadata, info, index); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(result); @@ -223,7 +464,7 @@ private: LOG_INFO(Service_Mii, "called, interface_version={:08X}", interface_version); - manager.SetInterfaceVersion(metadata, interface_version); + manager->SetInterfaceVersion(metadata, interface_version); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); @@ -236,51 +477,96 @@ private: LOG_INFO(Service_Mii, "called"); CharInfo char_info{}; - manager.ConvertV3ToCharInfo(char_info, mii_v3); + const auto result = manager->ConvertV3ToCharInfo(char_info, mii_v3); IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; - rb.Push(ResultSuccess); + rb.Push(result); rb.PushRaw<CharInfo>(char_info); } - MiiManager manager{}; - DatabaseSessionMetadata metadata{}; - bool is_system{}; -}; + void ConvertCoreDataToCharInfo(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto core_data{rp.PopRaw<CoreData>()}; -class MiiDBModule final : public ServiceFramework<MiiDBModule> { -public: - explicit MiiDBModule(Core::System& system_, const char* name_, bool is_system_) - : ServiceFramework{system_, name_}, is_system{is_system_} { - // clang-format off - static const FunctionInfo functions[] = { - {0, &MiiDBModule::GetDatabaseService, "GetDatabaseService"}, - }; - // clang-format on + LOG_INFO(Service_Mii, "called"); - RegisterHandlers(functions); + CharInfo char_info{}; + const auto result = manager->ConvertCoreDataToCharInfo(char_info, core_data); + + IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; + rb.Push(result); + rb.PushRaw<CharInfo>(char_info); } -private: - void GetDatabaseService(HLERequestContext& ctx) { - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface<IDatabaseService>(system, is_system); + void ConvertCharInfoToCoreData(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto char_info{rp.PopRaw<CharInfo>()}; - LOG_DEBUG(Service_Mii, "called"); + LOG_INFO(Service_Mii, "called"); + + CoreData core_data{}; + const auto result = manager->ConvertCharInfoToCoreData(core_data, char_info); + + IPC::ResponseBuilder rb{ctx, 2 + sizeof(CoreData) / sizeof(u32)}; + rb.Push(result); + rb.PushRaw<CoreData>(core_data); + } + + void Append(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto char_info{rp.PopRaw<CharInfo>()}; + + LOG_INFO(Service_Mii, "called"); + + const auto result = manager->Append(metadata, char_info); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); } + std::shared_ptr<MiiManager> manager = nullptr; + DatabaseSessionMetadata metadata{}; bool is_system{}; }; +MiiDBModule::MiiDBModule(Core::System& system_, const char* name_, + std::shared_ptr<MiiManager> mii_manager, bool is_system_) + : ServiceFramework{system_, name_}, manager{mii_manager}, is_system{is_system_} { + // clang-format off + static const FunctionInfo functions[] = { + {0, &MiiDBModule::GetDatabaseService, "GetDatabaseService"}, + }; + // clang-format on + + RegisterHandlers(functions); + + if (manager == nullptr) { + manager = std::make_shared<MiiManager>(); + } +} + +MiiDBModule::~MiiDBModule() = default; + +void MiiDBModule::GetDatabaseService(HLERequestContext& ctx) { + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IDatabaseService>(system, manager, is_system); + + LOG_DEBUG(Service_Mii, "called"); +} + +std::shared_ptr<MiiManager> MiiDBModule::GetMiiManager() { + return manager; +} + class MiiImg final : public ServiceFramework<MiiImg> { public: explicit MiiImg(Core::System& system_) : ServiceFramework{system_, "miiimg"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "Initialize"}, + {0, &MiiImg::Initialize, "Initialize"}, {10, nullptr, "Reload"}, - {11, nullptr, "GetCount"}, + {11, &MiiImg::GetCount, "GetCount"}, {12, nullptr, "IsEmpty"}, {13, nullptr, "IsFull"}, {14, nullptr, "GetAttribute"}, @@ -297,15 +583,32 @@ public: RegisterHandlers(functions); } + +private: + void Initialize(HLERequestContext& ctx) { + LOG_INFO(Service_Mii, "called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void GetCount(HLERequestContext& ctx) { + LOG_DEBUG(Service_Mii, "called"); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + rb.Push(0); + } }; void LoopProcess(Core::System& system) { auto server_manager = std::make_unique<ServerManager>(system); + std::shared_ptr<MiiManager> manager = nullptr; - server_manager->RegisterNamedService("mii:e", - std::make_shared<MiiDBModule>(system, "mii:e", true)); - server_manager->RegisterNamedService("mii:u", - std::make_shared<MiiDBModule>(system, "mii:u", false)); + server_manager->RegisterNamedService( + "mii:e", std::make_shared<MiiDBModule>(system, "mii:e", manager, true)); + server_manager->RegisterNamedService( + "mii:u", std::make_shared<MiiDBModule>(system, "mii:u", manager, false)); server_manager->RegisterNamedService("miiimg", std::make_shared<MiiImg>(system)); ServerManager::RunServer(std::move(server_manager)); } diff --git a/src/core/hle/service/mii/mii.h b/src/core/hle/service/mii/mii.h index ed4e3f62b..9aa4426f6 100644 --- a/src/core/hle/service/mii/mii.h +++ b/src/core/hle/service/mii/mii.h @@ -3,11 +3,29 @@ #pragma once +#include "core/hle/service/service.h" + namespace Core { class System; } namespace Service::Mii { +class MiiManager; + +class MiiDBModule final : public ServiceFramework<MiiDBModule> { +public: + explicit MiiDBModule(Core::System& system_, const char* name_, + std::shared_ptr<MiiManager> mii_manager, bool is_system_); + ~MiiDBModule() override; + + std::shared_ptr<MiiManager> GetMiiManager(); + +private: + void GetDatabaseService(HLERequestContext& ctx); + + std::shared_ptr<MiiManager> manager = nullptr; + bool is_system{}; +}; void LoopProcess(Core::System& system); diff --git a/src/core/hle/service/mii/mii_database.cpp b/src/core/hle/service/mii/mii_database.cpp new file mode 100644 index 000000000..3803e58e2 --- /dev/null +++ b/src/core/hle/service/mii/mii_database.cpp @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/mii/mii_database.h" +#include "core/hle/service/mii/mii_result.h" +#include "core/hle/service/mii/mii_util.h" + +namespace Service::Mii { + +u8 NintendoFigurineDatabase::GetDatabaseLength() const { + return database_length; +} + +bool NintendoFigurineDatabase::IsFull() const { + return database_length >= MaxDatabaseLength; +} + +StoreData NintendoFigurineDatabase::Get(std::size_t index) const { + StoreData store_data = miis.at(index); + + // This hack is to make external database dumps compatible + store_data.SetDeviceChecksum(); + + return store_data; +} + +u32 NintendoFigurineDatabase::GetCount(const DatabaseSessionMetadata& metadata) const { + if (magic == MiiMagic) { + return GetDatabaseLength(); + } + + u32 mii_count{}; + for (std::size_t index = 0; index < mii_count; ++index) { + const auto& store_data = Get(index); + if (!store_data.IsSpecial()) { + mii_count++; + } + } + + return mii_count; +} + +bool NintendoFigurineDatabase::GetIndexByCreatorId(u32& out_index, + const Common::UUID& create_id) const { + for (std::size_t index = 0; index < database_length; ++index) { + if (miis[index].GetCreateId() == create_id) { + out_index = static_cast<u32>(index); + return true; + } + } + + return false; +} + +Result NintendoFigurineDatabase::Move(u32 current_index, u32 new_index) { + if (current_index == new_index) { + return ResultNotUpdated; + } + + const StoreData store_data = miis[current_index]; + + if (new_index > current_index) { + // Shift left + const u32 index_diff = new_index - current_index; + for (std::size_t i = 0; i < index_diff; i++) { + miis[current_index + i] = miis[current_index + i + 1]; + } + } else { + // Shift right + const u32 index_diff = current_index - new_index; + for (std::size_t i = 0; i < index_diff; i++) { + miis[current_index - i] = miis[current_index - i - 1]; + } + } + + miis[new_index] = store_data; + crc = GenerateDatabaseCrc(); + return ResultSuccess; +} + +void NintendoFigurineDatabase::Replace(u32 index, const StoreData& store_data) { + miis[index] = store_data; + crc = GenerateDatabaseCrc(); +} + +void NintendoFigurineDatabase::Add(const StoreData& store_data) { + miis[database_length] = store_data; + database_length++; + crc = GenerateDatabaseCrc(); +} + +void NintendoFigurineDatabase::Delete(u32 index) { + // Shift left + const s32 new_database_size = database_length - 1; + if (static_cast<s32>(index) < new_database_size) { + for (std::size_t i = index; i < static_cast<std::size_t>(new_database_size); i++) { + miis[i] = miis[i + 1]; + } + } + + database_length = static_cast<u8>(new_database_size); + crc = GenerateDatabaseCrc(); +} + +void NintendoFigurineDatabase::CleanDatabase() { + miis = {}; + version = 1; + magic = DatabaseMagic; + database_length = 0; + crc = GenerateDatabaseCrc(); +} + +void NintendoFigurineDatabase::CorruptCrc() { + crc = GenerateDatabaseCrc(); + crc = ~crc; +} + +Result NintendoFigurineDatabase::CheckIntegrity() { + if (magic != DatabaseMagic) { + return ResultInvalidDatabaseSignature; + } + + if (version != 1) { + return ResultInvalidDatabaseVersion; + } + + if (crc != GenerateDatabaseCrc()) { + return ResultInvalidDatabaseChecksum; + } + + if (database_length >= MaxDatabaseLength) { + return ResultInvalidDatabaseLength; + } + + return ResultSuccess; +} + +u16 NintendoFigurineDatabase::GenerateDatabaseCrc() { + return MiiUtil::CalculateCrc16(&magic, sizeof(NintendoFigurineDatabase) - sizeof(crc)); +} + +} // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_database.h b/src/core/hle/service/mii/mii_database.h new file mode 100644 index 000000000..3bd240f93 --- /dev/null +++ b/src/core/hle/service/mii/mii_database.h @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/result.h" +#include "core/hle/service/mii/types/store_data.h" + +namespace Service::Mii { + +constexpr std::size_t MaxDatabaseLength{100}; +constexpr u32 MiiMagic{0xa523b78f}; +constexpr u32 DatabaseMagic{0x4244464e}; // NFDB + +class NintendoFigurineDatabase { +public: + /// Returns the total mii count. + u8 GetDatabaseLength() const; + + /// Returns true if database is full. + bool IsFull() const; + + /// Returns the mii of the specified index. + StoreData Get(std::size_t index) const; + + /// Returns the total mii count. Ignoring special mii. + u32 GetCount(const DatabaseSessionMetadata& metadata) const; + + /// Returns the index of a mii. If the mii isn't found returns false. + bool GetIndexByCreatorId(u32& out_index, const Common::UUID& create_id) const; + + /// Moves the location of a specific mii. + Result Move(u32 current_index, u32 new_index); + + /// Replaces mii with new data. + void Replace(u32 index, const StoreData& store_data); + + /// Adds a new mii to the end of the database. + void Add(const StoreData& store_data); + + /// Removes mii from database and shifts left the remainding data. + void Delete(u32 index); + + /// Deletes all contents with a fresh database + void CleanDatabase(); + + /// Intentionally sets a bad checksum + void CorruptCrc(); + + /// Returns success if database is valid otherwise returns the corresponding error code. + Result CheckIntegrity(); + +private: + /// Returns the checksum of the database + u16 GenerateDatabaseCrc(); + + u32 magic{}; // 'NFDB' + std::array<StoreData, MaxDatabaseLength> miis{}; + u8 version{}; + u8 database_length{}; + u16 crc{}; +}; +static_assert(sizeof(NintendoFigurineDatabase) == 0x1A98, + "NintendoFigurineDatabase has incorrect size."); + +}; // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_database_manager.cpp b/src/core/hle/service/mii/mii_database_manager.cpp new file mode 100644 index 000000000..0080b6705 --- /dev/null +++ b/src/core/hle/service/mii/mii_database_manager.cpp @@ -0,0 +1,420 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" +#include "common/string_util.h" + +#include "core/hle/service/mii/mii_database_manager.h" +#include "core/hle/service/mii/mii_result.h" +#include "core/hle/service/mii/mii_util.h" +#include "core/hle/service/mii/types/char_info.h" +#include "core/hle/service/mii/types/store_data.h" + +namespace Service::Mii { +const char* DbFileName = "MiiDatabase.dat"; + +DatabaseManager::DatabaseManager() {} + +Result DatabaseManager::MountSaveData() { + if (!is_save_data_mounted) { + system_save_dir = + Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/8000000000000030"; + if (is_test_db) { + system_save_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / + "system/save/8000000000000031"; + } + + // mount point should be "mii:" + + if (!Common::FS::CreateDirs(system_save_dir)) { + return ResultUnknown; + } + } + + is_save_data_mounted = true; + return ResultSuccess; +} + +Result DatabaseManager::Initialize(DatabaseSessionMetadata& metadata, bool& is_database_broken) { + is_database_broken = false; + if (!is_save_data_mounted) { + return ResultInvalidArgument; + } + + database.CleanDatabase(); + update_counter++; + metadata.update_counter = update_counter; + + const Common::FS::IOFile db_file{system_save_dir / DbFileName, Common::FS::FileAccessMode::Read, + Common::FS::FileType::BinaryFile}; + + if (!db_file.IsOpen()) { + return SaveDatabase(); + } + + if (Common::FS::GetSize(system_save_dir / DbFileName) != sizeof(NintendoFigurineDatabase)) { + is_database_broken = true; + } + + if (db_file.Read(database) != 1) { + is_database_broken = true; + } + + if (is_database_broken) { + // Dragons happen here for simplicity just clean the database + LOG_ERROR(Service_Mii, "Mii database is corrupted"); + database.CleanDatabase(); + return ResultUnknown; + } + + const auto result = database.CheckIntegrity(); + + if (result.IsError()) { + LOG_ERROR(Service_Mii, "Mii database is corrupted 0x{:0x}", result.raw); + database.CleanDatabase(); + return ResultSuccess; + } + + LOG_INFO(Service_Mii, "Successfully loaded mii database. size={}", + database.GetDatabaseLength()); + return ResultSuccess; +} + +bool DatabaseManager::IsFullDatabase() const { + return database.GetDatabaseLength() == MaxDatabaseLength; +} + +bool DatabaseManager::IsModified() const { + return is_moddified; +} + +u64 DatabaseManager::GetUpdateCounter() const { + return update_counter; +} + +u32 DatabaseManager::GetCount(const DatabaseSessionMetadata& metadata) const { + const u32 database_size = database.GetDatabaseLength(); + if (metadata.magic == MiiMagic) { + return database_size; + } + + // Special mii can't be used. Skip those. + + u32 mii_count{}; + for (std::size_t index = 0; index < database_size; ++index) { + const auto& store_data = database.Get(index); + if (store_data.IsSpecial()) { + continue; + } + mii_count++; + } + + return mii_count; +} + +void DatabaseManager::Get(StoreData& out_store_data, std::size_t index, + const DatabaseSessionMetadata& metadata) const { + if (metadata.magic == MiiMagic) { + out_store_data = database.Get(index); + return; + } + + // The index refeers to the mii index without special mii. + // Search on the database until we find it + + u32 virtual_index = 0; + const u32 database_size = database.GetDatabaseLength(); + for (std::size_t i = 0; i < database_size; ++i) { + const auto& store_data = database.Get(i); + if (store_data.IsSpecial()) { + continue; + } + if (virtual_index == index) { + out_store_data = store_data; + return; + } + virtual_index++; + } + + // This function doesn't fail. It returns the first mii instead + out_store_data = database.Get(0); +} + +Result DatabaseManager::FindIndex(s32& out_index, const Common::UUID& create_id, + bool is_special) const { + u32 index{}; + const bool is_found = database.GetIndexByCreatorId(index, create_id); + + if (!is_found) { + return ResultNotFound; + } + + if (is_special) { + out_index = index; + return ResultSuccess; + } + + if (database.Get(index).IsSpecial()) { + return ResultNotFound; + } + + out_index = 0; + + if (index < 1) { + return ResultSuccess; + } + + for (std::size_t i = 0; i < index; ++i) { + if (database.Get(i).IsSpecial()) { + continue; + } + out_index++; + } + return ResultSuccess; +} + +Result DatabaseManager::FindIndex(const DatabaseSessionMetadata& metadata, u32& out_index, + const Common::UUID& create_id) const { + u32 index{}; + const bool is_found = database.GetIndexByCreatorId(index, create_id); + + if (!is_found) { + return ResultNotFound; + } + + if (metadata.magic == MiiMagic) { + out_index = index; + return ResultSuccess; + } + + if (database.Get(index).IsSpecial()) { + return ResultNotFound; + } + + out_index = 0; + + if (index < 1) { + return ResultSuccess; + } + + // The index refeers to the mii index without special mii. + // Search on the database until we find it + + for (std::size_t i = 0; i <= index; ++i) { + const auto& store_data = database.Get(i); + if (store_data.IsSpecial()) { + continue; + } + out_index++; + } + return ResultSuccess; +} + +Result DatabaseManager::FindMoveIndex(u32& out_index, u32 new_index, + const Common::UUID& create_id) const { + const auto database_size = database.GetDatabaseLength(); + + if (database_size >= 1) { + u32 virtual_index{}; + for (std::size_t i = 0; i < database_size; ++i) { + const StoreData& store_data = database.Get(i); + if (store_data.IsSpecial()) { + continue; + } + if (virtual_index == new_index) { + const bool is_found = database.GetIndexByCreatorId(out_index, create_id); + if (!is_found) { + return ResultNotFound; + } + if (store_data.IsSpecial()) { + return ResultInvalidOperation; + } + return ResultSuccess; + } + virtual_index++; + } + } + + const bool is_found = database.GetIndexByCreatorId(out_index, create_id); + if (!is_found) { + return ResultNotFound; + } + const StoreData& store_data = database.Get(out_index); + if (store_data.IsSpecial()) { + return ResultInvalidOperation; + } + return ResultSuccess; +} + +Result DatabaseManager::Move(DatabaseSessionMetadata& metadata, u32 new_index, + const Common::UUID& create_id) { + u32 current_index{}; + if (metadata.magic == MiiMagic) { + const bool is_found = database.GetIndexByCreatorId(current_index, create_id); + if (!is_found) { + return ResultNotFound; + } + } else { + const auto result = FindMoveIndex(current_index, new_index, create_id); + if (result.IsError()) { + return result; + } + } + + const auto result = database.Move(current_index, new_index); + if (result.IsFailure()) { + return result; + } + + is_moddified = true; + update_counter++; + metadata.update_counter = update_counter; + return ResultSuccess; +} + +Result DatabaseManager::AddOrReplace(DatabaseSessionMetadata& metadata, + const StoreData& store_data) { + if (store_data.IsValid() != ValidationResult::NoErrors) { + return ResultInvalidStoreData; + } + if (metadata.magic != MiiMagic && store_data.IsSpecial()) { + return ResultInvalidOperation; + } + + u32 index{}; + const bool is_found = database.GetIndexByCreatorId(index, store_data.GetCreateId()); + if (is_found) { + const StoreData& old_store_data = database.Get(index); + + if (store_data.IsSpecial() != old_store_data.IsSpecial()) { + return ResultInvalidOperation; + } + + database.Replace(index, store_data); + } else { + if (database.IsFull()) { + return ResultDatabaseFull; + } + + database.Add(store_data); + } + + is_moddified = true; + update_counter++; + metadata.update_counter = update_counter; + return ResultSuccess; +} + +Result DatabaseManager::Delete(DatabaseSessionMetadata& metadata, const Common::UUID& create_id) { + u32 index{}; + const bool is_found = database.GetIndexByCreatorId(index, create_id); + if (!is_found) { + return ResultNotFound; + } + + if (metadata.magic != MiiMagic) { + const auto& store_data = database.Get(index); + if (store_data.IsSpecial()) { + return ResultInvalidOperation; + } + } + + database.Delete(index); + + is_moddified = true; + update_counter++; + metadata.update_counter = update_counter; + return ResultSuccess; +} + +Result DatabaseManager::Append(DatabaseSessionMetadata& metadata, const CharInfo& char_info) { + if (char_info.Verify() != ValidationResult::NoErrors) { + return ResultInvalidCharInfo2; + } + if (char_info.GetType() == 1) { + return ResultInvalidCharInfoType; + } + + u32 index{}; + StoreData store_data{}; + + // Loop until the mii we created is not on the database + do { + store_data.BuildWithCharInfo(char_info); + } while (database.GetIndexByCreatorId(index, store_data.GetCreateId())); + + const Result result = store_data.Restore(); + + if (result.IsSuccess() || result == ResultNotUpdated) { + return AddOrReplace(metadata, store_data); + } + + return result; +} + +Result DatabaseManager::DestroyFile(DatabaseSessionMetadata& metadata) { + database.CorruptCrc(); + + is_moddified = true; + update_counter++; + metadata.update_counter = update_counter; + + const auto result = SaveDatabase(); + database.CleanDatabase(); + + return result; +} + +Result DatabaseManager::DeleteFile() { + const bool result = Common::FS::RemoveFile(system_save_dir / DbFileName); + // TODO: Return proper FS error here + return result ? ResultSuccess : ResultUnknown; +} + +void DatabaseManager::Format(DatabaseSessionMetadata& metadata) { + database.CleanDatabase(); + is_moddified = true; + update_counter++; + metadata.update_counter = update_counter; +} + +Result DatabaseManager::SaveDatabase() { + // TODO: Replace unknown error codes with proper FS error codes when available + + if (!Common::FS::Exists(system_save_dir / DbFileName)) { + if (!Common::FS::NewFile(system_save_dir / DbFileName)) { + LOG_ERROR(Service_Mii, "Failed to create mii database"); + return ResultUnknown; + } + } + + const auto file_size = Common::FS::GetSize(system_save_dir / DbFileName); + if (file_size != 0 && file_size != sizeof(NintendoFigurineDatabase)) { + if (!Common::FS::RemoveFile(system_save_dir / DbFileName)) { + LOG_ERROR(Service_Mii, "Failed to delete mii database"); + return ResultUnknown; + } + if (!Common::FS::NewFile(system_save_dir / DbFileName)) { + LOG_ERROR(Service_Mii, "Failed to create mii database"); + return ResultUnknown; + } + } + + const Common::FS::IOFile db_file{system_save_dir / DbFileName, + Common::FS::FileAccessMode::ReadWrite, + Common::FS::FileType::BinaryFile}; + + if (db_file.Write(database) != 1) { + LOG_ERROR(Service_Mii, "Failed to save mii database"); + return ResultUnknown; + } + + is_moddified = false; + return ResultSuccess; +} + +} // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_database_manager.h b/src/core/hle/service/mii/mii_database_manager.h new file mode 100644 index 000000000..52c32be82 --- /dev/null +++ b/src/core/hle/service/mii/mii_database_manager.h @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/fs/fs.h" +#include "core/hle/result.h" +#include "core/hle/service/mii/mii_database.h" + +namespace Service::Mii { +class CharInfo; +class StoreData; + +class DatabaseManager { +public: + DatabaseManager(); + Result MountSaveData(); + Result Initialize(DatabaseSessionMetadata& metadata, bool& is_database_broken); + + bool IsFullDatabase() const; + bool IsModified() const; + u64 GetUpdateCounter() const; + + void Get(StoreData& out_store_data, std::size_t index, + const DatabaseSessionMetadata& metadata) const; + u32 GetCount(const DatabaseSessionMetadata& metadata) const; + + Result FindIndex(s32& out_index, const Common::UUID& create_id, bool is_special) const; + Result FindIndex(const DatabaseSessionMetadata& metadata, u32& out_index, + const Common::UUID& create_id) const; + Result FindMoveIndex(u32& out_index, u32 new_index, const Common::UUID& create_id) const; + + Result Move(DatabaseSessionMetadata& metadata, u32 current_index, + const Common::UUID& create_id); + Result AddOrReplace(DatabaseSessionMetadata& metadata, const StoreData& out_store_data); + Result Delete(DatabaseSessionMetadata& metadata, const Common::UUID& create_id); + Result Append(DatabaseSessionMetadata& metadata, const CharInfo& char_info); + + Result DestroyFile(DatabaseSessionMetadata& metadata); + Result DeleteFile(); + void Format(DatabaseSessionMetadata& metadata); + + Result SaveDatabase(); + +private: + // This is the global value of + // nn::settings::fwdbg::GetSettingsItemValue("is_db_test_mode_enabled"); + bool is_test_db{}; + + bool is_moddified{}; + bool is_save_data_mounted{}; + u64 update_counter{}; + NintendoFigurineDatabase database{}; + + std::filesystem::path system_save_dir{}; +}; + +}; // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_manager.cpp b/src/core/hle/service/mii/mii_manager.cpp index 292d63777..dcfd6b2e2 100644 --- a/src/core/hle/service/mii/mii_manager.cpp +++ b/src/core/hle/service/mii/mii_manager.cpp @@ -1,38 +1,63 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include <cstring> -#include <random> - -#include "common/assert.h" #include "common/logging/log.h" -#include "common/string_util.h" - -#include "core/hle/service/acc/profile_manager.h" +#include "core/hle/service/mii/mii_database_manager.h" #include "core/hle/service/mii/mii_manager.h" #include "core/hle/service/mii/mii_result.h" #include "core/hle/service/mii/mii_util.h" +#include "core/hle/service/mii/types/char_info.h" #include "core/hle/service/mii/types/core_data.h" #include "core/hle/service/mii/types/raw_data.h" +#include "core/hle/service/mii/types/store_data.h" +#include "core/hle/service/mii/types/ver3_store_data.h" namespace Service::Mii { constexpr std::size_t DefaultMiiCount{RawData::DefaultMii.size()}; MiiManager::MiiManager() {} +Result MiiManager::Initialize(DatabaseSessionMetadata& metadata) { + database_manager.MountSaveData(); + database_manager.Initialize(metadata, is_broken_with_clear_flag); + return ResultSuccess; +} + +void MiiManager::BuildDefault(CharInfo& out_char_info, u32 index) const { + StoreData store_data{}; + store_data.BuildDefault(index); + out_char_info.SetFromStoreData(store_data); +} + +void MiiManager::BuildBase(CharInfo& out_char_info, Gender gender) const { + StoreData store_data{}; + store_data.BuildBase(gender); + out_char_info.SetFromStoreData(store_data); +} + +void MiiManager::BuildRandom(CharInfo& out_char_info, Age age, Gender gender, Race race) const { + StoreData store_data{}; + store_data.BuildRandom(age, gender, race); + out_char_info.SetFromStoreData(store_data); +} + +bool MiiManager::IsFullDatabase() const { + return database_manager.IsFullDatabase(); +} + +void MiiManager::SetInterfaceVersion(DatabaseSessionMetadata& metadata, u32 version) const { + metadata.interface_version = version; +} + bool MiiManager::IsUpdated(DatabaseSessionMetadata& metadata, SourceFlag source_flag) const { if ((source_flag & SourceFlag::Database) == SourceFlag::None) { return false; } - const auto metadata_update_counter = metadata.update_counter; - metadata.update_counter = update_counter; - return metadata_update_counter != update_counter; -} - -bool MiiManager::IsFullDatabase() const { - // TODO(bunnei): We don't implement the Mii database, so it cannot be full - return false; + const u64 metadata_update_counter = metadata.update_counter; + const u64 database_update_counter = database_manager.GetUpdateCounter(); + metadata.update_counter = database_update_counter; + return metadata_update_counter != database_update_counter; } u32 MiiManager::GetCount(const DatabaseSessionMetadata& metadata, SourceFlag source_flag) const { @@ -41,72 +66,343 @@ u32 MiiManager::GetCount(const DatabaseSessionMetadata& metadata, SourceFlag sou mii_count += DefaultMiiCount; } if ((source_flag & SourceFlag::Database) != SourceFlag::None) { - // TODO(bunnei): We don't implement the Mii database, but when we do, update this + mii_count += database_manager.GetCount(metadata); } return mii_count; } -Result MiiManager::UpdateLatest(DatabaseSessionMetadata& metadata, CharInfo& out_char_info, - const CharInfo& char_info, SourceFlag source_flag) { - if ((source_flag & SourceFlag::Database) == SourceFlag::None) { +Result MiiManager::Move(DatabaseSessionMetadata& metadata, u32 index, + const Common::UUID& create_id) { + const auto result = database_manager.Move(metadata, index, create_id); + + if (result.IsFailure()) { + return result; + } + + if (!database_manager.IsModified()) { + return ResultNotUpdated; + } + + return database_manager.SaveDatabase(); +} + +Result MiiManager::AddOrReplace(DatabaseSessionMetadata& metadata, const StoreData& store_data) { + const auto result = database_manager.AddOrReplace(metadata, store_data); + + if (result.IsFailure()) { + return result; + } + + if (!database_manager.IsModified()) { + return ResultNotUpdated; + } + + return database_manager.SaveDatabase(); +} + +Result MiiManager::Delete(DatabaseSessionMetadata& metadata, const Common::UUID& create_id) { + const auto result = database_manager.Delete(metadata, create_id); + + if (result.IsFailure()) { + return result; + } + + if (!database_manager.IsModified()) { + return ResultNotUpdated; + } + + return database_manager.SaveDatabase(); +} + +s32 MiiManager::FindIndex(const Common::UUID& create_id, bool is_special) const { + s32 index{}; + const auto result = database_manager.FindIndex(index, create_id, is_special); + if (result.IsError()) { + index = -1; + } + return index; +} + +Result MiiManager::GetIndex(const DatabaseSessionMetadata& metadata, const CharInfo& char_info, + s32& out_index) const { + if (char_info.Verify() != ValidationResult::NoErrors) { + return ResultInvalidCharInfo; + } + + s32 index{}; + const bool is_special = metadata.magic == MiiMagic; + const auto result = database_manager.FindIndex(index, char_info.GetCreateId(), is_special); + + if (result.IsError()) { + index = -1; + } + + if (index == -1) { return ResultNotFound; } - // TODO(bunnei): We don't implement the Mii database, so we can't have an entry - return ResultNotFound; + out_index = index; + return ResultSuccess; } -void MiiManager::BuildDefault(CharInfo& out_char_info, u32 index) const { - StoreData store_data{}; - store_data.BuildDefault(index); - out_char_info.SetFromStoreData(store_data); +Result MiiManager::Append(DatabaseSessionMetadata& metadata, const CharInfo& char_info) { + const auto result = database_manager.Append(metadata, char_info); + + if (result.IsError()) { + return ResultNotFound; + } + + if (!database_manager.IsModified()) { + return ResultNotUpdated; + } + + return database_manager.SaveDatabase(); } -void MiiManager::BuildBase(CharInfo& out_char_info, Gender gender) const { +bool MiiManager::IsBrokenWithClearFlag(DatabaseSessionMetadata& metadata) { + const bool is_broken = is_broken_with_clear_flag; + if (is_broken_with_clear_flag) { + is_broken_with_clear_flag = false; + database_manager.Format(metadata); + database_manager.SaveDatabase(); + } + return is_broken; +} + +Result MiiManager::DestroyFile(DatabaseSessionMetadata& metadata) { + is_broken_with_clear_flag = true; + return database_manager.DestroyFile(metadata); +} + +Result MiiManager::DeleteFile() { + return database_manager.DeleteFile(); +} + +Result MiiManager::Format(DatabaseSessionMetadata& metadata) { + database_manager.Format(metadata); + + if (!database_manager.IsModified()) { + return ResultNotUpdated; + } + return database_manager.SaveDatabase(); +} + +Result MiiManager::ConvertV3ToCharInfo(CharInfo& out_char_info, const Ver3StoreData& mii_v3) const { + if (!mii_v3.IsValid()) { + return ResultInvalidCharInfo; + } + StoreData store_data{}; - store_data.BuildBase(gender); + mii_v3.BuildToStoreData(store_data); + const auto name = store_data.GetNickname(); + if (!MiiUtil::IsFontRegionValid(store_data.GetFontRegion(), name.data)) { + store_data.SetInvalidName(); + } + out_char_info.SetFromStoreData(store_data); + return ResultSuccess; } -void MiiManager::BuildRandom(CharInfo& out_char_info, Age age, Gender gender, Race race) const { +Result MiiManager::ConvertCoreDataToCharInfo(CharInfo& out_char_info, + const CoreData& core_data) const { + if (core_data.IsValid() != ValidationResult::NoErrors) { + return ResultInvalidCharInfo; + } + StoreData store_data{}; - store_data.BuildRandom(age, gender, race); + store_data.BuildWithCoreData(core_data); + const auto name = store_data.GetNickname(); + if (!MiiUtil::IsFontRegionValid(store_data.GetFontRegion(), name.data)) { + store_data.SetInvalidName(); + } + out_char_info.SetFromStoreData(store_data); + return ResultSuccess; +} + +Result MiiManager::ConvertCharInfoToCoreData(CoreData& out_core_data, + const CharInfo& char_info) const { + if (char_info.Verify() != ValidationResult::NoErrors) { + return ResultInvalidCharInfo; + } + + out_core_data.BuildFromCharInfo(char_info); + const auto name = out_core_data.GetNickname(); + if (!MiiUtil::IsFontRegionValid(out_core_data.GetFontRegion(), name.data)) { + out_core_data.SetNickname(out_core_data.GetInvalidNickname()); + } + + return ResultSuccess; } -void MiiManager::ConvertV3ToCharInfo(CharInfo& out_char_info, const Ver3StoreData& mii_v3) const { +Result MiiManager::UpdateLatest(const DatabaseSessionMetadata& metadata, CharInfo& out_char_info, + const CharInfo& char_info, SourceFlag source_flag) const { + if ((source_flag & SourceFlag::Database) == SourceFlag::None) { + return ResultNotFound; + } + + if (metadata.IsInterfaceVersionSupported(1)) { + if (char_info.Verify() != ValidationResult::NoErrors) { + return ResultInvalidCharInfo; + } + } + + u32 index{}; + Result result = database_manager.FindIndex(metadata, index, char_info.GetCreateId()); + + if (result.IsError()) { + return result; + } + StoreData store_data{}; - mii_v3.BuildToStoreData(store_data); + database_manager.Get(store_data, index, metadata); + + if (store_data.GetType() != char_info.GetType()) { + return ResultNotFound; + } + out_char_info.SetFromStoreData(store_data); + + if (char_info == out_char_info) { + return ResultNotUpdated; + } + + return ResultSuccess; +} + +Result MiiManager::UpdateLatest(const DatabaseSessionMetadata& metadata, StoreData& out_store_data, + const StoreData& store_data, SourceFlag source_flag) const { + if ((source_flag & SourceFlag::Database) == SourceFlag::None) { + return ResultNotFound; + } + + if (metadata.IsInterfaceVersionSupported(1)) { + if (store_data.IsValid() != ValidationResult::NoErrors) { + return ResultInvalidCharInfo; + } + } + + u32 index{}; + Result result = database_manager.FindIndex(metadata, index, store_data.GetCreateId()); + + if (result.IsError()) { + return result; + } + + database_manager.Get(out_store_data, index, metadata); + + if (out_store_data.GetType() != store_data.GetType()) { + return ResultNotFound; + } + + if (store_data == out_store_data) { + return ResultNotUpdated; + } + + return ResultSuccess; } Result MiiManager::Get(const DatabaseSessionMetadata& metadata, std::span<CharInfoElement> out_elements, u32& out_count, - SourceFlag source_flag) { + SourceFlag source_flag) const { if ((source_flag & SourceFlag::Database) == SourceFlag::None) { return BuildDefault(out_elements, out_count, source_flag); } - // TODO(bunnei): We don't implement the Mii database, so we can't have an entry + const auto mii_count = database_manager.GetCount(metadata); + + for (std::size_t index = 0; index < mii_count; ++index) { + if (out_elements.size() <= static_cast<std::size_t>(out_count)) { + return ResultInvalidArgumentSize; + } + + StoreData store_data{}; + database_manager.Get(store_data, index, metadata); + + out_elements[out_count].source = Source::Database; + out_elements[out_count].char_info.SetFromStoreData(store_data); + out_count++; + } // Include default Mii at the end of the list return BuildDefault(out_elements, out_count, source_flag); } Result MiiManager::Get(const DatabaseSessionMetadata& metadata, std::span<CharInfo> out_char_info, - u32& out_count, SourceFlag source_flag) { + u32& out_count, SourceFlag source_flag) const { if ((source_flag & SourceFlag::Database) == SourceFlag::None) { return BuildDefault(out_char_info, out_count, source_flag); } - // TODO(bunnei): We don't implement the Mii database, so we can't have an entry + const auto mii_count = database_manager.GetCount(metadata); + + for (std::size_t index = 0; index < mii_count; ++index) { + if (out_char_info.size() <= static_cast<std::size_t>(out_count)) { + return ResultInvalidArgumentSize; + } + + StoreData store_data{}; + database_manager.Get(store_data, index, metadata); + + out_char_info[out_count].SetFromStoreData(store_data); + out_count++; + } // Include default Mii at the end of the list return BuildDefault(out_char_info, out_count, source_flag); } +Result MiiManager::Get(const DatabaseSessionMetadata& metadata, + std::span<StoreDataElement> out_elements, u32& out_count, + SourceFlag source_flag) const { + if ((source_flag & SourceFlag::Database) == SourceFlag::None) { + return BuildDefault(out_elements, out_count, source_flag); + } + + const auto mii_count = database_manager.GetCount(metadata); + + for (std::size_t index = 0; index < mii_count; ++index) { + if (out_elements.size() <= static_cast<std::size_t>(out_count)) { + return ResultInvalidArgumentSize; + } + + StoreData store_data{}; + database_manager.Get(store_data, index, metadata); + + out_elements[out_count].store_data = store_data; + out_elements[out_count].source = Source::Database; + out_count++; + } + + // Include default Mii at the end of the list + return BuildDefault(out_elements, out_count, source_flag); +} + +Result MiiManager::Get(const DatabaseSessionMetadata& metadata, std::span<StoreData> out_store_data, + u32& out_count, SourceFlag source_flag) const { + if ((source_flag & SourceFlag::Database) == SourceFlag::None) { + return BuildDefault(out_store_data, out_count, source_flag); + } + + const auto mii_count = database_manager.GetCount(metadata); + + for (std::size_t index = 0; index < mii_count; ++index) { + if (out_store_data.size() <= static_cast<std::size_t>(out_count)) { + return ResultInvalidArgumentSize; + } + + StoreData store_data{}; + database_manager.Get(store_data, index, metadata); + + out_store_data[out_count] = store_data; + out_count++; + } + + // Include default Mii at the end of the list + return BuildDefault(out_store_data, out_count, source_flag); +} Result MiiManager::BuildDefault(std::span<CharInfoElement> out_elements, u32& out_count, - SourceFlag source_flag) { + SourceFlag source_flag) const { if ((source_flag & SourceFlag::Default) == SourceFlag::None) { return ResultSuccess; } @@ -129,7 +425,7 @@ Result MiiManager::BuildDefault(std::span<CharInfoElement> out_elements, u32& ou } Result MiiManager::BuildDefault(std::span<CharInfo> out_char_info, u32& out_count, - SourceFlag source_flag) { + SourceFlag source_flag) const { if ((source_flag & SourceFlag::Default) == SourceFlag::None) { return ResultSuccess; } @@ -150,23 +446,41 @@ Result MiiManager::BuildDefault(std::span<CharInfo> out_char_info, u32& out_coun return ResultSuccess; } -Result MiiManager::GetIndex(const DatabaseSessionMetadata& metadata, const CharInfo& char_info, - s32& out_index) { - - if (char_info.Verify() != ValidationResult::NoErrors) { - return ResultInvalidCharInfo; +Result MiiManager::BuildDefault(std::span<StoreDataElement> out_elements, u32& out_count, + SourceFlag source_flag) const { + if ((source_flag & SourceFlag::Default) == SourceFlag::None) { + return ResultSuccess; } - constexpr u32 INVALID_INDEX{0xFFFFFFFF}; + for (std::size_t index = 0; index < DefaultMiiCount; ++index) { + if (out_elements.size() <= static_cast<std::size_t>(out_count)) { + return ResultInvalidArgumentSize; + } - out_index = INVALID_INDEX; + out_elements[out_count].store_data.BuildDefault(static_cast<u32>(index)); + out_elements[out_count].source = Source::Default; + out_count++; + } - // TODO(bunnei): We don't implement the Mii database, so we can't have an index - return ResultNotFound; + return ResultSuccess; } -void MiiManager::SetInterfaceVersion(DatabaseSessionMetadata& metadata, u32 version) { - metadata.interface_version = version; +Result MiiManager::BuildDefault(std::span<StoreData> out_char_info, u32& out_count, + SourceFlag source_flag) const { + if ((source_flag & SourceFlag::Default) == SourceFlag::None) { + return ResultSuccess; + } + + for (std::size_t index = 0; index < DefaultMiiCount; ++index) { + if (out_char_info.size() <= static_cast<std::size_t>(out_count)) { + return ResultInvalidArgumentSize; + } + + out_char_info[out_count].BuildDefault(static_cast<u32>(index)); + out_count++; + } + + return ResultSuccess; } } // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_manager.h b/src/core/hle/service/mii/mii_manager.h index a2e7a6d73..48d8e8bb7 100644 --- a/src/core/hle/service/mii/mii_manager.h +++ b/src/core/hle/service/mii/mii_manager.h @@ -3,47 +3,85 @@ #pragma once -#include <vector> +#include <span> #include "core/hle/result.h" +#include "core/hle/service/mii/mii_database_manager.h" #include "core/hle/service/mii/mii_types.h" -#include "core/hle/service/mii/types/char_info.h" -#include "core/hle/service/mii/types/store_data.h" -#include "core/hle/service/mii/types/ver3_store_data.h" namespace Service::Mii { +class CharInfo; +class CoreData; +class StoreData; +class Ver3StoreData; -// The Mii manager is responsible for loading and storing the Miis to the database in NAND along -// with providing an easy interface for HLE emulation of the mii service. +struct CharInfoElement; +struct StoreDataElement; + +// The Mii manager is responsible for handling mii operations along with providing an easy interface +// for HLE emulation of the mii service. class MiiManager { public: MiiManager(); + Result Initialize(DatabaseSessionMetadata& metadata); - bool IsUpdated(DatabaseSessionMetadata& metadata, SourceFlag source_flag) const; + // Auto generated mii + void BuildDefault(CharInfo& out_char_info, u32 index) const; + void BuildBase(CharInfo& out_char_info, Gender gender) const; + void BuildRandom(CharInfo& out_char_info, Age age, Gender gender, Race race) const; + // Database operations bool IsFullDatabase() const; + void SetInterfaceVersion(DatabaseSessionMetadata& metadata, u32 version) const; + bool IsUpdated(DatabaseSessionMetadata& metadata, SourceFlag source_flag) const; u32 GetCount(const DatabaseSessionMetadata& metadata, SourceFlag source_flag) const; - Result UpdateLatest(DatabaseSessionMetadata& metadata, CharInfo& out_char_info, - const CharInfo& char_info, SourceFlag source_flag); + Result Move(DatabaseSessionMetadata& metadata, u32 index, const Common::UUID& create_id); + Result AddOrReplace(DatabaseSessionMetadata& metadata, const StoreData& store_data); + Result Delete(DatabaseSessionMetadata& metadata, const Common::UUID& create_id); + s32 FindIndex(const Common::UUID& create_id, bool is_special) const; + Result GetIndex(const DatabaseSessionMetadata& metadata, const CharInfo& char_info, + s32& out_index) const; + Result Append(DatabaseSessionMetadata& metadata, const CharInfo& char_info); + + // Test database operations + bool IsBrokenWithClearFlag(DatabaseSessionMetadata& metadata); + Result DestroyFile(DatabaseSessionMetadata& metadata); + Result DeleteFile(); + Result Format(DatabaseSessionMetadata& metadata); + + // Mii conversions + Result ConvertV3ToCharInfo(CharInfo& out_char_info, const Ver3StoreData& mii_v3) const; + Result ConvertCoreDataToCharInfo(CharInfo& out_char_info, const CoreData& core_data) const; + Result ConvertCharInfoToCoreData(CoreData& out_core_data, const CharInfo& char_info) const; + Result UpdateLatest(const DatabaseSessionMetadata& metadata, CharInfo& out_char_info, + const CharInfo& char_info, SourceFlag source_flag) const; + Result UpdateLatest(const DatabaseSessionMetadata& metadata, StoreData& out_store_data, + const StoreData& store_data, SourceFlag source_flag) const; + + // Overloaded getters Result Get(const DatabaseSessionMetadata& metadata, std::span<CharInfoElement> out_elements, - u32& out_count, SourceFlag source_flag); + u32& out_count, SourceFlag source_flag) const; Result Get(const DatabaseSessionMetadata& metadata, std::span<CharInfo> out_char_info, - u32& out_count, SourceFlag source_flag); - void BuildDefault(CharInfo& out_char_info, u32 index) const; - void BuildBase(CharInfo& out_char_info, Gender gender) const; - void BuildRandom(CharInfo& out_char_info, Age age, Gender gender, Race race) const; - void ConvertV3ToCharInfo(CharInfo& out_char_info, const Ver3StoreData& mii_v3) const; - std::vector<CharInfoElement> GetDefault(SourceFlag source_flag); - Result GetIndex(const DatabaseSessionMetadata& metadata, const CharInfo& char_info, - s32& out_index); - void SetInterfaceVersion(DatabaseSessionMetadata& metadata, u32 version); + u32& out_count, SourceFlag source_flag) const; + Result Get(const DatabaseSessionMetadata& metadata, std::span<StoreDataElement> out_elements, + u32& out_count, SourceFlag source_flag) const; + Result Get(const DatabaseSessionMetadata& metadata, std::span<StoreData> out_store_data, + u32& out_count, SourceFlag source_flag) const; private: Result BuildDefault(std::span<CharInfoElement> out_elements, u32& out_count, - SourceFlag source_flag); - Result BuildDefault(std::span<CharInfo> out_char_info, u32& out_count, SourceFlag source_flag); + SourceFlag source_flag) const; + Result BuildDefault(std::span<CharInfo> out_char_info, u32& out_count, + SourceFlag source_flag) const; + Result BuildDefault(std::span<StoreDataElement> out_char_info, u32& out_count, + SourceFlag source_flag) const; + Result BuildDefault(std::span<StoreData> out_char_info, u32& out_count, + SourceFlag source_flag) const; + + DatabaseManager database_manager{}; - u64 update_counter{}; + // This should be a global value + bool is_broken_with_clear_flag{}; }; }; // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_result.h b/src/core/hle/service/mii/mii_result.h index 021cb76da..e2c36e556 100644 --- a/src/core/hle/service/mii/mii_result.h +++ b/src/core/hle/service/mii/mii_result.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -13,8 +13,15 @@ constexpr Result ResultNotUpdated{ErrorModule::Mii, 3}; constexpr Result ResultNotFound{ErrorModule::Mii, 4}; constexpr Result ResultDatabaseFull{ErrorModule::Mii, 5}; constexpr Result ResultInvalidCharInfo{ErrorModule::Mii, 100}; +constexpr Result ResultInvalidDatabaseChecksum{ErrorModule::Mii, 101}; +constexpr Result ResultInvalidDatabaseSignature{ErrorModule::Mii, 103}; +constexpr Result ResultInvalidDatabaseVersion{ErrorModule::Mii, 104}; +constexpr Result ResultInvalidDatabaseLength{ErrorModule::Mii, 105}; +constexpr Result ResultInvalidCharInfo2{ErrorModule::Mii, 107}; constexpr Result ResultInvalidStoreData{ErrorModule::Mii, 109}; constexpr Result ResultInvalidOperation{ErrorModule::Mii, 202}; constexpr Result ResultPermissionDenied{ErrorModule::Mii, 203}; +constexpr Result ResultTestModeOnly{ErrorModule::Mii, 204}; +constexpr Result ResultInvalidCharInfoType{ErrorModule::Mii, 205}; }; // namespace Service::Mii diff --git a/src/core/hle/service/mii/mii_types.h b/src/core/hle/service/mii/mii_types.h index 611ff4f81..08c6029df 100644 --- a/src/core/hle/service/mii/mii_types.h +++ b/src/core/hle/service/mii/mii_types.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -13,6 +13,7 @@ namespace Service::Mii { +constexpr std::size_t MaxNameSize = 10; constexpr u8 MaxHeight = 127; constexpr u8 MaxBuild = 127; constexpr u8 MaxType = 1; @@ -26,14 +27,14 @@ constexpr u8 MaxEyebrowScale = 8; constexpr u8 MaxEyebrowAspect = 6; constexpr u8 MaxEyebrowRotate = 11; constexpr u8 MaxEyebrowX = 12; -constexpr u8 MaxEyebrowY = 18; +constexpr u8 MaxEyebrowY = 15; constexpr u8 MaxNoseScale = 8; constexpr u8 MaxNoseY = 18; constexpr u8 MaxMouthScale = 8; constexpr u8 MaxMoutAspect = 6; constexpr u8 MaxMouthY = 18; constexpr u8 MaxMustacheScale = 8; -constexpr u8 MasMustacheY = 16; +constexpr u8 MaxMustacheY = 16; constexpr u8 MaxGlassScale = 7; constexpr u8 MaxGlassY = 20; constexpr u8 MaxMoleScale = 8; @@ -599,12 +600,12 @@ enum class ValidationResult : u32 { InvalidRegionMove = 0x31, InvalidCreateId = 0x32, InvalidName = 0x33, + InvalidChecksum = 0x34, InvalidType = 0x35, }; struct Nickname { - static constexpr std::size_t MaxNameSize = 10; - std::array<char16_t, MaxNameSize> data; + std::array<char16_t, MaxNameSize> data{}; // Checks for null or dirty strings bool IsValid() const { @@ -613,7 +614,7 @@ struct Nickname { } std::size_t index = 1; - while (data[index] != 0) { + while (index < MaxNameSize && data[index] != 0) { index++; } while (index < MaxNameSize && data[index] == 0) { diff --git a/src/core/hle/service/mii/mii_util.h b/src/core/hle/service/mii/mii_util.h index ddb544c23..3534fa31d 100644 --- a/src/core/hle/service/mii/mii_util.h +++ b/src/core/hle/service/mii/mii_util.h @@ -28,6 +28,32 @@ public: return Common::swap16(static_cast<u16>(crc)); } + static u16 CalculateDeviceCrc16(const Common::UUID& uuid, std::size_t data_size) { + constexpr u16 magic{0x1021}; + s32 crc{}; + + for (std::size_t i = 0; i < uuid.uuid.size(); i++) { + for (std::size_t j = 0; j < 8; j++) { + crc <<= 1; + if ((crc & 0x10000) != 0) { + crc = crc ^ magic; + } + } + crc ^= uuid.uuid[i]; + } + + // As much as this looks wrong this is what N's does + + for (std::size_t i = 0; i < data_size * 8; i++) { + crc <<= 1; + if ((crc & 0x10000) != 0) { + crc = crc ^ magic; + } + } + + return Common::swap16(static_cast<u16>(crc)); + } + static Common::UUID MakeCreateId() { return Common::UUID::MakeRandomRFC4122V4(); } diff --git a/src/core/hle/service/mii/types/char_info.cpp b/src/core/hle/service/mii/types/char_info.cpp index bb948c628..e90124af4 100644 --- a/src/core/hle/service/mii/types/char_info.cpp +++ b/src/core/hle/service/mii/types/char_info.cpp @@ -37,7 +37,7 @@ void CharInfo::SetFromStoreData(const StoreData& store_data) { eyebrow_aspect = store_data.GetEyebrowAspect(); eyebrow_rotate = store_data.GetEyebrowRotate(); eyebrow_x = store_data.GetEyebrowX(); - eyebrow_y = store_data.GetEyebrowY(); + eyebrow_y = store_data.GetEyebrowY() + 3; nose_type = store_data.GetNoseType(); nose_scale = store_data.GetNoseScale(); nose_y = store_data.GetNoseY(); @@ -150,7 +150,7 @@ ValidationResult CharInfo::Verify() const { if (eyebrow_x > MaxEyebrowX) { return ValidationResult::InvalidEyebrowX; } - if (eyebrow_y > MaxEyebrowY) { + if (eyebrow_y - 3 > MaxEyebrowY) { return ValidationResult::InvalidEyebrowY; } if (nose_type > NoseType::Max) { @@ -189,7 +189,7 @@ ValidationResult CharInfo::Verify() const { if (mustache_scale > MaxMustacheScale) { return ValidationResult::InvalidMustacheScale; } - if (mustache_y > MasMustacheY) { + if (mustache_y > MaxMustacheY) { return ValidationResult::InvalidMustacheY; } if (glass_type > GlassType::Max) { diff --git a/src/core/hle/service/mii/types/char_info.h b/src/core/hle/service/mii/types/char_info.h index d069b221f..d0c457fd5 100644 --- a/src/core/hle/service/mii/types/char_info.h +++ b/src/core/hle/service/mii/types/char_info.h @@ -70,59 +70,59 @@ public: bool operator==(const CharInfo& info); private: - Common::UUID create_id; - Nickname name; - u16 null_terminator; - FontRegion font_region; - FavoriteColor favorite_color; - Gender gender; - u8 height; - u8 build; - u8 type; - u8 region_move; - FacelineType faceline_type; - FacelineColor faceline_color; - FacelineWrinkle faceline_wrinkle; - FacelineMake faceline_make; - HairType hair_type; - CommonColor hair_color; - HairFlip hair_flip; - EyeType eye_type; - CommonColor eye_color; - u8 eye_scale; - u8 eye_aspect; - u8 eye_rotate; - u8 eye_x; - u8 eye_y; - EyebrowType eyebrow_type; - CommonColor eyebrow_color; - u8 eyebrow_scale; - u8 eyebrow_aspect; - u8 eyebrow_rotate; - u8 eyebrow_x; - u8 eyebrow_y; - NoseType nose_type; - u8 nose_scale; - u8 nose_y; - MouthType mouth_type; - CommonColor mouth_color; - u8 mouth_scale; - u8 mouth_aspect; - u8 mouth_y; - CommonColor beard_color; - BeardType beard_type; - MustacheType mustache_type; - u8 mustache_scale; - u8 mustache_y; - GlassType glass_type; - CommonColor glass_color; - u8 glass_scale; - u8 glass_y; - MoleType mole_type; - u8 mole_scale; - u8 mole_x; - u8 mole_y; - u8 padding; + Common::UUID create_id{}; + Nickname name{}; + u16 null_terminator{}; + FontRegion font_region{}; + FavoriteColor favorite_color{}; + Gender gender{}; + u8 height{}; + u8 build{}; + u8 type{}; + u8 region_move{}; + FacelineType faceline_type{}; + FacelineColor faceline_color{}; + FacelineWrinkle faceline_wrinkle{}; + FacelineMake faceline_make{}; + HairType hair_type{}; + CommonColor hair_color{}; + HairFlip hair_flip{}; + EyeType eye_type{}; + CommonColor eye_color{}; + u8 eye_scale{}; + u8 eye_aspect{}; + u8 eye_rotate{}; + u8 eye_x{}; + u8 eye_y{}; + EyebrowType eyebrow_type{}; + CommonColor eyebrow_color{}; + u8 eyebrow_scale{}; + u8 eyebrow_aspect{}; + u8 eyebrow_rotate{}; + u8 eyebrow_x{}; + u8 eyebrow_y{}; + NoseType nose_type{}; + u8 nose_scale{}; + u8 nose_y{}; + MouthType mouth_type{}; + CommonColor mouth_color{}; + u8 mouth_scale{}; + u8 mouth_aspect{}; + u8 mouth_y{}; + CommonColor beard_color{}; + BeardType beard_type{}; + MustacheType mustache_type{}; + u8 mustache_scale{}; + u8 mustache_y{}; + GlassType glass_type{}; + CommonColor glass_color{}; + u8 glass_scale{}; + u8 glass_y{}; + MoleType mole_type{}; + u8 mole_scale{}; + u8 mole_x{}; + u8 mole_y{}; + u8 padding{}; }; static_assert(sizeof(CharInfo) == 0x58, "CharInfo has incorrect size."); static_assert(std::has_unique_object_representations_v<CharInfo>, diff --git a/src/core/hle/service/mii/types/core_data.cpp b/src/core/hle/service/mii/types/core_data.cpp index 659288b51..970c748ca 100644 --- a/src/core/hle/service/mii/types/core_data.cpp +++ b/src/core/hle/service/mii/types/core_data.cpp @@ -3,6 +3,7 @@ #include "common/assert.h" #include "core/hle/service/mii/mii_util.h" +#include "core/hle/service/mii/types/char_info.h" #include "core/hle/service/mii/types/core_data.h" #include "core/hle/service/mii/types/raw_data.h" @@ -112,7 +113,7 @@ void CoreData::BuildRandom(Age age, Gender gender, Race race) { .values[MiiUtil::GetRandomValue<std::size_t>(eyebrow_type_info.values_count)]); const auto eyebrow_rotate_1{race == Race::Asian ? 6 : 0}; - const auto eyebrow_y{race == Race::Asian ? 9 : 10}; + const auto eyebrow_y{race == Race::Asian ? 6 : 7}; const auto eyebrow_rotate_offset{32 - RawData::EyebrowRotateLookup[eyebrow_rotate_1] + 6}; const auto eyebrow_rotate{ 32 - RawData::EyebrowRotateLookup[static_cast<std::size_t>(data.eyebrow_type.Value())]}; @@ -170,7 +171,7 @@ void CoreData::BuildRandom(Age age, Gender gender, Race race) { u8 glasses_type{}; while (glasses_type_start < glasses_type_info.values[glasses_type]) { if (++glasses_type >= glasses_type_info.values_count) { - ASSERT(false); + glasses_type = 0; break; } } @@ -178,6 +179,7 @@ void CoreData::BuildRandom(Age age, Gender gender, Race race) { SetGlassType(static_cast<GlassType>(glasses_type)); SetGlassColor(RawData::GetGlassColorFromVer3(0)); SetGlassScale(4); + SetGlassY(static_cast<u8>(axis_y + 10)); SetMoleType(MoleType::None); SetMoleScale(4); @@ -185,9 +187,211 @@ void CoreData::BuildRandom(Age age, Gender gender, Race race) { SetMoleY(20); } -u32 CoreData::IsValid() const { - // TODO: Complete this - return 0; +void CoreData::BuildFromCharInfo(const CharInfo& char_info) { + name = char_info.GetNickname(); + SetFontRegion(char_info.GetFontRegion()); + SetFavoriteColor(char_info.GetFavoriteColor()); + SetGender(char_info.GetGender()); + SetHeight(char_info.GetHeight()); + SetBuild(char_info.GetBuild()); + SetType(char_info.GetType()); + SetRegionMove(char_info.GetRegionMove()); + SetFacelineType(char_info.GetFacelineType()); + SetFacelineColor(char_info.GetFacelineColor()); + SetFacelineWrinkle(char_info.GetFacelineWrinkle()); + SetFacelineMake(char_info.GetFacelineMake()); + SetHairType(char_info.GetHairType()); + SetHairColor(char_info.GetHairColor()); + SetHairFlip(char_info.GetHairFlip()); + SetEyeType(char_info.GetEyeType()); + SetEyeColor(char_info.GetEyeColor()); + SetEyeScale(char_info.GetEyeScale()); + SetEyeAspect(char_info.GetEyeAspect()); + SetEyeRotate(char_info.GetEyeRotate()); + SetEyeX(char_info.GetEyeX()); + SetEyeY(char_info.GetEyeY()); + SetEyebrowType(char_info.GetEyebrowType()); + SetEyebrowColor(char_info.GetEyebrowColor()); + SetEyebrowScale(char_info.GetEyebrowScale()); + SetEyebrowAspect(char_info.GetEyebrowAspect()); + SetEyebrowRotate(char_info.GetEyebrowRotate()); + SetEyebrowX(char_info.GetEyebrowX()); + SetEyebrowY(char_info.GetEyebrowY() - 3); + SetNoseType(char_info.GetNoseType()); + SetNoseScale(char_info.GetNoseScale()); + SetNoseY(char_info.GetNoseY()); + SetMouthType(char_info.GetMouthType()); + SetMouthColor(char_info.GetMouthColor()); + SetMouthScale(char_info.GetMouthScale()); + SetMouthAspect(char_info.GetMouthAspect()); + SetMouthY(char_info.GetMouthY()); + SetBeardColor(char_info.GetBeardColor()); + SetBeardType(char_info.GetBeardType()); + SetMustacheType(char_info.GetMustacheType()); + SetMustacheScale(char_info.GetMustacheScale()); + SetMustacheY(char_info.GetMustacheY()); + SetGlassType(char_info.GetGlassType()); + SetGlassColor(char_info.GetGlassColor()); + SetGlassScale(char_info.GetGlassScale()); + SetGlassY(char_info.GetGlassY()); + SetMoleType(char_info.GetMoleType()); + SetMoleScale(char_info.GetMoleScale()); + SetMoleX(char_info.GetMoleX()); + SetMoleY(char_info.GetMoleY()); +} + +ValidationResult CoreData::IsValid() const { + if (!name.IsValid()) { + return ValidationResult::InvalidName; + } + if (GetFontRegion() > FontRegion::Max) { + return ValidationResult::InvalidFont; + } + if (GetFavoriteColor() > FavoriteColor::Max) { + return ValidationResult::InvalidColor; + } + if (GetGender() > Gender::Max) { + return ValidationResult::InvalidGender; + } + if (GetHeight() > MaxHeight) { + return ValidationResult::InvalidHeight; + } + if (GetBuild() > MaxBuild) { + return ValidationResult::InvalidBuild; + } + if (GetType() > MaxType) { + return ValidationResult::InvalidType; + } + if (GetRegionMove() > MaxRegionMove) { + return ValidationResult::InvalidRegionMove; + } + if (GetFacelineType() > FacelineType::Max) { + return ValidationResult::InvalidFacelineType; + } + if (GetFacelineColor() > FacelineColor::Max) { + return ValidationResult::InvalidFacelineColor; + } + if (GetFacelineWrinkle() > FacelineWrinkle::Max) { + return ValidationResult::InvalidFacelineWrinkle; + } + if (GetFacelineMake() > FacelineMake::Max) { + return ValidationResult::InvalidFacelineMake; + } + if (GetHairType() > HairType::Max) { + return ValidationResult::InvalidHairType; + } + if (GetHairColor() > CommonColor::Max) { + return ValidationResult::InvalidHairColor; + } + if (GetHairFlip() > HairFlip::Max) { + return ValidationResult::InvalidHairFlip; + } + if (GetEyeType() > EyeType::Max) { + return ValidationResult::InvalidEyeType; + } + if (GetEyeColor() > CommonColor::Max) { + return ValidationResult::InvalidEyeColor; + } + if (GetEyeScale() > MaxEyeScale) { + return ValidationResult::InvalidEyeScale; + } + if (GetEyeAspect() > MaxEyeAspect) { + return ValidationResult::InvalidEyeAspect; + } + if (GetEyeRotate() > MaxEyeRotate) { + return ValidationResult::InvalidEyeRotate; + } + if (GetEyeX() > MaxEyeX) { + return ValidationResult::InvalidEyeX; + } + if (GetEyeY() > MaxEyeY) { + return ValidationResult::InvalidEyeY; + } + if (GetEyebrowType() > EyebrowType::Max) { + return ValidationResult::InvalidEyebrowType; + } + if (GetEyebrowColor() > CommonColor::Max) { + return ValidationResult::InvalidEyebrowColor; + } + if (GetEyebrowScale() > MaxEyebrowScale) { + return ValidationResult::InvalidEyebrowScale; + } + if (GetEyebrowAspect() > MaxEyebrowAspect) { + return ValidationResult::InvalidEyebrowAspect; + } + if (GetEyebrowRotate() > MaxEyebrowRotate) { + return ValidationResult::InvalidEyebrowRotate; + } + if (GetEyebrowX() > MaxEyebrowX) { + return ValidationResult::InvalidEyebrowX; + } + if (GetEyebrowY() > MaxEyebrowY) { + return ValidationResult::InvalidEyebrowY; + } + if (GetNoseType() > NoseType::Max) { + return ValidationResult::InvalidNoseType; + } + if (GetNoseScale() > MaxNoseScale) { + return ValidationResult::InvalidNoseScale; + } + if (GetNoseY() > MaxNoseY) { + return ValidationResult::InvalidNoseY; + } + if (GetMouthType() > MouthType::Max) { + return ValidationResult::InvalidMouthType; + } + if (GetMouthColor() > CommonColor::Max) { + return ValidationResult::InvalidMouthColor; + } + if (GetMouthScale() > MaxMouthScale) { + return ValidationResult::InvalidMouthScale; + } + if (GetMouthAspect() > MaxMoutAspect) { + return ValidationResult::InvalidMouthAspect; + } + if (GetMouthY() > MaxMouthY) { + return ValidationResult::InvalidMouthY; + } + if (GetBeardColor() > CommonColor::Max) { + return ValidationResult::InvalidBeardColor; + } + if (GetBeardType() > BeardType::Max) { + return ValidationResult::InvalidBeardType; + } + if (GetMustacheType() > MustacheType::Max) { + return ValidationResult::InvalidMustacheType; + } + if (GetMustacheScale() > MaxMustacheScale) { + return ValidationResult::InvalidMustacheScale; + } + if (GetMustacheY() > MaxMustacheY) { + return ValidationResult::InvalidMustacheY; + } + if (GetGlassType() > GlassType::Max) { + return ValidationResult::InvalidGlassType; + } + if (GetGlassColor() > CommonColor::Max) { + return ValidationResult::InvalidGlassColor; + } + if (GetGlassScale() > MaxGlassScale) { + return ValidationResult::InvalidGlassScale; + } + if (GetGlassY() > MaxGlassY) { + return ValidationResult::InvalidGlassY; + } + if (GetMoleType() > MoleType::Max) { + return ValidationResult::InvalidMoleType; + } + if (GetMoleScale() > MaxMoleScale) { + return ValidationResult::InvalidMoleScale; + } + if (GetMoleX() > MaxMoleX) { + return ValidationResult::InvalidMoleX; + } + if (GetMoleY() > MaxMoleY) { + return ValidationResult::InvalidMoleY; + } + return ValidationResult::NoErrors; } void CoreData::SetFontRegion(FontRegion value) { @@ -314,8 +518,8 @@ void CoreData::SetNoseY(u8 value) { data.nose_y.Assign(value); } -void CoreData::SetMouthType(u8 value) { - data.mouth_type.Assign(value); +void CoreData::SetMouthType(MouthType value) { + data.mouth_type.Assign(static_cast<u32>(value)); } void CoreData::SetMouthColor(CommonColor value) { diff --git a/src/core/hle/service/mii/types/core_data.h b/src/core/hle/service/mii/types/core_data.h index cebcd2ee4..8897e4f3b 100644 --- a/src/core/hle/service/mii/types/core_data.h +++ b/src/core/hle/service/mii/types/core_data.h @@ -6,6 +6,7 @@ #include "core/hle/service/mii/mii_types.h" namespace Service::Mii { +class CharInfo; struct StoreDataBitFields { union { @@ -100,8 +101,9 @@ class CoreData { public: void SetDefault(); void BuildRandom(Age age, Gender gender, Race race); + void BuildFromCharInfo(const CharInfo& char_info); - u32 IsValid() const; + ValidationResult IsValid() const; void SetFontRegion(FontRegion value); void SetFavoriteColor(FavoriteColor value); @@ -134,7 +136,7 @@ public: void SetNoseType(NoseType value); void SetNoseScale(u8 value); void SetNoseY(u8 value); - void SetMouthType(u8 value); + void SetMouthType(MouthType value); void SetMouthColor(CommonColor value); void SetMouthScale(u8 value); void SetMouthAspect(u8 value); @@ -212,5 +214,6 @@ private: Nickname name{}; }; static_assert(sizeof(CoreData) == 0x30, "CoreData has incorrect size."); +static_assert(std::is_trivially_copyable_v<CoreData>, "CoreData type must be trivially copyable."); }; // namespace Service::Mii diff --git a/src/core/hle/service/mii/types/raw_data.cpp b/src/core/hle/service/mii/types/raw_data.cpp index 5143abcc8..0e1a07fd7 100644 --- a/src/core/hle/service/mii/types/raw_data.cpp +++ b/src/core/hle/service/mii/types/raw_data.cpp @@ -1716,18 +1716,18 @@ const std::array<RandomMiiData4, 18> RandomMiiMouthType{ const std::array<RandomMiiData2, 3> RandomMiiGlassType{ RandomMiiData2{ .arg_1 = 0, - .values_count = 9, - .values = {90, 94, 96, 100, 0, 0, 0, 0, 0}, + .values_count = 4, + .values = {90, 94, 96, 100}, }, RandomMiiData2{ .arg_1 = 1, - .values_count = 9, - .values = {83, 86, 90, 93, 94, 96, 98, 100, 0}, + .values_count = 8, + .values = {83, 86, 90, 93, 94, 96, 98, 100}, }, RandomMiiData2{ .arg_1 = 2, - .values_count = 9, - .values = {78, 83, 0, 93, 0, 0, 98, 100, 0}, + .values_count = 8, + .values = {78, 83, 0, 93, 0, 0, 98, 100}, }, }; diff --git a/src/core/hle/service/mii/types/store_data.cpp b/src/core/hle/service/mii/types/store_data.cpp index 8fce636c7..127221fdb 100644 --- a/src/core/hle/service/mii/types/store_data.cpp +++ b/src/core/hle/service/mii/types/store_data.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "core/hle/service/mii/mii_result.h" #include "core/hle/service/mii/mii_util.h" #include "core/hle/service/mii/types/raw_data.h" #include "core/hle/service/mii/types/store_data.h" @@ -35,13 +36,13 @@ void StoreData::BuildDefault(u32 mii_index) { core_data.SetEyebrowAspect(static_cast<u8>(default_mii.eyebrow_aspect)); core_data.SetEyebrowRotate(static_cast<u8>(default_mii.eyebrow_rotate)); core_data.SetEyebrowX(static_cast<u8>(default_mii.eyebrow_x)); - core_data.SetEyebrowY(static_cast<u8>(default_mii.eyebrow_y)); + core_data.SetEyebrowY(static_cast<u8>(default_mii.eyebrow_y - 3)); core_data.SetNoseType(static_cast<NoseType>(default_mii.nose_type)); core_data.SetNoseScale(static_cast<u8>(default_mii.nose_scale)); core_data.SetNoseY(static_cast<u8>(default_mii.nose_y)); - core_data.SetMouthType(static_cast<u8>(default_mii.mouth_type)); + core_data.SetMouthType(static_cast<MouthType>(default_mii.mouth_type)); core_data.SetMouthColor( RawData::GetMouthColorFromVer3(static_cast<u8>(default_mii.mouth_color))); core_data.SetMouthScale(static_cast<u8>(default_mii.mouth_scale)); @@ -75,10 +76,8 @@ void StoreData::BuildDefault(u32 mii_index) { core_data.SetType(static_cast<u8>(default_mii.type)); core_data.SetNickname(default_mii.nickname); - const auto device_id = MiiUtil::GetDeviceId(); create_id = MiiUtil::MakeCreateId(); - device_crc = MiiUtil::CalculateCrc16(&device_id, sizeof(Common::UUID)); - data_crc = MiiUtil::CalculateCrc16(&core_data, sizeof(CoreData)); + SetChecksum(); } void StoreData::BuildBase(Gender gender) { @@ -109,13 +108,13 @@ void StoreData::BuildBase(Gender gender) { core_data.SetEyebrowAspect(static_cast<u8>(default_mii.eyebrow_aspect)); core_data.SetEyebrowRotate(static_cast<u8>(default_mii.eyebrow_rotate)); core_data.SetEyebrowX(static_cast<u8>(default_mii.eyebrow_x)); - core_data.SetEyebrowY(static_cast<u8>(default_mii.eyebrow_y)); + core_data.SetEyebrowY(static_cast<u8>(default_mii.eyebrow_y - 3)); core_data.SetNoseType(static_cast<NoseType>(default_mii.nose_type)); core_data.SetNoseScale(static_cast<u8>(default_mii.nose_scale)); core_data.SetNoseY(static_cast<u8>(default_mii.nose_y)); - core_data.SetMouthType(static_cast<u8>(default_mii.mouth_type)); + core_data.SetMouthType(static_cast<MouthType>(default_mii.mouth_type)); core_data.SetMouthColor( RawData::GetMouthColorFromVer3(static_cast<u8>(default_mii.mouth_color))); core_data.SetMouthScale(static_cast<u8>(default_mii.mouth_scale)); @@ -149,37 +148,51 @@ void StoreData::BuildBase(Gender gender) { core_data.SetType(static_cast<u8>(default_mii.type)); core_data.SetNickname(default_mii.nickname); - const auto device_id = MiiUtil::GetDeviceId(); create_id = MiiUtil::MakeCreateId(); - device_crc = MiiUtil::CalculateCrc16(&device_id, sizeof(Common::UUID)); - data_crc = MiiUtil::CalculateCrc16(&core_data, sizeof(CoreData)); + SetChecksum(); } void StoreData::BuildRandom(Age age, Gender gender, Race race) { core_data.BuildRandom(age, gender, race); - const auto device_id = MiiUtil::GetDeviceId(); create_id = MiiUtil::MakeCreateId(); - device_crc = MiiUtil::CalculateCrc16(&device_id, sizeof(Common::UUID)); - data_crc = MiiUtil::CalculateCrc16(&core_data, sizeof(CoreData)); + SetChecksum(); } -void StoreData::SetInvalidName() { - const auto& invalid_name = core_data.GetInvalidNickname(); +void StoreData::BuildWithCharInfo(const CharInfo& char_info) { + core_data.BuildFromCharInfo(char_info); + create_id = MiiUtil::MakeCreateId(); + SetChecksum(); +} + +void StoreData::BuildWithCoreData(const CoreData& in_core_data) { + core_data = in_core_data; + create_id = MiiUtil::MakeCreateId(); + SetChecksum(); +} + +Result StoreData::Restore() { + // TODO: Implement this + return ResultNotUpdated; +} + +ValidationResult StoreData::IsValid() const { + if (core_data.IsValid() != ValidationResult::NoErrors) { + return core_data.IsValid(); + } + if (data_crc != MiiUtil::CalculateCrc16(&core_data, sizeof(CoreData) + sizeof(Common::UUID))) { + return ValidationResult::InvalidChecksum; + } const auto device_id = MiiUtil::GetDeviceId(); - core_data.SetNickname(invalid_name); - device_crc = MiiUtil::CalculateCrc16(&device_id, sizeof(Common::UUID)); - data_crc = MiiUtil::CalculateCrc16(&core_data, sizeof(CoreData)); + if (device_crc != MiiUtil::CalculateDeviceCrc16(device_id, sizeof(StoreData))) { + return ValidationResult::InvalidChecksum; + } + return ValidationResult::NoErrors; } bool StoreData::IsSpecial() const { return GetType() == 1; } -u32 StoreData::IsValid() const { - // TODO: complete this - return 0; -} - void StoreData::SetFontRegion(FontRegion value) { core_data.SetFontRegion(value); } @@ -304,7 +317,7 @@ void StoreData::SetNoseY(u8 value) { core_data.SetNoseY(value); } -void StoreData::SetMouthType(u8 value) { +void StoreData::SetMouthType(MouthType value) { core_data.SetMouthType(value); } @@ -380,6 +393,26 @@ void StoreData::SetNickname(Nickname value) { core_data.SetNickname(value); } +void StoreData::SetInvalidName() { + const auto& invalid_name = core_data.GetInvalidNickname(); + core_data.SetNickname(invalid_name); + SetChecksum(); +} + +void StoreData::SetChecksum() { + SetDataChecksum(); + SetDeviceChecksum(); +} + +void StoreData::SetDataChecksum() { + data_crc = MiiUtil::CalculateCrc16(&core_data, sizeof(CoreData) + sizeof(Common::UUID)); +} + +void StoreData::SetDeviceChecksum() { + const auto device_id = MiiUtil::GetDeviceId(); + device_crc = MiiUtil::CalculateDeviceCrc16(device_id, sizeof(StoreData)); +} + Common::UUID StoreData::GetCreateId() const { return create_id; } @@ -585,7 +618,7 @@ Nickname StoreData::GetNickname() const { } bool StoreData::operator==(const StoreData& data) { - bool is_identical = data.core_data.IsValid() == 0; + bool is_identical = data.core_data.IsValid() == ValidationResult::NoErrors; is_identical &= core_data.GetNickname().data == data.core_data.GetNickname().data; is_identical &= GetCreateId() == data.GetCreateId(); is_identical &= GetFontRegion() == data.GetFontRegion(); diff --git a/src/core/hle/service/mii/types/store_data.h b/src/core/hle/service/mii/types/store_data.h index 224c32cf8..ed5dfb949 100644 --- a/src/core/hle/service/mii/types/store_data.h +++ b/src/core/hle/service/mii/types/store_data.h @@ -3,6 +3,7 @@ #pragma once +#include "core/hle/result.h" #include "core/hle/service/mii/mii_types.h" #include "core/hle/service/mii/types/core_data.h" @@ -10,17 +11,16 @@ namespace Service::Mii { class StoreData { public: - // nn::mii::detail::StoreDataRaw::BuildDefault void BuildDefault(u32 mii_index); - // nn::mii::detail::StoreDataRaw::BuildDefault - void BuildBase(Gender gender); - // nn::mii::detail::StoreDataRaw::BuildRandom void BuildRandom(Age age, Gender gender, Race race); + void BuildWithCharInfo(const CharInfo& char_info); + void BuildWithCoreData(const CoreData& in_core_data); + Result Restore(); - bool IsSpecial() const; + ValidationResult IsValid() const; - u32 IsValid() const; + bool IsSpecial() const; void SetFontRegion(FontRegion value); void SetFavoriteColor(FavoriteColor value); @@ -53,7 +53,7 @@ public: void SetNoseType(NoseType value); void SetNoseScale(u8 value); void SetNoseY(u8 value); - void SetMouthType(u8 value); + void SetMouthType(MouthType value); void SetMouthColor(CommonColor value); void SetMouthScale(u8 value); void SetMouthAspect(u8 value); @@ -73,6 +73,9 @@ public: void SetMoleY(u8 value); void SetNickname(Nickname nickname); void SetInvalidName(); + void SetChecksum(); + void SetDataChecksum(); + void SetDeviceChecksum(); Common::UUID GetCreateId() const; FontRegion GetFontRegion() const; @@ -135,6 +138,8 @@ private: u16 device_crc{}; }; static_assert(sizeof(StoreData) == 0x44, "StoreData has incorrect size."); +static_assert(std::is_trivially_copyable_v<StoreData>, + "StoreData type must be trivially copyable."); struct StoreDataElement { StoreData store_data{}; diff --git a/src/core/hle/service/mii/types/ver3_store_data.cpp b/src/core/hle/service/mii/types/ver3_store_data.cpp index 1c28e0b1b..a019cc9f7 100644 --- a/src/core/hle/service/mii/types/ver3_store_data.cpp +++ b/src/core/hle/service/mii/types/ver3_store_data.cpp @@ -22,12 +22,6 @@ void NfpStoreDataExtension::SetFromStoreData(const StoreData& store_data) { void Ver3StoreData::BuildToStoreData(StoreData& out_store_data) const { out_store_data.BuildBase(Gender::Male); - if (!IsValid()) { - return; - } - - // TODO: We are ignoring a bunch of data from the mii_v3 - out_store_data.SetGender(static_cast<Gender>(mii_information.gender.Value())); out_store_data.SetFavoriteColor( static_cast<FavoriteColor>(mii_information.favorite_color.Value())); @@ -36,65 +30,71 @@ void Ver3StoreData::BuildToStoreData(StoreData& out_store_data) const { out_store_data.SetNickname(mii_name); out_store_data.SetFontRegion( - static_cast<FontRegion>(static_cast<u8>(region_information.font_region))); + static_cast<FontRegion>(static_cast<u8>(region_information.font_region.Value()))); out_store_data.SetFacelineType( static_cast<FacelineType>(appearance_bits1.faceline_type.Value())); out_store_data.SetFacelineColor( - static_cast<FacelineColor>(appearance_bits1.faceline_color.Value())); + RawData::GetFacelineColorFromVer3(appearance_bits1.faceline_color.Value())); out_store_data.SetFacelineWrinkle( static_cast<FacelineWrinkle>(appearance_bits2.faceline_wrinkle.Value())); out_store_data.SetFacelineMake( static_cast<FacelineMake>(appearance_bits2.faceline_make.Value())); out_store_data.SetHairType(static_cast<HairType>(hair_type)); - out_store_data.SetHairColor(static_cast<CommonColor>(appearance_bits3.hair_color.Value())); + out_store_data.SetHairColor(RawData::GetHairColorFromVer3(appearance_bits3.hair_color.Value())); out_store_data.SetHairFlip(static_cast<HairFlip>(appearance_bits3.hair_flip.Value())); out_store_data.SetEyeType(static_cast<EyeType>(appearance_bits4.eye_type.Value())); - out_store_data.SetEyeColor(static_cast<CommonColor>(appearance_bits4.eye_color.Value())); - out_store_data.SetEyeScale(static_cast<u8>(appearance_bits4.eye_scale)); - out_store_data.SetEyeAspect(static_cast<u8>(appearance_bits4.eye_aspect)); - out_store_data.SetEyeRotate(static_cast<u8>(appearance_bits4.eye_rotate)); - out_store_data.SetEyeX(static_cast<u8>(appearance_bits4.eye_x)); - out_store_data.SetEyeY(static_cast<u8>(appearance_bits4.eye_y)); + out_store_data.SetEyeColor(RawData::GetEyeColorFromVer3(appearance_bits4.eye_color.Value())); + out_store_data.SetEyeScale(static_cast<u8>(appearance_bits4.eye_scale.Value())); + out_store_data.SetEyeAspect(static_cast<u8>(appearance_bits4.eye_aspect.Value())); + out_store_data.SetEyeRotate(static_cast<u8>(appearance_bits4.eye_rotate.Value())); + out_store_data.SetEyeX(static_cast<u8>(appearance_bits4.eye_x.Value())); + out_store_data.SetEyeY(static_cast<u8>(appearance_bits4.eye_y.Value())); out_store_data.SetEyebrowType(static_cast<EyebrowType>(appearance_bits5.eyebrow_type.Value())); out_store_data.SetEyebrowColor( - static_cast<CommonColor>(appearance_bits5.eyebrow_color.Value())); - out_store_data.SetEyebrowScale(static_cast<u8>(appearance_bits5.eyebrow_scale)); - out_store_data.SetEyebrowAspect(static_cast<u8>(appearance_bits5.eyebrow_aspect)); - out_store_data.SetEyebrowRotate(static_cast<u8>(appearance_bits5.eyebrow_rotate)); - out_store_data.SetEyebrowX(static_cast<u8>(appearance_bits5.eyebrow_x)); - out_store_data.SetEyebrowY(static_cast<u8>(appearance_bits5.eyebrow_y)); + RawData::GetHairColorFromVer3(appearance_bits5.eyebrow_color.Value())); + out_store_data.SetEyebrowScale(static_cast<u8>(appearance_bits5.eyebrow_scale.Value())); + out_store_data.SetEyebrowAspect(static_cast<u8>(appearance_bits5.eyebrow_aspect.Value())); + out_store_data.SetEyebrowRotate(static_cast<u8>(appearance_bits5.eyebrow_rotate.Value())); + out_store_data.SetEyebrowX(static_cast<u8>(appearance_bits5.eyebrow_x.Value())); + out_store_data.SetEyebrowY(static_cast<u8>(appearance_bits5.eyebrow_y.Value() - 3)); out_store_data.SetNoseType(static_cast<NoseType>(appearance_bits6.nose_type.Value())); - out_store_data.SetNoseScale(static_cast<u8>(appearance_bits6.nose_scale)); - out_store_data.SetNoseY(static_cast<u8>(appearance_bits6.nose_y)); + out_store_data.SetNoseScale(static_cast<u8>(appearance_bits6.nose_scale.Value())); + out_store_data.SetNoseY(static_cast<u8>(appearance_bits6.nose_y.Value())); - out_store_data.SetMouthType(static_cast<u8>(appearance_bits7.mouth_type)); - out_store_data.SetMouthColor(static_cast<CommonColor>(appearance_bits7.mouth_color.Value())); - out_store_data.SetMouthScale(static_cast<u8>(appearance_bits7.mouth_scale)); - out_store_data.SetMouthAspect(static_cast<u8>(appearance_bits7.mouth_aspect)); - out_store_data.SetMouthY(static_cast<u8>(appearance_bits8.mouth_y)); + out_store_data.SetMouthType(static_cast<MouthType>(appearance_bits7.mouth_type.Value())); + out_store_data.SetMouthColor( + RawData::GetMouthColorFromVer3(appearance_bits7.mouth_color.Value())); + out_store_data.SetMouthScale(static_cast<u8>(appearance_bits7.mouth_scale.Value())); + out_store_data.SetMouthAspect(static_cast<u8>(appearance_bits7.mouth_aspect.Value())); + out_store_data.SetMouthY(static_cast<u8>(appearance_bits8.mouth_y.Value())); out_store_data.SetMustacheType( static_cast<MustacheType>(appearance_bits8.mustache_type.Value())); - out_store_data.SetMustacheScale(static_cast<u8>(appearance_bits9.mustache_scale)); - out_store_data.SetMustacheY(static_cast<u8>(appearance_bits9.mustache_y)); + out_store_data.SetMustacheScale(static_cast<u8>(appearance_bits9.mustache_scale.Value())); + out_store_data.SetMustacheY(static_cast<u8>(appearance_bits9.mustache_y.Value())); out_store_data.SetBeardType(static_cast<BeardType>(appearance_bits9.beard_type.Value())); - out_store_data.SetBeardColor(static_cast<CommonColor>(appearance_bits9.beard_color.Value())); + out_store_data.SetBeardColor( + RawData::GetHairColorFromVer3(appearance_bits9.beard_color.Value())); + // Glass type is compatible as it is. It doesn't need a table out_store_data.SetGlassType(static_cast<GlassType>(appearance_bits10.glass_type.Value())); - out_store_data.SetGlassColor(static_cast<CommonColor>(appearance_bits10.glass_color.Value())); - out_store_data.SetGlassScale(static_cast<u8>(appearance_bits10.glass_scale)); - out_store_data.SetGlassY(static_cast<u8>(appearance_bits10.glass_y)); + out_store_data.SetGlassColor( + RawData::GetGlassColorFromVer3(appearance_bits10.glass_color.Value())); + out_store_data.SetGlassScale(static_cast<u8>(appearance_bits10.glass_scale.Value())); + out_store_data.SetGlassY(static_cast<u8>(appearance_bits10.glass_y.Value())); out_store_data.SetMoleType(static_cast<MoleType>(appearance_bits11.mole_type.Value())); - out_store_data.SetMoleScale(static_cast<u8>(appearance_bits11.mole_scale)); - out_store_data.SetMoleX(static_cast<u8>(appearance_bits11.mole_x)); - out_store_data.SetMoleY(static_cast<u8>(appearance_bits11.mole_y)); + out_store_data.SetMoleScale(static_cast<u8>(appearance_bits11.mole_scale.Value())); + out_store_data.SetMoleX(static_cast<u8>(appearance_bits11.mole_x.Value())); + out_store_data.SetMoleY(static_cast<u8>(appearance_bits11.mole_y.Value())); + + out_store_data.SetChecksum(); } void Ver3StoreData::BuildFromStoreData(const StoreData& store_data) { @@ -220,7 +220,7 @@ u32 Ver3StoreData::IsValid() const { is_valid = is_valid && (appearance_bits8.mustache_type <= static_cast<u8>(MustacheType::Max)); is_valid = is_valid && (appearance_bits9.mustache_scale < MaxMustacheScale); - is_valid = is_valid && (appearance_bits9.mustache_y <= MasMustacheY); + is_valid = is_valid && (appearance_bits9.mustache_y <= MaxMustacheY); is_valid = is_valid && (appearance_bits9.beard_type <= static_cast<u8>(BeardType::Max)); is_valid = is_valid && (appearance_bits9.beard_color <= MaxVer3CommonColor); @@ -228,7 +228,7 @@ u32 Ver3StoreData::IsValid() const { is_valid = is_valid && (appearance_bits10.glass_type <= MaxVer3GlassType); is_valid = is_valid && (appearance_bits10.glass_color <= MaxVer3CommonColor - 2); is_valid = is_valid && (appearance_bits10.glass_scale <= MaxGlassScale); - is_valid = is_valid && (appearance_bits10.glass_y <= MaxGlassScale); + is_valid = is_valid && (appearance_bits10.glass_y <= MaxGlassY); is_valid = is_valid && (appearance_bits11.mole_type <= static_cast<u8>(MoleType::Max)); is_valid = is_valid && (appearance_bits11.mole_scale <= MaxMoleScale); diff --git a/src/core/hle/service/nfc/common/device.cpp b/src/core/hle/service/nfc/common/device.cpp index 674d2e4b2..e7a00deb3 100644 --- a/src/core/hle/service/nfc/common/device.cpp +++ b/src/core/hle/service/nfc/common/device.cpp @@ -439,6 +439,7 @@ Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target device_state = DeviceState::TagMounted; mount_target = mount_target_; + return ResultSuccess; } @@ -716,12 +717,13 @@ Result NfcDevice::GetRegisterInfoPrivate(NFP::RegisterInfoPrivate& register_info return ResultRegistrationIsNotInitialized; } - Service::Mii::MiiManager manager; + Mii::StoreData store_data{}; const auto& settings = tag_data.settings; + tag_data.owner_mii.BuildToStoreData(store_data); // TODO: Validate and complete this data register_info = { - .mii_store_data = {}, + .mii_store_data = store_data, .creation_date = settings.init_date.GetWriteDate(), .amiibo_name = GetAmiiboName(settings), .font_region = settings.settings.font_region, @@ -828,11 +830,6 @@ Result NfcDevice::SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& registe return ResultWrongDeviceState; } - Service::Mii::StoreData store_data{}; - Service::Mii::NfpStoreDataExtension extension{}; - store_data.BuildBase(Mii::Gender::Male); - extension.SetFromStoreData(store_data); - auto& settings = tag_data.settings; if (tag_data.settings.settings.amiibo_initialized == 0) { @@ -841,8 +838,8 @@ Result NfcDevice::SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& registe } SetAmiiboName(settings, register_info.amiibo_name); - tag_data.owner_mii.BuildFromStoreData(store_data); - tag_data.mii_extension = extension; + tag_data.owner_mii.BuildFromStoreData(register_info.mii_store_data); + tag_data.mii_extension.SetFromStoreData(register_info.mii_store_data); tag_data.unknown = 0; tag_data.unknown2 = {}; settings.country_code_id = 0; 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/iplatform_service_manager.cpp b/src/core/hle/service/ns/iplatform_service_manager.cpp index 6c2f5e70b..46268be95 100644 --- a/src/core/hle/service/ns/iplatform_service_manager.cpp +++ b/src/core/hle/service/ns/iplatform_service_manager.cpp @@ -144,7 +144,7 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch {3, &IPlatformServiceManager::GetSharedMemoryAddressOffset, "GetSharedMemoryAddressOffset"}, {4, &IPlatformServiceManager::GetSharedMemoryNativeHandle, "GetSharedMemoryNativeHandle"}, {5, &IPlatformServiceManager::GetSharedFontInOrderOfPriority, "GetSharedFontInOrderOfPriority"}, - {6, nullptr, "GetSharedFontInOrderOfPriorityForSystem"}, + {6, &IPlatformServiceManager::GetSharedFontInOrderOfPriority, "GetSharedFontInOrderOfPriorityForSystem"}, {100, nullptr, "RequestApplicationFunctionAuthorization"}, {101, nullptr, "RequestApplicationFunctionAuthorizationByProcessId"}, {102, nullptr, "RequestApplicationFunctionAuthorizationByApplicationId"}, @@ -262,8 +262,17 @@ void IPlatformServiceManager::GetSharedMemoryNativeHandle(HLERequestContext& ctx } void IPlatformServiceManager::GetSharedFontInOrderOfPriority(HLERequestContext& ctx) { + // The maximum number of elements that can be returned is 6. Regardless of the available fonts + // or buffer size. + constexpr std::size_t MaxElementCount = 6; IPC::RequestParser rp{ctx}; const u64 language_code{rp.Pop<u64>()}; // TODO(ogniK): Find out what this is used for + const std::size_t font_codes_count = + std::min(MaxElementCount, ctx.GetWriteBufferNumElements<u32>(0)); + const std::size_t font_offsets_count = + std::min(MaxElementCount, ctx.GetWriteBufferNumElements<u32>(1)); + const std::size_t font_sizes_count = + std::min(MaxElementCount, ctx.GetWriteBufferNumElements<u32>(2)); LOG_DEBUG(Service_NS, "called, language_code={:X}", language_code); IPC::ResponseBuilder rb{ctx, 4}; @@ -280,9 +289,9 @@ void IPlatformServiceManager::GetSharedFontInOrderOfPriority(HLERequestContext& } // Resize buffers if game requests smaller size output - font_codes.resize(std::min(font_codes.size(), ctx.GetWriteBufferNumElements<u32>(0))); - font_offsets.resize(std::min(font_offsets.size(), ctx.GetWriteBufferNumElements<u32>(1))); - font_sizes.resize(std::min(font_sizes.size(), ctx.GetWriteBufferNumElements<u32>(2))); + font_codes.resize(std::min(font_codes.size(), font_codes_count)); + font_offsets.resize(std::min(font_offsets.size(), font_offsets_count)); + font_sizes.resize(std::min(font_sizes.size(), font_sizes_count)); ctx.WriteBuffer(font_codes, 0); ctx.WriteBuffer(font_offsets, 1); 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/nvdrv/devices/nvmap.h b/src/core/hle/service/nvdrv/devices/nvmap.h index 40c65b430..4c0cc71cd 100644 --- a/src/core/hle/service/nvdrv/devices/nvmap.h +++ b/src/core/hle/service/nvdrv/devices/nvmap.h @@ -45,13 +45,6 @@ public: IsSharedMemMapped = 6 }; -private: - /// Id to use for the next handle that is created. - u32 next_handle = 0; - - /// Id to use for the next object that is created. - u32 next_id = 0; - struct IocCreateParams { // Input u32_le size{}; @@ -113,6 +106,13 @@ private: NvResult IocParam(std::span<const u8> input, std::span<u8> output); NvResult IocFree(std::span<const u8> input, std::span<u8> output); +private: + /// Id to use for the next handle that is created. + u32 next_handle = 0; + + /// Id to use for the next object that is created. + u32 next_id = 0; + NvCore::Container& container; NvCore::NvMap& file; }; diff --git a/src/core/hle/service/nvnflinger/buffer_item.h b/src/core/hle/service/nvnflinger/buffer_item.h index 7fd808f54..3da8cc3aa 100644 --- a/src/core/hle/service/nvnflinger/buffer_item.h +++ b/src/core/hle/service/nvnflinger/buffer_item.h @@ -15,7 +15,7 @@ namespace Service::android { -class GraphicBuffer; +struct GraphicBuffer; class BufferItem final { public: diff --git a/src/core/hle/service/nvnflinger/buffer_slot.h b/src/core/hle/service/nvnflinger/buffer_slot.h index d25bca049..d8c9dec3b 100644 --- a/src/core/hle/service/nvnflinger/buffer_slot.h +++ b/src/core/hle/service/nvnflinger/buffer_slot.h @@ -13,7 +13,7 @@ namespace Service::android { -class GraphicBuffer; +struct GraphicBuffer; enum class BufferState : u32 { Free = 0, diff --git a/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp new file mode 100644 index 000000000..469a53244 --- /dev/null +++ b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp @@ -0,0 +1,351 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <random> + +#include "core/core.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_system_resource.h" +#include "core/hle/service/nvdrv/devices/nvmap.h" +#include "core/hle/service/nvdrv/nvdrv.h" +#include "core/hle/service/nvnflinger/buffer_queue_producer.h" +#include "core/hle/service/nvnflinger/fb_share_buffer_manager.h" +#include "core/hle/service/nvnflinger/pixel_format.h" +#include "core/hle/service/nvnflinger/ui/graphic_buffer.h" +#include "core/hle/service/vi/layer/vi_layer.h" +#include "core/hle/service/vi/vi_results.h" + +namespace Service::Nvnflinger { + +namespace { + +Result AllocateIoForProcessAddressSpace(Common::ProcessAddress* out_map_address, + std::unique_ptr<Kernel::KPageGroup>* out_page_group, + Core::System& system, u32 size) { + using Core::Memory::YUZU_PAGESIZE; + + // Allocate memory for the system shared buffer. + // FIXME: Because the gmmu can only point to cpu addresses, we need + // to map this in the application space to allow it to be used. + // FIXME: Add proper smmu emulation. + // FIXME: This memory belongs to vi's .data section. + auto& kernel = system.Kernel(); + auto* process = system.ApplicationProcess(); + auto& page_table = process->GetPageTable(); + + // Hold a temporary page group reference while we try to map it. + auto pg = std::make_unique<Kernel::KPageGroup>( + kernel, std::addressof(kernel.GetSystemSystemResource().GetBlockInfoManager())); + + // Allocate memory from secure pool. + R_TRY(kernel.MemoryManager().AllocateAndOpen( + pg.get(), size / YUZU_PAGESIZE, + Kernel::KMemoryManager::EncodeOption(Kernel::KMemoryManager::Pool::Secure, + Kernel::KMemoryManager::Direction::FromBack))); + + // Get bounds of where mapping is possible. + const VAddr alias_code_begin = GetInteger(page_table.GetAliasCodeRegionStart()); + const VAddr alias_code_size = page_table.GetAliasCodeRegionSize() / YUZU_PAGESIZE; + const auto state = Kernel::KMemoryState::Io; + const auto perm = Kernel::KMemoryPermission::UserReadWrite; + std::mt19937_64 rng{process->GetRandomEntropy(0)}; + + // Retry up to 64 times to map into alias code range. + Result res = ResultSuccess; + int i; + for (i = 0; i < 64; i++) { + *out_map_address = alias_code_begin + ((rng() % alias_code_size) * YUZU_PAGESIZE); + res = page_table.MapPageGroup(*out_map_address, *pg, state, perm); + if (R_SUCCEEDED(res)) { + break; + } + } + + // Return failure, if necessary + R_UNLESS(i < 64, res); + + // Return the mapped page group. + *out_page_group = std::move(pg); + + // We succeeded. + R_SUCCEED(); +} + +template <typename T> +std::span<u8> SerializeIoc(T& params) { + return std::span(reinterpret_cast<u8*>(std::addressof(params)), sizeof(T)); +} + +Result CreateNvMapHandle(u32* out_nv_map_handle, Nvidia::Devices::nvmap& nvmap, u32 size) { + // Create a handle. + Nvidia::Devices::nvmap::IocCreateParams create_in_params{ + .size = size, + .handle = 0, + }; + Nvidia::Devices::nvmap::IocCreateParams create_out_params{}; + R_UNLESS(nvmap.IocCreate(SerializeIoc(create_in_params), SerializeIoc(create_out_params)) == + Nvidia::NvResult::Success, + VI::ResultOperationFailed); + + // Assign the output handle. + *out_nv_map_handle = create_out_params.handle; + + // We succeeded. + R_SUCCEED(); +} + +Result FreeNvMapHandle(Nvidia::Devices::nvmap& nvmap, u32 handle) { + // Free the handle. + Nvidia::Devices::nvmap::IocFreeParams free_in_params{ + .handle = handle, + }; + Nvidia::Devices::nvmap::IocFreeParams free_out_params{}; + R_UNLESS(nvmap.IocFree(SerializeIoc(free_in_params), SerializeIoc(free_out_params)) == + Nvidia::NvResult::Success, + VI::ResultOperationFailed); + + // We succeeded. + R_SUCCEED(); +} + +Result AllocNvMapHandle(Nvidia::Devices::nvmap& nvmap, u32 handle, Common::ProcessAddress buffer, + u32 size) { + // Assign the allocated memory to the handle. + Nvidia::Devices::nvmap::IocAllocParams alloc_in_params{ + .handle = handle, + .heap_mask = 0, + .flags = {}, + .align = 0, + .kind = 0, + .address = GetInteger(buffer), + }; + Nvidia::Devices::nvmap::IocAllocParams alloc_out_params{}; + R_UNLESS(nvmap.IocAlloc(SerializeIoc(alloc_in_params), SerializeIoc(alloc_out_params)) == + Nvidia::NvResult::Success, + VI::ResultOperationFailed); + + // We succeeded. + R_SUCCEED(); +} + +Result AllocateHandleForBuffer(u32* out_handle, Nvidia::Module& nvdrv, + Common::ProcessAddress buffer, u32 size) { + // Get the nvmap device. + auto nvmap_fd = nvdrv.Open("/dev/nvmap"); + auto nvmap = nvdrv.GetDevice<Nvidia::Devices::nvmap>(nvmap_fd); + ASSERT(nvmap != nullptr); + + // Create a handle. + R_TRY(CreateNvMapHandle(out_handle, *nvmap, size)); + + // Ensure we maintain a clean state on failure. + ON_RESULT_FAILURE { + ASSERT(R_SUCCEEDED(FreeNvMapHandle(*nvmap, *out_handle))); + }; + + // Assign the allocated memory to the handle. + R_RETURN(AllocNvMapHandle(*nvmap, *out_handle, buffer, size)); +} + +constexpr auto SharedBufferBlockLinearFormat = android::PixelFormat::Rgba8888; +constexpr u32 SharedBufferBlockLinearBpp = 4; + +constexpr u32 SharedBufferBlockLinearWidth = 1280; +constexpr u32 SharedBufferBlockLinearHeight = 768; +constexpr u32 SharedBufferBlockLinearStride = + SharedBufferBlockLinearWidth * SharedBufferBlockLinearBpp; +constexpr u32 SharedBufferNumSlots = 7; + +constexpr u32 SharedBufferWidth = 1280; +constexpr u32 SharedBufferHeight = 720; +constexpr u32 SharedBufferAsync = false; + +constexpr u32 SharedBufferSlotSize = + SharedBufferBlockLinearWidth * SharedBufferBlockLinearHeight * SharedBufferBlockLinearBpp; +constexpr u32 SharedBufferSize = SharedBufferSlotSize * SharedBufferNumSlots; + +constexpr SharedMemoryPoolLayout SharedBufferPoolLayout = [] { + SharedMemoryPoolLayout layout{}; + layout.num_slots = SharedBufferNumSlots; + + for (u32 i = 0; i < SharedBufferNumSlots; i++) { + layout.slots[i].buffer_offset = i * SharedBufferSlotSize; + layout.slots[i].size = SharedBufferSlotSize; + layout.slots[i].width = SharedBufferWidth; + layout.slots[i].height = SharedBufferHeight; + } + + return layout; +}(); + +void MakeGraphicBuffer(android::BufferQueueProducer& producer, u32 slot, u32 handle) { + auto buffer = std::make_shared<android::GraphicBuffer>(); + buffer->width = SharedBufferWidth; + buffer->height = SharedBufferHeight; + buffer->stride = SharedBufferBlockLinearStride; + buffer->format = SharedBufferBlockLinearFormat; + buffer->buffer_id = handle; + buffer->offset = slot * SharedBufferSlotSize; + ASSERT(producer.SetPreallocatedBuffer(slot, buffer) == android::Status::NoError); +} + +} // namespace + +FbShareBufferManager::FbShareBufferManager(Core::System& system, Nvnflinger& flinger, + std::shared_ptr<Nvidia::Module> nvdrv) + : m_system(system), m_flinger(flinger), m_nvdrv(std::move(nvdrv)) {} + +FbShareBufferManager::~FbShareBufferManager() = default; + +Result FbShareBufferManager::Initialize(u64* out_buffer_id, u64* out_layer_id, u64 display_id) { + std::scoped_lock lk{m_guard}; + + // Ensure we have not already created a buffer. + R_UNLESS(m_buffer_id == 0, VI::ResultOperationFailed); + + // Allocate memory and space for the shared buffer. + Common::ProcessAddress map_address; + R_TRY(AllocateIoForProcessAddressSpace(std::addressof(map_address), + std::addressof(m_buffer_page_group), m_system, + SharedBufferSize)); + + // Create an nvmap handle for the buffer and assign the memory to it. + R_TRY(AllocateHandleForBuffer(std::addressof(m_buffer_nvmap_handle), *m_nvdrv, map_address, + SharedBufferSize)); + + // Record the display id. + m_display_id = display_id; + + // Create a layer for the display. + m_layer_id = m_flinger.CreateLayer(m_display_id).value(); + + // Set up the buffer. + m_buffer_id = m_next_buffer_id++; + + // Get the layer. + VI::Layer* layer = m_flinger.FindLayer(m_display_id, m_layer_id); + ASSERT(layer != nullptr); + + // Get the producer and set preallocated buffers. + auto& producer = layer->GetBufferQueue(); + MakeGraphicBuffer(producer, 0, m_buffer_nvmap_handle); + MakeGraphicBuffer(producer, 1, m_buffer_nvmap_handle); + + // Assign outputs. + *out_buffer_id = m_buffer_id; + *out_layer_id = m_layer_id; + + // We succeeded. + R_SUCCEED(); +} + +Result FbShareBufferManager::GetSharedBufferMemoryHandleId(u64* out_buffer_size, + s32* out_nvmap_handle, + SharedMemoryPoolLayout* out_pool_layout, + u64 buffer_id, + u64 applet_resource_user_id) { + std::scoped_lock lk{m_guard}; + + R_UNLESS(m_buffer_id > 0, VI::ResultNotFound); + R_UNLESS(buffer_id == m_buffer_id, VI::ResultNotFound); + + *out_pool_layout = SharedBufferPoolLayout; + *out_buffer_size = SharedBufferSize; + *out_nvmap_handle = m_buffer_nvmap_handle; + + R_SUCCEED(); +} + +Result FbShareBufferManager::GetLayerFromId(VI::Layer** out_layer, u64 layer_id) { + // Ensure the layer id is valid. + R_UNLESS(m_layer_id > 0 && layer_id == m_layer_id, VI::ResultNotFound); + + // Get the layer. + VI::Layer* layer = m_flinger.FindLayer(m_display_id, layer_id); + R_UNLESS(layer != nullptr, VI::ResultNotFound); + + // We succeeded. + *out_layer = layer; + R_SUCCEED(); +} + +Result FbShareBufferManager::AcquireSharedFrameBuffer(android::Fence* out_fence, + std::array<s32, 4>& out_slot_indexes, + s64* out_target_slot, u64 layer_id) { + std::scoped_lock lk{m_guard}; + + // Get the layer. + VI::Layer* layer; + R_TRY(this->GetLayerFromId(std::addressof(layer), layer_id)); + + // Get the producer. + auto& producer = layer->GetBufferQueue(); + + // Get the next buffer from the producer. + s32 slot; + R_UNLESS(producer.DequeueBuffer(std::addressof(slot), out_fence, SharedBufferAsync != 0, + SharedBufferWidth, SharedBufferHeight, + SharedBufferBlockLinearFormat, 0) == android::Status::NoError, + VI::ResultOperationFailed); + + // Assign remaining outputs. + *out_target_slot = slot; + out_slot_indexes = {0, 1, -1, -1}; + + // We succeeded. + R_SUCCEED(); +} + +Result FbShareBufferManager::PresentSharedFrameBuffer(android::Fence fence, + Common::Rectangle<s32> crop_region, + u32 transform, s32 swap_interval, + u64 layer_id, s64 slot) { + std::scoped_lock lk{m_guard}; + + // Get the layer. + VI::Layer* layer; + R_TRY(this->GetLayerFromId(std::addressof(layer), layer_id)); + + // Get the producer. + auto& producer = layer->GetBufferQueue(); + + // Request to queue the buffer. + std::shared_ptr<android::GraphicBuffer> buffer; + R_UNLESS(producer.RequestBuffer(static_cast<s32>(slot), std::addressof(buffer)) == + android::Status::NoError, + VI::ResultOperationFailed); + + // Queue the buffer to the producer. + android::QueueBufferInput input{}; + android::QueueBufferOutput output{}; + input.crop = crop_region; + input.fence = fence; + input.transform = static_cast<android::NativeWindowTransform>(transform); + input.swap_interval = swap_interval; + R_UNLESS(producer.QueueBuffer(static_cast<s32>(slot), input, std::addressof(output)) == + android::Status::NoError, + VI::ResultOperationFailed); + + // We succeeded. + R_SUCCEED(); +} + +Result FbShareBufferManager::GetSharedFrameBufferAcquirableEvent(Kernel::KReadableEvent** out_event, + u64 layer_id) { + std::scoped_lock lk{m_guard}; + + // Get the layer. + VI::Layer* layer; + R_TRY(this->GetLayerFromId(std::addressof(layer), layer_id)); + + // Get the producer. + auto& producer = layer->GetBufferQueue(); + + // Set the event. + *out_event = std::addressof(producer.GetNativeHandle()); + + // We succeeded. + R_SUCCEED(); +} + +} // namespace Service::Nvnflinger diff --git a/src/core/hle/service/nvnflinger/fb_share_buffer_manager.h b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.h new file mode 100644 index 000000000..c809c01b4 --- /dev/null +++ b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/math_util.h" +#include "core/hle/service/nvnflinger/nvnflinger.h" +#include "core/hle/service/nvnflinger/ui/fence.h" + +namespace Kernel { +class KPageGroup; +} + +namespace Service::Nvnflinger { + +struct SharedMemorySlot { + u64 buffer_offset; + u64 size; + s32 width; + s32 height; +}; +static_assert(sizeof(SharedMemorySlot) == 0x18, "SharedMemorySlot has wrong size"); + +struct SharedMemoryPoolLayout { + s32 num_slots; + std::array<SharedMemorySlot, 0x10> slots; +}; +static_assert(sizeof(SharedMemoryPoolLayout) == 0x188, "SharedMemoryPoolLayout has wrong size"); + +class FbShareBufferManager final { +public: + explicit FbShareBufferManager(Core::System& system, Nvnflinger& flinger, + std::shared_ptr<Nvidia::Module> nvdrv); + ~FbShareBufferManager(); + + Result Initialize(u64* out_buffer_id, u64* out_layer_handle, u64 display_id); + Result GetSharedBufferMemoryHandleId(u64* out_buffer_size, s32* out_nvmap_handle, + SharedMemoryPoolLayout* out_pool_layout, u64 buffer_id, + u64 applet_resource_user_id); + Result AcquireSharedFrameBuffer(android::Fence* out_fence, std::array<s32, 4>& out_slots, + s64* out_target_slot, u64 layer_id); + Result PresentSharedFrameBuffer(android::Fence fence, Common::Rectangle<s32> crop_region, + u32 transform, s32 swap_interval, u64 layer_id, s64 slot); + Result GetSharedFrameBufferAcquirableEvent(Kernel::KReadableEvent** out_event, u64 layer_id); + +private: + Result GetLayerFromId(VI::Layer** out_layer, u64 layer_id); + +private: + u64 m_next_buffer_id = 1; + u64 m_display_id = 0; + u64 m_buffer_id = 0; + u64 m_layer_id = 0; + u32 m_buffer_nvmap_handle = 0; + SharedMemoryPoolLayout m_pool_layout = {}; + + std::unique_ptr<Kernel::KPageGroup> m_buffer_page_group; + + std::mutex m_guard; + Core::System& m_system; + Nvnflinger& m_flinger; + std::shared_ptr<Nvidia::Module> m_nvdrv; +}; + +} // namespace Service::Nvnflinger diff --git a/src/core/hle/service/nvnflinger/graphic_buffer_producer.h b/src/core/hle/service/nvnflinger/graphic_buffer_producer.h index 21d7b31f3..5d7cff7d3 100644 --- a/src/core/hle/service/nvnflinger/graphic_buffer_producer.h +++ b/src/core/hle/service/nvnflinger/graphic_buffer_producer.h @@ -19,6 +19,7 @@ class InputParcel; #pragma pack(push, 1) struct QueueBufferInput final { explicit QueueBufferInput(InputParcel& parcel); + explicit QueueBufferInput() = default; void Deflate(s64* timestamp_, bool* is_auto_timestamp_, Common::Rectangle<s32>* crop_, NativeWindowScalingMode* scaling_mode_, NativeWindowTransform* transform_, @@ -34,7 +35,6 @@ struct QueueBufferInput final { *fence_ = fence; } -private: s64 timestamp{}; s32 is_auto_timestamp{}; Common::Rectangle<s32> crop{}; diff --git a/src/core/hle/service/nvnflinger/nvnflinger.cpp b/src/core/hle/service/nvnflinger/nvnflinger.cpp index 21f31f7a0..a07c621d9 100644 --- a/src/core/hle/service/nvnflinger/nvnflinger.cpp +++ b/src/core/hle/service/nvnflinger/nvnflinger.cpp @@ -17,6 +17,7 @@ #include "core/hle/service/nvdrv/nvdrv.h" #include "core/hle/service/nvnflinger/buffer_item_consumer.h" #include "core/hle/service/nvnflinger/buffer_queue_core.h" +#include "core/hle/service/nvnflinger/fb_share_buffer_manager.h" #include "core/hle/service/nvnflinger/hos_binder_driver_server.h" #include "core/hle/service/nvnflinger/nvnflinger.h" #include "core/hle/service/nvnflinger/ui/graphic_buffer.h" @@ -331,4 +332,14 @@ s64 Nvnflinger::GetNextTicks() const { return static_cast<s64>(speed_scale * (1000000000.f / effective_fps)); } +FbShareBufferManager& Nvnflinger::GetSystemBufferManager() { + const auto lock_guard = Lock(); + + if (!system_buffer_manager) { + system_buffer_manager = std::make_unique<FbShareBufferManager>(system, *this, nvdrv); + } + + return *system_buffer_manager; +} + } // namespace Service::Nvnflinger diff --git a/src/core/hle/service/nvnflinger/nvnflinger.h b/src/core/hle/service/nvnflinger/nvnflinger.h index f478c2bc6..14c783582 100644 --- a/src/core/hle/service/nvnflinger/nvnflinger.h +++ b/src/core/hle/service/nvnflinger/nvnflinger.h @@ -45,6 +45,9 @@ class BufferQueueProducer; namespace Service::Nvnflinger { +class FbShareBufferManager; +class HosBinderDriverServer; + class Nvnflinger final { public: explicit Nvnflinger(Core::System& system_, HosBinderDriverServer& hos_binder_driver_server_); @@ -90,12 +93,16 @@ public: [[nodiscard]] s64 GetNextTicks() const; + FbShareBufferManager& GetSystemBufferManager(); + private: struct Layer { std::unique_ptr<android::BufferQueueCore> core; std::unique_ptr<android::BufferQueueProducer> producer; }; + friend class FbShareBufferManager; + private: [[nodiscard]] std::unique_lock<std::mutex> Lock() const { return std::unique_lock{*guard}; @@ -140,6 +147,8 @@ private: std::shared_ptr<Core::Timing::EventType> multi_composition_event; std::shared_ptr<Core::Timing::EventType> single_composition_event; + std::unique_ptr<FbShareBufferManager> system_buffer_manager; + std::shared_ptr<std::mutex> guard; Core::System& system; diff --git a/src/core/hle/service/nvnflinger/ui/fence.h b/src/core/hle/service/nvnflinger/ui/fence.h index 536e8156d..177aed758 100644 --- a/src/core/hle/service/nvnflinger/ui/fence.h +++ b/src/core/hle/service/nvnflinger/ui/fence.h @@ -20,6 +20,9 @@ public: static constexpr Fence NoFence() { Fence fence; fence.fences[0].id = -1; + fence.fences[1].id = -1; + fence.fences[2].id = -1; + fence.fences[3].id = -1; return fence; } diff --git a/src/core/hle/service/nvnflinger/ui/graphic_buffer.h b/src/core/hle/service/nvnflinger/ui/graphic_buffer.h index 75d1705a8..3eac5cedd 100644 --- a/src/core/hle/service/nvnflinger/ui/graphic_buffer.h +++ b/src/core/hle/service/nvnflinger/ui/graphic_buffer.h @@ -12,8 +12,7 @@ namespace Service::android { -class GraphicBuffer final { -public: +struct GraphicBuffer final { constexpr GraphicBuffer() = default; constexpr GraphicBuffer(u32 width_, u32 height_, PixelFormat format_, u32 usage_) @@ -77,7 +76,6 @@ public: return false; } -private: u32 magic{}; s32 width{}; s32 height{}; 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/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp index 2eb978379..b1bfb9898 100644 --- a/src/core/hle/service/vi/vi.cpp +++ b/src/core/hle/service/vi/vi.cpp @@ -20,9 +20,12 @@ #include "core/hle/kernel/k_readable_event.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/nvdrv/devices/nvmap.h" #include "core/hle/service/nvdrv/nvdata.h" +#include "core/hle/service/nvdrv/nvdrv.h" #include "core/hle/service/nvnflinger/binder.h" #include "core/hle/service/nvnflinger/buffer_queue_producer.h" +#include "core/hle/service/nvnflinger/fb_share_buffer_manager.h" #include "core/hle/service/nvnflinger/hos_binder_driver_server.h" #include "core/hle/service/nvnflinger/nvnflinger.h" #include "core/hle/service/nvnflinger/parcel.h" @@ -131,8 +134,9 @@ private: class ISystemDisplayService final : public ServiceFramework<ISystemDisplayService> { public: - explicit ISystemDisplayService(Core::System& system_) - : ServiceFramework{system_, "ISystemDisplayService"} { + explicit ISystemDisplayService(Core::System& system_, Nvnflinger::Nvnflinger& nvnflinger_) + : ServiceFramework{system_, "ISystemDisplayService"}, nvnflinger{nvnflinger_} { + // clang-format off static const FunctionInfo functions[] = { {1200, nullptr, "GetZOrderCountMin"}, {1202, nullptr, "GetZOrderCountMax"}, @@ -170,22 +174,126 @@ public: {3217, nullptr, "SetDisplayCmuLuma"}, {3218, nullptr, "SetDisplayCrcMode"}, {6013, nullptr, "GetLayerPresentationSubmissionTimestamps"}, - {8225, nullptr, "GetSharedBufferMemoryHandleId"}, - {8250, nullptr, "OpenSharedLayer"}, + {8225, &ISystemDisplayService::GetSharedBufferMemoryHandleId, "GetSharedBufferMemoryHandleId"}, + {8250, &ISystemDisplayService::OpenSharedLayer, "OpenSharedLayer"}, {8251, nullptr, "CloseSharedLayer"}, - {8252, nullptr, "ConnectSharedLayer"}, + {8252, &ISystemDisplayService::ConnectSharedLayer, "ConnectSharedLayer"}, {8253, nullptr, "DisconnectSharedLayer"}, - {8254, nullptr, "AcquireSharedFrameBuffer"}, - {8255, nullptr, "PresentSharedFrameBuffer"}, - {8256, nullptr, "GetSharedFrameBufferAcquirableEvent"}, + {8254, &ISystemDisplayService::AcquireSharedFrameBuffer, "AcquireSharedFrameBuffer"}, + {8255, &ISystemDisplayService::PresentSharedFrameBuffer, "PresentSharedFrameBuffer"}, + {8256, &ISystemDisplayService::GetSharedFrameBufferAcquirableEvent, "GetSharedFrameBufferAcquirableEvent"}, {8257, nullptr, "FillSharedFrameBufferColor"}, {8258, nullptr, "CancelSharedFrameBuffer"}, {9000, nullptr, "GetDp2hdmiController"}, }; + // clang-format on RegisterHandlers(functions); } private: + void GetSharedBufferMemoryHandleId(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u64 buffer_id = rp.PopRaw<u64>(); + + LOG_INFO(Service_VI, "called. buffer_id={:#x}", buffer_id); + + struct OutputParameters { + s32 nvmap_handle; + u64 size; + }; + + OutputParameters out{}; + Nvnflinger::SharedMemoryPoolLayout layout{}; + const auto result = nvnflinger.GetSystemBufferManager().GetSharedBufferMemoryHandleId( + &out.size, &out.nvmap_handle, &layout, buffer_id, 0); + + ctx.WriteBuffer(&layout, sizeof(layout)); + + IPC::ResponseBuilder rb{ctx, 6}; + rb.Push(result); + rb.PushRaw(out); + } + + void OpenSharedLayer(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u64 layer_id = rp.PopRaw<u64>(); + + LOG_INFO(Service_VI, "(STUBBED) called. layer_id={:#x}", layer_id); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void ConnectSharedLayer(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u64 layer_id = rp.PopRaw<u64>(); + + LOG_INFO(Service_VI, "(STUBBED) called. layer_id={:#x}", layer_id); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); + } + + void GetSharedFrameBufferAcquirableEvent(HLERequestContext& ctx) { + LOG_DEBUG(Service_VI, "called"); + + IPC::RequestParser rp{ctx}; + const u64 layer_id = rp.PopRaw<u64>(); + + Kernel::KReadableEvent* event{}; + const auto result = nvnflinger.GetSystemBufferManager().GetSharedFrameBufferAcquirableEvent( + &event, layer_id); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(result); + rb.PushCopyObjects(event); + } + + void AcquireSharedFrameBuffer(HLERequestContext& ctx) { + LOG_DEBUG(Service_VI, "called"); + + IPC::RequestParser rp{ctx}; + const u64 layer_id = rp.PopRaw<u64>(); + + struct OutputParameters { + android::Fence fence; + std::array<s32, 4> slots; + s64 target_slot; + }; + static_assert(sizeof(OutputParameters) == 0x40, "OutputParameters has wrong size"); + + OutputParameters out{}; + const auto result = nvnflinger.GetSystemBufferManager().AcquireSharedFrameBuffer( + &out.fence, out.slots, &out.target_slot, layer_id); + + IPC::ResponseBuilder rb{ctx, 18}; + rb.Push(result); + rb.PushRaw(out); + } + + void PresentSharedFrameBuffer(HLERequestContext& ctx) { + LOG_DEBUG(Service_VI, "called"); + + struct InputParameters { + android::Fence fence; + Common::Rectangle<s32> crop_region; + u32 window_transform; + s32 swap_interval; + u64 layer_id; + s64 surface_id; + }; + static_assert(sizeof(InputParameters) == 0x50, "InputParameters has wrong size"); + + IPC::RequestParser rp{ctx}; + auto input = rp.PopRaw<InputParameters>(); + + const auto result = nvnflinger.GetSystemBufferManager().PresentSharedFrameBuffer( + input.fence, input.crop_region, input.window_transform, input.swap_interval, + input.layer_id, input.surface_id); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + void SetLayerZ(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const u64 layer_id = rp.Pop<u64>(); @@ -228,6 +336,9 @@ private: rb.PushRaw<float>(60.0f); // This wouldn't seem to be correct for 30 fps games. rb.Push<u32>(0); } + +private: + Nvnflinger::Nvnflinger& nvnflinger; }; class IManagerDisplayService final : public ServiceFramework<IManagerDisplayService> { @@ -453,7 +564,7 @@ private: IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); - rb.PushIpcInterface<ISystemDisplayService>(system); + rb.PushIpcInterface<ISystemDisplayService>(system, nv_flinger); } void GetManagerDisplayService(HLERequestContext& ctx) { 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/core/tools/renderdoc.cpp b/src/core/tools/renderdoc.cpp index 44d24822a..947fa6cb3 100644 --- a/src/core/tools/renderdoc.cpp +++ b/src/core/tools/renderdoc.cpp @@ -7,7 +7,7 @@ #include "common/dynamic_library.h" #include "core/tools/renderdoc.h" -#ifdef WIN32 +#ifdef _WIN32 #include <windows.h> #else #include <dlfcn.h> diff --git a/src/input_common/drivers/virtual_gamepad.h b/src/input_common/drivers/virtual_gamepad.h index dfbc45a28..3a40e3fd3 100644 --- a/src/input_common/drivers/virtual_gamepad.h +++ b/src/input_common/drivers/virtual_gamepad.h @@ -67,7 +67,7 @@ public: * @param player_index the player number that will take this action * @param delta_timestamp time passed since last reading * @param gyro_x,gyro_y,gyro_z the gyro sensor readings - * @param accel_x,accel_y,accel_z the acelerometer reading + * @param accel_x,accel_y,accel_z the accelerometer reading */ void SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); diff --git a/src/input_common/helpers/joycon_protocol/generic_functions.h b/src/input_common/helpers/joycon_protocol/generic_functions.h index 90fcd17f6..b94567f82 100644 --- a/src/input_common/helpers/joycon_protocol/generic_functions.h +++ b/src/input_common/helpers/joycon_protocol/generic_functions.h @@ -55,7 +55,7 @@ public: /** * Configures the motion sensor with the specified parameters - * @param gsen gyroscope sensor sensitvity in degrees per second + * @param gsen gyroscope sensor sensitivity in degrees per second * @param gfrec gyroscope sensor frequency in hertz * @param asen accelerometer sensitivity in G force * @param afrec accelerometer frequency in hertz diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp b/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp index d508ee567..4e8ba4ae6 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp +++ b/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp @@ -55,7 +55,7 @@ void CompositeInsert(EmitContext& ctx, IR::Inst& inst, Register composite, Objec "MOV.{} {}.{},{};", type, ret, composite, type, ret, swizzle, object); } else { - // The return value is alised so we can just insert the object, it doesn't matter if it's + // The return value is aliased so we can just insert the object, it doesn't matter if it's // aliased ctx.Add("MOV.{} {}.{},{};", type, ret, swizzle, object); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image_atomic.cpp index 2a12feddc..dde0f6e9c 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image_atomic.cpp @@ -7,15 +7,12 @@ namespace Shader::Backend::SPIRV { namespace { -Id Image(EmitContext& ctx, const IR::Value& index, IR::TextureInstInfo info) { - if (!index.IsImmediate()) { - throw NotImplementedException("Indirect image indexing"); - } +Id Image(EmitContext& ctx, IR::TextureInstInfo info) { if (info.type == TextureType::Buffer) { - const ImageBufferDefinition def{ctx.image_buffers.at(index.U32())}; + const ImageBufferDefinition def{ctx.image_buffers.at(info.descriptor_index)}; return def.id; } else { - const ImageDefinition def{ctx.images.at(index.U32())}; + const ImageDefinition def{ctx.images.at(info.descriptor_index)}; return def.id; } } @@ -28,8 +25,12 @@ std::pair<Id, Id> AtomicArgs(EmitContext& ctx) { Id ImageAtomicU32(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords, Id value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { + if (!index.IsImmediate() || index.U32() != 0) { + // TODO: handle layers + throw NotImplementedException("Image indexing"); + } const auto info{inst->Flags<IR::TextureInstInfo>()}; - const Id image{Image(ctx, index, info)}; + const Id image{Image(ctx, info)}; const Id pointer{ctx.OpImageTexelPointer(ctx.image_u32, image, coords, ctx.Const(0U))}; const auto [scope, semantics]{AtomicArgs(ctx)}; return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics, value); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 72f69b7aa..57df6fc34 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -74,11 +74,6 @@ spv::ImageFormat GetImageFormat(ImageFormat format) { throw InvalidArgument("Invalid image format {}", format); } -spv::ImageFormat GetImageFormatForBuffer(ImageFormat format) { - const auto spv_format = GetImageFormat(format); - return spv_format == spv::ImageFormat::Unknown ? spv::ImageFormat::R32ui : spv_format; -} - Id ImageType(EmitContext& ctx, const ImageDescriptor& desc) { const spv::ImageFormat format{GetImageFormat(desc.format)}; const Id type{ctx.U32[1]}; @@ -1275,7 +1270,7 @@ void EmitContext::DefineImageBuffers(const Info& info, u32& binding) { if (desc.count != 1) { throw NotImplementedException("Array of image buffers"); } - const spv::ImageFormat format{GetImageFormatForBuffer(desc.format)}; + const spv::ImageFormat format{GetImageFormat(desc.format)}; const Id image_type{TypeImage(U32[1], spv::Dim::Buffer, false, false, false, 2, format)}; const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, image_type)}; const Id id{AddGlobalVariable(pointer_type, spv::StorageClass::UniformConstant)}; diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 9b13ccbab..cf9266d54 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -95,6 +95,12 @@ add_library(video_core STATIC memory_manager.h precompiled_headers.h pte_kind.h + query_cache/bank_base.h + query_cache/query_base.h + query_cache/query_cache_base.h + query_cache/query_cache.h + query_cache/query_stream.h + query_cache/types.h query_cache.h rasterizer_accelerated.cpp rasterizer_accelerated.h diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 8be7bd594..9e90c587c 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -272,13 +272,19 @@ std::pair<typename P::Buffer*, u32> BufferCache<P>::ObtainBuffer(GPUVAddr gpu_ad if (!cpu_addr) { return {&slot_buffers[NULL_BUFFER_ID], 0}; } - const BufferId buffer_id = FindBuffer(*cpu_addr, size); + return ObtainCPUBuffer(*cpu_addr, size, sync_info, post_op); +} + +template <class P> +std::pair<typename P::Buffer*, u32> BufferCache<P>::ObtainCPUBuffer( + VAddr cpu_addr, u32 size, ObtainBufferSynchronize sync_info, ObtainBufferOperation post_op) { + const BufferId buffer_id = FindBuffer(cpu_addr, size); Buffer& buffer = slot_buffers[buffer_id]; // synchronize op switch (sync_info) { case ObtainBufferSynchronize::FullSynchronize: - SynchronizeBuffer(buffer, *cpu_addr, size); + SynchronizeBuffer(buffer, cpu_addr, size); break; default: break; @@ -286,11 +292,11 @@ std::pair<typename P::Buffer*, u32> BufferCache<P>::ObtainBuffer(GPUVAddr gpu_ad switch (post_op) { case ObtainBufferOperation::MarkAsWritten: - MarkWrittenBuffer(buffer_id, *cpu_addr, size); + MarkWrittenBuffer(buffer_id, cpu_addr, size); break; case ObtainBufferOperation::DiscardWrite: { - VAddr cpu_addr_start = Common::AlignDown(*cpu_addr, 64); - VAddr cpu_addr_end = Common::AlignUp(*cpu_addr + size, 64); + VAddr cpu_addr_start = Common::AlignDown(cpu_addr, 64); + VAddr cpu_addr_end = Common::AlignUp(cpu_addr + size, 64); IntervalType interval{cpu_addr_start, cpu_addr_end}; ClearDownload(interval); common_ranges.subtract(interval); @@ -300,7 +306,7 @@ std::pair<typename P::Buffer*, u32> BufferCache<P>::ObtainBuffer(GPUVAddr gpu_ad break; } - return {&buffer, buffer.Offset(*cpu_addr)}; + return {&buffer, buffer.Offset(cpu_addr)}; } template <class P> diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 0b7135d49..c4f6e8d12 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -295,6 +295,10 @@ public: [[nodiscard]] std::pair<Buffer*, u32> ObtainBuffer(GPUVAddr gpu_addr, u32 size, ObtainBufferSynchronize sync_info, ObtainBufferOperation post_op); + + [[nodiscard]] std::pair<Buffer*, u32> ObtainCPUBuffer(VAddr gpu_addr, u32 size, + ObtainBufferSynchronize sync_info, + ObtainBufferOperation post_op); void FlushCachedWrites(); /// Return true when there are uncommitted buffers to be downloaded @@ -335,6 +339,14 @@ public: [[nodiscard]] std::pair<Buffer*, u32> GetDrawIndirectBuffer(); + template <typename Func> + void BufferOperations(Func&& func) { + do { + channel_state->has_deleted_buffers = false; + func(); + } while (channel_state->has_deleted_buffers); + } + std::recursive_mutex mutex; Runtime& runtime; diff --git a/src/video_core/control/channel_state_cache.h b/src/video_core/control/channel_state_cache.h index 46bc9e322..5574e1fba 100644 --- a/src/video_core/control/channel_state_cache.h +++ b/src/video_core/control/channel_state_cache.h @@ -51,7 +51,7 @@ public: virtual void CreateChannel(Tegra::Control::ChannelState& channel); /// Bind a channel for execution. - void BindToChannel(s32 id); + virtual void BindToChannel(s32 id); /// Erase channel's state. void EraseChannel(s32 id); diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h index c9fab2d90..e46a8fa5c 100644 --- a/src/video_core/dma_pusher.h +++ b/src/video_core/dma_pusher.h @@ -161,7 +161,7 @@ private: u32 method_count; ///< Current method count u32 length_pending; ///< Large NI command length pending GPUVAddr dma_get; ///< Currently read segment - u64 dma_word_offset; ///< Current word ofset from address + u64 dma_word_offset; ///< Current word offset from address bool non_incrementing; ///< Current command's NI flag bool is_last_call; }; diff --git a/src/video_core/engines/draw_manager.h b/src/video_core/engines/draw_manager.h index 7c22c49f1..18d959143 100644 --- a/src/video_core/engines/draw_manager.h +++ b/src/video_core/engines/draw_manager.h @@ -46,6 +46,7 @@ public: }; struct IndirectParams { + bool is_byte_count; bool is_indexed; bool include_count; GPUVAddr count_start_address; diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 06e349e43..32d767d85 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -20,8 +20,6 @@ namespace Tegra::Engines { -using VideoCore::QueryType; - /// First register id that is actually a Macro call. constexpr u32 MacroRegistersStart = 0xE00; @@ -500,27 +498,21 @@ void Maxwell3D::StampQueryResult(u64 payload, bool long_query) { } void Maxwell3D::ProcessQueryGet() { + VideoCommon::QueryPropertiesFlags flags{}; + if (regs.report_semaphore.query.short_query == 0) { + flags |= VideoCommon::QueryPropertiesFlags::HasTimeout; + } + const GPUVAddr sequence_address{regs.report_semaphore.Address()}; + const VideoCommon::QueryType query_type = + static_cast<VideoCommon::QueryType>(regs.report_semaphore.query.report.Value()); + const u32 payload = regs.report_semaphore.payload; + const u32 subreport = regs.report_semaphore.query.sub_report; switch (regs.report_semaphore.query.operation) { case Regs::ReportSemaphore::Operation::Release: if (regs.report_semaphore.query.short_query != 0) { - const GPUVAddr sequence_address{regs.report_semaphore.Address()}; - const u32 payload = regs.report_semaphore.payload; - std::function<void()> operation([this, sequence_address, payload] { - memory_manager.Write<u32>(sequence_address, payload); - }); - rasterizer->SignalFence(std::move(operation)); - } else { - struct LongQueryResult { - u64_le value; - u64_le timestamp; - }; - const GPUVAddr sequence_address{regs.report_semaphore.Address()}; - const u32 payload = regs.report_semaphore.payload; - [this, sequence_address, payload] { - memory_manager.Write<u64>(sequence_address + sizeof(u64), system.GPU().GetTicks()); - memory_manager.Write<u64>(sequence_address, payload); - }(); + flags |= VideoCommon::QueryPropertiesFlags::IsAFence; } + rasterizer->Query(sequence_address, query_type, flags, payload, subreport); break; case Regs::ReportSemaphore::Operation::Acquire: // TODO(Blinkhawk): Under this operation, the GPU waits for the CPU to write a value that @@ -528,11 +520,7 @@ void Maxwell3D::ProcessQueryGet() { UNIMPLEMENTED_MSG("Unimplemented query operation ACQUIRE"); break; case Regs::ReportSemaphore::Operation::ReportOnly: - if (const std::optional<u64> result = GetQueryResult()) { - // If the query returns an empty optional it means it's cached and deferred. - // In this case we have a non-empty result, so we stamp it immediately. - StampQueryResult(*result, regs.report_semaphore.query.short_query == 0); - } + rasterizer->Query(sequence_address, query_type, flags, payload, subreport); break; case Regs::ReportSemaphore::Operation::Trap: UNIMPLEMENTED_MSG("Unimplemented query operation TRAP"); @@ -544,6 +532,10 @@ void Maxwell3D::ProcessQueryGet() { } void Maxwell3D::ProcessQueryCondition() { + if (rasterizer->AccelerateConditionalRendering()) { + execute_on = true; + return; + } const GPUVAddr condition_address{regs.render_enable.Address()}; switch (regs.render_enable_override) { case Regs::RenderEnable::Override::AlwaysRender: @@ -553,10 +545,6 @@ void Maxwell3D::ProcessQueryCondition() { execute_on = false; break; case Regs::RenderEnable::Override::UseRenderEnable: { - if (rasterizer->AccelerateConditionalRendering()) { - execute_on = true; - return; - } switch (regs.render_enable.mode) { case Regs::RenderEnable::Mode::True: { execute_on = true; @@ -598,15 +586,9 @@ void Maxwell3D::ProcessQueryCondition() { } void Maxwell3D::ProcessCounterReset() { -#if ANDROID - if (!Settings::IsGPULevelHigh()) { - // This is problematic on Android, disable on GPU Normal. - return; - } -#endif switch (regs.clear_report_value) { case Regs::ClearReport::ZPassPixelCount: - rasterizer->ResetCounter(QueryType::SamplesPassed); + rasterizer->ResetCounter(VideoCommon::QueryType::ZPassPixelCount64); break; default: LOG_DEBUG(Render_OpenGL, "Unimplemented counter reset={}", regs.clear_report_value); @@ -620,28 +602,6 @@ void Maxwell3D::ProcessSyncPoint() { rasterizer->SignalSyncPoint(sync_point); } -std::optional<u64> Maxwell3D::GetQueryResult() { - switch (regs.report_semaphore.query.report) { - case Regs::ReportSemaphore::Report::Payload: - return regs.report_semaphore.payload; - case Regs::ReportSemaphore::Report::ZPassPixelCount64: -#if ANDROID - if (!Settings::IsGPULevelHigh()) { - // This is problematic on Android, disable on GPU Normal. - return 120; - } -#endif - // Deferred. - rasterizer->Query(regs.report_semaphore.Address(), QueryType::SamplesPassed, - system.GPU().GetTicks()); - return std::nullopt; - default: - LOG_DEBUG(HW_GPU, "Unimplemented query report type {}", - regs.report_semaphore.query.report.Value()); - return 1; - } -} - void Maxwell3D::ProcessCBBind(size_t stage_index) { // Bind the buffer currently in CB_ADDRESS to the specified index in the desired shader // stage. diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 6c19354e1..17faacc37 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3182,9 +3182,6 @@ private: /// Handles writes to syncing register. void ProcessSyncPoint(); - /// Returns a query's value or an empty object if the value will be deferred through a cache. - std::optional<u64> GetQueryResult(); - void RefreshParametersImpl(); bool IsMethodExecutable(u32 method); diff --git a/src/video_core/engines/maxwell_dma.cpp b/src/video_core/engines/maxwell_dma.cpp index da8eab7ee..422d4d859 100644 --- a/src/video_core/engines/maxwell_dma.cpp +++ b/src/video_core/engines/maxwell_dma.cpp @@ -109,10 +109,11 @@ void MaxwellDMA::Launch() { const bool is_const_a_dst = regs.remap_const.dst_x == RemapConst::Swizzle::CONST_A; if (regs.launch_dma.remap_enable != 0 && is_const_a_dst) { ASSERT(regs.remap_const.component_size_minus_one == 3); - accelerate.BufferClear(regs.offset_out, regs.line_length_in, regs.remap_consta_value); + accelerate.BufferClear(regs.offset_out, regs.line_length_in, + regs.remap_const.remap_consta_value); read_buffer.resize_destructive(regs.line_length_in * sizeof(u32)); std::span<u32> span(reinterpret_cast<u32*>(read_buffer.data()), regs.line_length_in); - std::ranges::fill(span, regs.remap_consta_value); + std::ranges::fill(span, regs.remap_const.remap_consta_value); memory_manager.WriteBlockUnsafe(regs.offset_out, reinterpret_cast<u8*>(read_buffer.data()), regs.line_length_in * sizeof(u32)); @@ -361,21 +362,17 @@ void MaxwellDMA::ReleaseSemaphore() { const auto type = regs.launch_dma.semaphore_type; const GPUVAddr address = regs.semaphore.address; const u32 payload = regs.semaphore.payload; + VideoCommon::QueryPropertiesFlags flags{VideoCommon::QueryPropertiesFlags::IsAFence}; switch (type) { case LaunchDMA::SemaphoreType::NONE: break; case LaunchDMA::SemaphoreType::RELEASE_ONE_WORD_SEMAPHORE: { - std::function<void()> operation( - [this, address, payload] { memory_manager.Write<u32>(address, payload); }); - rasterizer->SignalFence(std::move(operation)); + rasterizer->Query(address, VideoCommon::QueryType::Payload, flags, payload, 0); break; } case LaunchDMA::SemaphoreType::RELEASE_FOUR_WORD_SEMAPHORE: { - std::function<void()> operation([this, address, payload] { - memory_manager.Write<u64>(address + sizeof(u64), system.GPU().GetTicks()); - memory_manager.Write<u64>(address, payload); - }); - rasterizer->SignalFence(std::move(operation)); + rasterizer->Query(address, VideoCommon::QueryType::Payload, + flags | VideoCommon::QueryPropertiesFlags::HasTimeout, payload, 0); break; } default: diff --git a/src/video_core/engines/maxwell_dma.h b/src/video_core/engines/maxwell_dma.h index 69e26cb32..1a43e24b6 100644 --- a/src/video_core/engines/maxwell_dma.h +++ b/src/video_core/engines/maxwell_dma.h @@ -214,14 +214,15 @@ public: NO_WRITE = 6, }; - PackedGPUVAddr address; + u32 remap_consta_value; + u32 remap_constb_value; union { + BitField<0, 12, u32> dst_components_raw; BitField<0, 3, Swizzle> dst_x; BitField<4, 3, Swizzle> dst_y; BitField<8, 3, Swizzle> dst_z; BitField<12, 3, Swizzle> dst_w; - BitField<0, 12, u32> dst_components_raw; BitField<16, 2, u32> component_size_minus_one; BitField<20, 2, u32> num_src_components_minus_one; BitField<24, 2, u32> num_dst_components_minus_one; @@ -274,55 +275,57 @@ private: struct Regs { union { struct { - u32 reserved[0x40]; + INSERT_PADDING_BYTES_NOINIT(0x100); u32 nop; - u32 reserved01[0xf]; + INSERT_PADDING_BYTES_NOINIT(0x3C); u32 pm_trigger; - u32 reserved02[0x3f]; + INSERT_PADDING_BYTES_NOINIT(0xFC); Semaphore semaphore; - u32 reserved03[0x2]; + INSERT_PADDING_BYTES_NOINIT(0x8); RenderEnable render_enable; PhysMode src_phys_mode; PhysMode dst_phys_mode; - u32 reserved04[0x26]; + INSERT_PADDING_BYTES_NOINIT(0x98); LaunchDMA launch_dma; - u32 reserved05[0x3f]; + INSERT_PADDING_BYTES_NOINIT(0xFC); PackedGPUVAddr offset_in; PackedGPUVAddr offset_out; s32 pitch_in; s32 pitch_out; u32 line_length_in; u32 line_count; - u32 reserved06[0xb6]; - u32 remap_consta_value; - u32 remap_constb_value; + INSERT_PADDING_BYTES_NOINIT(0x2E0); RemapConst remap_const; DMA::Parameters dst_params; - u32 reserved07[0x1]; + INSERT_PADDING_BYTES_NOINIT(0x4); DMA::Parameters src_params; - u32 reserved08[0x275]; + INSERT_PADDING_BYTES_NOINIT(0x9D4); u32 pm_trigger_end; - u32 reserved09[0x3ba]; + INSERT_PADDING_BYTES_NOINIT(0xEE8); }; std::array<u32, NUM_REGS> reg_array; }; } regs{}; + static_assert(sizeof(Regs) == NUM_REGS * 4); #define ASSERT_REG_POSITION(field_name, position) \ - static_assert(offsetof(MaxwellDMA::Regs, field_name) == position * 4, \ + static_assert(offsetof(MaxwellDMA::Regs, field_name) == position, \ "Field " #field_name " has invalid position") - ASSERT_REG_POSITION(launch_dma, 0xC0); - ASSERT_REG_POSITION(offset_in, 0x100); - ASSERT_REG_POSITION(offset_out, 0x102); - ASSERT_REG_POSITION(pitch_in, 0x104); - ASSERT_REG_POSITION(pitch_out, 0x105); - ASSERT_REG_POSITION(line_length_in, 0x106); - ASSERT_REG_POSITION(line_count, 0x107); - ASSERT_REG_POSITION(remap_const, 0x1C0); - ASSERT_REG_POSITION(dst_params, 0x1C3); - ASSERT_REG_POSITION(src_params, 0x1CA); - + ASSERT_REG_POSITION(semaphore, 0x240); + ASSERT_REG_POSITION(render_enable, 0x254); + ASSERT_REG_POSITION(src_phys_mode, 0x260); + ASSERT_REG_POSITION(launch_dma, 0x300); + ASSERT_REG_POSITION(offset_in, 0x400); + ASSERT_REG_POSITION(offset_out, 0x408); + ASSERT_REG_POSITION(pitch_in, 0x410); + ASSERT_REG_POSITION(pitch_out, 0x414); + ASSERT_REG_POSITION(line_length_in, 0x418); + ASSERT_REG_POSITION(line_count, 0x41C); + ASSERT_REG_POSITION(remap_const, 0x700); + ASSERT_REG_POSITION(dst_params, 0x70C); + ASSERT_REG_POSITION(src_params, 0x728); + ASSERT_REG_POSITION(pm_trigger_end, 0x1114); #undef ASSERT_REG_POSITION }; diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp index 6de2543b7..8dd34c04a 100644 --- a/src/video_core/engines/puller.cpp +++ b/src/video_core/engines/puller.cpp @@ -82,10 +82,8 @@ void Puller::ProcessSemaphoreTriggerMethod() { if (op == GpuSemaphoreOperation::WriteLong) { const GPUVAddr sequence_address{regs.semaphore_address.SemaphoreAddress()}; const u32 payload = regs.semaphore_sequence; - [this, sequence_address, payload] { - memory_manager.Write<u64>(sequence_address + sizeof(u64), gpu.GetTicks()); - memory_manager.Write<u64>(sequence_address, payload); - }(); + rasterizer->Query(sequence_address, VideoCommon::QueryType::Payload, + VideoCommon::QueryPropertiesFlags::HasTimeout, payload, 0); } else { do { const u32 word{memory_manager.Read<u32>(regs.semaphore_address.SemaphoreAddress())}; @@ -120,10 +118,8 @@ void Puller::ProcessSemaphoreTriggerMethod() { void Puller::ProcessSemaphoreRelease() { const GPUVAddr sequence_address{regs.semaphore_address.SemaphoreAddress()}; const u32 payload = regs.semaphore_release; - std::function<void()> operation([this, sequence_address, payload] { - memory_manager.Write<u32>(sequence_address, payload); - }); - rasterizer->SignalFence(std::move(operation)); + rasterizer->Query(sequence_address, VideoCommon::QueryType::Payload, + VideoCommon::QueryPropertiesFlags::IsAFence, payload, 0); } void Puller::ProcessSemaphoreAcquire() { @@ -132,7 +128,6 @@ void Puller::ProcessSemaphoreAcquire() { while (word != value) { regs.acquire_active = true; regs.acquire_value = value; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); rasterizer->ReleaseFences(); word = memory_manager.Read<u32>(regs.semaphore_address.SemaphoreAddress()); // TODO(kemathe73) figure out how to do the acquire_timeout diff --git a/src/video_core/fence_manager.h b/src/video_core/fence_manager.h index ab20ff30f..805a89900 100644 --- a/src/video_core/fence_manager.h +++ b/src/video_core/fence_manager.h @@ -55,6 +55,9 @@ public: // Unlike other fences, this one doesn't void SignalOrdering() { + if constexpr (!can_async_check) { + TryReleasePendingFences<false>(); + } std::scoped_lock lock{buffer_cache.mutex}; buffer_cache.AccumulateFlushes(); } @@ -104,9 +107,25 @@ public: SignalFence(std::move(func)); } - void WaitPendingFences() { + void WaitPendingFences([[maybe_unused]] bool force) { if constexpr (!can_async_check) { TryReleasePendingFences<true>(); + } else { + if (!force) { + return; + } + std::mutex wait_mutex; + std::condition_variable wait_cv; + std::atomic<bool> wait_finished{}; + std::function<void()> func([&] { + std::scoped_lock lk(wait_mutex); + wait_finished.store(true, std::memory_order_relaxed); + wait_cv.notify_all(); + }); + SignalFence(std::move(func)); + std::unique_lock lk(wait_mutex); + wait_cv.wait( + lk, [&wait_finished] { return wait_finished.load(std::memory_order_relaxed); }); } } diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index c192e33b2..11549d448 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -102,7 +102,8 @@ struct GPU::Impl { /// Signal the ending of command list. void OnCommandListEnd() { - rasterizer->ReleaseFences(); + rasterizer->ReleaseFences(false); + Settings::UpdateGPUAccuracy(); } /// Request a host GPU memory flush from the CPU. @@ -220,6 +221,7 @@ struct GPU::Impl { /// This can be used to launch any necessary threads and register any necessary /// core timing events. void Start() { + Settings::UpdateGPUAccuracy(); gpu_thread.StartThread(*renderer, renderer->Context(), *scheduler); } diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index c4d459077..8bb429578 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -19,6 +19,7 @@ set(SHADER_FILES block_linear_unswizzle_2d.comp block_linear_unswizzle_3d.comp convert_abgr8_to_d24s8.frag + convert_d32f_to_abgr8.frag convert_d24s8_to_abgr8.frag convert_depth_to_float.frag convert_float_to_depth.frag @@ -41,6 +42,9 @@ set(SHADER_FILES pitch_unswizzle.comp present_bicubic.frag present_gaussian.frag + queries_prefix_scan_sum.comp + queries_prefix_scan_sum_nosubgroups.comp + resolve_conditional_render.comp smaa_edge_detection.vert smaa_edge_detection.frag smaa_blending_weight_calculation.vert @@ -70,6 +74,7 @@ if ("${GLSLANGVALIDATOR}" STREQUAL "GLSLANGVALIDATOR-NOTFOUND") endif() set(GLSL_FLAGS "") +set(SPIR_V_VERSION "spirv1.3") set(QUIET_FLAG "--quiet") set(SHADER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/include) @@ -123,7 +128,7 @@ foreach(FILENAME IN ITEMS ${SHADER_FILES}) OUTPUT ${SPIRV_HEADER_FILE} COMMAND - ${GLSLANGVALIDATOR} -V ${QUIET_FLAG} -I"${FIDELITYFX_INCLUDE_DIR}" ${GLSL_FLAGS} --variable-name ${SPIRV_VARIABLE_NAME} -o ${SPIRV_HEADER_FILE} ${SOURCE_FILE} + ${GLSLANGVALIDATOR} -V ${QUIET_FLAG} -I"${FIDELITYFX_INCLUDE_DIR}" ${GLSL_FLAGS} --variable-name ${SPIRV_VARIABLE_NAME} -o ${SPIRV_HEADER_FILE} ${SOURCE_FILE} --target-env ${SPIR_V_VERSION} MAIN_DEPENDENCY ${SOURCE_FILE} ) diff --git a/src/video_core/host_shaders/convert_d32f_to_abgr8.frag b/src/video_core/host_shaders/convert_d32f_to_abgr8.frag new file mode 100644 index 000000000..04cfef8b5 --- /dev/null +++ b/src/video_core/host_shaders/convert_d32f_to_abgr8.frag @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#version 450 + +layout(binding = 0) uniform sampler2D depth_tex; + +layout(location = 0) out vec4 color; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + float depth = textureLod(depth_tex, coord, 0).r; + color = vec4(depth, depth, depth, 1.0); +} diff --git a/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp b/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp index fc3854d18..66f2ad483 100644 --- a/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp +++ b/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp @@ -15,11 +15,14 @@ void main() { // TODO: Specialization constants for num_samples? const int num_samples = imageSamples(msaa_in); + const ivec3 msaa_size = imageSize(msaa_in); + const ivec3 out_size = imageSize(output_img); + const ivec3 scale = out_size / msaa_size; for (int curr_sample = 0; curr_sample < num_samples; ++curr_sample) { const vec4 pixel = imageLoad(msaa_in, coords, curr_sample); - const int single_sample_x = 2 * coords.x + (curr_sample & 1); - const int single_sample_y = 2 * coords.y + ((curr_sample / 2) & 1); + const int single_sample_x = scale.x * coords.x + (curr_sample & 1); + const int single_sample_y = scale.y * coords.y + ((curr_sample / 2) & 1); const ivec3 dest_coords = ivec3(single_sample_x, single_sample_y, coords.z); if (any(greaterThanEqual(dest_coords, imageSize(output_img)))) { diff --git a/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp b/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp index dedd962f1..c7ce38efa 100644 --- a/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp +++ b/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp @@ -15,9 +15,12 @@ void main() { // TODO: Specialization constants for num_samples? const int num_samples = imageSamples(output_msaa); + const ivec3 msaa_size = imageSize(output_msaa); + const ivec3 out_size = imageSize(img_in); + const ivec3 scale = out_size / msaa_size; for (int curr_sample = 0; curr_sample < num_samples; ++curr_sample) { - const int single_sample_x = 2 * coords.x + (curr_sample & 1); - const int single_sample_y = 2 * coords.y + ((curr_sample / 2) & 1); + const int single_sample_x = scale.x * coords.x + (curr_sample & 1); + const int single_sample_y = scale.y * coords.y + ((curr_sample / 2) & 1); const ivec3 single_coords = ivec3(single_sample_x, single_sample_y, coords.z); if (any(greaterThanEqual(single_coords, imageSize(img_in)))) { diff --git a/src/video_core/host_shaders/queries_prefix_scan_sum.comp b/src/video_core/host_shaders/queries_prefix_scan_sum.comp new file mode 100644 index 000000000..6faa8981f --- /dev/null +++ b/src/video_core/host_shaders/queries_prefix_scan_sum.comp @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#version 460 core + +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_shuffle : require +#extension GL_KHR_shader_subgroup_shuffle_relative : require +#extension GL_KHR_shader_subgroup_arithmetic : require + +#ifdef VULKAN + +#define HAS_EXTENDED_TYPES 1 +#define BEGIN_PUSH_CONSTANTS layout(push_constant) uniform PushConstants { +#define END_PUSH_CONSTANTS }; +#define UNIFORM(n) +#define BINDING_INPUT_BUFFER 0 +#define BINDING_OUTPUT_IMAGE 1 + +#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv + +#extension GL_NV_gpu_shader5 : enable +#ifdef GL_NV_gpu_shader5 +#define HAS_EXTENDED_TYPES 1 +#else +#define HAS_EXTENDED_TYPES 0 +#endif +#define BEGIN_PUSH_CONSTANTS +#define END_PUSH_CONSTANTS +#define UNIFORM(n) layout(location = n) uniform +#define BINDING_INPUT_BUFFER 0 +#define BINDING_OUTPUT_IMAGE 0 + +#endif + +BEGIN_PUSH_CONSTANTS +UNIFORM(0) uint min_accumulation_base; +UNIFORM(1) uint max_accumulation_base; +UNIFORM(2) uint accumulation_limit; +UNIFORM(3) uint buffer_offset; +END_PUSH_CONSTANTS + +#define LOCAL_RESULTS 8 +#define QUERIES_PER_INVOC 2048 + +layout(local_size_x = QUERIES_PER_INVOC / LOCAL_RESULTS) in; + +layout(std430, binding = 0) readonly buffer block1 { + uvec2 input_data[]; +}; + +layout(std430, binding = 1) coherent buffer block2 { + uvec2 output_data[]; +}; + +layout(std430, binding = 2) coherent buffer block3 { + uvec2 accumulated_data; +}; + +shared uvec2 shared_data[128]; + +// Simple Uint64 add that uses 2 uint variables for GPUs that don't support uint64 +uvec2 AddUint64(uvec2 value_1, uvec2 value_2) { + uint carry = 0; + uvec2 result; + result.x = uaddCarry(value_1.x, value_2.x, carry); + result.y = value_1.y + value_2.y + carry; + return result; +} + +// do subgroup Prefix Sum using Hillis and Steele's algorithm +uvec2 subgroupInclusiveAddUint64(uvec2 value) { + uvec2 result = value; + for (uint i = 1; i < gl_SubgroupSize; i *= 2) { + uvec2 other = subgroupShuffleUp(result, i); // get value from subgroup_inv_id - i; + if (i <= gl_SubgroupInvocationID) { + result = AddUint64(result, other); + } + } + return result; +} + +// Writes down the results to the output buffer and to the accumulation buffer +void WriteResults(uvec2 results[LOCAL_RESULTS]) { + const uint current_id = gl_LocalInvocationID.x; + const uvec2 accum = accumulated_data; + for (uint i = 0; i < LOCAL_RESULTS; i++) { + uvec2 base_data = current_id * LOCAL_RESULTS + i < min_accumulation_base ? accum : uvec2(0, 0); + AddUint64(results[i], base_data); + } + for (uint i = 0; i < LOCAL_RESULTS; i++) { + output_data[buffer_offset + current_id * LOCAL_RESULTS + i] = results[i]; + } + uint index = accumulation_limit % LOCAL_RESULTS; + uint base_id = accumulation_limit / LOCAL_RESULTS; + if (min_accumulation_base >= accumulation_limit + 1) { + if (current_id == base_id) { + accumulated_data = results[index]; + } + return; + } + // We have that ugly case in which the accumulation data is reset in the middle somewhere. + barrier(); + groupMemoryBarrier(); + + if (current_id == base_id) { + uvec2 reset_value = output_data[max_accumulation_base - 1]; + // Calculate two complement / negate manually + reset_value = AddUint64(uvec2(1,0), ~reset_value); + accumulated_data = AddUint64(results[index], reset_value); + } +} + +void main() { + const uint subgroup_inv_id = gl_SubgroupInvocationID; + const uint subgroup_id = gl_SubgroupID + gl_WorkGroupID.x * gl_NumSubgroups; + const uint last_subgroup_id = subgroupMax(subgroup_inv_id); + const uint current_id = gl_LocalInvocationID.x; + const uint total_work = accumulation_limit; + const uint last_result_id = LOCAL_RESULTS - 1; + uvec2 data[LOCAL_RESULTS]; + for (uint i = 0; i < LOCAL_RESULTS; i++) { + data[i] = input_data[buffer_offset + current_id * LOCAL_RESULTS + i]; + } + uvec2 results[LOCAL_RESULTS]; + results[0] = data[0]; + for (uint i = 1; i < LOCAL_RESULTS; i++) { + results[i] = AddUint64(data[i], results[i - 1]); + } + // make sure all input data has been loaded + subgroupBarrier(); + subgroupMemoryBarrier(); + + // on the last local result, do a subgroup inclusive scan sum + results[last_result_id] = subgroupInclusiveAddUint64(results[last_result_id]); + // get the last local result from the subgroup behind the current + uvec2 result_behind = subgroupShuffleUp(results[last_result_id], 1); + if (subgroup_inv_id != 0) { + for (uint i = 1; i < LOCAL_RESULTS; i++) { + results[i - 1] = AddUint64(results[i - 1], result_behind); + } + } + + // if we had less queries than our subgroup, just write down the results. + if (total_work <= gl_SubgroupSize * LOCAL_RESULTS) { // This condition is constant per dispatch. + WriteResults(results); + return; + } + + // We now have more, so lets write the last result into shared memory. + // Only pick the last subgroup. + if (subgroup_inv_id == last_subgroup_id) { + shared_data[subgroup_id] = results[last_result_id]; + } + // wait until everyone loaded their stuffs + barrier(); + memoryBarrierShared(); + + // only if it's not the first subgroup + if (subgroup_id != 0) { + // get the results from some previous invocation + uvec2 tmp = shared_data[subgroup_inv_id]; + subgroupBarrier(); + subgroupMemoryBarrierShared(); + tmp = subgroupInclusiveAddUint64(tmp); + // obtain the result that would be equivalent to the previous result + uvec2 shuffled_result = subgroupShuffle(tmp, subgroup_id - 1); + for (uint i = 0; i < LOCAL_RESULTS; i++) { + results[i] = AddUint64(results[i], shuffled_result); + } + } + WriteResults(results); +}
\ No newline at end of file diff --git a/src/video_core/host_shaders/queries_prefix_scan_sum_nosubgroups.comp b/src/video_core/host_shaders/queries_prefix_scan_sum_nosubgroups.comp new file mode 100644 index 000000000..559a213b9 --- /dev/null +++ b/src/video_core/host_shaders/queries_prefix_scan_sum_nosubgroups.comp @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: Copyright 2015 Graham Sellers, Richard Wright Jr. and Nicholas Haemel +// SPDX-License-Identifier: MIT + +// Code obtained from OpenGL SuperBible, Seventh Edition by Graham Sellers, Richard Wright Jr. and +// Nicholas Haemel. Modified to suit needs. + +#version 460 core + +#ifdef VULKAN + +#define HAS_EXTENDED_TYPES 1 +#define BEGIN_PUSH_CONSTANTS layout(push_constant) uniform PushConstants { +#define END_PUSH_CONSTANTS }; +#define UNIFORM(n) +#define BINDING_INPUT_BUFFER 0 +#define BINDING_OUTPUT_IMAGE 1 + +#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv + +#extension GL_NV_gpu_shader5 : enable +#ifdef GL_NV_gpu_shader5 +#define HAS_EXTENDED_TYPES 1 +#else +#define HAS_EXTENDED_TYPES 0 +#endif +#define BEGIN_PUSH_CONSTANTS +#define END_PUSH_CONSTANTS +#define UNIFORM(n) layout(location = n) uniform +#define BINDING_INPUT_BUFFER 0 +#define BINDING_OUTPUT_IMAGE 0 + +#endif + +BEGIN_PUSH_CONSTANTS +UNIFORM(0) uint min_accumulation_base; +UNIFORM(1) uint max_accumulation_base; +UNIFORM(2) uint accumulation_limit; +UNIFORM(3) uint buffer_offset; +END_PUSH_CONSTANTS + +#define LOCAL_RESULTS 4 +#define QUERIES_PER_INVOC 2048 + +layout(local_size_x = QUERIES_PER_INVOC / LOCAL_RESULTS) in; + +layout(std430, binding = 0) readonly buffer block1 { + uvec2 input_data[gl_WorkGroupSize.x * LOCAL_RESULTS]; +}; + +layout(std430, binding = 1) writeonly coherent buffer block2 { + uvec2 output_data[gl_WorkGroupSize.x * LOCAL_RESULTS]; +}; + +layout(std430, binding = 2) coherent buffer block3 { + uvec2 accumulated_data; +}; + +shared uvec2 shared_data[gl_WorkGroupSize.x * LOCAL_RESULTS]; + +uvec2 AddUint64(uvec2 value_1, uvec2 value_2) { + uint carry = 0; + uvec2 result; + result.x = uaddCarry(value_1.x, value_2.x, carry); + result.y = value_1.y + value_2.y + carry; + return result; +} + +void main(void) { + uint id = gl_LocalInvocationID.x; + uvec2 base_value[LOCAL_RESULTS]; + const uvec2 accum = accumulated_data; + for (uint i = 0; i < LOCAL_RESULTS; i++) { + base_value[i] = (buffer_offset + id * LOCAL_RESULTS + i) < min_accumulation_base + ? accumulated_data + : uvec2(0); + } + uint work_size = gl_WorkGroupSize.x; + uint rd_id; + uint wr_id; + uint mask; + uvec2 inputs[LOCAL_RESULTS]; + for (uint i = 0; i < LOCAL_RESULTS; i++) { + inputs[i] = input_data[buffer_offset + id * LOCAL_RESULTS + i]; + } + // The number of steps is the log base 2 of the + // work group size, which should be a power of 2 + const uint steps = uint(log2(work_size)) + uint(log2(LOCAL_RESULTS)); + uint step = 0; + + // Each invocation is responsible for the content of + // two elements of the output array + for (uint i = 0; i < LOCAL_RESULTS; i++) { + shared_data[id * LOCAL_RESULTS + i] = inputs[i]; + } + // Synchronize to make sure that everyone has initialized + // their elements of shared_data[] with data loaded from + // the input arrays + barrier(); + memoryBarrierShared(); + // For each step... + for (step = 0; step < steps; step++) { + // Calculate the read and write index in the + // shared array + mask = (1 << step) - 1; + rd_id = ((id >> step) << (step + 1)) + mask; + wr_id = rd_id + 1 + (id & mask); + // Accumulate the read data into our element + + shared_data[wr_id] = AddUint64(shared_data[rd_id], shared_data[wr_id]); + // Synchronize again to make sure that everyone + // has caught up with us + barrier(); + memoryBarrierShared(); + } + // Add the accumulation + for (uint i = 0; i < LOCAL_RESULTS; i++) { + shared_data[id * LOCAL_RESULTS + i] = + AddUint64(shared_data[id * LOCAL_RESULTS + i], base_value[i]); + } + barrier(); + memoryBarrierShared(); + + // Finally write our data back to the output buffer + for (uint i = 0; i < LOCAL_RESULTS; i++) { + output_data[buffer_offset + id * LOCAL_RESULTS + i] = shared_data[id * LOCAL_RESULTS + i]; + } + if (id == 0) { + if (min_accumulation_base >= accumulation_limit + 1) { + accumulated_data = shared_data[accumulation_limit]; + return; + } + uvec2 reset_value = shared_data[max_accumulation_base - 1]; + uvec2 final_value = shared_data[accumulation_limit]; + // Two complements + reset_value = AddUint64(uvec2(1, 0), ~reset_value); + accumulated_data = AddUint64(final_value, reset_value); + } +}
\ No newline at end of file diff --git a/src/video_core/host_shaders/resolve_conditional_render.comp b/src/video_core/host_shaders/resolve_conditional_render.comp new file mode 100644 index 000000000..307e77d1a --- /dev/null +++ b/src/video_core/host_shaders/resolve_conditional_render.comp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#version 450 + +layout(local_size_x = 1) in; + +layout(std430, binding = 0) buffer Query { + uvec2 initial; + uvec2 unknown; + uvec2 current; +}; + +layout(std430, binding = 1) buffer Result { + uint result; +}; + +void main() { + result = all(equal(initial, current)) ? 1 : 0; +} diff --git a/src/video_core/macro/macro_hle.cpp b/src/video_core/macro/macro_hle.cpp index 6272a4652..046c8085e 100644 --- a/src/video_core/macro/macro_hle.cpp +++ b/src/video_core/macro/macro_hle.cpp @@ -67,6 +67,7 @@ public: } auto& params = maxwell3d.draw_manager->GetIndirectParams(); + params.is_byte_count = false; params.is_indexed = false; params.include_count = false; params.count_start_address = 0; @@ -161,6 +162,7 @@ public: 0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance); } auto& params = maxwell3d.draw_manager->GetIndirectParams(); + params.is_byte_count = false; params.is_indexed = true; params.include_count = false; params.count_start_address = 0; @@ -256,6 +258,7 @@ public: const u32 estimate = static_cast<u32>(maxwell3d.EstimateIndexBufferSize()); maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true; auto& params = maxwell3d.draw_manager->GetIndirectParams(); + params.is_byte_count = false; params.is_indexed = true; params.include_count = true; params.count_start_address = maxwell3d.GetMacroAddress(4); @@ -319,6 +322,47 @@ private: } }; +class HLE_DrawIndirectByteCount final : public HLEMacroImpl { +public: + explicit HLE_DrawIndirectByteCount(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {} + + void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override { + auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0] & 0xFFFFU); + if (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology)) { + Fallback(parameters); + return; + } + + auto& params = maxwell3d.draw_manager->GetIndirectParams(); + params.is_byte_count = true; + params.is_indexed = false; + params.include_count = false; + params.count_start_address = 0; + params.indirect_start_address = maxwell3d.GetMacroAddress(2); + params.buffer_size = 4; + params.max_draw_counts = 1; + params.stride = parameters[1]; + maxwell3d.regs.draw.begin = parameters[0]; + maxwell3d.regs.draw_auto_stride = parameters[1]; + maxwell3d.regs.draw_auto_byte_count = parameters[2]; + + maxwell3d.draw_manager->DrawArrayIndirect(topology); + } + +private: + void Fallback(const std::vector<u32>& parameters) { + maxwell3d.RefreshParameters(); + + maxwell3d.regs.draw.begin = parameters[0]; + maxwell3d.regs.draw_auto_stride = parameters[1]; + maxwell3d.regs.draw_auto_byte_count = parameters[2]; + + maxwell3d.draw_manager->DrawArray( + maxwell3d.regs.draw.topology, 0, + maxwell3d.regs.draw_auto_byte_count / maxwell3d.regs.draw_auto_stride, 0, 1); + } +}; + class HLE_C713C83D8F63CCF3 final : public HLEMacroImpl { public: explicit HLE_C713C83D8F63CCF3(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {} @@ -536,6 +580,11 @@ HLEMacro::HLEMacro(Maxwell3D& maxwell3d_) : maxwell3d{maxwell3d_} { [](Maxwell3D& maxwell3d__) -> std::unique_ptr<CachedMacro> { return std::make_unique<HLE_TransformFeedbackSetup>(maxwell3d__); })); + builders.emplace(0xB5F74EDB717278ECULL, + std::function<std::unique_ptr<CachedMacro>(Maxwell3D&)>( + [](Maxwell3D& maxwell3d__) -> std::unique_ptr<CachedMacro> { + return std::make_unique<HLE_DrawIndirectByteCount>(maxwell3d__); + })); } HLEMacro::~HLEMacro() = default; diff --git a/src/video_core/query_cache.h b/src/video_core/query_cache.h index 7047e2e63..9fcaeeac7 100644 --- a/src/video_core/query_cache.h +++ b/src/video_core/query_cache.h @@ -25,6 +25,13 @@ #include "video_core/rasterizer_interface.h" #include "video_core/texture_cache/slot_vector.h" +namespace VideoCore { +enum class QueryType { + SamplesPassed, +}; +constexpr std::size_t NumQueryTypes = 1; +} // namespace VideoCore + namespace VideoCommon { using AsyncJobId = SlotId; @@ -98,10 +105,10 @@ private: }; template <class QueryCache, class CachedQuery, class CounterStream, class HostCounter> -class QueryCacheBase : public VideoCommon::ChannelSetupCaches<VideoCommon::ChannelInfo> { +class QueryCacheLegacy : public VideoCommon::ChannelSetupCaches<VideoCommon::ChannelInfo> { public: - explicit QueryCacheBase(VideoCore::RasterizerInterface& rasterizer_, - Core::Memory::Memory& cpu_memory_) + explicit QueryCacheLegacy(VideoCore::RasterizerInterface& rasterizer_, + Core::Memory::Memory& cpu_memory_) : rasterizer{rasterizer_}, // Use reinterpret_cast instead of static_cast as workaround for // UBSan bug (https://github.com/llvm/llvm-project/issues/59060) diff --git a/src/video_core/query_cache/bank_base.h b/src/video_core/query_cache/bank_base.h new file mode 100644 index 000000000..44769ea97 --- /dev/null +++ b/src/video_core/query_cache/bank_base.h @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <atomic> +#include <deque> +#include <utility> + +#include "common/common_types.h" + +namespace VideoCommon { + +class BankBase { +protected: + const size_t base_bank_size{}; + size_t bank_size{}; + std::atomic<size_t> references{}; + size_t current_slot{}; + +public: + explicit BankBase(size_t bank_size_) : base_bank_size{bank_size_}, bank_size(bank_size_) {} + + virtual ~BankBase() = default; + + virtual std::pair<bool, size_t> Reserve() { + if (IsClosed()) { + return {false, bank_size}; + } + const size_t result = current_slot++; + return {true, result}; + } + + virtual void Reset() { + current_slot = 0; + references = 0; + bank_size = base_bank_size; + } + + size_t Size() const { + return bank_size; + } + + void AddReference(size_t how_many = 1) { + references.fetch_add(how_many, std::memory_order_relaxed); + } + + void CloseReference(size_t how_many = 1) { + if (how_many > references.load(std::memory_order_relaxed)) { + UNREACHABLE(); + } + references.fetch_sub(how_many, std::memory_order_relaxed); + } + + void Close() { + bank_size = current_slot; + } + + bool IsClosed() const { + return current_slot >= bank_size; + } + + bool IsDead() const { + return IsClosed() && references == 0; + } +}; + +template <typename BankType> +class BankPool { +private: + std::deque<BankType> bank_pool; + std::deque<size_t> bank_indices; + +public: + BankPool() = default; + ~BankPool() = default; + + // Reserve a bank from the pool and return its index + template <typename Func> + size_t ReserveBank(Func&& builder) { + if (!bank_indices.empty() && bank_pool[bank_indices.front()].IsDead()) { + size_t new_index = bank_indices.front(); + bank_indices.pop_front(); + bank_pool[new_index].Reset(); + bank_indices.push_back(new_index); + return new_index; + } + size_t new_index = bank_pool.size(); + builder(bank_pool, new_index); + bank_indices.push_back(new_index); + return new_index; + } + + // Get a reference to a bank using its index + BankType& GetBank(size_t index) { + return bank_pool[index]; + } + + // Get the total number of banks in the pool + size_t BankCount() const { + return bank_pool.size(); + } +}; + +} // namespace VideoCommon diff --git a/src/video_core/query_cache/query_base.h b/src/video_core/query_cache/query_base.h new file mode 100644 index 000000000..1d786b3a7 --- /dev/null +++ b/src/video_core/query_cache/query_base.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace VideoCommon { + +enum class QueryFlagBits : u32 { + HasTimestamp = 1 << 0, ///< Indicates if this query has a timestamp. + IsFinalValueSynced = 1 << 1, ///< Indicates if the query has been synced in the host + IsHostSynced = 1 << 2, ///< Indicates if the query has been synced in the host + IsGuestSynced = 1 << 3, ///< Indicates if the query has been synced with the guest. + IsHostManaged = 1 << 4, ///< Indicates if this query points to a host query + IsRewritten = 1 << 5, ///< Indicates if this query was rewritten by another query + IsInvalidated = 1 << 6, ///< Indicates the value of th query has been nullified. + IsOrphan = 1 << 7, ///< Indicates the query has not been set by a guest query. + IsFence = 1 << 8, ///< Indicates the query is a fence. +}; +DECLARE_ENUM_FLAG_OPERATORS(QueryFlagBits) + +class QueryBase { +public: + VAddr guest_address{}; + QueryFlagBits flags{}; + u64 value{}; + +protected: + // Default constructor + QueryBase() = default; + + // Parameterized constructor + QueryBase(VAddr address, QueryFlagBits flags_, u64 value_) + : guest_address(address), flags(flags_), value{value_} {} +}; + +class GuestQuery : public QueryBase { +public: + // Parameterized constructor + GuestQuery(bool isLong, VAddr address, u64 queryValue) + : QueryBase(address, QueryFlagBits::IsFinalValueSynced, queryValue) { + if (isLong) { + flags |= QueryFlagBits::HasTimestamp; + } + } +}; + +class HostQueryBase : public QueryBase { +public: + // Default constructor + HostQueryBase() : QueryBase(0, QueryFlagBits::IsHostManaged | QueryFlagBits::IsOrphan, 0) {} + + // Parameterized constructor + HostQueryBase(bool has_timestamp, VAddr address) + : QueryBase(address, QueryFlagBits::IsHostManaged, 0), start_bank_id{}, size_banks{}, + start_slot{}, size_slots{} { + if (has_timestamp) { + flags |= QueryFlagBits::HasTimestamp; + } + } + + u32 start_bank_id{}; + u32 size_banks{}; + size_t start_slot{}; + size_t size_slots{}; +}; + +} // namespace VideoCommon
\ No newline at end of file diff --git a/src/video_core/query_cache/query_cache.h b/src/video_core/query_cache/query_cache.h new file mode 100644 index 000000000..78b42b518 --- /dev/null +++ b/src/video_core/query_cache/query_cache.h @@ -0,0 +1,580 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <deque> +#include <memory> +#include <mutex> +#include <unordered_map> +#include <utility> + +#include "common/assert.h" +#include "common/common_types.h" +#include "common/logging/log.h" +#include "common/scope_exit.h" +#include "common/settings.h" +#include "core/memory.h" +#include "video_core/engines/maxwell_3d.h" +#include "video_core/gpu.h" +#include "video_core/memory_manager.h" +#include "video_core/query_cache/bank_base.h" +#include "video_core/query_cache/query_base.h" +#include "video_core/query_cache/query_cache_base.h" +#include "video_core/query_cache/query_stream.h" +#include "video_core/query_cache/types.h" + +namespace VideoCommon { + +using Maxwell = Tegra::Engines::Maxwell3D; + +struct SyncValuesStruct { + VAddr address; + u64 value; + u64 size; + + static constexpr bool GeneratesBaseBuffer = true; +}; + +template <typename Traits> +class GuestStreamer : public SimpleStreamer<GuestQuery> { +public: + using RuntimeType = typename Traits::RuntimeType; + + GuestStreamer(size_t id_, RuntimeType& runtime_) + : SimpleStreamer<GuestQuery>(id_), runtime{runtime_} {} + + virtual ~GuestStreamer() = default; + + size_t WriteCounter(VAddr address, bool has_timestamp, u32 value, + std::optional<u32> subreport = std::nullopt) override { + auto new_id = BuildQuery(has_timestamp, address, static_cast<u64>(value)); + pending_sync.push_back(new_id); + return new_id; + } + + bool HasPendingSync() const override { + return !pending_sync.empty(); + } + + void SyncWrites() override { + if (pending_sync.empty()) { + return; + } + std::vector<SyncValuesStruct> sync_values; + sync_values.reserve(pending_sync.size()); + for (size_t pending_id : pending_sync) { + auto& query = slot_queries[pending_id]; + if (True(query.flags & QueryFlagBits::IsRewritten) || + True(query.flags & QueryFlagBits::IsInvalidated)) { + continue; + } + query.flags |= QueryFlagBits::IsHostSynced; + sync_values.emplace_back(SyncValuesStruct{ + .address = query.guest_address, + .value = query.value, + .size = static_cast<u64>(True(query.flags & QueryFlagBits::HasTimestamp) ? 8 : 4)}); + } + pending_sync.clear(); + if (sync_values.size() > 0) { + runtime.template SyncValues<SyncValuesStruct>(sync_values); + } + } + +private: + RuntimeType& runtime; + std::deque<size_t> pending_sync; +}; + +template <typename Traits> +class StubStreamer : public GuestStreamer<Traits> { +public: + using RuntimeType = typename Traits::RuntimeType; + + StubStreamer(size_t id_, RuntimeType& runtime_, u32 stub_value_) + : GuestStreamer<Traits>(id_, runtime_), stub_value{stub_value_} {} + + ~StubStreamer() override = default; + + size_t WriteCounter(VAddr address, bool has_timestamp, [[maybe_unused]] u32 value, + std::optional<u32> subreport = std::nullopt) override { + size_t new_id = + GuestStreamer<Traits>::WriteCounter(address, has_timestamp, stub_value, subreport); + return new_id; + } + +private: + u32 stub_value; +}; + +template <typename Traits> +struct QueryCacheBase<Traits>::QueryCacheBaseImpl { + using RuntimeType = typename Traits::RuntimeType; + + QueryCacheBaseImpl(QueryCacheBase<Traits>* owner_, VideoCore::RasterizerInterface& rasterizer_, + Core::Memory::Memory& cpu_memory_, RuntimeType& runtime_, Tegra::GPU& gpu_) + : owner{owner_}, rasterizer{rasterizer_}, + cpu_memory{cpu_memory_}, runtime{runtime_}, gpu{gpu_} { + streamer_mask = 0; + for (size_t i = 0; i < static_cast<size_t>(QueryType::MaxQueryTypes); i++) { + streamers[i] = runtime.GetStreamerInterface(static_cast<QueryType>(i)); + if (streamers[i]) { + streamer_mask |= 1ULL << streamers[i]->GetId(); + } + } + } + + template <typename Func> + void ForEachStreamerIn(u64 mask, Func&& func) { + static constexpr bool RETURNS_BOOL = + std::is_same_v<std::invoke_result<Func, StreamerInterface*>, bool>; + while (mask != 0) { + size_t position = std::countr_zero(mask); + mask &= ~(1ULL << position); + if constexpr (RETURNS_BOOL) { + if (func(streamers[position])) { + return; + } + } else { + func(streamers[position]); + } + } + } + + template <typename Func> + void ForEachStreamer(Func&& func) { + ForEachStreamerIn(streamer_mask, func); + } + + QueryBase* ObtainQuery(QueryCacheBase<Traits>::QueryLocation location) { + size_t which_stream = location.stream_id.Value(); + auto* streamer = streamers[which_stream]; + if (!streamer) { + return nullptr; + } + return streamer->GetQuery(location.query_id.Value()); + } + + QueryCacheBase<Traits>* owner; + VideoCore::RasterizerInterface& rasterizer; + Core::Memory::Memory& cpu_memory; + RuntimeType& runtime; + Tegra::GPU& gpu; + std::array<StreamerInterface*, static_cast<size_t>(QueryType::MaxQueryTypes)> streamers; + u64 streamer_mask; + std::mutex flush_guard; + std::deque<u64> flushes_pending; + std::vector<QueryCacheBase<Traits>::QueryLocation> pending_unregister; +}; + +template <typename Traits> +QueryCacheBase<Traits>::QueryCacheBase(Tegra::GPU& gpu_, + VideoCore::RasterizerInterface& rasterizer_, + Core::Memory::Memory& cpu_memory_, RuntimeType& runtime_) + : cached_queries{} { + impl = std::make_unique<QueryCacheBase<Traits>::QueryCacheBaseImpl>( + this, rasterizer_, cpu_memory_, runtime_, gpu_); +} + +template <typename Traits> +QueryCacheBase<Traits>::~QueryCacheBase() = default; + +template <typename Traits> +void QueryCacheBase<Traits>::CounterEnable(QueryType counter_type, bool is_enabled) { + size_t index = static_cast<size_t>(counter_type); + StreamerInterface* streamer = impl->streamers[index]; + if (!streamer) [[unlikely]] { + UNREACHABLE(); + return; + } + if (is_enabled) { + streamer->StartCounter(); + } else { + streamer->PauseCounter(); + } +} + +template <typename Traits> +void QueryCacheBase<Traits>::CounterClose(QueryType counter_type) { + size_t index = static_cast<size_t>(counter_type); + StreamerInterface* streamer = impl->streamers[index]; + if (!streamer) [[unlikely]] { + UNREACHABLE(); + return; + } + streamer->CloseCounter(); +} + +template <typename Traits> +void QueryCacheBase<Traits>::CounterReset(QueryType counter_type) { + size_t index = static_cast<size_t>(counter_type); + StreamerInterface* streamer = impl->streamers[index]; + if (!streamer) [[unlikely]] { + UNIMPLEMENTED(); + return; + } + streamer->ResetCounter(); +} + +template <typename Traits> +void QueryCacheBase<Traits>::BindToChannel(s32 id) { + VideoCommon::ChannelSetupCaches<VideoCommon::ChannelInfo>::BindToChannel(id); + impl->runtime.Bind3DEngine(maxwell3d); +} + +template <typename Traits> +void QueryCacheBase<Traits>::CounterReport(GPUVAddr addr, QueryType counter_type, + QueryPropertiesFlags flags, u32 payload, u32 subreport) { + const bool has_timestamp = True(flags & QueryPropertiesFlags::HasTimeout); + const bool is_fence = True(flags & QueryPropertiesFlags::IsAFence); + size_t streamer_id = static_cast<size_t>(counter_type); + auto* streamer = impl->streamers[streamer_id]; + if (streamer == nullptr) [[unlikely]] { + counter_type = QueryType::Payload; + payload = 1U; + streamer_id = static_cast<size_t>(counter_type); + streamer = impl->streamers[streamer_id]; + } + auto cpu_addr_opt = gpu_memory->GpuToCpuAddress(addr); + if (!cpu_addr_opt) [[unlikely]] { + return; + } + VAddr cpu_addr = *cpu_addr_opt; + const size_t new_query_id = streamer->WriteCounter(cpu_addr, has_timestamp, payload, subreport); + auto* query = streamer->GetQuery(new_query_id); + if (is_fence) { + query->flags |= QueryFlagBits::IsFence; + } + QueryLocation query_location{}; + query_location.stream_id.Assign(static_cast<u32>(streamer_id)); + query_location.query_id.Assign(static_cast<u32>(new_query_id)); + const auto gen_caching_indexing = [](VAddr cur_addr) { + return std::make_pair<u64, u32>(cur_addr >> Core::Memory::YUZU_PAGEBITS, + static_cast<u32>(cur_addr & Core::Memory::YUZU_PAGEMASK)); + }; + u8* pointer = impl->cpu_memory.GetPointer(cpu_addr); + u8* pointer_timestamp = impl->cpu_memory.GetPointer(cpu_addr + 8); + bool is_synced = !Settings::IsGPULevelHigh() && is_fence; + + std::function<void()> operation([this, is_synced, streamer, query_base = query, query_location, + pointer, pointer_timestamp] { + if (True(query_base->flags & QueryFlagBits::IsInvalidated)) { + if (!is_synced) [[likely]] { + impl->pending_unregister.push_back(query_location); + } + return; + } + if (False(query_base->flags & QueryFlagBits::IsFinalValueSynced)) [[unlikely]] { + UNREACHABLE(); + return; + } + query_base->value += streamer->GetAmmendValue(); + streamer->SetAccumulationValue(query_base->value); + if (True(query_base->flags & QueryFlagBits::HasTimestamp)) { + u64 timestamp = impl->gpu.GetTicks(); + std::memcpy(pointer_timestamp, ×tamp, sizeof(timestamp)); + std::memcpy(pointer, &query_base->value, sizeof(query_base->value)); + } else { + u32 value = static_cast<u32>(query_base->value); + std::memcpy(pointer, &value, sizeof(value)); + } + if (!is_synced) [[likely]] { + impl->pending_unregister.push_back(query_location); + } + }); + if (is_fence) { + impl->rasterizer.SignalFence(std::move(operation)); + } else { + if (!Settings::IsGPULevelHigh() && counter_type == QueryType::Payload) { + if (has_timestamp) { + u64 timestamp = impl->gpu.GetTicks(); + u64 value = static_cast<u64>(payload); + std::memcpy(pointer_timestamp, ×tamp, sizeof(timestamp)); + std::memcpy(pointer, &value, sizeof(value)); + } else { + std::memcpy(pointer, &payload, sizeof(payload)); + } + streamer->Free(new_query_id); + return; + } + impl->rasterizer.SyncOperation(std::move(operation)); + } + if (is_synced) { + streamer->Free(new_query_id); + return; + } + auto [cont_addr, base] = gen_caching_indexing(cpu_addr); + { + std::scoped_lock lock(cache_mutex); + auto it1 = cached_queries.try_emplace(cont_addr); + auto& sub_container = it1.first->second; + auto it_current = sub_container.find(base); + if (it_current == sub_container.end()) { + sub_container.insert_or_assign(base, query_location); + return; + } + auto* old_query = impl->ObtainQuery(it_current->second); + old_query->flags |= QueryFlagBits::IsRewritten; + sub_container.insert_or_assign(base, query_location); + } +} + +template <typename Traits> +void QueryCacheBase<Traits>::UnregisterPending() { + const auto gen_caching_indexing = [](VAddr cur_addr) { + return std::make_pair<u64, u32>(cur_addr >> Core::Memory::YUZU_PAGEBITS, + static_cast<u32>(cur_addr & Core::Memory::YUZU_PAGEMASK)); + }; + std::scoped_lock lock(cache_mutex); + for (QueryLocation loc : impl->pending_unregister) { + const auto [streamer_id, query_id] = loc.unpack(); + auto* streamer = impl->streamers[streamer_id]; + if (!streamer) [[unlikely]] { + continue; + } + auto* query = streamer->GetQuery(query_id); + auto [cont_addr, base] = gen_caching_indexing(query->guest_address); + auto it1 = cached_queries.find(cont_addr); + if (it1 != cached_queries.end()) { + auto it2 = it1->second.find(base); + if (it2 != it1->second.end()) { + if (it2->second.raw == loc.raw) { + it1->second.erase(it2); + } + } + } + streamer->Free(query_id); + } + impl->pending_unregister.clear(); +} + +template <typename Traits> +void QueryCacheBase<Traits>::NotifyWFI() { + bool should_sync = false; + impl->ForEachStreamer( + [&should_sync](StreamerInterface* streamer) { should_sync |= streamer->HasPendingSync(); }); + if (!should_sync) { + return; + } + + impl->ForEachStreamer([](StreamerInterface* streamer) { streamer->PresyncWrites(); }); + impl->runtime.Barriers(true); + impl->ForEachStreamer([](StreamerInterface* streamer) { streamer->SyncWrites(); }); + impl->runtime.Barriers(false); +} + +template <typename Traits> +void QueryCacheBase<Traits>::NotifySegment(bool resume) { + if (resume) { + impl->runtime.ResumeHostConditionalRendering(); + } else { + CounterClose(VideoCommon::QueryType::ZPassPixelCount64); + CounterClose(VideoCommon::QueryType::StreamingByteCount); + impl->runtime.PauseHostConditionalRendering(); + } +} + +template <typename Traits> +bool QueryCacheBase<Traits>::AccelerateHostConditionalRendering() { + bool qc_dirty = false; + const auto gen_lookup = [this, &qc_dirty](GPUVAddr address) -> VideoCommon::LookupData { + auto cpu_addr_opt = gpu_memory->GpuToCpuAddress(address); + if (!cpu_addr_opt) [[unlikely]] { + return VideoCommon::LookupData{ + .address = 0, + .found_query = nullptr, + }; + } + VAddr cpu_addr = *cpu_addr_opt; + std::scoped_lock lock(cache_mutex); + auto it1 = cached_queries.find(cpu_addr >> Core::Memory::YUZU_PAGEBITS); + if (it1 == cached_queries.end()) { + return VideoCommon::LookupData{ + .address = cpu_addr, + .found_query = nullptr, + }; + } + auto& sub_container = it1->second; + auto it_current = sub_container.find(cpu_addr & Core::Memory::YUZU_PAGEMASK); + + if (it_current == sub_container.end()) { + auto it_current_2 = sub_container.find((cpu_addr & Core::Memory::YUZU_PAGEMASK) + 4); + if (it_current_2 == sub_container.end()) { + return VideoCommon::LookupData{ + .address = cpu_addr, + .found_query = nullptr, + }; + } + } + auto* query = impl->ObtainQuery(it_current->second); + qc_dirty |= True(query->flags & QueryFlagBits::IsHostManaged) && + False(query->flags & QueryFlagBits::IsGuestSynced); + return VideoCommon::LookupData{ + .address = cpu_addr, + .found_query = query, + }; + }; + + auto& regs = maxwell3d->regs; + if (regs.render_enable_override != Maxwell::Regs::RenderEnable::Override::UseRenderEnable) { + impl->runtime.EndHostConditionalRendering(); + return false; + } + const ComparisonMode mode = static_cast<ComparisonMode>(regs.render_enable.mode); + const GPUVAddr address = regs.render_enable.Address(); + switch (mode) { + case ComparisonMode::True: + impl->runtime.EndHostConditionalRendering(); + return false; + case ComparisonMode::False: + impl->runtime.EndHostConditionalRendering(); + return false; + case ComparisonMode::Conditional: { + VideoCommon::LookupData object_1{gen_lookup(address)}; + return impl->runtime.HostConditionalRenderingCompareValue(object_1, qc_dirty); + } + case ComparisonMode::IfEqual: { + VideoCommon::LookupData object_1{gen_lookup(address)}; + VideoCommon::LookupData object_2{gen_lookup(address + 16)}; + return impl->runtime.HostConditionalRenderingCompareValues(object_1, object_2, qc_dirty, + true); + } + case ComparisonMode::IfNotEqual: { + VideoCommon::LookupData object_1{gen_lookup(address)}; + VideoCommon::LookupData object_2{gen_lookup(address + 16)}; + return impl->runtime.HostConditionalRenderingCompareValues(object_1, object_2, qc_dirty, + false); + } + default: + return false; + } +} + +// Async downloads +template <typename Traits> +void QueryCacheBase<Traits>::CommitAsyncFlushes() { + // Make sure to have the results synced in Host. + NotifyWFI(); + + u64 mask{}; + { + std::scoped_lock lk(impl->flush_guard); + impl->ForEachStreamer([&mask](StreamerInterface* streamer) { + bool local_result = streamer->HasUnsyncedQueries(); + if (local_result) { + mask |= 1ULL << streamer->GetId(); + } + }); + impl->flushes_pending.push_back(mask); + } + std::function<void()> func([this] { UnregisterPending(); }); + impl->rasterizer.SyncOperation(std::move(func)); + if (mask == 0) { + return; + } + u64 ran_mask = ~mask; + while (mask) { + impl->ForEachStreamerIn(mask, [&mask, &ran_mask](StreamerInterface* streamer) { + u64 dep_mask = streamer->GetDependentMask(); + if ((dep_mask & ~ran_mask) != 0) { + return; + } + u64 index = streamer->GetId(); + ran_mask |= (1ULL << index); + mask &= ~(1ULL << index); + streamer->PushUnsyncedQueries(); + }); + } +} + +template <typename Traits> +bool QueryCacheBase<Traits>::HasUncommittedFlushes() const { + bool result = false; + impl->ForEachStreamer([&result](StreamerInterface* streamer) { + result |= streamer->HasUnsyncedQueries(); + return result; + }); + return result; +} + +template <typename Traits> +bool QueryCacheBase<Traits>::ShouldWaitAsyncFlushes() { + std::scoped_lock lk(impl->flush_guard); + return !impl->flushes_pending.empty() && impl->flushes_pending.front() != 0ULL; +} + +template <typename Traits> +void QueryCacheBase<Traits>::PopAsyncFlushes() { + u64 mask; + { + std::scoped_lock lk(impl->flush_guard); + mask = impl->flushes_pending.front(); + impl->flushes_pending.pop_front(); + } + if (mask == 0) { + return; + } + u64 ran_mask = ~mask; + while (mask) { + impl->ForEachStreamerIn(mask, [&mask, &ran_mask](StreamerInterface* streamer) { + u64 dep_mask = streamer->GetDependenceMask(); + if ((dep_mask & ~ran_mask) != 0) { + return; + } + u64 index = streamer->GetId(); + ran_mask |= (1ULL << index); + mask &= ~(1ULL << index); + streamer->PopUnsyncedQueries(); + }); + } +} + +// Invalidation + +template <typename Traits> +void QueryCacheBase<Traits>::InvalidateQuery(QueryCacheBase<Traits>::QueryLocation location) { + auto* query_base = impl->ObtainQuery(location); + if (!query_base) { + return; + } + query_base->flags |= QueryFlagBits::IsInvalidated; +} + +template <typename Traits> +bool QueryCacheBase<Traits>::IsQueryDirty(QueryCacheBase<Traits>::QueryLocation location) { + auto* query_base = impl->ObtainQuery(location); + if (!query_base) { + return false; + } + return True(query_base->flags & QueryFlagBits::IsHostManaged) && + False(query_base->flags & QueryFlagBits::IsGuestSynced); +} + +template <typename Traits> +bool QueryCacheBase<Traits>::SemiFlushQueryDirty(QueryCacheBase<Traits>::QueryLocation location) { + auto* query_base = impl->ObtainQuery(location); + if (!query_base) { + return false; + } + if (True(query_base->flags & QueryFlagBits::IsFinalValueSynced) && + False(query_base->flags & QueryFlagBits::IsGuestSynced)) { + auto* ptr = impl->cpu_memory.GetPointer(query_base->guest_address); + if (True(query_base->flags & QueryFlagBits::HasTimestamp)) { + std::memcpy(ptr, &query_base->value, sizeof(query_base->value)); + return false; + } + u32 value_l = static_cast<u32>(query_base->value); + std::memcpy(ptr, &value_l, sizeof(value_l)); + return false; + } + return True(query_base->flags & QueryFlagBits::IsHostManaged) && + False(query_base->flags & QueryFlagBits::IsGuestSynced); +} + +template <typename Traits> +void QueryCacheBase<Traits>::RequestGuestHostSync() { + impl->rasterizer.ReleaseFences(); +} + +} // namespace VideoCommon diff --git a/src/video_core/query_cache/query_cache_base.h b/src/video_core/query_cache/query_cache_base.h new file mode 100644 index 000000000..07be421c6 --- /dev/null +++ b/src/video_core/query_cache/query_cache_base.h @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <functional> +#include <mutex> +#include <optional> +#include <span> +#include <unordered_map> +#include <utility> + +#include "common/assert.h" +#include "common/bit_field.h" +#include "common/common_types.h" +#include "core/memory.h" +#include "video_core/control/channel_state_cache.h" +#include "video_core/query_cache/query_base.h" +#include "video_core/query_cache/types.h" + +namespace Core::Memory { +class Memory; +} + +namespace VideoCore { +class RasterizerInterface; +} + +namespace Tegra { +class GPU; +} + +namespace VideoCommon { + +struct LookupData { + VAddr address; + QueryBase* found_query; +}; + +template <typename Traits> +class QueryCacheBase : public VideoCommon::ChannelSetupCaches<VideoCommon::ChannelInfo> { + using RuntimeType = typename Traits::RuntimeType; + +public: + union QueryLocation { + BitField<27, 5, u32> stream_id; + BitField<0, 27, u32> query_id; + u32 raw; + + std::pair<size_t, size_t> unpack() const { + return {static_cast<size_t>(stream_id.Value()), static_cast<size_t>(query_id.Value())}; + } + }; + + explicit QueryCacheBase(Tegra::GPU& gpu, VideoCore::RasterizerInterface& rasterizer_, + Core::Memory::Memory& cpu_memory_, RuntimeType& runtime_); + + ~QueryCacheBase(); + + void InvalidateRegion(VAddr addr, std::size_t size) { + IterateCache<true>(addr, size, + [this](QueryLocation location) { InvalidateQuery(location); }); + } + + void FlushRegion(VAddr addr, std::size_t size) { + bool result = false; + IterateCache<false>(addr, size, [this, &result](QueryLocation location) { + result |= SemiFlushQueryDirty(location); + return result; + }); + if (result) { + RequestGuestHostSync(); + } + } + + static u64 BuildMask(std::span<const QueryType> types) { + u64 mask = 0; + for (auto query_type : types) { + mask |= 1ULL << (static_cast<u64>(query_type)); + } + return mask; + } + + /// Return true when a CPU region is modified from the GPU + [[nodiscard]] bool IsRegionGpuModified(VAddr addr, size_t size) { + bool result = false; + IterateCache<false>(addr, size, [this, &result](QueryLocation location) { + result |= IsQueryDirty(location); + return result; + }); + return result; + } + + void CounterEnable(QueryType counter_type, bool is_enabled); + + void CounterReset(QueryType counter_type); + + void CounterClose(QueryType counter_type); + + void CounterReport(GPUVAddr addr, QueryType counter_type, QueryPropertiesFlags flags, + u32 payload, u32 subreport); + + void NotifyWFI(); + + bool AccelerateHostConditionalRendering(); + + // Async downloads + void CommitAsyncFlushes(); + + bool HasUncommittedFlushes() const; + + bool ShouldWaitAsyncFlushes(); + + void PopAsyncFlushes(); + + void NotifySegment(bool resume); + + void BindToChannel(s32 id) override; + +protected: + template <bool remove_from_cache, typename Func> + void IterateCache(VAddr addr, std::size_t size, Func&& func) { + static constexpr bool RETURNS_BOOL = + std::is_same_v<std::invoke_result<Func, QueryLocation>, bool>; + const u64 addr_begin = addr; + const u64 addr_end = addr_begin + size; + + const u64 page_end = addr_end >> Core::Memory::YUZU_PAGEBITS; + std::scoped_lock lock(cache_mutex); + for (u64 page = addr_begin >> Core::Memory::YUZU_PAGEBITS; page <= page_end; ++page) { + const u64 page_start = page << Core::Memory::YUZU_PAGEBITS; + const auto in_range = [page_start, addr_begin, addr_end](const u32 query_location) { + const u64 cache_begin = page_start + query_location; + const u64 cache_end = cache_begin + sizeof(u32); + return cache_begin < addr_end && addr_begin < cache_end; + }; + const auto& it = cached_queries.find(page); + if (it == std::end(cached_queries)) { + continue; + } + auto& contents = it->second; + for (auto& query : contents) { + if (!in_range(query.first)) { + continue; + } + if constexpr (RETURNS_BOOL) { + if (func(query.second)) { + return; + } + } else { + func(query.second); + } + } + if constexpr (remove_from_cache) { + const auto in_range2 = [&](const std::pair<u32, QueryLocation>& pair) { + return in_range(pair.first); + }; + std::erase_if(contents, in_range2); + } + } + } + + using ContentCache = std::unordered_map<u64, std::unordered_map<u32, QueryLocation>>; + + void InvalidateQuery(QueryLocation location); + bool IsQueryDirty(QueryLocation location); + bool SemiFlushQueryDirty(QueryLocation location); + void RequestGuestHostSync(); + void UnregisterPending(); + + std::unordered_map<u64, std::unordered_map<u32, QueryLocation>> cached_queries; + std::mutex cache_mutex; + + struct QueryCacheBaseImpl; + friend struct QueryCacheBaseImpl; + friend RuntimeType; + + std::unique_ptr<QueryCacheBaseImpl> impl; +}; + +} // namespace VideoCommon
\ No newline at end of file diff --git a/src/video_core/query_cache/query_stream.h b/src/video_core/query_cache/query_stream.h new file mode 100644 index 000000000..39da6ac07 --- /dev/null +++ b/src/video_core/query_cache/query_stream.h @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <deque> +#include <optional> +#include <vector> + +#include "common/assert.h" +#include "common/common_types.h" +#include "video_core/query_cache/bank_base.h" +#include "video_core/query_cache/query_base.h" + +namespace VideoCommon { + +class StreamerInterface { +public: + explicit StreamerInterface(size_t id_) : id{id_}, dependence_mask{}, dependent_mask{} {} + virtual ~StreamerInterface() = default; + + virtual QueryBase* GetQuery(size_t id) = 0; + + virtual void StartCounter() { + /* Do Nothing */ + } + + virtual void PauseCounter() { + /* Do Nothing */ + } + + virtual void ResetCounter() { + /* Do Nothing */ + } + + virtual void CloseCounter() { + /* Do Nothing */ + } + + virtual bool HasPendingSync() const { + return false; + } + + virtual void PresyncWrites() { + /* Do Nothing */ + } + + virtual void SyncWrites() { + /* Do Nothing */ + } + + virtual size_t WriteCounter(VAddr address, bool has_timestamp, u32 value, + std::optional<u32> subreport = std::nullopt) = 0; + + virtual bool HasUnsyncedQueries() const { + return false; + } + + virtual void PushUnsyncedQueries() { + /* Do Nothing */ + } + + virtual void PopUnsyncedQueries() { + /* Do Nothing */ + } + + virtual void Free(size_t query_id) = 0; + + size_t GetId() const { + return id; + } + + u64 GetDependenceMask() const { + return dependence_mask; + } + + u64 GetDependentMask() const { + return dependence_mask; + } + + u64 GetAmmendValue() const { + return ammend_value; + } + + void SetAccumulationValue(u64 new_value) { + acumulation_value = new_value; + } + +protected: + void MakeDependent(StreamerInterface* depend_on) { + dependence_mask |= 1ULL << depend_on->id; + depend_on->dependent_mask |= 1ULL << id; + } + + const size_t id; + u64 dependence_mask; + u64 dependent_mask; + u64 ammend_value{}; + u64 acumulation_value{}; +}; + +template <typename QueryType> +class SimpleStreamer : public StreamerInterface { +public: + explicit SimpleStreamer(size_t id_) : StreamerInterface{id_} {} + virtual ~SimpleStreamer() = default; + +protected: + virtual QueryType* GetQuery(size_t query_id) override { + if (query_id < slot_queries.size()) { + return &slot_queries[query_id]; + } + return nullptr; + } + + virtual void Free(size_t query_id) override { + std::scoped_lock lk(guard); + ReleaseQuery(query_id); + } + + template <typename... Args, typename = decltype(QueryType(std::declval<Args>()...))> + size_t BuildQuery(Args&&... args) { + std::scoped_lock lk(guard); + if (!old_queries.empty()) { + size_t new_id = old_queries.front(); + old_queries.pop_front(); + new (&slot_queries[new_id]) QueryType(std::forward<Args>(args)...); + return new_id; + } + size_t new_id = slot_queries.size(); + slot_queries.emplace_back(std::forward<Args>(args)...); + return new_id; + } + + void ReleaseQuery(size_t query_id) { + + if (query_id < slot_queries.size()) { + old_queries.push_back(query_id); + return; + } + UNREACHABLE(); + } + + std::mutex guard; + std::deque<QueryType> slot_queries; + std::deque<size_t> old_queries; +}; + +} // namespace VideoCommon
\ No newline at end of file diff --git a/src/video_core/query_cache/types.h b/src/video_core/query_cache/types.h new file mode 100644 index 000000000..e9226bbfc --- /dev/null +++ b/src/video_core/query_cache/types.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace VideoCommon { + +enum class QueryPropertiesFlags : u32 { + HasTimeout = 1 << 0, + IsAFence = 1 << 1, +}; +DECLARE_ENUM_FLAG_OPERATORS(QueryPropertiesFlags) + +// This should always be equivalent to maxwell3d Report Semaphore Reports +enum class QueryType : u32 { + Payload = 0, // "None" in docs, but confirmed via hardware to return the payload + VerticesGenerated = 1, + ZPassPixelCount = 2, + PrimitivesGenerated = 3, + AlphaBetaClocks = 4, + VertexShaderInvocations = 5, + StreamingPrimitivesNeededMinusSucceeded = 6, + GeometryShaderInvocations = 7, + GeometryShaderPrimitivesGenerated = 9, + ZCullStats0 = 10, + StreamingPrimitivesSucceeded = 11, + ZCullStats1 = 12, + StreamingPrimitivesNeeded = 13, + ZCullStats2 = 14, + ClipperInvocations = 15, + ZCullStats3 = 16, + ClipperPrimitivesGenerated = 17, + VtgPrimitivesOut = 18, + PixelShaderInvocations = 19, + ZPassPixelCount64 = 21, + IEEECleanColorTarget = 24, + IEEECleanZetaTarget = 25, + StreamingByteCount = 26, + TessellationInitInvocations = 27, + BoundingRectangle = 28, + TessellationShaderInvocations = 29, + TotalStreamingPrimitivesNeededMinusSucceeded = 30, + TessellationShaderPrimitivesGenerated = 31, + // max. + MaxQueryTypes, +}; + +// Comparison modes for Host Conditional Rendering +enum class ComparisonMode : u32 { + False = 0, + True = 1, + Conditional = 2, + IfEqual = 3, + IfNotEqual = 4, + MaxComparisonMode, +}; + +// Reduction ops. +enum class ReductionOp : u32 { + RedAdd = 0, + RedMin = 1, + RedMax = 2, + RedInc = 3, + RedDec = 4, + RedAnd = 5, + RedOr = 6, + RedXor = 7, + MaxReductionOp, +}; + +} // namespace VideoCommon
\ No newline at end of file diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h index cb8029a4f..af1469147 100644 --- a/src/video_core/rasterizer_interface.h +++ b/src/video_core/rasterizer_interface.h @@ -12,6 +12,7 @@ #include "video_core/cache_types.h" #include "video_core/engines/fermi_2d.h" #include "video_core/gpu.h" +#include "video_core/query_cache/types.h" #include "video_core/rasterizer_download_area.h" namespace Tegra { @@ -26,11 +27,6 @@ struct ChannelState; namespace VideoCore { -enum class QueryType { - SamplesPassed, -}; -constexpr std::size_t NumQueryTypes = 1; - enum class LoadCallbackStage { Prepare, Build, @@ -58,10 +54,11 @@ public: virtual void DispatchCompute() = 0; /// Resets the counter of a query - virtual void ResetCounter(QueryType type) = 0; + virtual void ResetCounter(VideoCommon::QueryType type) = 0; /// Records a GPU query and caches it - virtual void Query(GPUVAddr gpu_addr, QueryType type, std::optional<u64> timestamp) = 0; + virtual void Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) = 0; /// Signal an uniform buffer binding virtual void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, @@ -83,7 +80,7 @@ public: virtual void SignalReference() = 0; /// Release all pending fences. - virtual void ReleaseFences() = 0; + virtual void ReleaseFences(bool force = true) = 0; /// Notify rasterizer that all caches should be flushed to Switch memory virtual void FlushAll() = 0; 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_null/null_rasterizer.cpp b/src/video_core/renderer_null/null_rasterizer.cpp index 92ecf6682..65cd5aa06 100644 --- a/src/video_core/renderer_null/null_rasterizer.cpp +++ b/src/video_core/renderer_null/null_rasterizer.cpp @@ -26,16 +26,18 @@ void RasterizerNull::Draw(bool is_indexed, u32 instance_count) {} void RasterizerNull::DrawTexture() {} void RasterizerNull::Clear(u32 layer_count) {} void RasterizerNull::DispatchCompute() {} -void RasterizerNull::ResetCounter(VideoCore::QueryType type) {} -void RasterizerNull::Query(GPUVAddr gpu_addr, VideoCore::QueryType type, - std::optional<u64> timestamp) { +void RasterizerNull::ResetCounter(VideoCommon::QueryType type) {} +void RasterizerNull::Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) { if (!gpu_memory) { return; } - - gpu_memory->Write(gpu_addr, u64{0}); - if (timestamp) { - gpu_memory->Write(gpu_addr + 8, *timestamp); + if (True(flags & VideoCommon::QueryPropertiesFlags::HasTimeout)) { + u64 ticks = m_gpu.GetTicks(); + gpu_memory->Write<u64>(gpu_addr + 8, ticks); + gpu_memory->Write<u64>(gpu_addr, static_cast<u64>(payload)); + } else { + gpu_memory->Write<u32>(gpu_addr, payload); } } void RasterizerNull::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, @@ -74,7 +76,7 @@ void RasterizerNull::SignalSyncPoint(u32 value) { syncpoint_manager.IncrementHost(value); } void RasterizerNull::SignalReference() {} -void RasterizerNull::ReleaseFences() {} +void RasterizerNull::ReleaseFences(bool) {} void RasterizerNull::FlushAndInvalidateRegion(VAddr addr, u64 size, VideoCommon::CacheType) {} void RasterizerNull::WaitForIdle() {} void RasterizerNull::FragmentBarrier() {} diff --git a/src/video_core/renderer_null/null_rasterizer.h b/src/video_core/renderer_null/null_rasterizer.h index 93b9a6971..23001eeb8 100644 --- a/src/video_core/renderer_null/null_rasterizer.h +++ b/src/video_core/renderer_null/null_rasterizer.h @@ -42,8 +42,9 @@ public: void DrawTexture() override; void Clear(u32 layer_count) override; void DispatchCompute() override; - void ResetCounter(VideoCore::QueryType type) override; - void Query(GPUVAddr gpu_addr, VideoCore::QueryType type, std::optional<u64> timestamp) override; + void ResetCounter(VideoCommon::QueryType type) override; + void Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) override; void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, u32 size) override; void DisableGraphicsUniformBuffer(size_t stage, u32 index) override; void FlushAll() override; @@ -63,7 +64,7 @@ public: void SyncOperation(std::function<void()>&& func) override; void SignalSyncPoint(u32 value) override; void SignalReference() override; - void ReleaseFences() override; + void ReleaseFences(bool force) override; void FlushAndInvalidateRegion( VAddr addr, u64 size, VideoCommon::CacheType which = VideoCommon::CacheType::All) override; void WaitForIdle() override; diff --git a/src/video_core/renderer_opengl/gl_query_cache.cpp b/src/video_core/renderer_opengl/gl_query_cache.cpp index 99d7347f5..ec142d48e 100644 --- a/src/video_core/renderer_opengl/gl_query_cache.cpp +++ b/src/video_core/renderer_opengl/gl_query_cache.cpp @@ -27,7 +27,7 @@ constexpr GLenum GetTarget(VideoCore::QueryType type) { } // Anonymous namespace QueryCache::QueryCache(RasterizerOpenGL& rasterizer_, Core::Memory::Memory& cpu_memory_) - : QueryCacheBase(rasterizer_, cpu_memory_), gl_rasterizer{rasterizer_} {} + : QueryCacheLegacy(rasterizer_, cpu_memory_), gl_rasterizer{rasterizer_} {} QueryCache::~QueryCache() = default; diff --git a/src/video_core/renderer_opengl/gl_query_cache.h b/src/video_core/renderer_opengl/gl_query_cache.h index 872513f22..0721e0b3d 100644 --- a/src/video_core/renderer_opengl/gl_query_cache.h +++ b/src/video_core/renderer_opengl/gl_query_cache.h @@ -26,7 +26,7 @@ class RasterizerOpenGL; using CounterStream = VideoCommon::CounterStreamBase<QueryCache, HostCounter>; class QueryCache final - : public VideoCommon::QueryCacheBase<QueryCache, CachedQuery, CounterStream, HostCounter> { + : public VideoCommon::QueryCacheLegacy<QueryCache, CachedQuery, CounterStream, HostCounter> { public: explicit QueryCache(RasterizerOpenGL& rasterizer_, Core::Memory::Memory& cpu_memory_); ~QueryCache(); diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index dd03efecd..27e2de1bf 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -396,13 +396,39 @@ void RasterizerOpenGL::DispatchCompute() { has_written_global_memory |= pipeline->WritesGlobalMemory(); } -void RasterizerOpenGL::ResetCounter(VideoCore::QueryType type) { - query_cache.ResetCounter(type); +void RasterizerOpenGL::ResetCounter(VideoCommon::QueryType type) { + if (type == VideoCommon::QueryType::ZPassPixelCount64) { + query_cache.ResetCounter(VideoCore::QueryType::SamplesPassed); + } } -void RasterizerOpenGL::Query(GPUVAddr gpu_addr, VideoCore::QueryType type, - std::optional<u64> timestamp) { - query_cache.Query(gpu_addr, type, timestamp); +void RasterizerOpenGL::Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) { + if (type == VideoCommon::QueryType::ZPassPixelCount64) { + if (True(flags & VideoCommon::QueryPropertiesFlags::HasTimeout)) { + query_cache.Query(gpu_addr, VideoCore::QueryType::SamplesPassed, {gpu.GetTicks()}); + } else { + query_cache.Query(gpu_addr, VideoCore::QueryType::SamplesPassed, std::nullopt); + } + return; + } + if (type != VideoCommon::QueryType::Payload) { + payload = 1u; + } + std::function<void()> func([this, gpu_addr, flags, memory_manager = gpu_memory, payload]() { + if (True(flags & VideoCommon::QueryPropertiesFlags::HasTimeout)) { + u64 ticks = gpu.GetTicks(); + memory_manager->Write<u64>(gpu_addr + 8, ticks); + memory_manager->Write<u64>(gpu_addr, static_cast<u64>(payload)); + } else { + memory_manager->Write<u32>(gpu_addr, payload); + } + }); + if (True(flags & VideoCommon::QueryPropertiesFlags::IsAFence)) { + SignalFence(std::move(func)); + return; + } + func(); } void RasterizerOpenGL::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, @@ -573,8 +599,8 @@ void RasterizerOpenGL::SignalReference() { fence_manager.SignalOrdering(); } -void RasterizerOpenGL::ReleaseFences() { - fence_manager.WaitPendingFences(); +void RasterizerOpenGL::ReleaseFences(bool force) { + fence_manager.WaitPendingFences(force); } void RasterizerOpenGL::FlushAndInvalidateRegion(VAddr addr, u64 size, diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h index 8eda2ddba..ceffe1f1e 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.h +++ b/src/video_core/renderer_opengl/gl_rasterizer.h @@ -86,8 +86,9 @@ public: void DrawTexture() override; void Clear(u32 layer_count) override; void DispatchCompute() override; - void ResetCounter(VideoCore::QueryType type) override; - void Query(GPUVAddr gpu_addr, VideoCore::QueryType type, std::optional<u64> timestamp) override; + void ResetCounter(VideoCommon::QueryType type) override; + void Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) override; void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, u32 size) override; void DisableGraphicsUniformBuffer(size_t stage, u32 index) override; void FlushAll() override; @@ -107,7 +108,7 @@ public: void SyncOperation(std::function<void()>&& func) override; void SignalSyncPoint(u32 value) override; void SignalReference() override; - void ReleaseFences() override; + void ReleaseFences(bool force = true) override; void FlushAndInvalidateRegion( VAddr addr, u64 size, VideoCommon::CacheType which = VideoCommon::CacheType::All) override; void WaitForIdle() override; 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 1032c9d12..f01d2394e 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -9,6 +9,7 @@ #include "video_core/host_shaders/blit_color_float_frag_spv.h" #include "video_core/host_shaders/convert_abgr8_to_d24s8_frag_spv.h" #include "video_core/host_shaders/convert_d24s8_to_abgr8_frag_spv.h" +#include "video_core/host_shaders/convert_d32f_to_abgr8_frag_spv.h" #include "video_core/host_shaders/convert_depth_to_float_frag_spv.h" #include "video_core/host_shaders/convert_float_to_depth_frag_spv.h" #include "video_core/host_shaders/convert_s8d24_to_abgr8_frag_spv.h" @@ -433,6 +434,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, convert_depth_to_float_frag(BuildShader(device, CONVERT_DEPTH_TO_FLOAT_FRAG_SPV)), convert_float_to_depth_frag(BuildShader(device, CONVERT_FLOAT_TO_DEPTH_FRAG_SPV)), convert_abgr8_to_d24s8_frag(BuildShader(device, CONVERT_ABGR8_TO_D24S8_FRAG_SPV)), + convert_d32f_to_abgr8_frag(BuildShader(device, CONVERT_D32F_TO_ABGR8_FRAG_SPV)), convert_d24s8_to_abgr8_frag(BuildShader(device, CONVERT_D24S8_TO_ABGR8_FRAG_SPV)), convert_s8d24_to_abgr8_frag(BuildShader(device, CONVERT_S8D24_TO_ABGR8_FRAG_SPV)), linear_sampler(device.GetLogical().CreateSampler(SAMPLER_CREATE_INFO<VK_FILTER_LINEAR>)), @@ -557,6 +559,13 @@ void BlitImageHelper::ConvertABGR8ToD24S8(const Framebuffer* dst_framebuffer, Convert(*convert_abgr8_to_d24s8_pipeline, dst_framebuffer, src_image_view); } +void BlitImageHelper::ConvertD32FToABGR8(const Framebuffer* dst_framebuffer, + ImageView& src_image_view) { + ConvertPipelineColorTargetEx(convert_d32f_to_abgr8_pipeline, dst_framebuffer->RenderPass(), + convert_d32f_to_abgr8_frag); + ConvertDepthStencil(*convert_d32f_to_abgr8_pipeline, dst_framebuffer, src_image_view); +} + void BlitImageHelper::ConvertD24S8ToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view) { ConvertPipelineColorTargetEx(convert_d24s8_to_abgr8_pipeline, dst_framebuffer->RenderPass(), @@ -609,6 +618,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); @@ -865,7 +876,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/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h index dcfe217aa..a032c71fb 100644 --- a/src/video_core/renderer_vulkan/blit_image.h +++ b/src/video_core/renderer_vulkan/blit_image.h @@ -67,6 +67,8 @@ public: void ConvertABGR8ToD24S8(const Framebuffer* dst_framebuffer, const ImageView& src_image_view); + void ConvertD32FToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view); + void ConvertD24S8ToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view); void ConvertS8D24ToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view); @@ -128,6 +130,7 @@ private: vk::ShaderModule convert_depth_to_float_frag; vk::ShaderModule convert_float_to_depth_frag; vk::ShaderModule convert_abgr8_to_d24s8_frag; + vk::ShaderModule convert_d32f_to_abgr8_frag; vk::ShaderModule convert_d24s8_to_abgr8_frag; vk::ShaderModule convert_s8d24_to_abgr8_frag; vk::Sampler linear_sampler; @@ -146,6 +149,7 @@ private: vk::Pipeline convert_d16_to_r16_pipeline; vk::Pipeline convert_r16_to_d16_pipeline; vk::Pipeline convert_abgr8_to_d24s8_pipeline; + vk::Pipeline convert_d32f_to_abgr8_pipeline; vk::Pipeline convert_d24s8_to_abgr8_pipeline; vk::Pipeline convert_s8d24_to_abgr8_pipeline; }; diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index 35bf80ea3..a08f2f67f 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -185,7 +185,7 @@ struct FormatTuple { {VK_FORMAT_BC2_SRGB_BLOCK}, // BC2_SRGB {VK_FORMAT_BC3_SRGB_BLOCK}, // BC3_SRGB {VK_FORMAT_BC7_SRGB_BLOCK}, // BC7_SRGB - {VK_FORMAT_R4G4B4A4_UNORM_PACK16}, // A4B4G4R4_UNORM + {VK_FORMAT_A4B4G4R4_UNORM_PACK16_EXT}, // A4B4G4R4_UNORM {VK_FORMAT_R4G4_UNORM_PACK8}, // G4R4_UNORM {VK_FORMAT_ASTC_4x4_SRGB_BLOCK}, // ASTC_2D_4X4_SRGB {VK_FORMAT_ASTC_8x8_SRGB_BLOCK}, // ASTC_2D_8X8_SRGB @@ -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_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp index e15865d16..d8148e89a 100644 --- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp @@ -61,6 +61,9 @@ vk::Buffer CreateBuffer(const Device& device, const MemoryAllocator& memory_allo if (device.IsExtTransformFeedbackSupported()) { flags |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT; } + if (device.IsExtConditionalRendering()) { + flags |= VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT; + } const VkBufferCreateInfo buffer_ci = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = nullptr, diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index 54ee030ce..617f92910 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -3,6 +3,7 @@ #include <array> #include <memory> +#include <numeric> #include <optional> #include <utility> @@ -11,7 +12,13 @@ #include "common/assert.h" #include "common/common_types.h" #include "common/div_ceil.h" +#include "common/vector_math.h" #include "video_core/host_shaders/astc_decoder_comp_spv.h" +#include "video_core/host_shaders/convert_msaa_to_non_msaa_comp_spv.h" +#include "video_core/host_shaders/convert_non_msaa_to_msaa_comp_spv.h" +#include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h" +#include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h" +#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h" #include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h" #include "video_core/host_shaders/vulkan_uint8_comp_spv.h" #include "video_core/renderer_vulkan/vk_compute_pass.h" @@ -57,6 +64,30 @@ constexpr std::array<VkDescriptorSetLayoutBinding, 2> INPUT_OUTPUT_DESCRIPTOR_SE }, }}; +constexpr std::array<VkDescriptorSetLayoutBinding, 3> QUERIES_SCAN_DESCRIPTOR_SET_BINDINGS{{ + { + .binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, + { + .binding = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, + { + .binding = 2, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, +}}; + constexpr DescriptorBankInfo INPUT_OUTPUT_BANK_INFO{ .uniform_buffers = 0, .storage_buffers = 2, @@ -67,6 +98,16 @@ constexpr DescriptorBankInfo INPUT_OUTPUT_BANK_INFO{ .score = 2, }; +constexpr DescriptorBankInfo QUERIES_SCAN_BANK_INFO{ + .uniform_buffers = 0, + .storage_buffers = 3, + .texture_buffers = 0, + .image_buffers = 0, + .textures = 0, + .images = 0, + .score = 3, +}; + constexpr std::array<VkDescriptorSetLayoutBinding, ASTC_NUM_BINDINGS> ASTC_DESCRIPTOR_SET_BINDINGS{{ { .binding = ASTC_BINDING_INPUT_BUFFER, @@ -94,6 +135,33 @@ constexpr DescriptorBankInfo ASTC_BANK_INFO{ .score = 2, }; +constexpr std::array<VkDescriptorSetLayoutBinding, ASTC_NUM_BINDINGS> MSAA_DESCRIPTOR_SET_BINDINGS{{ + { + .binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, + { + .binding = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, +}}; + +constexpr DescriptorBankInfo MSAA_BANK_INFO{ + .uniform_buffers = 0, + .storage_buffers = 0, + .texture_buffers = 0, + .image_buffers = 0, + .textures = 0, + .images = 2, + .score = 2, +}; + constexpr VkDescriptorUpdateTemplateEntry INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE{ .dstBinding = 0, .dstArrayElement = 0, @@ -103,6 +171,24 @@ constexpr VkDescriptorUpdateTemplateEntry INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLAT .stride = sizeof(DescriptorUpdateEntry), }; +constexpr VkDescriptorUpdateTemplateEntry QUERIES_SCAN_DESCRIPTOR_UPDATE_TEMPLATE{ + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 3, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .offset = 0, + .stride = sizeof(DescriptorUpdateEntry), +}; + +constexpr VkDescriptorUpdateTemplateEntry MSAA_DESCRIPTOR_UPDATE_TEMPLATE{ + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 2, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .offset = 0, + .stride = sizeof(DescriptorUpdateEntry), +}; + constexpr std::array<VkDescriptorUpdateTemplateEntry, ASTC_NUM_BINDINGS> ASTC_PASS_DESCRIPTOR_UPDATE_TEMPLATE_ENTRY{{ { @@ -131,13 +217,21 @@ struct AstcPushConstants { u32 block_height; u32 block_height_mask; }; + +struct QueriesPrefixScanPushConstants { + u32 min_accumulation_base; + u32 max_accumulation_base; + u32 accumulation_limit; + u32 buffer_offset; +}; } // Anonymous namespace ComputePass::ComputePass(const Device& device_, DescriptorPool& descriptor_pool, vk::Span<VkDescriptorSetLayoutBinding> bindings, vk::Span<VkDescriptorUpdateTemplateEntry> templates, const DescriptorBankInfo& bank_info, - vk::Span<VkPushConstantRange> push_constants, std::span<const u32> code) + vk::Span<VkPushConstantRange> push_constants, std::span<const u32> code, + std::optional<u32> optional_subgroup_size) : device{device_} { descriptor_set_layout = device.GetLogical().CreateDescriptorSetLayout({ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, @@ -170,6 +264,9 @@ ComputePass::ComputePass(const Device& device_, DescriptorPool& descriptor_pool, }); descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, bank_info); } + if (code.empty()) { + return; + } module = device.GetLogical().CreateShaderModule({ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .pNext = nullptr, @@ -178,13 +275,19 @@ ComputePass::ComputePass(const Device& device_, DescriptorPool& descriptor_pool, .pCode = code.data(), }); device.SaveShader(code); + const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, + .pNext = nullptr, + .requiredSubgroupSize = optional_subgroup_size ? *optional_subgroup_size : 32U, + }; + bool use_setup_size = device.IsExtSubgroupSizeControlSupported() && optional_subgroup_size; pipeline = device.GetLogical().CreateComputePipeline({ .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, .pNext = nullptr, .flags = 0, .stage{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - .pNext = nullptr, + .pNext = use_setup_size ? &subgroup_size_ci : nullptr, .flags = 0, .stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = *module, @@ -302,6 +405,123 @@ std::pair<VkBuffer, VkDeviceSize> QuadIndexedPass::Assemble( return {staging.buffer, staging.offset}; } +ConditionalRenderingResolvePass::ConditionalRenderingResolvePass( + const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_) + : ComputePass(device_, descriptor_pool_, INPUT_OUTPUT_DESCRIPTOR_SET_BINDINGS, + INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE, INPUT_OUTPUT_BANK_INFO, nullptr, + RESOLVE_CONDITIONAL_RENDER_COMP_SPV), + scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {} + +void ConditionalRenderingResolvePass::Resolve(VkBuffer dst_buffer, VkBuffer src_buffer, + u32 src_offset, bool compare_to_zero) { + const size_t compare_size = compare_to_zero ? 8 : 24; + + compute_pass_descriptor_queue.Acquire(); + compute_pass_descriptor_queue.AddBuffer(src_buffer, src_offset, compare_size); + compute_pass_descriptor_queue.AddBuffer(dst_buffer, 0, sizeof(u32)); + const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, descriptor_data](vk::CommandBuffer cmdbuf) { + static constexpr VkMemoryBarrier read_barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + }; + static constexpr VkMemoryBarrier write_barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_CONDITIONAL_RENDERING_READ_BIT_EXT, + }; + const VkDescriptorSet set = descriptor_allocator.Commit(); + device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); + + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, read_barrier); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.Dispatch(1, 1, 1); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_CONDITIONAL_RENDERING_BIT_EXT, 0, write_barrier); + }); +} + +QueriesPrefixScanPass::QueriesPrefixScanPass( + const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_) + : ComputePass( + device_, descriptor_pool_, QUERIES_SCAN_DESCRIPTOR_SET_BINDINGS, + QUERIES_SCAN_DESCRIPTOR_UPDATE_TEMPLATE, QUERIES_SCAN_BANK_INFO, + COMPUTE_PUSH_CONSTANT_RANGE<sizeof(QueriesPrefixScanPushConstants)>, + device_.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_BASIC_BIT) && + device_.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) && + device_.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_SHUFFLE_BIT) && + device_.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT) + ? std::span<const u32>(QUERIES_PREFIX_SCAN_SUM_COMP_SPV) + : std::span<const u32>(QUERIES_PREFIX_SCAN_SUM_NOSUBGROUPS_COMP_SPV)), + scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {} + +void QueriesPrefixScanPass::Run(VkBuffer accumulation_buffer, VkBuffer dst_buffer, + VkBuffer src_buffer, size_t number_of_sums, + size_t min_accumulation_limit, size_t max_accumulation_limit) { + size_t current_runs = number_of_sums; + size_t offset = 0; + while (current_runs != 0) { + static constexpr size_t DISPATCH_SIZE = 2048U; + size_t runs_to_do = std::min<size_t>(current_runs, DISPATCH_SIZE); + current_runs -= runs_to_do; + compute_pass_descriptor_queue.Acquire(); + compute_pass_descriptor_queue.AddBuffer(src_buffer, 0, number_of_sums * sizeof(u64)); + compute_pass_descriptor_queue.AddBuffer(dst_buffer, 0, number_of_sums * sizeof(u64)); + compute_pass_descriptor_queue.AddBuffer(accumulation_buffer, 0, sizeof(u64)); + const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; + size_t used_offset = offset; + offset += runs_to_do; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, descriptor_data, min_accumulation_limit, max_accumulation_limit, + runs_to_do, used_offset](vk::CommandBuffer cmdbuf) { + static constexpr VkMemoryBarrier read_barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + }; + static constexpr VkMemoryBarrier write_barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | + VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | + VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_INDEX_READ_BIT | + VK_ACCESS_UNIFORM_READ_BIT | + VK_ACCESS_CONDITIONAL_RENDERING_READ_BIT_EXT, + }; + const QueriesPrefixScanPushConstants uniforms{ + .min_accumulation_base = static_cast<u32>(min_accumulation_limit), + .max_accumulation_base = static_cast<u32>(max_accumulation_limit), + .accumulation_limit = static_cast<u32>(runs_to_do - 1), + .buffer_offset = static_cast<u32>(used_offset), + }; + const VkDescriptorSet set = descriptor_allocator.Commit(); + device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); + + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, read_barrier); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, uniforms); + cmdbuf.Dispatch(1, 1, 1); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_CONDITIONAL_RENDERING_BIT_EXT, 0, + write_barrier); + }); + } +} + ASTCDecoderPass::ASTCDecoderPass(const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_, StagingBufferPool& staging_buffer_pool_, @@ -413,4 +633,100 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, scheduler.Finish(); } +MSAACopyPass::MSAACopyPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, + StagingBufferPool& staging_buffer_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_) + : ComputePass(device_, descriptor_pool_, MSAA_DESCRIPTOR_SET_BINDINGS, + MSAA_DESCRIPTOR_UPDATE_TEMPLATE, MSAA_BANK_INFO, {}, + CONVERT_NON_MSAA_TO_MSAA_COMP_SPV), + scheduler{scheduler_}, staging_buffer_pool{staging_buffer_pool_}, + compute_pass_descriptor_queue{compute_pass_descriptor_queue_} { + const auto make_msaa_pipeline = [this](size_t i, std::span<const u32> code) { + modules[i] = device.GetLogical().CreateShaderModule({ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .codeSize = static_cast<u32>(code.size_bytes()), + .pCode = code.data(), + }); + pipelines[i] = device.GetLogical().CreateComputePipeline({ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *modules[i], + .pName = "main", + .pSpecializationInfo = nullptr, + }, + .layout = *layout, + .basePipelineHandle = nullptr, + .basePipelineIndex = 0, + }); + }; + make_msaa_pipeline(0, CONVERT_NON_MSAA_TO_MSAA_COMP_SPV); + make_msaa_pipeline(1, CONVERT_MSAA_TO_NON_MSAA_COMP_SPV); +} + +MSAACopyPass::~MSAACopyPass() = default; + +void MSAACopyPass::CopyImage(Image& dst_image, Image& src_image, + std::span<const VideoCommon::ImageCopy> copies, + bool msaa_to_non_msaa) { + const VkPipeline msaa_pipeline = *pipelines[msaa_to_non_msaa ? 1 : 0]; + scheduler.RequestOutsideRenderPassOperationContext(); + for (const VideoCommon::ImageCopy& copy : copies) { + ASSERT(copy.src_subresource.base_layer == 0); + ASSERT(copy.src_subresource.num_layers == 1); + ASSERT(copy.dst_subresource.base_layer == 0); + ASSERT(copy.dst_subresource.num_layers == 1); + + compute_pass_descriptor_queue.Acquire(); + compute_pass_descriptor_queue.AddImage( + src_image.StorageImageView(copy.src_subresource.base_level)); + compute_pass_descriptor_queue.AddImage( + dst_image.StorageImageView(copy.dst_subresource.base_level)); + const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; + + const Common::Vec3<u32> num_dispatches = { + Common::DivCeil(copy.extent.width, 8U), + Common::DivCeil(copy.extent.height, 8U), + copy.extent.depth, + }; + + scheduler.Record([this, dst = dst_image.Handle(), msaa_pipeline, num_dispatches, + descriptor_data](vk::CommandBuffer cmdbuf) { + const VkDescriptorSet set = descriptor_allocator.Commit(); + device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, msaa_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.Dispatch(num_dispatches.x, num_dispatches.y, num_dispatches.z); + const VkImageMemoryBarrier write_barrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = dst, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, write_barrier); + }); + } +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index dd3927376..7b8f938c1 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h @@ -3,6 +3,7 @@ #pragma once +#include <optional> #include <span> #include <utility> @@ -10,6 +11,7 @@ #include "video_core/engines/maxwell_3d.h" #include "video_core/renderer_vulkan/vk_descriptor_pool.h" #include "video_core/renderer_vulkan/vk_update_descriptor.h" +#include "video_core/texture_cache/types.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h" #include "video_core/vulkan_common/vulkan_wrapper.h" @@ -31,7 +33,8 @@ public: vk::Span<VkDescriptorSetLayoutBinding> bindings, vk::Span<VkDescriptorUpdateTemplateEntry> templates, const DescriptorBankInfo& bank_info, - vk::Span<VkPushConstantRange> push_constants, std::span<const u32> code); + vk::Span<VkPushConstantRange> push_constants, std::span<const u32> code, + std::optional<u32> optional_subgroup_size = std::nullopt); ~ComputePass(); protected: @@ -82,6 +85,33 @@ private: ComputePassDescriptorQueue& compute_pass_descriptor_queue; }; +class ConditionalRenderingResolvePass final : public ComputePass { +public: + explicit ConditionalRenderingResolvePass( + const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_); + + void Resolve(VkBuffer dst_buffer, VkBuffer src_buffer, u32 src_offset, bool compare_to_zero); + +private: + Scheduler& scheduler; + ComputePassDescriptorQueue& compute_pass_descriptor_queue; +}; + +class QueriesPrefixScanPass final : public ComputePass { +public: + explicit QueriesPrefixScanPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_); + + void Run(VkBuffer accumulation_buffer, VkBuffer dst_buffer, VkBuffer src_buffer, + size_t number_of_sums, size_t min_accumulation_limit, size_t max_accumulation_limit); + +private: + Scheduler& scheduler; + ComputePassDescriptorQueue& compute_pass_descriptor_queue; +}; + class ASTCDecoderPass final : public ComputePass { public: explicit ASTCDecoderPass(const Device& device_, Scheduler& scheduler_, @@ -101,4 +131,22 @@ private: MemoryAllocator& memory_allocator; }; +class MSAACopyPass final : public ComputePass { +public: + explicit MSAACopyPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, StagingBufferPool& staging_buffer_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_); + ~MSAACopyPass(); + + void CopyImage(Image& dst_image, Image& src_image, + std::span<const VideoCommon::ImageCopy> copies, bool msaa_to_non_msaa); + +private: + Scheduler& scheduler; + StagingBufferPool& staging_buffer_pool; + ComputePassDescriptorQueue& compute_pass_descriptor_queue; + std::array<vk::ShaderModule, 2> modules; + std::array<vk::Pipeline, 2> pipelines; +}; + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_fence_manager.h b/src/video_core/renderer_vulkan/vk_fence_manager.h index 145359d4e..336573574 100644 --- a/src/video_core/renderer_vulkan/vk_fence_manager.h +++ b/src/video_core/renderer_vulkan/vk_fence_manager.h @@ -7,6 +7,7 @@ #include "video_core/fence_manager.h" #include "video_core/renderer_vulkan/vk_buffer_cache.h" +#include "video_core/renderer_vulkan/vk_query_cache.h" #include "video_core/renderer_vulkan/vk_texture_cache.h" namespace Core { @@ -20,7 +21,6 @@ class RasterizerInterface; namespace Vulkan { class Device; -class QueryCache; class Scheduler; class InnerFence : public VideoCommon::FenceBase { 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 29e0b797b..2edaafa7e 100644 --- a/src/video_core/renderer_vulkan/vk_query_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_query_cache.cpp @@ -1,139 +1,1555 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later -#include <algorithm> #include <cstddef> +#include <limits> +#include <map> +#include <memory> +#include <span> +#include <type_traits> +#include <unordered_map> #include <utility> #include <vector> +#include "common/bit_util.h" +#include "common/common_types.h" +#include "core/memory.h" +#include "video_core/engines/draw_manager.h" +#include "video_core/query_cache/query_cache.h" +#include "video_core/renderer_vulkan/vk_buffer_cache.h" +#include "video_core/renderer_vulkan/vk_compute_pass.h" #include "video_core/renderer_vulkan/vk_query_cache.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" #include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h" +#include "video_core/renderer_vulkan/vk_update_descriptor.h" #include "video_core/vulkan_common/vulkan_device.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" #include "video_core/vulkan_common/vulkan_wrapper.h" namespace Vulkan { -using VideoCore::QueryType; +using Tegra::Engines::Maxwell3D; +using VideoCommon::QueryType; namespace { +class SamplesQueryBank : public VideoCommon::BankBase { +public: + static constexpr size_t BANK_SIZE = 256; + static constexpr size_t QUERY_SIZE = 8; + explicit SamplesQueryBank(const Device& device_, size_t index_) + : BankBase(BANK_SIZE), device{device_}, index{index_} { + const auto& dev = device.GetLogical(); + query_pool = dev.CreateQueryPool({ + .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .queryType = VK_QUERY_TYPE_OCCLUSION, + .queryCount = BANK_SIZE, + .pipelineStatistics = 0, + }); + Reset(); + } -constexpr std::array QUERY_TARGETS = {VK_QUERY_TYPE_OCCLUSION}; + ~SamplesQueryBank() = default; -constexpr VkQueryType GetTarget(QueryType type) { - return QUERY_TARGETS[static_cast<std::size_t>(type)]; -} + void Reset() override { + ASSERT(references == 0); + VideoCommon::BankBase::Reset(); + const auto& dev = device.GetLogical(); + dev.ResetQueryPool(*query_pool, 0, BANK_SIZE); + host_results.fill(0ULL); + next_bank = 0; + } + + void Sync(size_t start, size_t size) { + const auto& dev = device.GetLogical(); + const VkResult query_result = dev.GetQueryResults( + *query_pool, static_cast<u32>(start), static_cast<u32>(size), sizeof(u64) * size, + &host_results[start], sizeof(u64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + switch (query_result) { + case VK_SUCCESS: + return; + case VK_ERROR_DEVICE_LOST: + device.ReportLoss(); + [[fallthrough]]; + default: + throw vk::Exception(query_result); + } + } + + VkQueryPool GetInnerPool() { + return *query_pool; + } + + size_t GetIndex() const { + return index; + } + + const std::array<u64, BANK_SIZE>& GetResults() const { + return host_results; + } + + size_t next_bank; + +private: + const Device& device; + const size_t index; + vk::QueryPool query_pool; + std::array<u64, BANK_SIZE> host_results; +}; + +using BaseStreamer = VideoCommon::SimpleStreamer<VideoCommon::HostQueryBase>; + +struct HostSyncValues { + VAddr address; + size_t size; + size_t offset; + + static constexpr bool GeneratesBaseBuffer = false; +}; + +class SamplesStreamer : public BaseStreamer { +public: + explicit SamplesStreamer(size_t id_, QueryCacheRuntime& runtime_, + VideoCore::RasterizerInterface* rasterizer_, const Device& device_, + Scheduler& scheduler_, const MemoryAllocator& memory_allocator_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue, + DescriptorPool& descriptor_pool) + : BaseStreamer(id_), runtime{runtime_}, rasterizer{rasterizer_}, device{device_}, + scheduler{scheduler_}, memory_allocator{memory_allocator_} { + current_bank = nullptr; + current_query = nullptr; + ammend_value = 0; + acumulation_value = 0; + queries_prefix_scan_pass = std::make_unique<QueriesPrefixScanPass>( + device, scheduler, descriptor_pool, compute_pass_descriptor_queue); + + const VkBufferCreateInfo buffer_ci = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = 8, + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + accumulation_buffer = memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::DeviceLocal); + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([buffer = *accumulation_buffer](vk::CommandBuffer cmdbuf) { + cmdbuf.FillBuffer(buffer, 0, 8, 0); + }); + } + + ~SamplesStreamer() = default; + + void StartCounter() override { + if (has_started) { + return; + } + ReserveHostQuery(); + scheduler.Record([query_pool = current_query_pool, + query_index = current_bank_slot](vk::CommandBuffer cmdbuf) { + const bool use_precise = Settings::IsGPULevelHigh(); + cmdbuf.BeginQuery(query_pool, static_cast<u32>(query_index), + use_precise ? VK_QUERY_CONTROL_PRECISE_BIT : 0); + }); + has_started = true; + } + + void PauseCounter() override { + if (!has_started) { + return; + } + scheduler.Record([query_pool = current_query_pool, + query_index = current_bank_slot](vk::CommandBuffer cmdbuf) { + cmdbuf.EndQuery(query_pool, static_cast<u32>(query_index)); + }); + has_started = false; + } + + void ResetCounter() override { + if (has_started) { + PauseCounter(); + } + AbandonCurrentQuery(); + std::function<void()> func([this, counts = pending_flush_queries.size()] { + ammend_value = 0; + acumulation_value = 0; + }); + rasterizer->SyncOperation(std::move(func)); + accumulation_since_last_sync = false; + first_accumulation_checkpoint = std::min(first_accumulation_checkpoint, num_slots_used); + last_accumulation_checkpoint = std::max(last_accumulation_checkpoint, num_slots_used); + } + + void CloseCounter() override { + PauseCounter(); + } -} // Anonymous namespace + bool HasPendingSync() const override { + return !pending_sync.empty(); + } + + void SyncWrites() override { + if (sync_values_stash.empty()) { + return; + } -QueryPool::QueryPool(const Device& device_, Scheduler& scheduler, QueryType type_) - : ResourcePool{scheduler.GetMasterSemaphore(), GROW_STEP}, device{device_}, type{type_} {} + for (size_t i = 0; i < sync_values_stash.size(); i++) { + runtime.template SyncValues<HostSyncValues>(sync_values_stash[i], + *buffers[resolve_buffers[i]]); + } + + sync_values_stash.clear(); + } -QueryPool::~QueryPool() = default; + void PresyncWrites() override { + if (pending_sync.empty()) { + return; + } + PauseCounter(); + sync_values_stash.clear(); + sync_values_stash.emplace_back(); + std::vector<HostSyncValues>* sync_values = &sync_values_stash.back(); + sync_values->reserve(num_slots_used); + std::unordered_map<size_t, std::pair<size_t, size_t>> offsets; + resolve_buffers.clear(); + size_t resolve_buffer_index = ObtainBuffer<true>(num_slots_used); + resolve_buffers.push_back(resolve_buffer_index); + size_t base_offset = 0; -std::pair<VkQueryPool, u32> QueryPool::Commit() { - std::size_t index; - do { - index = CommitResource(); - } while (usage[index]); - usage[index] = true; + ApplyBanksWideOp<true>(pending_sync, [&](SamplesQueryBank* bank, size_t start, + size_t amount) { + size_t bank_id = bank->GetIndex(); + auto& resolve_buffer = buffers[resolve_buffer_index]; + VkQueryPool query_pool = bank->GetInnerPool(); + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([start, amount, base_offset, query_pool, + buffer = *resolve_buffer](vk::CommandBuffer cmdbuf) { + const VkBufferMemoryBarrier copy_query_pool_barrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = buffer, + .offset = base_offset, + .size = amount * SamplesQueryBank::QUERY_SIZE, + }; + + cmdbuf.CopyQueryPoolResults( + query_pool, static_cast<u32>(start), static_cast<u32>(amount), buffer, + static_cast<u32>(base_offset), SamplesQueryBank::QUERY_SIZE, + VK_QUERY_RESULT_WAIT_BIT | VK_QUERY_RESULT_64_BIT); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, copy_query_pool_barrier); + }); + offsets[bank_id] = {start, base_offset}; + base_offset += amount * SamplesQueryBank::QUERY_SIZE; + }); + + // Convert queries + bool has_multi_queries = false; + for (auto q : pending_sync) { + auto* query = GetQuery(q); + size_t sync_value_slot = 0; + if (True(query->flags & VideoCommon::QueryFlagBits::IsRewritten)) { + continue; + } + if (True(query->flags & VideoCommon::QueryFlagBits::IsInvalidated)) { + continue; + } + if (accumulation_since_last_sync || query->size_slots > 1) { + if (!has_multi_queries) { + has_multi_queries = true; + sync_values_stash.emplace_back(); + } + sync_value_slot = 1; + } + query->flags |= VideoCommon::QueryFlagBits::IsHostSynced; + auto loc_data = offsets[query->start_bank_id]; + sync_values_stash[sync_value_slot].emplace_back(HostSyncValues{ + .address = query->guest_address, + .size = SamplesQueryBank::QUERY_SIZE, + .offset = + loc_data.second + (query->start_slot - loc_data.first + query->size_slots - 1) * + SamplesQueryBank::QUERY_SIZE, + }); + } + + if (has_multi_queries) { + size_t intermediary_buffer_index = ObtainBuffer<false>(num_slots_used); + resolve_buffers.push_back(intermediary_buffer_index); + queries_prefix_scan_pass->Run(*accumulation_buffer, *buffers[intermediary_buffer_index], + *buffers[resolve_buffer_index], num_slots_used, + std::min(first_accumulation_checkpoint, num_slots_used), + last_accumulation_checkpoint); + + } else { + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([buffer = *accumulation_buffer](vk::CommandBuffer cmdbuf) { + cmdbuf.FillBuffer(buffer, 0, 8, 0); + }); + } + + ReplicateCurrentQueryIfNeeded(); + std::function<void()> func([this] { ammend_value = acumulation_value; }); + rasterizer->SyncOperation(std::move(func)); + AbandonCurrentQuery(); + num_slots_used = 0; + first_accumulation_checkpoint = std::numeric_limits<size_t>::max(); + last_accumulation_checkpoint = 0; + accumulation_since_last_sync = has_multi_queries; + pending_sync.clear(); + } - return {*pools[index / GROW_STEP], static_cast<u32>(index % GROW_STEP)}; + size_t WriteCounter(VAddr address, bool has_timestamp, u32 value, + [[maybe_unused]] std::optional<u32> subreport) override { + PauseCounter(); + auto index = BuildQuery(); + auto* new_query = GetQuery(index); + new_query->guest_address = address; + new_query->value = 0; + new_query->flags &= ~VideoCommon::QueryFlagBits::IsOrphan; + if (has_timestamp) { + new_query->flags |= VideoCommon::QueryFlagBits::HasTimestamp; + } + if (!current_query) { + new_query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + return index; + } + new_query->start_bank_id = current_query->start_bank_id; + new_query->size_banks = current_query->size_banks; + new_query->start_slot = current_query->start_slot; + new_query->size_slots = current_query->size_slots; + ApplyBankOp(new_query, [](SamplesQueryBank* bank, size_t start, size_t amount) { + bank->AddReference(amount); + }); + pending_sync.push_back(index); + pending_flush_queries.push_back(index); + return index; + } + + bool HasUnsyncedQueries() const override { + return !pending_flush_queries.empty(); + } + + void PushUnsyncedQueries() override { + PauseCounter(); + current_bank->Close(); + { + std::scoped_lock lk(flush_guard); + pending_flush_sets.emplace_back(std::move(pending_flush_queries)); + } + } + + void PopUnsyncedQueries() override { + std::vector<size_t> current_flush_queries; + { + std::scoped_lock lk(flush_guard); + current_flush_queries = std::move(pending_flush_sets.front()); + pending_flush_sets.pop_front(); + } + ApplyBanksWideOp<false>( + current_flush_queries, + [](SamplesQueryBank* bank, size_t start, size_t amount) { bank->Sync(start, amount); }); + for (auto q : current_flush_queries) { + auto* query = GetQuery(q); + u64 total = 0; + ApplyBankOp(query, [&total](SamplesQueryBank* bank, size_t start, size_t amount) { + const auto& results = bank->GetResults(); + for (size_t i = 0; i < amount; i++) { + total += results[start + i]; + } + }); + query->value = total; + query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + } + } + +private: + template <typename Func> + void ApplyBankOp(VideoCommon::HostQueryBase* query, Func&& func) { + size_t size_slots = query->size_slots; + if (size_slots == 0) { + return; + } + size_t bank_id = query->start_bank_id; + size_t banks_set = query->size_banks; + size_t start_slot = query->start_slot; + for (size_t i = 0; i < banks_set; i++) { + auto& the_bank = bank_pool.GetBank(bank_id); + size_t amount = std::min(the_bank.Size() - start_slot, size_slots); + func(&the_bank, start_slot, amount); + bank_id = the_bank.next_bank - 1; + start_slot = 0; + size_slots -= amount; + } + } + + template <bool is_ordered, typename Func> + void ApplyBanksWideOp(std::vector<size_t>& queries, Func&& func) { + std::conditional_t<is_ordered, std::map<size_t, std::pair<size_t, size_t>>, + std::unordered_map<size_t, std::pair<size_t, size_t>>> + indexer; + for (auto q : queries) { + auto* query = GetQuery(q); + ApplyBankOp(query, [&indexer](SamplesQueryBank* bank, size_t start, size_t amount) { + auto id_ = bank->GetIndex(); + auto pair = indexer.try_emplace(id_, std::numeric_limits<size_t>::max(), + std::numeric_limits<size_t>::min()); + auto& current_pair = pair.first->second; + current_pair.first = std::min(current_pair.first, start); + current_pair.second = std::max(current_pair.second, amount + start); + }); + } + for (auto& cont : indexer) { + func(&bank_pool.GetBank(cont.first), cont.second.first, + cont.second.second - cont.second.first); + } + } + + void ReserveBank() { + current_bank_id = + bank_pool.ReserveBank([this](std::deque<SamplesQueryBank>& queue, size_t index) { + queue.emplace_back(device, index); + }); + if (current_bank) { + current_bank->next_bank = current_bank_id + 1; + } + current_bank = &bank_pool.GetBank(current_bank_id); + current_query_pool = current_bank->GetInnerPool(); + } + + size_t ReserveBankSlot() { + if (!current_bank || current_bank->IsClosed()) { + ReserveBank(); + } + auto [built, index] = current_bank->Reserve(); + current_bank_slot = index; + return index; + } + + void ReserveHostQuery() { + size_t new_slot = ReserveBankSlot(); + current_bank->AddReference(1); + num_slots_used++; + if (current_query) { + size_t bank_id = current_query->start_bank_id; + size_t banks_set = current_query->size_banks - 1; + bool found = bank_id == current_bank_id; + while (!found && banks_set > 0) { + SamplesQueryBank& some_bank = bank_pool.GetBank(bank_id); + bank_id = some_bank.next_bank - 1; + found = bank_id == current_bank_id; + banks_set--; + } + if (!found) { + current_query->size_banks++; + } + current_query->size_slots++; + } else { + current_query_id = BuildQuery(); + current_query = GetQuery(current_query_id); + current_query->start_bank_id = static_cast<u32>(current_bank_id); + current_query->size_banks = 1; + current_query->start_slot = new_slot; + current_query->size_slots = 1; + } + } + + void Free(size_t query_id) override { + std::scoped_lock lk(guard); + auto* query = GetQuery(query_id); + ApplyBankOp(query, [](SamplesQueryBank* bank, size_t start, size_t amount) { + bank->CloseReference(amount); + }); + ReleaseQuery(query_id); + } + + void AbandonCurrentQuery() { + if (!current_query) { + return; + } + Free(current_query_id); + current_query = nullptr; + current_query_id = 0; + } + + void ReplicateCurrentQueryIfNeeded() { + if (pending_sync.empty()) { + return; + } + if (!current_query) { + return; + } + auto index = BuildQuery(); + auto* new_query = GetQuery(index); + new_query->guest_address = 0; + new_query->value = 0; + new_query->flags &= ~VideoCommon::QueryFlagBits::IsOrphan; + new_query->start_bank_id = current_query->start_bank_id; + new_query->size_banks = current_query->size_banks; + new_query->start_slot = current_query->start_slot; + new_query->size_slots = current_query->size_slots; + ApplyBankOp(new_query, [](SamplesQueryBank* bank, size_t start, size_t amount) { + bank->AddReference(amount); + }); + pending_flush_queries.push_back(index); + std::function<void()> func([this, index] { + auto* query = GetQuery(index); + query->value += GetAmmendValue(); + SetAccumulationValue(query->value); + Free(index); + }); + rasterizer->SyncOperation(std::move(func)); + } + + template <bool is_resolve> + size_t ObtainBuffer(size_t num_needed) { + const size_t log_2 = std::max<size_t>(11U, Common::Log2Ceil64(num_needed)); + if constexpr (is_resolve) { + if (resolve_table[log_2] != 0) { + return resolve_table[log_2] - 1; + } + } else { + if (intermediary_table[log_2] != 0) { + return intermediary_table[log_2] - 1; + } + } + const VkBufferCreateInfo buffer_ci = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = SamplesQueryBank::QUERY_SIZE * (1ULL << log_2), + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + buffers.emplace_back(memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::DeviceLocal)); + if constexpr (is_resolve) { + resolve_table[log_2] = buffers.size(); + } else { + intermediary_table[log_2] = buffers.size(); + } + return buffers.size() - 1; + } + + QueryCacheRuntime& runtime; + VideoCore::RasterizerInterface* rasterizer; + const Device& device; + Scheduler& scheduler; + const MemoryAllocator& memory_allocator; + VideoCommon::BankPool<SamplesQueryBank> bank_pool; + std::deque<vk::Buffer> buffers; + std::array<size_t, 32> resolve_table{}; + std::array<size_t, 32> intermediary_table{}; + vk::Buffer accumulation_buffer; + std::deque<std::vector<HostSyncValues>> sync_values_stash; + std::vector<size_t> resolve_buffers; + + // syncing queue + std::vector<size_t> pending_sync; + + // flush levels + std::vector<size_t> pending_flush_queries; + std::deque<std::vector<size_t>> pending_flush_sets; + + // State Machine + size_t current_bank_slot; + size_t current_bank_id; + SamplesQueryBank* current_bank; + VkQueryPool current_query_pool; + size_t current_query_id; + size_t num_slots_used{}; + size_t first_accumulation_checkpoint{}; + size_t last_accumulation_checkpoint{}; + bool accumulation_since_last_sync{}; + VideoCommon::HostQueryBase* current_query; + bool has_started{}; + std::mutex flush_guard; + + std::unique_ptr<QueriesPrefixScanPass> queries_prefix_scan_pass; +}; + +// Transform feedback queries +class TFBQueryBank : public VideoCommon::BankBase { +public: + static constexpr size_t BANK_SIZE = 1024; + static constexpr size_t QUERY_SIZE = 4; + explicit TFBQueryBank(Scheduler& scheduler_, const MemoryAllocator& memory_allocator, + size_t index_) + : BankBase(BANK_SIZE), scheduler{scheduler_}, index{index_} { + const VkBufferCreateInfo buffer_ci = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = QUERY_SIZE * BANK_SIZE, + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + buffer = memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::DeviceLocal); + } + + ~TFBQueryBank() = default; + + void Reset() override { + ASSERT(references == 0); + VideoCommon::BankBase::Reset(); + } + + void Sync(StagingBufferRef& stagging_buffer, size_t extra_offset, size_t start, size_t size) { + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, dst_buffer = stagging_buffer.buffer, extra_offset, start, + size](vk::CommandBuffer cmdbuf) { + std::array<VkBufferCopy, 1> copy{VkBufferCopy{ + .srcOffset = start * QUERY_SIZE, + .dstOffset = extra_offset, + .size = size * QUERY_SIZE, + }}; + cmdbuf.CopyBuffer(*buffer, dst_buffer, copy); + }); + } + + size_t GetIndex() const { + return index; + } + + VkBuffer GetBuffer() const { + return *buffer; + } + +private: + Scheduler& scheduler; + const size_t index; + vk::Buffer buffer; +}; + +class PrimitivesSucceededStreamer; + +class TFBCounterStreamer : public BaseStreamer { +public: + explicit TFBCounterStreamer(size_t id_, QueryCacheRuntime& runtime_, const Device& device_, + Scheduler& scheduler_, const MemoryAllocator& memory_allocator_, + StagingBufferPool& staging_pool_) + : BaseStreamer(id_), runtime{runtime_}, device{device_}, scheduler{scheduler_}, + memory_allocator{memory_allocator_}, staging_pool{staging_pool_} { + buffers_count = 0; + current_bank = nullptr; + counter_buffers.fill(VK_NULL_HANDLE); + offsets.fill(0); + last_queries.fill(0); + last_queries_stride.fill(1); + const VkBufferCreateInfo buffer_ci = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = TFBQueryBank::QUERY_SIZE * NUM_STREAMS, + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | + VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + + counters_buffer = memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::DeviceLocal); + for (auto& c : counter_buffers) { + c = *counters_buffer; + } + size_t base_offset = 0; + for (auto& o : offsets) { + o = base_offset; + base_offset += TFBQueryBank::QUERY_SIZE; + } + } + + ~TFBCounterStreamer() = default; + + void StartCounter() override { + FlushBeginTFB(); + has_started = true; + } + + void PauseCounter() override { + CloseCounter(); + } + + void ResetCounter() override { + CloseCounter(); + } + + void CloseCounter() override { + if (has_flushed_end_pending) { + FlushEndTFB(); + } + runtime.View3DRegs([this](Maxwell3D& maxwell3d) { + if (maxwell3d.regs.transform_feedback_enabled == 0) { + streams_mask = 0; + has_started = false; + } + }); + } + + bool HasPendingSync() const override { + return !pending_sync.empty(); + } + + void SyncWrites() override { + CloseCounter(); + std::unordered_map<size_t, std::vector<HostSyncValues>> sync_values_stash; + for (auto q : pending_sync) { + auto* query = GetQuery(q); + if (True(query->flags & VideoCommon::QueryFlagBits::IsRewritten)) { + continue; + } + if (True(query->flags & VideoCommon::QueryFlagBits::IsInvalidated)) { + continue; + } + query->flags |= VideoCommon::QueryFlagBits::IsHostSynced; + sync_values_stash.try_emplace(query->start_bank_id); + sync_values_stash[query->start_bank_id].emplace_back(HostSyncValues{ + .address = query->guest_address, + .size = TFBQueryBank::QUERY_SIZE, + .offset = query->start_slot * TFBQueryBank::QUERY_SIZE, + }); + } + for (auto& p : sync_values_stash) { + auto& bank = bank_pool.GetBank(p.first); + runtime.template SyncValues<HostSyncValues>(p.second, bank.GetBuffer()); + } + pending_sync.clear(); + } + + size_t WriteCounter(VAddr address, bool has_timestamp, u32 value, + std::optional<u32> subreport_) override { + auto index = BuildQuery(); + auto* new_query = GetQuery(index); + new_query->guest_address = address; + new_query->value = 0; + new_query->flags &= ~VideoCommon::QueryFlagBits::IsOrphan; + if (has_timestamp) { + new_query->flags |= VideoCommon::QueryFlagBits::HasTimestamp; + } + if (!subreport_) { + new_query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + return index; + } + const size_t subreport = static_cast<size_t>(*subreport_); + last_queries[subreport] = address; + if ((streams_mask & (1ULL << subreport)) == 0) { + new_query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + return index; + } + CloseCounter(); + auto [bank_slot, data_slot] = ProduceCounterBuffer(subreport); + new_query->start_bank_id = static_cast<u32>(bank_slot); + new_query->size_banks = 1; + new_query->start_slot = static_cast<u32>(data_slot); + new_query->size_slots = 1; + pending_sync.push_back(index); + pending_flush_queries.push_back(index); + return index; + } + + std::optional<std::pair<VAddr, size_t>> GetLastQueryStream(size_t stream) { + if (last_queries[stream] != 0) { + std::pair<VAddr, size_t> result(last_queries[stream], last_queries_stride[stream]); + return result; + } + return std::nullopt; + } + + Maxwell3D::Regs::PrimitiveTopology GetOutputTopology() const { + return out_topology; + } + + bool HasUnsyncedQueries() const override { + return !pending_flush_queries.empty(); + } + + void PushUnsyncedQueries() override { + CloseCounter(); + auto staging_ref = staging_pool.Request( + pending_flush_queries.size() * TFBQueryBank::QUERY_SIZE, MemoryUsage::Download, true); + size_t offset_base = staging_ref.offset; + for (auto q : pending_flush_queries) { + auto* query = GetQuery(q); + auto& bank = bank_pool.GetBank(query->start_bank_id); + bank.Sync(staging_ref, offset_base, query->start_slot, 1); + offset_base += TFBQueryBank::QUERY_SIZE; + bank.CloseReference(); + } + static constexpr VkMemoryBarrier WRITE_BARRIER{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, + }; + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([](vk::CommandBuffer cmdbuf) { + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, WRITE_BARRIER); + }); + + std::scoped_lock lk(flush_guard); + for (auto& str : free_queue) { + staging_pool.FreeDeferred(str); + } + free_queue.clear(); + download_buffers.emplace_back(staging_ref); + pending_flush_sets.emplace_back(std::move(pending_flush_queries)); + } + + void PopUnsyncedQueries() override { + StagingBufferRef staging_ref; + std::vector<size_t> flushed_queries; + { + std::scoped_lock lk(flush_guard); + staging_ref = download_buffers.front(); + flushed_queries = std::move(pending_flush_sets.front()); + download_buffers.pop_front(); + pending_flush_sets.pop_front(); + } + + size_t offset_base = staging_ref.offset; + for (auto q : flushed_queries) { + auto* query = GetQuery(q); + u32 result = 0; + std::memcpy(&result, staging_ref.mapped_span.data() + offset_base, sizeof(u32)); + query->value = static_cast<u64>(result); + query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + offset_base += TFBQueryBank::QUERY_SIZE; + } + + { + std::scoped_lock lk(flush_guard); + free_queue.emplace_back(staging_ref); + } + } + +private: + void FlushBeginTFB() { + if (has_flushed_end_pending) [[unlikely]] { + return; + } + has_flushed_end_pending = true; + if (!has_started || buffers_count == 0) { + scheduler.Record([](vk::CommandBuffer cmdbuf) { + cmdbuf.BeginTransformFeedbackEXT(0, 0, nullptr, nullptr); + }); + UpdateBuffers(); + return; + } + scheduler.Record([this, total = static_cast<u32>(buffers_count)](vk::CommandBuffer cmdbuf) { + cmdbuf.BeginTransformFeedbackEXT(0, total, counter_buffers.data(), offsets.data()); + }); + UpdateBuffers(); + } + + void FlushEndTFB() { + if (!has_flushed_end_pending) [[unlikely]] { + UNREACHABLE(); + return; + } + has_flushed_end_pending = false; + + if (buffers_count == 0) { + scheduler.Record([](vk::CommandBuffer cmdbuf) { + cmdbuf.EndTransformFeedbackEXT(0, 0, nullptr, nullptr); + }); + } else { + scheduler.Record([this, + total = static_cast<u32>(buffers_count)](vk::CommandBuffer cmdbuf) { + cmdbuf.EndTransformFeedbackEXT(0, total, counter_buffers.data(), offsets.data()); + }); + } + } + + void UpdateBuffers() { + last_queries.fill(0); + last_queries_stride.fill(1); + runtime.View3DRegs([this](Maxwell3D& maxwell3d) { + buffers_count = 0; + out_topology = maxwell3d.draw_manager->GetDrawState().topology; + for (size_t i = 0; i < Maxwell3D::Regs::NumTransformFeedbackBuffers; i++) { + const auto& tf = maxwell3d.regs.transform_feedback; + if (tf.buffers[i].enable == 0) { + continue; + } + const size_t stream = tf.controls[i].stream; + last_queries_stride[stream] = tf.controls[i].stride; + streams_mask |= 1ULL << stream; + buffers_count = std::max<size_t>(buffers_count, stream + 1); + } + }); + } + + std::pair<size_t, size_t> ProduceCounterBuffer(size_t stream) { + if (current_bank == nullptr || current_bank->IsClosed()) { + current_bank_id = + bank_pool.ReserveBank([this](std::deque<TFBQueryBank>& queue, size_t index) { + queue.emplace_back(scheduler, memory_allocator, index); + }); + current_bank = &bank_pool.GetBank(current_bank_id); + } + auto [dont_care, other] = current_bank->Reserve(); + const size_t slot = other; // workaround to compile bug. + current_bank->AddReference(); + + static constexpr VkMemoryBarrier READ_BARRIER{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + }; + static constexpr VkMemoryBarrier WRITE_BARRIER{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + }; + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([dst_buffer = current_bank->GetBuffer(), + src_buffer = counter_buffers[stream], src_offset = offsets[stream], + slot](vk::CommandBuffer cmdbuf) { + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, READ_BARRIER); + std::array<VkBufferCopy, 1> copy{VkBufferCopy{ + .srcOffset = src_offset, + .dstOffset = slot * TFBQueryBank::QUERY_SIZE, + .size = TFBQueryBank::QUERY_SIZE, + }}; + cmdbuf.CopyBuffer(src_buffer, dst_buffer, copy); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, WRITE_BARRIER); + }); + return {current_bank_id, slot}; + } + + friend class PrimitivesSucceededStreamer; + + static constexpr size_t NUM_STREAMS = 4; + + QueryCacheRuntime& runtime; + const Device& device; + Scheduler& scheduler; + const MemoryAllocator& memory_allocator; + StagingBufferPool& staging_pool; + VideoCommon::BankPool<TFBQueryBank> bank_pool; + size_t current_bank_id; + TFBQueryBank* current_bank; + vk::Buffer counters_buffer; + + // syncing queue + std::vector<size_t> pending_sync; + + // flush levels + std::vector<size_t> pending_flush_queries; + std::deque<StagingBufferRef> download_buffers; + std::deque<std::vector<size_t>> pending_flush_sets; + std::vector<StagingBufferRef> free_queue; + std::mutex flush_guard; + + // state machine + bool has_started{}; + bool has_flushed_end_pending{}; + size_t buffers_count{}; + std::array<VkBuffer, NUM_STREAMS> counter_buffers{}; + std::array<VkDeviceSize, NUM_STREAMS> offsets{}; + std::array<VAddr, NUM_STREAMS> last_queries; + std::array<size_t, NUM_STREAMS> last_queries_stride; + Maxwell3D::Regs::PrimitiveTopology out_topology; + u64 streams_mask; +}; + +class PrimitivesQueryBase : public VideoCommon::QueryBase { +public: + // Default constructor + PrimitivesQueryBase() + : VideoCommon::QueryBase(0, VideoCommon::QueryFlagBits::IsHostManaged, 0) {} + + // Parameterized constructor + PrimitivesQueryBase(bool has_timestamp, VAddr address) + : VideoCommon::QueryBase(address, VideoCommon::QueryFlagBits::IsHostManaged, 0) { + if (has_timestamp) { + flags |= VideoCommon::QueryFlagBits::HasTimestamp; + } + } + + u64 stride{}; + VAddr dependant_address{}; + Maxwell3D::Regs::PrimitiveTopology topology{Maxwell3D::Regs::PrimitiveTopology::Points}; + size_t dependant_index{}; + bool dependant_manage{}; +}; + +class PrimitivesSucceededStreamer : public VideoCommon::SimpleStreamer<PrimitivesQueryBase> { +public: + explicit PrimitivesSucceededStreamer(size_t id_, QueryCacheRuntime& runtime_, + TFBCounterStreamer& tfb_streamer_, + Core::Memory::Memory& cpu_memory_) + : VideoCommon::SimpleStreamer<PrimitivesQueryBase>(id_), runtime{runtime_}, + tfb_streamer{tfb_streamer_}, cpu_memory{cpu_memory_} { + MakeDependent(&tfb_streamer); + } + + ~PrimitivesSucceededStreamer() = default; + + size_t WriteCounter(VAddr address, bool has_timestamp, u32 value, + std::optional<u32> subreport_) override { + auto index = BuildQuery(); + auto* new_query = GetQuery(index); + new_query->guest_address = address; + new_query->value = 0; + if (has_timestamp) { + new_query->flags |= VideoCommon::QueryFlagBits::HasTimestamp; + } + if (!subreport_) { + new_query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + return index; + } + const size_t subreport = static_cast<size_t>(*subreport_); + auto dependant_address_opt = tfb_streamer.GetLastQueryStream(subreport); + bool must_manage_dependance = false; + new_query->topology = tfb_streamer.GetOutputTopology(); + if (dependant_address_opt) { + auto [dep_address, stride] = *dependant_address_opt; + new_query->dependant_address = dep_address; + new_query->stride = stride; + } else { + new_query->dependant_index = + tfb_streamer.WriteCounter(address, has_timestamp, value, subreport_); + auto* dependant_query = tfb_streamer.GetQuery(new_query->dependant_index); + dependant_query->flags |= VideoCommon::QueryFlagBits::IsInvalidated; + must_manage_dependance = true; + if (True(dependant_query->flags & VideoCommon::QueryFlagBits::IsFinalValueSynced)) { + new_query->value = 0; + new_query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + if (must_manage_dependance) { + tfb_streamer.Free(new_query->dependant_index); + } + return index; + } + new_query->stride = 1; + runtime.View3DRegs([new_query, subreport](Maxwell3D& maxwell3d) { + for (size_t i = 0; i < Maxwell3D::Regs::NumTransformFeedbackBuffers; i++) { + const auto& tf = maxwell3d.regs.transform_feedback; + if (tf.buffers[i].enable == 0) { + continue; + } + if (tf.controls[i].stream != subreport) { + continue; + } + new_query->stride = tf.controls[i].stride; + break; + } + }); + } + + new_query->dependant_manage = must_manage_dependance; + pending_flush_queries.push_back(index); + return index; + } + + bool HasUnsyncedQueries() const override { + return !pending_flush_queries.empty(); + } + + void PushUnsyncedQueries() override { + std::scoped_lock lk(flush_guard); + pending_flush_sets.emplace_back(std::move(pending_flush_queries)); + pending_flush_queries.clear(); + } + + void PopUnsyncedQueries() override { + std::vector<size_t> flushed_queries; + { + std::scoped_lock lk(flush_guard); + flushed_queries = std::move(pending_flush_sets.front()); + pending_flush_sets.pop_front(); + } + + for (auto q : flushed_queries) { + auto* query = GetQuery(q); + if (True(query->flags & VideoCommon::QueryFlagBits::IsFinalValueSynced)) { + continue; + } + + query->flags |= VideoCommon::QueryFlagBits::IsFinalValueSynced; + u64 num_vertices = 0; + if (query->dependant_manage) { + auto* dependant_query = tfb_streamer.GetQuery(query->dependant_index); + num_vertices = dependant_query->value / query->stride; + tfb_streamer.Free(query->dependant_index); + } else { + u8* pointer = cpu_memory.GetPointer(query->dependant_address); + u32 result; + std::memcpy(&result, pointer, sizeof(u32)); + num_vertices = static_cast<u64>(result) / query->stride; + } + query->value = [&]() -> u64 { + switch (query->topology) { + case Maxwell3D::Regs::PrimitiveTopology::Points: + return num_vertices; + case Maxwell3D::Regs::PrimitiveTopology::Lines: + return num_vertices / 2; + case Maxwell3D::Regs::PrimitiveTopology::LineLoop: + return (num_vertices / 2) + 1; + case Maxwell3D::Regs::PrimitiveTopology::LineStrip: + return num_vertices - 1; + case Maxwell3D::Regs::PrimitiveTopology::Patches: + case Maxwell3D::Regs::PrimitiveTopology::Triangles: + case Maxwell3D::Regs::PrimitiveTopology::TrianglesAdjacency: + return num_vertices / 3; + case Maxwell3D::Regs::PrimitiveTopology::TriangleFan: + case Maxwell3D::Regs::PrimitiveTopology::TriangleStrip: + case Maxwell3D::Regs::PrimitiveTopology::TriangleStripAdjacency: + return num_vertices - 2; + case Maxwell3D::Regs::PrimitiveTopology::Quads: + return num_vertices / 4; + case Maxwell3D::Regs::PrimitiveTopology::Polygon: + return 1U; + default: + return num_vertices; + } + }(); + } + } + +private: + QueryCacheRuntime& runtime; + TFBCounterStreamer& tfb_streamer; + Core::Memory::Memory& cpu_memory; + + // syncing queue + std::vector<size_t> pending_sync; + + // flush levels + std::vector<size_t> pending_flush_queries; + std::deque<std::vector<size_t>> pending_flush_sets; + std::mutex flush_guard; +}; + +} // namespace + +struct QueryCacheRuntimeImpl { + QueryCacheRuntimeImpl(QueryCacheRuntime& runtime, VideoCore::RasterizerInterface* rasterizer_, + Core::Memory::Memory& cpu_memory_, Vulkan::BufferCache& buffer_cache_, + const Device& device_, const MemoryAllocator& memory_allocator_, + Scheduler& scheduler_, StagingBufferPool& staging_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue, + DescriptorPool& descriptor_pool) + : rasterizer{rasterizer_}, cpu_memory{cpu_memory_}, + buffer_cache{buffer_cache_}, device{device_}, + memory_allocator{memory_allocator_}, scheduler{scheduler_}, staging_pool{staging_pool_}, + guest_streamer(0, runtime), + sample_streamer(static_cast<size_t>(QueryType::ZPassPixelCount64), runtime, rasterizer, + device, scheduler, memory_allocator, compute_pass_descriptor_queue, + descriptor_pool), + tfb_streamer(static_cast<size_t>(QueryType::StreamingByteCount), runtime, device, + scheduler, memory_allocator, staging_pool), + primitives_succeeded_streamer( + static_cast<size_t>(QueryType::StreamingPrimitivesSucceeded), runtime, tfb_streamer, + cpu_memory_), + primitives_needed_minus_suceeded_streamer( + static_cast<size_t>(QueryType::StreamingPrimitivesNeededMinusSucceeded), runtime, 0u), + hcr_setup{}, hcr_is_set{}, is_hcr_running{}, maxwell3d{} { + + hcr_setup.sType = VK_STRUCTURE_TYPE_CONDITIONAL_RENDERING_BEGIN_INFO_EXT; + hcr_setup.pNext = nullptr; + hcr_setup.flags = 0; + + conditional_resolve_pass = std::make_unique<ConditionalRenderingResolvePass>( + device, scheduler, descriptor_pool, compute_pass_descriptor_queue); + + const VkBufferCreateInfo buffer_ci = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = sizeof(u32), + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | + VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + hcr_resolve_buffer = memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::DeviceLocal); + } + + VideoCore::RasterizerInterface* rasterizer; + Core::Memory::Memory& cpu_memory; + Vulkan::BufferCache& buffer_cache; + + const Device& device; + const MemoryAllocator& memory_allocator; + Scheduler& scheduler; + StagingBufferPool& staging_pool; + + // Streamers + VideoCommon::GuestStreamer<QueryCacheParams> guest_streamer; + SamplesStreamer sample_streamer; + TFBCounterStreamer tfb_streamer; + PrimitivesSucceededStreamer primitives_succeeded_streamer; + VideoCommon::StubStreamer<QueryCacheParams> primitives_needed_minus_suceeded_streamer; + + std::vector<std::pair<VAddr, VAddr>> little_cache; + std::vector<std::pair<VkBuffer, VkDeviceSize>> buffers_to_upload_to; + std::vector<size_t> redirect_cache; + std::vector<std::vector<VkBufferCopy>> copies_setup; + + // Host conditional rendering data + std::unique_ptr<ConditionalRenderingResolvePass> conditional_resolve_pass; + vk::Buffer hcr_resolve_buffer; + VkConditionalRenderingBeginInfoEXT hcr_setup; + VkBuffer hcr_buffer; + size_t hcr_offset; + bool hcr_is_set; + bool is_hcr_running; + + // maxwell3d + Maxwell3D* maxwell3d; +}; + +QueryCacheRuntime::QueryCacheRuntime(VideoCore::RasterizerInterface* rasterizer, + Core::Memory::Memory& cpu_memory_, + Vulkan::BufferCache& buffer_cache_, const Device& device_, + const MemoryAllocator& memory_allocator_, + Scheduler& scheduler_, StagingBufferPool& staging_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue, + DescriptorPool& descriptor_pool) { + impl = std::make_unique<QueryCacheRuntimeImpl>( + *this, rasterizer, cpu_memory_, buffer_cache_, device_, memory_allocator_, scheduler_, + staging_pool_, compute_pass_descriptor_queue, descriptor_pool); } -void QueryPool::Allocate(std::size_t begin, std::size_t end) { - usage.resize(end); +void QueryCacheRuntime::Bind3DEngine(Maxwell3D* maxwell3d) { + impl->maxwell3d = maxwell3d; +} - pools.push_back(device.GetLogical().CreateQueryPool({ - .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .queryType = GetTarget(type), - .queryCount = static_cast<u32>(end - begin), - .pipelineStatistics = 0, - })); +template <typename Func> +void QueryCacheRuntime::View3DRegs(Func&& func) { + if (impl->maxwell3d) { + func(*impl->maxwell3d); + } +} + +void QueryCacheRuntime::EndHostConditionalRendering() { + PauseHostConditionalRendering(); + impl->hcr_is_set = false; + impl->is_hcr_running = false; + impl->hcr_buffer = nullptr; + impl->hcr_offset = 0; +} + +void QueryCacheRuntime::PauseHostConditionalRendering() { + if (!impl->hcr_is_set) { + return; + } + if (impl->is_hcr_running) { + impl->scheduler.Record( + [](vk::CommandBuffer cmdbuf) { cmdbuf.EndConditionalRenderingEXT(); }); + } + impl->is_hcr_running = false; } -void QueryPool::Reserve(std::pair<VkQueryPool, u32> query) { - const auto it = - std::find_if(pools.begin(), pools.end(), [query_pool = query.first](vk::QueryPool& pool) { - return query_pool == *pool; +void QueryCacheRuntime::ResumeHostConditionalRendering() { + if (!impl->hcr_is_set) { + return; + } + if (!impl->is_hcr_running) { + impl->scheduler.Record([hcr_setup = impl->hcr_setup](vk::CommandBuffer cmdbuf) { + cmdbuf.BeginConditionalRenderingEXT(hcr_setup); }); + } + impl->is_hcr_running = true; +} - if (it != std::end(pools)) { - const std::ptrdiff_t pool_index = std::distance(std::begin(pools), it); - usage[pool_index * GROW_STEP + static_cast<std::ptrdiff_t>(query.second)] = false; +void QueryCacheRuntime::HostConditionalRenderingCompareValueImpl(VideoCommon::LookupData object, + bool is_equal) { + { + std::scoped_lock lk(impl->buffer_cache.mutex); + static constexpr auto sync_info = VideoCommon::ObtainBufferSynchronize::FullSynchronize; + const auto post_op = VideoCommon::ObtainBufferOperation::DoNothing; + const auto [buffer, offset] = + impl->buffer_cache.ObtainCPUBuffer(object.address, 8, sync_info, post_op); + impl->hcr_buffer = buffer->Handle(); + impl->hcr_offset = offset; + } + if (impl->hcr_is_set) { + if (impl->hcr_setup.buffer == impl->hcr_buffer && + impl->hcr_setup.offset == impl->hcr_offset) { + ResumeHostConditionalRendering(); + return; + } + PauseHostConditionalRendering(); } + impl->hcr_setup.buffer = impl->hcr_buffer; + impl->hcr_setup.offset = impl->hcr_offset; + impl->hcr_setup.flags = is_equal ? VK_CONDITIONAL_RENDERING_INVERTED_BIT_EXT : 0; + impl->hcr_is_set = true; + impl->is_hcr_running = false; + ResumeHostConditionalRendering(); } -QueryCache::QueryCache(VideoCore::RasterizerInterface& rasterizer_, - Core::Memory::Memory& cpu_memory_, const Device& device_, - Scheduler& scheduler_) - : QueryCacheBase{rasterizer_, cpu_memory_}, device{device_}, scheduler{scheduler_}, - query_pools{ - QueryPool{device_, scheduler_, QueryType::SamplesPassed}, - } {} - -QueryCache::~QueryCache() { - // TODO(Rodrigo): This is a hack to destroy all HostCounter instances before the base class - // destructor is called. The query cache should be redesigned to have a proper ownership model - // instead of using shared pointers. - for (size_t query_type = 0; query_type < VideoCore::NumQueryTypes; ++query_type) { - auto& stream = Stream(static_cast<QueryType>(query_type)); - stream.Update(false); - stream.Reset(); +void QueryCacheRuntime::HostConditionalRenderingCompareBCImpl(VAddr address, bool is_equal) { + VkBuffer to_resolve; + u32 to_resolve_offset; + { + std::scoped_lock lk(impl->buffer_cache.mutex); + static constexpr auto sync_info = VideoCommon::ObtainBufferSynchronize::NoSynchronize; + const auto post_op = VideoCommon::ObtainBufferOperation::DoNothing; + const auto [buffer, offset] = + impl->buffer_cache.ObtainCPUBuffer(address, 24, sync_info, post_op); + to_resolve = buffer->Handle(); + to_resolve_offset = static_cast<u32>(offset); } + if (impl->is_hcr_running) { + PauseHostConditionalRendering(); + } + impl->conditional_resolve_pass->Resolve(*impl->hcr_resolve_buffer, to_resolve, + to_resolve_offset, false); + impl->hcr_setup.buffer = *impl->hcr_resolve_buffer; + impl->hcr_setup.offset = 0; + impl->hcr_setup.flags = is_equal ? 0 : VK_CONDITIONAL_RENDERING_INVERTED_BIT_EXT; + impl->hcr_is_set = true; + impl->is_hcr_running = false; + ResumeHostConditionalRendering(); } -std::pair<VkQueryPool, u32> QueryCache::AllocateQuery(QueryType type) { - return query_pools[static_cast<std::size_t>(type)].Commit(); +bool QueryCacheRuntime::HostConditionalRenderingCompareValue(VideoCommon::LookupData object_1, + [[maybe_unused]] bool qc_dirty) { + if (!impl->device.IsExtConditionalRendering()) { + return false; + } + HostConditionalRenderingCompareValueImpl(object_1, false); + return true; } -void QueryCache::Reserve(QueryType type, std::pair<VkQueryPool, u32> query) { - query_pools[static_cast<std::size_t>(type)].Reserve(query); +bool QueryCacheRuntime::HostConditionalRenderingCompareValues(VideoCommon::LookupData object_1, + VideoCommon::LookupData object_2, + bool qc_dirty, bool equal_check) { + if (!impl->device.IsExtConditionalRendering()) { + return false; + } + + const auto check_in_bc = [&](VAddr address) { + return impl->buffer_cache.IsRegionGpuModified(address, 8); + }; + const auto check_value = [&](VAddr address) { + u8* ptr = impl->cpu_memory.GetPointer(address); + u64 value{}; + std::memcpy(&value, ptr, sizeof(value)); + return value == 0; + }; + std::array<VideoCommon::LookupData*, 2> objects{&object_1, &object_2}; + std::array<bool, 2> is_in_bc{}; + std::array<bool, 2> is_in_qc{}; + std::array<bool, 2> is_in_ac{}; + std::array<bool, 2> is_null{}; + { + std::scoped_lock lk(impl->buffer_cache.mutex); + for (size_t i = 0; i < 2; i++) { + is_in_qc[i] = objects[i]->found_query != nullptr; + is_in_bc[i] = !is_in_qc[i] && check_in_bc(objects[i]->address); + is_in_ac[i] = is_in_qc[i] || is_in_bc[i]; + } + } + + if (!is_in_ac[0] && !is_in_ac[1]) { + EndHostConditionalRendering(); + return false; + } + + if (!qc_dirty && !is_in_bc[0] && !is_in_bc[1]) { + EndHostConditionalRendering(); + return false; + } + + const bool is_gpu_high = Settings::IsGPULevelHigh(); + if (!is_gpu_high && impl->device.GetDriverID() == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS) { + return true; + } + + for (size_t i = 0; i < 2; i++) { + is_null[i] = !is_in_ac[i] && check_value(objects[i]->address); + } + + for (size_t i = 0; i < 2; i++) { + if (is_null[i]) { + size_t j = (i + 1) % 2; + HostConditionalRenderingCompareValueImpl(*objects[j], equal_check); + return true; + } + } + + if (!is_gpu_high) { + return true; + } + + if (!is_in_bc[0] && !is_in_bc[1]) { + // Both queries are in query cache, it's best to just flush. + return true; + } + HostConditionalRenderingCompareBCImpl(object_1.address, equal_check); + return true; } -HostCounter::HostCounter(QueryCache& cache_, std::shared_ptr<HostCounter> dependency_, - QueryType type_) - : HostCounterBase{std::move(dependency_)}, cache{cache_}, type{type_}, - query{cache_.AllocateQuery(type_)}, tick{cache_.GetScheduler().CurrentTick()} { - const vk::Device* logical = &cache.GetDevice().GetLogical(); - cache.GetScheduler().Record([logical, query_ = query](vk::CommandBuffer cmdbuf) { - const bool use_precise = Settings::IsGPULevelHigh(); - logical->ResetQueryPool(query_.first, query_.second, 1); - cmdbuf.BeginQuery(query_.first, query_.second, - use_precise ? VK_QUERY_CONTROL_PRECISE_BIT : 0); - }); +QueryCacheRuntime::~QueryCacheRuntime() = default; + +VideoCommon::StreamerInterface* QueryCacheRuntime::GetStreamerInterface(QueryType query_type) { + switch (query_type) { + case QueryType::Payload: + return &impl->guest_streamer; + case QueryType::ZPassPixelCount64: + return &impl->sample_streamer; + case QueryType::StreamingByteCount: + return &impl->tfb_streamer; + case QueryType::StreamingPrimitivesNeeded: + case QueryType::VtgPrimitivesOut: + case QueryType::StreamingPrimitivesSucceeded: + return &impl->primitives_succeeded_streamer; + case QueryType::StreamingPrimitivesNeededMinusSucceeded: + return &impl->primitives_needed_minus_suceeded_streamer; + default: + return nullptr; + } } -HostCounter::~HostCounter() { - cache.Reserve(type, query); +void QueryCacheRuntime::Barriers(bool is_prebarrier) { + static constexpr VkMemoryBarrier READ_BARRIER{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, + }; + static constexpr VkMemoryBarrier WRITE_BARRIER{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, + }; + if (is_prebarrier) { + impl->scheduler.Record([](vk::CommandBuffer cmdbuf) { + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, READ_BARRIER); + }); + } else { + impl->scheduler.Record([](vk::CommandBuffer cmdbuf) { + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, WRITE_BARRIER); + }); + } } -void HostCounter::EndQuery() { - cache.GetScheduler().Record([query_ = query](vk::CommandBuffer cmdbuf) { - cmdbuf.EndQuery(query_.first, query_.second); +template <typename SyncValuesType> +void QueryCacheRuntime::SyncValues(std::span<SyncValuesType> values, VkBuffer base_src_buffer) { + if (values.size() == 0) { + return; + } + impl->redirect_cache.clear(); + impl->little_cache.clear(); + size_t total_size = 0; + for (auto& sync_val : values) { + total_size += sync_val.size; + bool found = false; + VAddr base = Common::AlignDown(sync_val.address, Core::Memory::YUZU_PAGESIZE); + VAddr base_end = base + Core::Memory::YUZU_PAGESIZE; + for (size_t i = 0; i < impl->little_cache.size(); i++) { + const auto set_found = [&] { + impl->redirect_cache.push_back(i); + found = true; + }; + auto& loc = impl->little_cache[i]; + if (base < loc.second && loc.first < base_end) { + set_found(); + break; + } + if (loc.first == base_end) { + loc.first = base; + set_found(); + break; + } + if (loc.second == base) { + loc.second = base_end; + set_found(); + break; + } + } + if (!found) { + impl->redirect_cache.push_back(impl->little_cache.size()); + impl->little_cache.emplace_back(base, base_end); + } + } + + // Vulkan part. + std::scoped_lock lk(impl->buffer_cache.mutex); + impl->buffer_cache.BufferOperations([&] { + impl->buffers_to_upload_to.clear(); + for (auto& pair : impl->little_cache) { + static constexpr auto sync_info = VideoCommon::ObtainBufferSynchronize::FullSynchronize; + const auto post_op = VideoCommon::ObtainBufferOperation::DoNothing; + const auto [buffer, offset] = impl->buffer_cache.ObtainCPUBuffer( + pair.first, static_cast<u32>(pair.second - pair.first), sync_info, post_op); + impl->buffers_to_upload_to.emplace_back(buffer->Handle(), offset); + } }); -} -u64 HostCounter::BlockingQuery(bool async) const { - if (!async) { - cache.GetScheduler().Wait(tick); - } - u64 data; - const VkResult query_result = cache.GetDevice().GetLogical().GetQueryResults( - query.first, query.second, 1, sizeof(data), &data, sizeof(data), - VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); - - switch (query_result) { - case VK_SUCCESS: - return data; - case VK_ERROR_DEVICE_LOST: - cache.GetDevice().ReportLoss(); - [[fallthrough]]; - default: - throw vk::Exception(query_result); + VkBuffer src_buffer; + [[maybe_unused]] StagingBufferRef ref; + impl->copies_setup.clear(); + impl->copies_setup.resize(impl->little_cache.size()); + if constexpr (SyncValuesType::GeneratesBaseBuffer) { + ref = impl->staging_pool.Request(total_size, MemoryUsage::Upload); + size_t current_offset = ref.offset; + size_t accumulated_size = 0; + for (size_t i = 0; i < values.size(); i++) { + size_t which_copy = impl->redirect_cache[i]; + impl->copies_setup[which_copy].emplace_back(VkBufferCopy{ + .srcOffset = current_offset + accumulated_size, + .dstOffset = impl->buffers_to_upload_to[which_copy].second + values[i].address - + impl->little_cache[which_copy].first, + .size = values[i].size, + }); + std::memcpy(ref.mapped_span.data() + accumulated_size, &values[i].value, + values[i].size); + accumulated_size += values[i].size; + } + src_buffer = ref.buffer; + } else { + for (size_t i = 0; i < values.size(); i++) { + size_t which_copy = impl->redirect_cache[i]; + impl->copies_setup[which_copy].emplace_back(VkBufferCopy{ + .srcOffset = values[i].offset, + .dstOffset = impl->buffers_to_upload_to[which_copy].second + values[i].address - + impl->little_cache[which_copy].first, + .size = values[i].size, + }); + } + src_buffer = base_src_buffer; } + + impl->scheduler.RequestOutsideRenderPassOperationContext(); + impl->scheduler.Record([src_buffer, dst_buffers = std::move(impl->buffers_to_upload_to), + vk_copies = std::move(impl->copies_setup)](vk::CommandBuffer cmdbuf) { + size_t size = dst_buffers.size(); + for (size_t i = 0; i < size; i++) { + cmdbuf.CopyBuffer(src_buffer, dst_buffers[i].first, vk_copies[i]); + } + }); } } // namespace Vulkan + +namespace VideoCommon { + +template class QueryCacheBase<Vulkan::QueryCacheParams>; + +} // namespace VideoCommon diff --git a/src/video_core/renderer_vulkan/vk_query_cache.h b/src/video_core/renderer_vulkan/vk_query_cache.h index c1b9552eb..e9a1ea169 100644 --- a/src/video_core/renderer_vulkan/vk_query_cache.h +++ b/src/video_core/renderer_vulkan/vk_query_cache.h @@ -1,101 +1,75 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once -#include <cstddef> #include <memory> -#include <utility> -#include <vector> -#include "common/common_types.h" -#include "video_core/query_cache.h" -#include "video_core/renderer_vulkan/vk_resource_pool.h" -#include "video_core/vulkan_common/vulkan_wrapper.h" +#include "video_core/query_cache/query_cache_base.h" +#include "video_core/renderer_vulkan/vk_buffer_cache.h" namespace VideoCore { class RasterizerInterface; } +namespace VideoCommon { +class StreamerInterface; +} + namespace Vulkan { -class CachedQuery; class Device; -class HostCounter; -class QueryCache; class Scheduler; +class StagingBufferPool; -using CounterStream = VideoCommon::CounterStreamBase<QueryCache, HostCounter>; +struct QueryCacheRuntimeImpl; -class QueryPool final : public ResourcePool { +class QueryCacheRuntime { public: - explicit QueryPool(const Device& device, Scheduler& scheduler, VideoCore::QueryType type); - ~QueryPool() override; + explicit QueryCacheRuntime(VideoCore::RasterizerInterface* rasterizer, + Core::Memory::Memory& cpu_memory_, + Vulkan::BufferCache& buffer_cache_, const Device& device_, + const MemoryAllocator& memory_allocator_, Scheduler& scheduler_, + StagingBufferPool& staging_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue, + DescriptorPool& descriptor_pool); + ~QueryCacheRuntime(); - std::pair<VkQueryPool, u32> Commit(); + template <typename SyncValuesType> + void SyncValues(std::span<SyncValuesType> values, VkBuffer base_src_buffer = nullptr); - void Reserve(std::pair<VkQueryPool, u32> query); + void Barriers(bool is_prebarrier); -protected: - void Allocate(std::size_t begin, std::size_t end) override; + void EndHostConditionalRendering(); -private: - static constexpr std::size_t GROW_STEP = 512; + void PauseHostConditionalRendering(); - const Device& device; - const VideoCore::QueryType type; + void ResumeHostConditionalRendering(); - std::vector<vk::QueryPool> pools; - std::vector<bool> usage; -}; + bool HostConditionalRenderingCompareValue(VideoCommon::LookupData object_1, bool qc_dirty); -class QueryCache final - : public VideoCommon::QueryCacheBase<QueryCache, CachedQuery, CounterStream, HostCounter> { -public: - explicit QueryCache(VideoCore::RasterizerInterface& rasterizer_, - Core::Memory::Memory& cpu_memory_, const Device& device_, - Scheduler& scheduler_); - ~QueryCache(); - - std::pair<VkQueryPool, u32> AllocateQuery(VideoCore::QueryType type); + bool HostConditionalRenderingCompareValues(VideoCommon::LookupData object_1, + VideoCommon::LookupData object_2, bool qc_dirty, + bool equal_check); - void Reserve(VideoCore::QueryType type, std::pair<VkQueryPool, u32> query); + VideoCommon::StreamerInterface* GetStreamerInterface(VideoCommon::QueryType query_type); - const Device& GetDevice() const noexcept { - return device; - } + void Bind3DEngine(Tegra::Engines::Maxwell3D* maxwell3d); - Scheduler& GetScheduler() const noexcept { - return scheduler; - } + template <typename Func> + void View3DRegs(Func&& func); private: - const Device& device; - Scheduler& scheduler; - std::array<QueryPool, VideoCore::NumQueryTypes> query_pools; + void HostConditionalRenderingCompareValueImpl(VideoCommon::LookupData object, bool is_equal); + void HostConditionalRenderingCompareBCImpl(VAddr address, bool is_equal); + friend struct QueryCacheRuntimeImpl; + std::unique_ptr<QueryCacheRuntimeImpl> impl; }; -class HostCounter final : public VideoCommon::HostCounterBase<QueryCache, HostCounter> { -public: - explicit HostCounter(QueryCache& cache_, std::shared_ptr<HostCounter> dependency_, - VideoCore::QueryType type_); - ~HostCounter(); - - void EndQuery(); - -private: - u64 BlockingQuery(bool async = false) const override; - - QueryCache& cache; - const VideoCore::QueryType type; - const std::pair<VkQueryPool, u32> query; - const u64 tick; +struct QueryCacheParams { + using RuntimeType = typename Vulkan::QueryCacheRuntime; }; -class CachedQuery : public VideoCommon::CachedQueryBase<HostCounter> { -public: - explicit CachedQuery(QueryCache&, VideoCore::QueryType, VAddr cpu_addr_, u8* host_ptr_) - : CachedQueryBase{cpu_addr_, host_ptr_} {} -}; +using QueryCache = VideoCommon::QueryCacheBase<QueryCacheParams>; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 01e76a82c..83f2b6045 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -24,6 +24,7 @@ #include "video_core/renderer_vulkan/vk_compute_pipeline.h" #include "video_core/renderer_vulkan/vk_descriptor_pool.h" #include "video_core/renderer_vulkan/vk_pipeline_cache.h" +#include "video_core/renderer_vulkan/vk_query_cache.h" #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/renderer_vulkan/vk_staging_buffer_pool.h" @@ -170,9 +171,11 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra buffer_cache_runtime(device, memory_allocator, scheduler, staging_pool, guest_descriptor_queue, compute_pass_descriptor_queue, descriptor_pool), buffer_cache(*this, cpu_memory_, buffer_cache_runtime), + query_cache_runtime(this, cpu_memory_, buffer_cache, device, memory_allocator, scheduler, + staging_pool, compute_pass_descriptor_queue, descriptor_pool), + query_cache(gpu, *this, cpu_memory_, query_cache_runtime), pipeline_cache(*this, device, scheduler, descriptor_pool, guest_descriptor_queue, render_pass_cache, buffer_cache, texture_cache, gpu.ShaderNotify()), - query_cache{*this, cpu_memory_, device, scheduler}, accelerate_dma(buffer_cache, texture_cache, scheduler), fence_manager(*this, gpu, texture_cache, buffer_cache, query_cache, device, scheduler), wfi_event(device.GetLogical().CreateEvent()) { @@ -189,14 +192,7 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { FlushWork(); gpu_memory->FlushCaching(); -#if ANDROID - if (Settings::IsGPULevelHigh()) { - // This is problematic on Android, disable on GPU Normal. - query_cache.UpdateCounters(); - } -#else - query_cache.UpdateCounters(); -#endif + query_cache.NotifySegment(true); GraphicsPipeline* const pipeline{pipeline_cache.CurrentGraphicsPipeline()}; if (!pipeline) { @@ -207,13 +203,12 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { pipeline->SetEngine(maxwell3d, gpu_memory); pipeline->Configure(is_indexed); - BeginTransformFeedback(); - UpdateDynamicStates(); + HandleTransformFeedback(); + query_cache.CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, + maxwell3d->regs.zpass_pixel_count_enable); draw_func(); - - EndTransformFeedback(); } void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { @@ -241,6 +236,14 @@ void RasterizerVulkan::DrawIndirect() { const auto indirect_buffer = buffer_cache.GetDrawIndirectBuffer(); const auto& buffer = indirect_buffer.first; const auto& offset = indirect_buffer.second; + if (params.is_byte_count) { + scheduler.Record([buffer_obj = buffer->Handle(), offset, + stride = params.stride](vk::CommandBuffer cmdbuf) { + cmdbuf.DrawIndirectByteCountEXT(1, 0, buffer_obj, offset, 0, + static_cast<u32>(stride)); + }); + return; + } if (params.include_count) { const auto count = buffer_cache.GetDrawIndirectCount(); const auto& draw_buffer = count.first; @@ -280,20 +283,15 @@ void RasterizerVulkan::DrawTexture() { SCOPE_EXIT({ gpu.TickWork(); }); FlushWork(); -#if ANDROID - if (Settings::IsGPULevelHigh()) { - // This is problematic on Android, disable on GPU Normal. - query_cache.UpdateCounters(); - } -#else - query_cache.UpdateCounters(); -#endif + query_cache.NotifySegment(true); texture_cache.SynchronizeGraphicsDescriptors(); texture_cache.UpdateRenderTargets(false); UpdateDynamicStates(); + query_cache.CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, + maxwell3d->regs.zpass_pixel_count_enable); const auto& draw_texture_state = maxwell3d->draw_manager->GetDrawTextureState(); const auto& sampler = texture_cache.GetGraphicsSampler(draw_texture_state.src_sampler); const auto& texture = texture_cache.GetImageView(draw_texture_state.src_texture); @@ -316,14 +314,9 @@ void RasterizerVulkan::Clear(u32 layer_count) { FlushWork(); gpu_memory->FlushCaching(); -#if ANDROID - if (Settings::IsGPULevelHigh()) { - // This is problematic on Android, disable on GPU Normal. - query_cache.UpdateCounters(); - } -#else - query_cache.UpdateCounters(); -#endif + query_cache.NotifySegment(true); + query_cache.CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, + maxwell3d->regs.zpass_pixel_count_enable); auto& regs = maxwell3d->regs; const bool use_color = regs.clear_surface.R || regs.clear_surface.G || regs.clear_surface.B || @@ -429,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), @@ -482,13 +476,13 @@ void RasterizerVulkan::DispatchCompute() { scheduler.Record([dim](vk::CommandBuffer cmdbuf) { cmdbuf.Dispatch(dim[0], dim[1], dim[2]); }); } -void RasterizerVulkan::ResetCounter(VideoCore::QueryType type) { - query_cache.ResetCounter(type); +void RasterizerVulkan::ResetCounter(VideoCommon::QueryType type) { + query_cache.CounterReset(type); } -void RasterizerVulkan::Query(GPUVAddr gpu_addr, VideoCore::QueryType type, - std::optional<u64> timestamp) { - query_cache.Query(gpu_addr, type, timestamp); +void RasterizerVulkan::Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) { + query_cache.CounterReport(gpu_addr, type, flags, payload, subreport); } void RasterizerVulkan::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, @@ -669,8 +663,8 @@ void RasterizerVulkan::SignalReference() { fence_manager.SignalReference(); } -void RasterizerVulkan::ReleaseFences() { - fence_manager.WaitPendingFences(); +void RasterizerVulkan::ReleaseFences(bool force) { + fence_manager.WaitPendingFences(force); } void RasterizerVulkan::FlushAndInvalidateRegion(VAddr addr, u64 size, @@ -694,6 +688,8 @@ void RasterizerVulkan::WaitForIdle() { flags |= VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT; } + query_cache.NotifyWFI(); + scheduler.RequestOutsideRenderPassOperationContext(); scheduler.Record([event = *wfi_event, flags](vk::CommandBuffer cmdbuf) { cmdbuf.SetEvent(event, flags); @@ -737,19 +733,7 @@ void RasterizerVulkan::TickFrame() { bool RasterizerVulkan::AccelerateConditionalRendering() { gpu_memory->FlushCaching(); - if (Settings::IsGPULevelHigh()) { - // TODO(Blinkhawk): Reimplement Host conditional rendering. - return false; - } - // Medium / Low Hack: stub any checks on queries written into the buffer cache. - const GPUVAddr condition_address{maxwell3d->regs.render_enable.Address()}; - Maxwell::ReportSemaphore::Compare cmp; - if (gpu_memory->IsMemoryDirty(condition_address, sizeof(cmp), - VideoCommon::CacheType::BufferCache | - VideoCommon::CacheType::QueryCache)) { - return true; - } - return false; + return query_cache.AccelerateHostConditionalRendering(); } bool RasterizerVulkan::AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surface& src, @@ -795,6 +779,7 @@ bool RasterizerVulkan::AccelerateDisplay(const Tegra::FramebufferConfig& config, if (!image_view) { return false; } + query_cache.NotifySegment(false); screen_info.image = image_view->ImageHandle(); screen_info.image_view = image_view->Handle(Shader::TextureType::Color2D); screen_info.width = image_view->size.width; @@ -933,31 +918,18 @@ void RasterizerVulkan::UpdateDynamicStates() { } } -void RasterizerVulkan::BeginTransformFeedback() { +void RasterizerVulkan::HandleTransformFeedback() { const auto& regs = maxwell3d->regs; - if (regs.transform_feedback_enabled == 0) { - return; - } if (!device.IsExtTransformFeedbackSupported()) { LOG_ERROR(Render_Vulkan, "Transform feedbacks used but not supported"); return; } - UNIMPLEMENTED_IF(regs.IsShaderConfigEnabled(Maxwell::ShaderType::TessellationInit) || - regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation)); - scheduler.Record( - [](vk::CommandBuffer cmdbuf) { cmdbuf.BeginTransformFeedbackEXT(0, 0, nullptr, nullptr); }); -} - -void RasterizerVulkan::EndTransformFeedback() { - const auto& regs = maxwell3d->regs; - if (regs.transform_feedback_enabled == 0) { - return; - } - if (!device.IsExtTransformFeedbackSupported()) { - return; + query_cache.CounterEnable(VideoCommon::QueryType::StreamingByteCount, + regs.transform_feedback_enabled); + if (regs.transform_feedback_enabled != 0) { + UNIMPLEMENTED_IF(regs.IsShaderConfigEnabled(Maxwell::ShaderType::TessellationInit) || + regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation)); } - scheduler.Record( - [](vk::CommandBuffer cmdbuf) { cmdbuf.EndTransformFeedbackEXT(0, 0, nullptr, nullptr); }); } void RasterizerVulkan::UpdateViewportsState(Tegra::Engines::Maxwell3D::Regs& regs) { @@ -1043,15 +1015,37 @@ void RasterizerVulkan::UpdateDepthBias(Tegra::Engines::Maxwell3D::Regs& regs) { regs.zeta.format == Tegra::DepthFormat::X8Z24_UNORM || regs.zeta.format == Tegra::DepthFormat::S8Z24_UNORM || regs.zeta.format == Tegra::DepthFormat::V8Z24_UNORM; - if (is_d24 && !device.SupportsD24DepthBuffer()) { + bool force_unorm = ([&] { + if (!is_d24 || device.SupportsD24DepthBuffer()) { + return false; + } + if (device.IsExtDepthBiasControlSupported()) { + return true; + } + if (!Settings::values.renderer_amdvlk_depth_bias_workaround) { + return false; + } // the base formulas can be obtained from here: // https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias const double rescale_factor = static_cast<double>(1ULL << (32 - 24)) / (static_cast<double>(0x1.ep+127)); units = static_cast<float>(static_cast<double>(units) * rescale_factor); - } + return false; + })(); scheduler.Record([constant = units, clamp = regs.depth_bias_clamp, - factor = regs.slope_scale_depth_bias](vk::CommandBuffer cmdbuf) { + factor = regs.slope_scale_depth_bias, force_unorm, + precise = device.HasExactDepthBiasControl()](vk::CommandBuffer cmdbuf) { + if (force_unorm) { + VkDepthBiasRepresentationInfoEXT info{ + .sType = VK_STRUCTURE_TYPE_DEPTH_BIAS_REPRESENTATION_INFO_EXT, + .pNext = nullptr, + .depthBiasRepresentation = + VK_DEPTH_BIAS_REPRESENTATION_LEAST_REPRESENTABLE_VALUE_FORCE_UNORM_EXT, + .depthBiasExact = precise ? VK_TRUE : VK_FALSE, + }; + cmdbuf.SetDepthBias(constant, clamp, factor, &info); + return; + } cmdbuf.SetDepthBias(constant, clamp, factor); }); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index b31982485..ad069556c 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -84,8 +84,9 @@ public: void DrawTexture() override; void Clear(u32 layer_count) override; void DispatchCompute() override; - void ResetCounter(VideoCore::QueryType type) override; - void Query(GPUVAddr gpu_addr, VideoCore::QueryType type, std::optional<u64> timestamp) override; + void ResetCounter(VideoCommon::QueryType type) override; + void Query(GPUVAddr gpu_addr, VideoCommon::QueryType type, + VideoCommon::QueryPropertiesFlags flags, u32 payload, u32 subreport) override; void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, u32 size) override; void DisableGraphicsUniformBuffer(size_t stage, u32 index) override; void FlushAll() override; @@ -106,7 +107,7 @@ public: void SyncOperation(std::function<void()>&& func) override; void SignalSyncPoint(u32 value) override; void SignalReference() override; - void ReleaseFences() override; + void ReleaseFences(bool force = true) override; void FlushAndInvalidateRegion( VAddr addr, u64 size, VideoCommon::CacheType which = VideoCommon::CacheType::All) override; void WaitForIdle() override; @@ -146,9 +147,7 @@ private: void UpdateDynamicStates(); - void BeginTransformFeedback(); - - void EndTransformFeedback(); + void HandleTransformFeedback(); void UpdateViewportsState(Tegra::Engines::Maxwell3D::Regs& regs); void UpdateScissorsState(Tegra::Engines::Maxwell3D::Regs& regs); @@ -195,8 +194,9 @@ private: TextureCache texture_cache; BufferCacheRuntime buffer_cache_runtime; BufferCache buffer_cache; - PipelineCache pipeline_cache; + QueryCacheRuntime query_cache_runtime; QueryCache query_cache; + PipelineCache pipeline_cache; AccelerateDMA accelerate_dma; FenceManager fence_manager; diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 89fd31b4f..3be7837f4 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -243,10 +243,10 @@ void Scheduler::AllocateNewContext() { #if ANDROID if (Settings::IsGPULevelHigh()) { // This is problematic on Android, disable on GPU Normal. - query_cache->UpdateCounters(); + query_cache->NotifySegment(true); } #else - query_cache->UpdateCounters(); + query_cache->NotifySegment(true); #endif } } @@ -261,11 +261,12 @@ void Scheduler::EndPendingOperations() { #if ANDROID if (Settings::IsGPULevelHigh()) { // This is problematic on Android, disable on GPU Normal. - query_cache->DisableStreams(); + // query_cache->DisableStreams(); } #else - query_cache->DisableStreams(); + // query_cache->DisableStreams(); #endif + query_cache->NotifySegment(false); EndRenderPass(); } diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 475c682eb..da03803aa 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -17,6 +17,11 @@ #include "video_core/renderer_vulkan/vk_master_semaphore.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +namespace VideoCommon { +template <typename Trait> +class QueryCacheBase; +} + namespace Vulkan { class CommandPool; @@ -24,7 +29,8 @@ class Device; class Framebuffer; class GraphicsPipeline; class StateTracker; -class QueryCache; + +struct QueryCacheParams; /// The scheduler abstracts command buffer and fence management with an interface that's able to do /// OpenGL-like operations on Vulkan command buffers. @@ -63,7 +69,7 @@ public: void InvalidateState(); /// Assigns the query cache. - void SetQueryCache(QueryCache& query_cache_) { + void SetQueryCache(VideoCommon::QueryCacheBase<QueryCacheParams>& query_cache_) { query_cache = &query_cache_; } @@ -219,7 +225,7 @@ private: std::unique_ptr<MasterSemaphore> master_semaphore; std::unique_ptr<CommandPool> command_pool; - QueryCache* query_cache = nullptr; + VideoCommon::QueryCacheBase<QueryCacheParams>* query_cache = nullptr; vk::CommandBuffer current_cmdbuf; 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 b3e17c332..00ab47268 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -120,19 +120,9 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { return usage; } -/// Returns the preferred format for a VkImage -[[nodiscard]] PixelFormat StorageFormat(PixelFormat format) { - switch (format) { - case PixelFormat::A8B8G8R8_SRGB: - return PixelFormat::A8B8G8R8_UNORM; - default: - return format; - } -} - [[nodiscard]] VkImageCreateInfo MakeImageCreateInfo(const Device& device, const ImageInfo& info) { - const PixelFormat format = StorageFormat(info.format); - const auto format_info = MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, format); + const auto format_info = + MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, info.format); VkImageCreateFlags flags{}; if (info.type == ImageType::e2D && info.resources.layers >= 6 && info.size.width == info.size.height && !device.HasBrokenCubeImageCompability()) { @@ -157,7 +147,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { .arrayLayers = static_cast<u32>(info.resources.layers), .samples = ConvertSampleCount(info.num_samples), .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = ImageUsageFlags(format_info, format), + .usage = ImageUsageFlags(format_info, info.format), .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .queueFamilyIndexCount = 0, .pQueueFamilyIndices = nullptr, @@ -186,6 +176,36 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { return allocator.CreateImage(image_ci); } +[[nodiscard]] vk::ImageView MakeStorageView(const vk::Device& device, u32 level, VkImage image, + VkFormat format) { + static constexpr VkImageViewUsageCreateInfo storage_image_view_usage_create_info{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, + .pNext = nullptr, + .usage = VK_IMAGE_USAGE_STORAGE_BIT, + }; + return device.CreateImageView(VkImageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = &storage_image_view_usage_create_info, + .flags = 0, + .image = image, + .viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY, + .format = format, + .components{ + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = level, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }, + }); +} + [[nodiscard]] VkImageAspectFlags ImageAspectMask(PixelFormat format) { switch (VideoCore::Surface::GetFormatType(format)) { case VideoCore::Surface::SurfaceType::ColorTexture: @@ -218,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; @@ -600,7 +621,7 @@ void CopyBufferToImage(vk::CommandBuffer cmdbuf, VkBuffer src_buffer, VkImage im } void TryTransformSwizzleIfNeeded(PixelFormat format, std::array<SwizzleSource, 4>& swizzle, - bool emulate_bgr565) { + bool emulate_bgr565, bool emulate_a4b4g4r4) { switch (format) { case PixelFormat::A1B5G5R5_UNORM: std::ranges::transform(swizzle, swizzle.begin(), SwapBlueRed); @@ -616,6 +637,11 @@ void TryTransformSwizzleIfNeeded(PixelFormat format, std::array<SwizzleSource, 4 case PixelFormat::G4R4_UNORM: std::ranges::transform(swizzle, swizzle.begin(), SwapGreenRed); break; + case PixelFormat::A4B4G4R4_UNORM: + if (emulate_a4b4g4r4) { + std::ranges::reverse(swizzle); + } + break; default: break; } @@ -822,6 +848,10 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue, memory_allocator); } + if (device.IsStorageImageMultisampleSupported()) { + msaa_copy_pass = std::make_unique<MSAACopyPass>( + device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue); + } if (!device.IsKhrImageFormatListSupported()) { return; } @@ -1044,15 +1074,27 @@ void TextureCacheRuntime::BlitImage(Framebuffer* dst_framebuffer, ImageView& dst dst_region, src_region, filter, operation); return; } + ASSERT(src.format == dst.format); if (aspect_mask == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) { - if (!device.IsBlitDepthStencilSupported()) { + const auto format = src.format; + const auto can_blit_depth_stencil = [this, format] { + switch (format) { + case VideoCore::Surface::PixelFormat::D24_UNORM_S8_UINT: + case VideoCore::Surface::PixelFormat::S8_UINT_D24_UNORM: + return device.IsBlitDepth24Stencil8Supported(); + case VideoCore::Surface::PixelFormat::D32_FLOAT_S8_UINT: + return device.IsBlitDepth32Stencil8Supported(); + default: + UNREACHABLE(); + } + }(); + if (!can_blit_depth_stencil) { UNIMPLEMENTED_IF(is_src_msaa || is_dst_msaa); blit_image_helper.BlitDepthStencil(dst_framebuffer, src.DepthView(), src.StencilView(), dst_region, src_region, filter, operation); return; } } - ASSERT(src.format == dst.format); ASSERT(!(is_dst_msaa && !is_src_msaa)); ASSERT(operation == Fermi2D::Operation::SrcCopy); @@ -1159,6 +1201,9 @@ void TextureCacheRuntime::ConvertImage(Framebuffer* dst, ImageView& dst_view, Im if (src_view.format == PixelFormat::D24_UNORM_S8_UINT) { return blit_image_helper.ConvertS8D24ToABGR8(dst, src_view); } + if (src_view.format == PixelFormat::D32_FLOAT) { + return blit_image_helper.ConvertD32FToABGR8(dst, src_view); + } break; case PixelFormat::R32_FLOAT: if (src_view.format == PixelFormat::D32_FLOAT) { @@ -1278,7 +1323,11 @@ void TextureCacheRuntime::CopyImage(Image& dst, Image& src, void TextureCacheRuntime::CopyImageMSAA(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies) { - UNIMPLEMENTED_MSG("Copying images with different samples is not implemented in Vulkan."); + const bool msaa_to_non_msaa = src.info.num_samples > 1 && dst.info.num_samples == 1; + if (msaa_copy_pass) { + return msaa_copy_pass->CopyImage(dst, src, copies, msaa_to_non_msaa); + } + UNIMPLEMENTED_MSG("Copying images with different samples is not supported."); } u64 TextureCacheRuntime::GetDeviceLocalMemory() const { @@ -1326,39 +1375,15 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu if (runtime->device.HasDebuggingToolAttached()) { original_image.SetObjectNameEXT(VideoCommon::Name(*this).c_str()); } - static constexpr VkImageViewUsageCreateInfo storage_image_view_usage_create_info{ - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, - .pNext = nullptr, - .usage = VK_IMAGE_USAGE_STORAGE_BIT, - }; current_image = *original_image; + storage_image_views.resize(info.resources.levels); if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported() && Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Uncompressed) { const auto& device = runtime->device.GetLogical(); - storage_image_views.reserve(info.resources.levels); for (s32 level = 0; level < info.resources.levels; ++level) { - storage_image_views.push_back(device.CreateImageView(VkImageViewCreateInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - .pNext = &storage_image_view_usage_create_info, - .flags = 0, - .image = *original_image, - .viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY, - .format = VK_FORMAT_A8B8G8R8_UNORM_PACK32, - .components{ - .r = VK_COMPONENT_SWIZZLE_IDENTITY, - .g = VK_COMPONENT_SWIZZLE_IDENTITY, - .b = VK_COMPONENT_SWIZZLE_IDENTITY, - .a = VK_COMPONENT_SWIZZLE_IDENTITY, - }, - .subresourceRange{ - .aspectMask = aspect_mask, - .baseMipLevel = static_cast<u32>(level), - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }, - })); + storage_image_views[level] = + MakeStorageView(device, level, *original_image, VK_FORMAT_A8B8G8R8_UNORM_PACK32); } } } @@ -1489,6 +1514,17 @@ void Image::DownloadMemory(const StagingBufferRef& map, std::span<const BufferIm DownloadMemory(buffers, offsets, copies); } +VkImageView Image::StorageImageView(s32 level) noexcept { + auto& view = storage_image_views[level]; + if (!view) { + const auto format_info = + MaxwellToVK::SurfaceFormat(runtime->device, FormatType::Optimal, true, info.format); + view = + MakeStorageView(runtime->device.GetLogical(), level, current_image, format_info.format); + } + return *view; +} + bool Image::IsRescaled() const noexcept { return True(flags & ImageFlagBits::Rescaled); } @@ -1626,8 +1662,8 @@ bool Image::NeedsScaleHelper() const { return true; } static constexpr auto OPTIMAL_FORMAT = FormatType::Optimal; - const PixelFormat format = StorageFormat(info.format); - const auto vk_format = MaxwellToVK::SurfaceFormat(device, OPTIMAL_FORMAT, false, format).format; + const auto vk_format = + MaxwellToVK::SurfaceFormat(device, OPTIMAL_FORMAT, false, info.format).format; const auto blit_usage = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; const bool needs_blit_helper = !device.IsFormatSupported(vk_format, blit_usage, OPTIMAL_FORMAT); return needs_blit_helper; @@ -1649,7 +1685,8 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI }; if (!info.IsRenderTarget()) { swizzle = info.Swizzle(); - TryTransformSwizzleIfNeeded(format, swizzle, device->MustEmulateBGR565()); + TryTransformSwizzleIfNeeded(format, swizzle, device->MustEmulateBGR565(), + !device->IsExt4444FormatsSupported()); if ((aspect_mask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0) { std::ranges::transform(swizzle, swizzle.begin(), ConvertGreenRed); } diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 565ce19a9..d6c5a15cc 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -117,6 +117,7 @@ public: BlitImageHelper& blit_image_helper; RenderPassCache& render_pass_cache; std::optional<ASTCDecoderPass> astc_decoder_pass; + std::unique_ptr<MSAACopyPass> msaa_copy_pass; const Settings::ResolutionScalingInfo& resolution; std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats; @@ -161,15 +162,13 @@ public: return aspect_mask; } - [[nodiscard]] VkImageView StorageImageView(s32 level) const noexcept { - return *storage_image_views[level]; - } - /// Returns true when the image is already initialized and mark it as initialized [[nodiscard]] bool ExchangeInitialization() noexcept { return std::exchange(initialized, true); } + VkImageView StorageImageView(s32 level) noexcept; + bool IsRescaled() const noexcept; bool ScaleUp(bool ignore = 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 11ced6c38..8c774f512 100644 --- a/src/video_core/texture_cache/format_lookup_table.cpp +++ b/src/video_core/texture_cache/format_lookup_table.cpp @@ -138,8 +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.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_base.h b/src/video_core/texture_cache/image_base.h index 55d49d017..0587d7b72 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -41,7 +41,7 @@ enum class ImageFlagBits : u32 { IsRescalable = 1 << 15, AsynchronousDecode = 1 << 16, - IsDecoding = 1 << 17, ///< Is currently being decoded asynchornously. + IsDecoding = 1 << 17, ///< Is currently being decoded asynchronously. }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) 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/util.cpp b/src/video_core/texture_cache/util.cpp index 0f8ef4277..2e8160db0 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -1194,7 +1194,7 @@ std::optional<SubresourceBase> FindSubresource(const ImageInfo& candidate, const return std::nullopt; } } else { - // Format comaptibility is not relaxed, ensure we are creating a view on a compatible format + // Format compatibility is not relaxed, ensure we are creating a view on a compatible format if (!IsViewCompatible(existing.format, candidate.format, broken_views, native_bgr)) { return std::nullopt; } diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 617417040..876cec2e8 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -76,12 +76,20 @@ constexpr std::array VK_FORMAT_R32G32B32_SFLOAT{ VK_FORMAT_UNDEFINED, }; +constexpr std::array VK_FORMAT_A4B4G4R4_UNORM_PACK16{ + VK_FORMAT_R4G4B4A4_UNORM_PACK16, + VK_FORMAT_UNDEFINED, +}; + } // namespace Alternatives enum class NvidiaArchitecture { - AmpereOrNewer, + KeplerOrOlder, + Maxwell, + Pascal, + Volta, Turing, - VoltaOrOlder, + AmpereOrNewer, }; template <typename T> @@ -110,6 +118,8 @@ constexpr const VkFormat* GetFormatAlternatives(VkFormat format) { return Alternatives::R8G8B8_SSCALED.data(); case VK_FORMAT_R32G32B32_SFLOAT: return Alternatives::VK_FORMAT_R32G32B32_SFLOAT.data(); + case VK_FORMAT_A4B4G4R4_UNORM_PACK16_EXT: + return Alternatives::VK_FORMAT_A4B4G4R4_UNORM_PACK16.data(); default: return nullptr; } @@ -193,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, @@ -238,6 +249,7 @@ std::unordered_map<VkFormat, VkFormatProperties> GetFormatProperties(vk::Physica VK_FORMAT_R32_SINT, VK_FORMAT_R32_UINT, VK_FORMAT_R4G4B4A4_UNORM_PACK16, + VK_FORMAT_A4B4G4R4_UNORM_PACK16_EXT, VK_FORMAT_R4G4_UNORM_PACK8, VK_FORMAT_R5G5B5A1_UNORM_PACK16, VK_FORMAT_R5G6B5_UNORM_PACK16, @@ -313,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( @@ -420,7 +457,8 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR first_next = &diagnostics_nv; } - is_blit_depth_stencil_supported = TestDepthStencilBlits(); + is_blit_depth24_stencil8_supported = TestDepthStencilBlits(VK_FORMAT_D24_UNORM_S8_UINT); + is_blit_depth32_stencil8_supported = TestDepthStencilBlits(VK_FORMAT_D32_SFLOAT_S8_UINT); is_optimal_astc_supported = ComputeIsOptimalAstcSupported(); is_warp_potentially_bigger = !extensions.subgroup_size_control || properties.subgroup_size_control.maxSubgroupSize > GuestWarpSize; @@ -495,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"); @@ -652,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"); @@ -774,14 +815,13 @@ bool Device::ComputeIsOptimalAstcSupported() const { return true; } -bool Device::TestDepthStencilBlits() const { +bool Device::TestDepthStencilBlits(VkFormat format) const { static constexpr VkFormatFeatureFlags required_features = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; const auto test_features = [](VkFormatProperties props) { return (props.optimalTilingFeatures & required_features) == required_features; }; - return test_features(format_properties.at(VK_FORMAT_D32_SFLOAT_S8_UINT)) && - test_features(format_properties.at(VK_FORMAT_D24_UNORM_S8_UINT)); + return test_features(format_properties.at(format)); } bool Device::IsFormatSupported(VkFormat wanted_format, VkFormatFeatureFlags wanted_usage, @@ -1051,6 +1091,13 @@ void Device::RemoveUnsuitableExtensions() { RemoveExtensionFeatureIfUnsuitable(extensions.custom_border_color, features.custom_border_color, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME); + // VK_EXT_depth_bias_control + extensions.depth_bias_control = + features.depth_bias_control.depthBiasControl && + features.depth_bias_control.leastRepresentableValueForceUnormRepresentation; + RemoveExtensionFeatureIfUnsuitable(extensions.depth_bias_control, features.depth_bias_control, + VK_EXT_DEPTH_BIAS_CONTROL_EXTENSION_NAME); + // VK_EXT_depth_clip_control extensions.depth_clip_control = features.depth_clip_control.depthClipControl; RemoveExtensionFeatureIfUnsuitable(extensions.depth_clip_control, features.depth_clip_control, diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 488fdd313..282a2925d 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -41,10 +41,12 @@ VK_DEFINE_HANDLE(VmaAllocator) // Define all features which may be used by the implementation and require an extension here. #define FOR_EACH_VK_FEATURE_EXT(FEATURE) \ FEATURE(EXT, CustomBorderColor, CUSTOM_BORDER_COLOR, custom_border_color) \ + FEATURE(EXT, DepthBiasControl, DEPTH_BIAS_CONTROL, depth_bias_control) \ FEATURE(EXT, DepthClipControl, DEPTH_CLIP_CONTROL, depth_clip_control) \ FEATURE(EXT, ExtendedDynamicState, EXTENDED_DYNAMIC_STATE, extended_dynamic_state) \ FEATURE(EXT, ExtendedDynamicState2, EXTENDED_DYNAMIC_STATE_2, extended_dynamic_state2) \ FEATURE(EXT, ExtendedDynamicState3, EXTENDED_DYNAMIC_STATE_3, extended_dynamic_state3) \ + FEATURE(EXT, 4444Formats, 4444_FORMATS, format_a4b4g4r4) \ FEATURE(EXT, IndexTypeUint8, INDEX_TYPE_UINT8, index_type_uint8) \ FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \ FEATURE(EXT, PrimitiveTopologyListRestart, PRIMITIVE_TOPOLOGY_LIST_RESTART, \ @@ -60,6 +62,7 @@ VK_DEFINE_HANDLE(VmaAllocator) // Define miscellaneous extensions which may be used by the implementation here. #define FOR_EACH_VK_EXTENSION(EXTENSION) \ + EXTENSION(EXT, CONDITIONAL_RENDERING, conditional_rendering) \ EXTENSION(EXT, CONSERVATIVE_RASTERIZATION, conservative_rasterization) \ EXTENSION(EXT, DEPTH_RANGE_UNRESTRICTED, depth_range_unrestricted) \ EXTENSION(EXT, MEMORY_BUDGET, memory_budget) \ @@ -92,11 +95,14 @@ VK_DEFINE_HANDLE(VmaAllocator) // Define extensions where the absence of the extension may result in a degraded experience. #define FOR_EACH_VK_RECOMMENDED_EXTENSION(EXTENSION_NAME) \ + EXTENSION_NAME(VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME) \ + EXTENSION_NAME(VK_EXT_DEPTH_BIAS_CONTROL_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME) \ + EXTENSION_NAME(VK_EXT_4444_FORMATS_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_LINE_RASTERIZATION_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME) \ @@ -143,7 +149,11 @@ VK_DEFINE_HANDLE(VmaAllocator) // Define features where the absence of the feature may result in a degraded experience. #define FOR_EACH_VK_RECOMMENDED_FEATURE(FEATURE_NAME) \ FEATURE_NAME(custom_border_color, customBorderColors) \ + FEATURE_NAME(depth_bias_control, depthBiasControl) \ + FEATURE_NAME(depth_bias_control, leastRepresentableValueForceUnormRepresentation) \ + FEATURE_NAME(depth_bias_control, depthBiasExact) \ FEATURE_NAME(extended_dynamic_state, extendedDynamicState) \ + FEATURE_NAME(format_a4b4g4r4, formatA4B4G4R4) \ FEATURE_NAME(index_type_uint8, indexTypeUint8) \ FEATURE_NAME(primitive_topology_list_restart, primitiveTopologyListRestart) \ FEATURE_NAME(provoking_vertex, provokingVertexLast) \ @@ -304,7 +314,7 @@ public: return GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY; } - /// Returns true if the device suppors float64 natively. + /// Returns true if the device supports float64 natively. bool IsFloat64Supported() const { return features.features.shaderFloat64; } @@ -319,6 +329,11 @@ public: return features.shader_float16_int8.shaderInt8; } + /// Returns true if the device supports binding multisample images as storage images. + bool IsStorageImageMultisampleSupported() const { + return features.features.shaderStorageImageMultisample; + } + /// Returns true if the device warp size can potentially be bigger than guest's warp size. bool IsWarpSizePotentiallyBiggerThanGuest() const { return is_warp_potentially_bigger; @@ -359,9 +374,14 @@ public: return features.features.depthBounds; } - /// Returns true when blitting from and to depth stencil images is supported. - bool IsBlitDepthStencilSupported() const { - return is_blit_depth_stencil_supported; + /// Returns true when blitting from and to D24S8 images is supported. + bool IsBlitDepth24Stencil8Supported() const { + return is_blit_depth24_stencil8_supported; + } + + /// Returns true when blitting from and to D32S8 images is supported. + bool IsBlitDepth32Stencil8Supported() const { + return is_blit_depth32_stencil8_supported; } /// Returns true if the device supports VK_NV_viewport_swizzle. @@ -449,6 +469,11 @@ public: return extensions.depth_clip_control; } + /// Returns true if the device supports VK_EXT_depth_bias_control. + bool IsExtDepthBiasControlSupported() const { + return extensions.depth_bias_control; + } + /// Returns true if the device supports VK_EXT_shader_viewport_index_layer. bool IsExtShaderViewportIndexLayerSupported() const { return extensions.shader_viewport_index_layer; @@ -488,6 +513,11 @@ public: return extensions.extended_dynamic_state3; } + /// Returns true if the device supports VK_EXT_4444_formats. + bool IsExt4444FormatsSupported() const { + return features.format_a4b4g4r4.formatA4B4G4R4; + } + /// Returns true if the device supports VK_EXT_extended_dynamic_state3. bool IsExtExtendedDynamicState3BlendingSupported() const { return dynamic_state3_blending; @@ -528,6 +558,10 @@ public: return extensions.shader_atomic_int64; } + bool IsExtConditionalRendering() const { + return extensions.conditional_rendering; + } + bool HasTimelineSemaphore() const; /// Returns the minimum supported version of SPIR-V. @@ -600,6 +634,10 @@ public: return features.robustness2.nullDescriptor; } + bool HasExactDepthBiasControl() const { + return features.depth_bias_control.depthBiasExact; + } + u32 GetMaxVertexInputAttributes() const { return properties.properties.limits.maxVertexInputAttributes; } @@ -666,7 +704,7 @@ private: bool ComputeIsOptimalAstcSupported() const; /// Returns true if the device natively supports blitting depth stencil images. - bool TestDepthStencilBlits() const; + bool TestDepthStencilBlits(VkFormat format) const; private: VkInstance instance; ///< Vulkan instance. @@ -730,25 +768,26 @@ private: VkPhysicalDeviceProperties2 properties2{}; // Misc features - bool is_optimal_astc_supported{}; ///< Support for all guest ASTC formats. - bool is_blit_depth_stencil_supported{}; ///< Support for blitting from and to depth stencil. - bool is_warp_potentially_bigger{}; ///< Host warp size can be bigger than guest. - bool is_integrated{}; ///< Is GPU an iGPU. - bool is_virtual{}; ///< Is GPU a virtual GPU. - bool is_non_gpu{}; ///< Is SoftwareRasterizer, FPGA, non-GPU device. - bool has_broken_compute{}; ///< Compute shaders can cause crashes - bool has_broken_cube_compatibility{}; ///< Has broken cube compatibility bit - bool has_renderdoc{}; ///< Has RenderDoc attached - bool has_nsight_graphics{}; ///< Has Nsight Graphics attached - bool supports_d24_depth{}; ///< Supports D24 depth buffers. - bool cant_blit_msaa{}; ///< Does not support MSAA<->MSAA blitting. - bool must_emulate_scaled_formats{}; ///< Requires scaled vertex format emulation - bool must_emulate_bgr565{}; ///< Emulates BGR565 by swizzling RGB565 format. - bool dynamic_state3_blending{}; ///< Has all blending features of dynamic_state3. - bool dynamic_state3_enables{}; ///< Has all enables features of dynamic_state3. - bool supports_conditional_barriers{}; ///< Allows barriers in conditional control flow. - u64 device_access_memory{}; ///< Total size of device local memory in bytes. - u32 sets_per_pool{}; ///< Sets per Description Pool + bool is_optimal_astc_supported{}; ///< Support for all guest ASTC formats. + bool is_blit_depth24_stencil8_supported{}; ///< Support for blitting from and to D24S8. + bool is_blit_depth32_stencil8_supported{}; ///< Support for blitting from and to D32S8. + bool is_warp_potentially_bigger{}; ///< Host warp size can be bigger than guest. + bool is_integrated{}; ///< Is GPU an iGPU. + bool is_virtual{}; ///< Is GPU a virtual GPU. + bool is_non_gpu{}; ///< Is SoftwareRasterizer, FPGA, non-GPU device. + bool has_broken_compute{}; ///< Compute shaders can cause crashes + bool has_broken_cube_compatibility{}; ///< Has broken cube compatibility bit + bool has_renderdoc{}; ///< Has RenderDoc attached + bool has_nsight_graphics{}; ///< Has Nsight Graphics attached + bool supports_d24_depth{}; ///< Supports D24 depth buffers. + bool cant_blit_msaa{}; ///< Does not support MSAA<->MSAA blitting. + bool must_emulate_scaled_formats{}; ///< Requires scaled vertex format emulation + bool must_emulate_bgr565{}; ///< Emulates BGR565 by swizzling RGB565 format. + bool dynamic_state3_blending{}; ///< Has all blending features of dynamic_state3. + bool dynamic_state3_enables{}; ///< Has all enables features of dynamic_state3. + bool supports_conditional_barriers{}; ///< Allows barriers in conditional control flow. + u64 device_access_memory{}; ///< Total size of device local memory in bytes. + u32 sets_per_pool{}; ///< Sets per Description Pool // Telemetry parameters std::set<std::string, std::less<>> supported_extensions; ///< Reported Vulkan extensions. diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 3ef381a38..82767fdf0 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" @@ -69,8 +70,7 @@ struct Range { case MemoryUsage::Download: return 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 +212,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; @@ -244,7 +257,7 @@ vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo& ci, MemoryUsa .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.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index c3f388d89..2f3254a97 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -75,6 +75,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkBeginCommandBuffer); X(vkBindBufferMemory); X(vkBindImageMemory); + X(vkCmdBeginConditionalRenderingEXT); X(vkCmdBeginQuery); X(vkCmdBeginRenderPass); X(vkCmdBeginTransformFeedbackEXT); @@ -91,6 +92,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkCmdCopyBufferToImage); X(vkCmdCopyImage); X(vkCmdCopyImageToBuffer); + X(vkCmdCopyQueryPoolResults); X(vkCmdDispatch); X(vkCmdDispatchIndirect); X(vkCmdDraw); @@ -99,6 +101,8 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkCmdDrawIndexedIndirect); X(vkCmdDrawIndirectCount); X(vkCmdDrawIndexedIndirectCount); + X(vkCmdDrawIndirectByteCountEXT); + X(vkCmdEndConditionalRenderingEXT); X(vkCmdEndQuery); X(vkCmdEndRenderPass); X(vkCmdEndTransformFeedbackEXT); @@ -109,6 +113,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkCmdPushDescriptorSetWithTemplateKHR); X(vkCmdSetBlendConstants); X(vkCmdSetDepthBias); + X(vkCmdSetDepthBias2EXT); X(vkCmdSetDepthBounds); X(vkCmdSetEvent); X(vkCmdSetScissor); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 049fa8038..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; @@ -185,6 +188,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkBeginCommandBuffer vkBeginCommandBuffer{}; PFN_vkBindBufferMemory vkBindBufferMemory{}; PFN_vkBindImageMemory vkBindImageMemory{}; + PFN_vkCmdBeginConditionalRenderingEXT vkCmdBeginConditionalRenderingEXT{}; PFN_vkCmdBeginDebugUtilsLabelEXT vkCmdBeginDebugUtilsLabelEXT{}; PFN_vkCmdBeginQuery vkCmdBeginQuery{}; PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass{}; @@ -202,6 +206,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage{}; PFN_vkCmdCopyImage vkCmdCopyImage{}; PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer{}; + PFN_vkCmdCopyQueryPoolResults vkCmdCopyQueryPoolResults{}; PFN_vkCmdDispatch vkCmdDispatch{}; PFN_vkCmdDispatchIndirect vkCmdDispatchIndirect{}; PFN_vkCmdDraw vkCmdDraw{}; @@ -210,6 +215,8 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkCmdDrawIndexedIndirect vkCmdDrawIndexedIndirect{}; PFN_vkCmdDrawIndirectCount vkCmdDrawIndirectCount{}; PFN_vkCmdDrawIndexedIndirectCount vkCmdDrawIndexedIndirectCount{}; + PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT{}; + PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT{}; PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT{}; PFN_vkCmdEndQuery vkCmdEndQuery{}; PFN_vkCmdEndRenderPass vkCmdEndRenderPass{}; @@ -222,6 +229,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkCmdSetBlendConstants vkCmdSetBlendConstants{}; PFN_vkCmdSetCullModeEXT vkCmdSetCullModeEXT{}; PFN_vkCmdSetDepthBias vkCmdSetDepthBias{}; + PFN_vkCmdSetDepthBias2EXT vkCmdSetDepthBias2EXT{}; PFN_vkCmdSetDepthBounds vkCmdSetDepthBounds{}; PFN_vkCmdSetDepthBoundsTestEnableEXT vkCmdSetDepthBoundsTestEnableEXT{}; PFN_vkCmdSetDepthCompareOpEXT vkCmdSetDepthCompareOpEXT{}; @@ -1182,6 +1190,13 @@ public: count_offset, draw_count, stride); } + void DrawIndirectByteCountEXT(u32 instance_count, u32 first_instance, VkBuffer counter_buffer, + VkDeviceSize counter_buffer_offset, u32 counter_offset, + u32 stride) { + dld->vkCmdDrawIndirectByteCountEXT(handle, instance_count, first_instance, counter_buffer, + counter_buffer_offset, counter_offset, stride); + } + void ClearAttachments(Span<VkClearAttachment> attachments, Span<VkClearRect> rects) const noexcept { dld->vkCmdClearAttachments(handle, attachments.size(), attachments.data(), rects.size(), @@ -1270,6 +1285,13 @@ public: regions.data()); } + void CopyQueryPoolResults(VkQueryPool query_pool, u32 first_query, u32 query_count, + VkBuffer dst_buffer, VkDeviceSize dst_offset, VkDeviceSize stride, + VkQueryResultFlags flags) const noexcept { + dld->vkCmdCopyQueryPoolResults(handle, query_pool, first_query, query_count, dst_buffer, + dst_offset, stride, flags); + } + void FillBuffer(VkBuffer dst_buffer, VkDeviceSize dst_offset, VkDeviceSize size, u32 data) const noexcept { dld->vkCmdFillBuffer(handle, dst_buffer, dst_offset, size, data); @@ -1315,6 +1337,18 @@ public: dld->vkCmdSetDepthBias(handle, constant_factor, clamp, slope_factor); } + void SetDepthBias(float constant_factor, float clamp, float slope_factor, + VkDepthBiasRepresentationInfoEXT* extra) const noexcept { + VkDepthBiasInfoEXT info{ + .sType = VK_STRUCTURE_TYPE_DEPTH_BIAS_INFO_EXT, + .pNext = extra, + .depthBiasConstantFactor = constant_factor, + .depthBiasClamp = clamp, + .depthBiasSlopeFactor = slope_factor, + }; + dld->vkCmdSetDepthBias2EXT(handle, &info); + } + void SetDepthBounds(float min_depth_bounds, float max_depth_bounds) const noexcept { dld->vkCmdSetDepthBounds(handle, min_depth_bounds, max_depth_bounds); } @@ -1448,6 +1482,15 @@ public: counter_buffers, counter_buffer_offsets); } + void BeginConditionalRenderingEXT( + const VkConditionalRenderingBeginInfoEXT& info) const noexcept { + dld->vkCmdBeginConditionalRenderingEXT(handle, &info); + } + + void EndConditionalRenderingEXT() const noexcept { + dld->vkCmdEndConditionalRenderingEXT(handle); + } + void BeginDebugUtilsLabelEXT(const char* label, std::span<float, 4> color) const noexcept { const VkDebugUtilsLabelEXT label_info{ .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT, diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 8f86a1553..9ebece907 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 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_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..a4e8af1b4 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", ""); diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index f254c1e1c..74f48031a 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,6 +824,7 @@ 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); // Delete any rows that might already exist if we're repopulating item_model->removeRows(0, item_model->rowCount()); @@ -825,7 +833,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { 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..588f1dd6e 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); } } @@ -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) { diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 24a4e92c3..2bb0a0cb6 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -13,6 +13,7 @@ #include <QString> #include "yuzu/compatibility_list.h" +#include "yuzu/play_time_manager.h" namespace Core { class System; @@ -36,7 +37,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,6 +79,7 @@ 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; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index d32aa9615..5427758c1 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -18,6 +18,8 @@ #include <sys/socket.h> #endif +#include <boost/container/flat_set.hpp> + // VFS includes must be before glad as they will conflict with Windows file api, which uses defines. #include "applets/qt_amiibo_settings.h" #include "applets/qt_controller.h" @@ -96,6 +98,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 @@ -148,6 +151,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" @@ -336,6 +340,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(); @@ -984,7 +990,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); @@ -1445,6 +1451,7 @@ void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { Settings::values.audio_muted = false; auto_muted = false; } + UpdateVolumeUI(); } } @@ -1458,6 +1465,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); @@ -1551,6 +1560,16 @@ 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, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartGameDataEraser); }); + connect_menu(ui->action_Load_Cabinet_Restorer, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartRestorer); }); + connect_menu(ui->action_Load_Cabinet_Formatter, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartFormatter); }); + connect_menu(ui->action_Load_Mii_Edit, &GMainWindow::OnMiiEdit); connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); // TAS @@ -1567,6 +1586,7 @@ void GMainWindow::ConnectMenuEvents() { void GMainWindow::UpdateMenuState() { const bool is_paused = emu_thread == nullptr || !emu_thread->IsRunning(); + const bool is_firmware_available = CheckFirmwarePresence(); const std::array running_actions{ ui->action_Stop, @@ -1577,10 +1597,23 @@ void GMainWindow::UpdateMenuState() { ui->action_Pause, }; + 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, + ui->action_Load_Cabinet_Formatter, + ui->action_Load_Mii_Edit, + }; + for (QAction* action : running_actions) { action->setEnabled(emulation_running); } + for (QAction* action : applet_actions) { + action->setEnabled(is_firmware_available && !emulation_running); + } + ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused); if (emulation_running && is_paused) { @@ -2100,6 +2133,8 @@ void GMainWindow::OnEmulationStopped() { OnTasStateChanged(); render_window->FinalizeCamera(); + system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::None); + // Enable all controllers system->HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All}); @@ -2508,6 +2543,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) { @@ -2799,7 +2845,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; @@ -2822,12 +2867,14 @@ 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__) +#if defined(WIN32) + const char* home = std::getenv("USERPROFILE"); +#else const char* home = std::getenv("HOME"); +#endif const std::filesystem::path home_path = (home == nullptr ? "~" : home); const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); @@ -2837,7 +2884,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga QMessageBox::critical( this, tr("Create Shortcut"), tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") - .arg(QString::fromStdString(target_directory)), + .arg(QString::fromStdString(target_directory.generic_string())), QMessageBox::StandardButton::Ok); return; } @@ -2845,15 +2892,15 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga 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); + 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.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 @@ -2875,9 +2922,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 @@ -2902,29 +2954,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"), @@ -3110,10 +3170,9 @@ void GMainWindow::OnMenuInstallToNAND() { QFuture<InstallResult> future; InstallResult result; - if (file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) || - file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { + if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { - future = QtConcurrent::run([this, &file] { return InstallNSPXCI(file); }); + future = QtConcurrent::run([this, &file] { return InstallNSP(file); }); while (!future.isFinished()) { QCoreApplication::processEvents(); @@ -3172,7 +3231,7 @@ void GMainWindow::OnMenuInstallToNAND() { ui->action_Install_File_NAND->setEnabled(true); } -InstallResult GMainWindow::InstallNSPXCI(const QString& filename) { +InstallResult GMainWindow::InstallNSP(const QString& filename) { const auto qt_raw_copy = [this](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, std::size_t block_size) { if (src == nullptr || dest == nullptr) { @@ -3206,9 +3265,7 @@ InstallResult GMainWindow::InstallNSPXCI(const QString& filename) { return InstallResult::Failure; } } else { - const auto xci = std::make_shared<FileSys::XCI>( - vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); - nsp = xci->GetSecurePartitionNSP(); + return InstallResult::Failure; } if (nsp->GetStatus() != Loader::ResultStatus::Success) { @@ -3334,6 +3391,9 @@ void GMainWindow::OnStartGame() { UpdateMenuState(); OnTasStateChanged(); + play_time_manager->SetProgramId(system->GetApplicationProcessProgramID()); + play_time_manager->Start(); + discord_rpc->Update(); } @@ -3349,6 +3409,7 @@ void GMainWindow::OnRestartGame() { void GMainWindow::OnPauseGame() { emu_thread->SetRunning(false); + play_time_manager->Stop(); UpdateMenuState(); AllowOSSleep(); } @@ -3369,6 +3430,9 @@ void GMainWindow::OnStopGame() { return; } + play_time_manager->Stop(); + // Update game list to show new play time + game_list->PopulateAsync(UISettings::values.game_dirs); if (OnShutdownBegin()) { OnShutdownBeginDialog(); } else { @@ -3942,6 +4006,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; } @@ -4134,6 +4226,76 @@ 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(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install the firmware to use the Cabinet applet.")); + return; + } + + auto cabinet_nca = bis_system->GetEntry(CabinetId, FileSys::ContentRecordType::Program); + if (!cabinet_nca) { + QMessageBox::warning(this, tr("Cabinet Applet"), + tr("Cabinet applet is not available. Please reinstall firmware.")); + return; + } + + system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::Cabinet); + system->GetAppletManager().SetCabinetMode(mode); + + const auto filename = QString::fromStdString(cabinet_nca->GetFullPath()); + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMiiEdit() { + constexpr u64 MiiEditId = 0x0100000000001009ull; + auto bis_system = system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install the firmware to use the Mii editor.")); + return; + } + + auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); + if (!mii_applet_nca) { + QMessageBox::warning(this, tr("Mii Edit Applet"), + tr("Mii editor is not available. Please reinstall firmware.")); + return; + } + + system->GetAppletManager().SetCurrentAppletId(Service::AM::Applets::AppletId::MiiEdit); + + const auto filename = QString::fromStdString((mii_applet_nca->GetFullPath())); + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + void GMainWindow::OnCaptureScreenshot() { if (emu_thread == nullptr || !emu_thread->IsRunning()) { return; @@ -4540,6 +4702,8 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { if (behavior == ReinitializeKeyBehavior::Warning) { game_list->PopulateAsync(UISettings::values.game_dirs); } + + UpdateMenuState(); } bool GMainWindow::CheckSystemArchiveDecryption() { @@ -4561,10 +4725,26 @@ bool GMainWindow::CheckSystemArchiveDecryption() { return mii_nca->GetRomFS().get() != nullptr; } +bool GMainWindow::CheckFirmwarePresence() { + constexpr u64 MiiEditId = 0x0100000000001009ull; + + auto bis_system = system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + return false; + } + + auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); + if (!mii_applet_nca) { + return false; + } + + return true; +} + bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id, u64* selected_title_id, u8* selected_content_record_type) { - using ContentInfo = std::pair<FileSys::TitleType, FileSys::ContentRecordType>; - boost::container::flat_map<u64, ContentInfo> available_title_ids; + using ContentInfo = std::tuple<u64, FileSys::TitleType, FileSys::ContentRecordType>; + boost::container::flat_set<ContentInfo> available_title_ids; const auto RetrieveEntries = [&](FileSys::TitleType title_type, FileSys::ContentRecordType record_type) { @@ -4572,12 +4752,14 @@ bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installe for (const auto& entry : entries) { if (FileSys::GetBaseTitleID(entry.title_id) == program_id && installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success) { - available_title_ids[entry.title_id] = {title_type, record_type}; + available_title_ids.insert({entry.title_id, title_type, record_type}); } } }; RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::Program); + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::HtmlDocument); + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::LegalInformation); RetrieveEntries(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); if (available_title_ids.empty()) { @@ -4588,10 +4770,14 @@ bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installe if (available_title_ids.size() > 1) { QStringList list; - for (auto& [title_id, content_info] : available_title_ids) { + for (auto& [title_id, title_type, record_type] : available_title_ids) { const auto hex_title_id = QString::fromStdString(fmt::format("{:X}", title_id)); - if (content_info.first == FileSys::TitleType::Application) { - list.push_back(QStringLiteral("Application [%1]").arg(hex_title_id)); + if (record_type == FileSys::ContentRecordType::Program) { + list.push_back(QStringLiteral("Program [%1]").arg(hex_title_id)); + } else if (record_type == FileSys::ContentRecordType::HtmlDocument) { + list.push_back(QStringLiteral("HTML document [%1]").arg(hex_title_id)); + } else if (record_type == FileSys::ContentRecordType::LegalInformation) { + list.push_back(QStringLiteral("Legal information [%1]").arg(hex_title_id)); } else { list.push_back( QStringLiteral("DLC %1 [%2]").arg(title_id & 0x7FF).arg(hex_title_id)); @@ -4609,9 +4795,9 @@ bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installe title_index = list.indexOf(res); } - const auto selected_info = available_title_ids.nth(title_index); - *selected_title_id = selected_info->first; - *selected_content_record_type = static_cast<u8>(selected_info->second.second); + const auto& [title_id, title_type, record_type] = *available_title_ids.nth(title_index); + *selected_title_id = title_id; + *selected_content_record_type = static_cast<u8>(record_type); return true; } diff --git a/src/yuzu/main.h b/src/yuzu/main.h index cf191f698..2346eb3bd 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -81,6 +81,10 @@ namespace DiscordRPC { class DiscordInterface; } +namespace PlayTime { +class PlayTimeManager; +} + namespace FileSys { class ContentProvider; class ManualContentProvider; @@ -102,6 +106,10 @@ namespace Service::NFC { class NfcDevice; } // namespace Service::NFC +namespace Service::NFP { +enum class CabinetMode : u8; +} // namespace Service::NFP + namespace Ui { class MainWindow; } @@ -319,6 +327,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); @@ -365,6 +374,9 @@ private slots: void ResetWindowSize720(); void ResetWindowSize900(); void ResetWindowSize1080(); + void OnAlbum(); + void OnCabinet(Service::NFP::CabinetMode mode); + void OnMiiEdit(); void OnCaptureScreenshot(); void OnReinitializeKeys(ReinitializeKeyBehavior behavior); void OnLanguageChanged(const QString& locale); @@ -383,10 +395,11 @@ 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); - InstallResult InstallNSPXCI(const QString& filename); + InstallResult InstallNSP(const QString& filename); InstallResult InstallNCA(const QString& filename); void MigrateConfigFiles(); void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, @@ -409,6 +422,7 @@ private: void OpenPerGameConfiguration(u64 title_id, const std::string& file_name); bool CheckDarkMode(); bool CheckSystemArchiveDecryption(); + bool CheckFirmwarePresence(); void ConfigureFilesystemProvider(const std::string& filepath); QString GetTasStateDescription() const; @@ -421,6 +435,7 @@ private: 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 e54d7d75d..88684ffb5 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -137,6 +137,15 @@ <property name="title"> <string>&Tools</string> </property> + <widget class="QMenu" name="menu_cabinet_applet"> + <property name="title"> + <string>&Amiibo</string> + </property> + <addaction name="action_Load_Cabinet_Nickname_Owner"/> + <addaction name="action_Load_Cabinet_Eraser"/> + <addaction name="action_Load_Cabinet_Restorer"/> + <addaction name="action_Load_Cabinet_Formatter"/> + </widget> <widget class="QMenu" name="menuTAS"> <property name="title"> <string>&TAS</string> @@ -150,6 +159,10 @@ <addaction name="action_Rederive"/> <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"/> <addaction name="menuTAS"/> </widget> @@ -217,7 +230,7 @@ </action> <action name="action_Verify_installed_contents"> <property name="text"> - <string>Verify installed contents</string> + <string>&Verify Installed Contents</string> </property> </action> <action name="action_About"> @@ -368,6 +381,36 @@ <string>&Capture Screenshot</string> </property> </action> + <action name="action_Load_Album"> + <property name="text"> + <string>Open &Album</string> + </property> + </action> + <action name="action_Load_Cabinet_Nickname_Owner"> + <property name="text"> + <string>&Set Nickname and Owner</string> + </property> + </action> + <action name="action_Load_Cabinet_Eraser"> + <property name="text"> + <string>&Delete Game Data</string> + </property> + </action> + <action name="action_Load_Cabinet_Restorer"> + <property name="text"> + <string>&Restore Amiibo</string> + </property> + </action> + <action name="action_Load_Cabinet_Formatter"> + <property name="text"> + <string>&Format Amiibo</string> + </property> + </action> + <action name="action_Load_Mii_Edit"> + <property name="text"> + <string>Open &Mii Editor</string> + </property> + </action> <action name="action_Configure_Tas"> <property name="text"> <string>&Configure TAS...</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..975008159 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -103,7 +103,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 +183,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..61cf00176 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,76 @@ 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) + + QImage source_image = image.convertToFormat(QImage::Format_RGB32); + constexpr int bytes_per_pixel = 4; + const int image_size = source_image.width() * source_image.height() * bytes_per_pixel; + + BITMAPINFOHEADER info_header{}; + info_header.biSize = sizeof(BITMAPINFOHEADER), info_header.biWidth = source_image.width(), + info_header.biHeight = source_image.height() * 2, info_header.biPlanes = 1, + info_header.biBitCount = bytes_per_pixel * 8, info_header.biCompression = BI_RGB; + + const IconDir icon_dir{.id_reserved = 0, .id_type = 1, .id_count = 1}; + const IconDirEntry icon_entry{.width = static_cast<BYTE>(source_image.width()), + .height = static_cast<BYTE>(source_image.height() * 2), + .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 = sizeof(IconDir) + sizeof(IconDirEntry)}; + + 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; + } + if (!icon_file.Write(icon_entry)) { + return false; + } + if (!icon_file.Write(info_header)) { + return false; + } + + for (int y = 0; y < image.height(); y++) { + const auto* line = source_image.scanLine(source_image.height() - 1 - y); + std::vector<u8> line_data(source_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); |