diff options
31 files changed, 575 insertions, 187 deletions
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/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/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index 2ff827c6b..7b8f99872 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt @@ -26,6 +26,7 @@ import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.ui.main.MainActivity class AboutFragment : Fragment() { private var _binding: FragmentAboutBinding? = null @@ -92,6 +93,12 @@ class AboutFragment : Fragment() { } } + val mainActivity = requireActivity() as MainActivity + binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") } + binding.buttonImport.setOnClickListener { + mainActivity.importUserData.launch(arrayOf("application/zip")) + } + binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt index f38aeea53..ee2d44718 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt @@ -187,6 +187,7 @@ class ImportExportSavesFragment : DialogFragment() { withContext(Dispatchers.Main) { if (!validZip) { MessageDialogFragment.newInstance( + requireActivity(), titleId = R.string.save_file_invalid_zip_structure, descriptionId = R.string.save_file_invalid_zip_structure_description ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) 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..0d16a7d37 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 @@ -4,6 +4,7 @@ package org.yuzu.yuzu_emu.fragments import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -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,27 @@ 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) { _: DialogInterface, _: Int -> + taskViewModel.setCancelled(true) + } + } + + val alertDialog = dialog.create() + alertDialog.setCanceledOnTouchOutside(false) if (!taskViewModel.isRunning.value) { taskViewModel.runTask() } - return dialog + return alertDialog } override fun onCreateView( @@ -53,21 +63,35 @@ 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() + 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 - ) + is MessageDialogFragment -> result.show( + requireActivity().supportFragmentManager, + MessageDialogFragment.TAG + ) + } + taskViewModel.clear() + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.cancelled.collect { + if (it) { + dialog?.setTitle(R.string.cancelling) } - taskViewModel.clear() } } } @@ -78,16 +102,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/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/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..d6418a666 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() { 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..6fa847631 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 @@ -46,13 +46,22 @@ import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream 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 @@ -307,6 +316,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 +346,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 @@ -376,6 +387,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { 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 +407,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { IndeterminateProgressDialogFragment.newInstance( this, R.string.firmware_installing, - task + task = task ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } @@ -407,6 +419,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 +447,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 @@ -583,12 +597,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 +612,119 @@ 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 zos = ZipOutputStream( + BufferedOutputStream(contentResolver.openOutputStream(result)) + ) + zos.use { stream -> + File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file -> + if (taskViewModel.cancelled.value) { + return@newInstance R.string.user_data_export_cancelled + } + + if (!file.isDirectory) { + val newPath = file.path.substring( + DirectoryInitialization.userDirectory!!.length, + file.path.length + ) + stream.putNextEntry(ZipEntry(newPath)) + + val buffer = ByteArray(8096) + var read: Int + FileInputStream(file).use { fis -> + while (fis.read(buffer).also { read = it } != -1) { + stream.write(buffer, 0, read) + } + } + + stream.closeEntry() + } + } + } + return@newInstance getString(R.string.user_data_export_success) + }.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 getString(R.string.invalid_yuzu_backup) + } + + File(DirectoryInitialization.userDirectory!!).deleteRecursively() + + val zis = + ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) + val userDirectory = File(DirectoryInitialization.userDirectory!!) + val canonicalPath = userDirectory.canonicalPath + '/' + zis.use { stream -> + var ze: ZipEntry? = stream.nextEntry + while (ze != null) { + val newFile = File(userDirectory, ze!!.name) + val destinationDirectory = + if (ze!!.isDirectory) newFile else newFile.parentFile + + if (!newFile.canonicalPath.startsWith(canonicalPath)) { + throw SecurityException( + "Zip file attempted path traversal! ${ze!!.name}" + ) + } + + if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) { + throw IOException("Failed to create directory $destinationDirectory") + } + + if (!ze!!.isDirectory) { + val buffer = ByteArray(8096) + var read: Int + BufferedOutputStream(FileOutputStream(newFile)).use { bos -> + while (zis.read(buffer).also { read = it } != -1) { + bos.write(buffer, 0, read) + } + } + } + ze = stream.nextEntry + } + } + + // Reinitialize relevant data + NativeLibrary.initializeEmulation() + gamesViewModel.reloadGames(false) + + return@newInstance getString(R.string.user_data_import_success) + }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + } } 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/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_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml index 3e1d98451..36b350338 100644 --- a/src/android/app/src/main/res/layout/fragment_about.xml +++ b/src/android/app/src/main/res/layout/fragment_about.xml @@ -184,6 +184,67 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" + android:orientation="horizontal"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="16dp" + android:paddingHorizontal="16dp" + android:orientation="vertical" + android:layout_weight="1"> + + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.Material3.TitleMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:textAlignment="viewStart" + android:text="@string/user_data" /> + + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.Material3.BodyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="6dp" + android:textAlignment="viewStart" + android:text="@string/user_data_description" /> + + </LinearLayout> + + <Button + android:id="@+id/button_import" + 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/string_import" + android:tooltipText="@string/string_import" + app:icon="@drawable/ic_import" /> + + <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_marginStart="12dp" + android:layout_marginEnd="24dp" + android:layout_gravity="center_vertical" + android:contentDescription="@string/export" + android:tooltipText="@string/export" + app:icon="@drawable/ic_export" /> + + </LinearLayout> + + <com.google.android.material.divider.MaterialDivider + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="20dp" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_horizontal" android:layout_marginTop="12dp" diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index b163e6fc1..0730143bd 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -128,6 +128,15 @@ <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="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 +224,9 @@ <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="cancelling">Cancelling</string> <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> 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/hle/service/am/applets/applet_mii_edit.cpp b/src/core/hle/service/am/applets/applet_mii_edit.cpp index 350a90818..ff77830d2 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{char_info}, + }; + MiiEditOutputForCharInfoEditing(MiiEditResult::Success, edit_char_info); + break; + } + case MiiEditAppletMode::EditMii: { 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{applet_input_v4.char_info.mii_info}, }; MiiEditOutputForCharInfoEditing(MiiEditResult::Success, edit_char_info); 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/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp index 8de806cfb..c28eed926 100644 --- a/src/core/hle/service/mii/mii.cpp +++ b/src/core/hle/service/mii/mii.cpp @@ -18,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"}, @@ -54,7 +56,7 @@ public: RegisterHandlers(functions); - manager.Initialize(metadata); + manager->Initialize(metadata); } private: @@ -64,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); @@ -74,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); @@ -85,7 +87,7 @@ private: IPC::RequestParser rp{ctx}; const auto source_flag{rp.PopRaw<SourceFlag>()}; - 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); @@ -101,7 +103,7 @@ private: u32 mii_count{}; std::vector<CharInfoElement> char_info_elements(output_size); - const auto 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); @@ -122,7 +124,7 @@ private: u32 mii_count{}; std::vector<CharInfo> char_info(output_size); - const auto 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); @@ -144,7 +146,7 @@ private: 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); @@ -183,7 +185,7 @@ 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); @@ -203,7 +205,7 @@ 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); @@ -217,7 +219,7 @@ private: u32 mii_count{}; std::vector<StoreDataElement> store_data_elements(output_size); - const auto result = manager.Get(metadata, store_data_elements, mii_count, source_flag); + const auto result = manager->Get(metadata, store_data_elements, mii_count, source_flag); if (mii_count != 0) { ctx.WriteBuffer(store_data_elements); @@ -238,7 +240,7 @@ private: u32 mii_count{}; std::vector<StoreData> store_data(output_size); - const auto result = manager.Get(metadata, store_data, mii_count, source_flag); + const auto result = manager->Get(metadata, store_data, mii_count, source_flag); if (mii_count != 0) { ctx.WriteBuffer(store_data); @@ -266,7 +268,7 @@ private: StoreData new_store_data{}; if (result.IsSuccess()) { - result = manager.UpdateLatest(metadata, new_store_data, store_data, source_flag); + result = manager->UpdateLatest(metadata, new_store_data, store_data, source_flag); } if (result.IsFailure()) { @@ -288,7 +290,7 @@ private: LOG_INFO(Service_Mii, "called with create_id={}, is_special={}", create_id.FormattedString(), is_special); - const s32 index = manager.FindIndex(create_id, is_special); + const s32 index = manager->FindIndex(create_id, is_special); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); @@ -309,14 +311,14 @@ private: } if (result.IsSuccess()) { - const u32 count = manager.GetCount(metadata, SourceFlag::Database); + 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); + result = manager->Move(metadata, new_index, create_id); } IPC::ResponseBuilder rb{ctx, 2}; @@ -336,7 +338,7 @@ private: } if (result.IsSuccess()) { - result = manager.AddOrReplace(metadata, store_data); + result = manager->AddOrReplace(metadata, store_data); } IPC::ResponseBuilder rb{ctx, 2}; @@ -356,7 +358,7 @@ private: } if (result.IsSuccess()) { - result = manager.Delete(metadata, create_id); + result = manager->Delete(metadata, create_id); } IPC::ResponseBuilder rb{ctx, 2}; @@ -376,7 +378,7 @@ private: } if (result.IsSuccess()) { - result = manager.DestroyFile(metadata); + result = manager->DestroyFile(metadata); } IPC::ResponseBuilder rb{ctx, 2}; @@ -396,7 +398,7 @@ private: } if (result.IsSuccess()) { - result = manager.DeleteFile(); + result = manager->DeleteFile(); } IPC::ResponseBuilder rb{ctx, 2}; @@ -416,7 +418,7 @@ private: } if (result.IsSuccess()) { - result = manager.Format(metadata); + result = manager->Format(metadata); } IPC::ResponseBuilder rb{ctx, 2}; @@ -434,7 +436,7 @@ private: } if (result.IsSuccess()) { - is_broken_with_clear_flag = manager.IsBrokenWithClearFlag(metadata); + is_broken_with_clear_flag = manager->IsBrokenWithClearFlag(metadata); } IPC::ResponseBuilder rb{ctx, 3}; @@ -449,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); @@ -462,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); @@ -475,7 +477,7 @@ private: LOG_INFO(Service_Mii, "called"); CharInfo char_info{}; - const auto result = 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(result); @@ -489,7 +491,7 @@ private: LOG_INFO(Service_Mii, "called"); CharInfo char_info{}; - const auto result = manager.ConvertCoreDataToCharInfo(char_info, core_data); + const auto result = manager->ConvertCoreDataToCharInfo(char_info, core_data); IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(result); @@ -503,7 +505,7 @@ private: LOG_INFO(Service_Mii, "called"); CoreData core_data{}; - const auto result = manager.ConvertCharInfoToCoreData(core_data, char_info); + const auto result = manager->ConvertCharInfoToCoreData(core_data, char_info); IPC::ResponseBuilder rb{ctx, 2 + sizeof(CoreData) / sizeof(u32)}; rb.Push(result); @@ -516,41 +518,46 @@ private: LOG_INFO(Service_Mii, "called"); - const auto result = manager.Append(metadata, char_info); + const auto result = manager->Append(metadata, char_info); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(result); } - MiiManager manager{}; + std::shared_ptr<MiiManager> manager = nullptr; DatabaseSessionMetadata metadata{}; bool is_system{}; }; -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 +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); + RegisterHandlers(functions); + + if (manager == nullptr) { + manager = std::make_shared<MiiManager>(); } +} -private: - void GetDatabaseService(HLERequestContext& ctx) { - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface<IDatabaseService>(system, is_system); +MiiDBModule::~MiiDBModule() = default; - LOG_DEBUG(Service_Mii, "called"); - } +void MiiDBModule::GetDatabaseService(HLERequestContext& ctx) { + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IDatabaseService>(system, manager, is_system); - bool is_system{}; -}; + LOG_DEBUG(Service_Mii, "called"); +} + +std::shared_ptr<MiiManager> MiiDBModule::GetMiiManager() { + return manager; +} class MiiImg final : public ServiceFramework<MiiImg> { public: @@ -596,11 +603,12 @@ private: 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_manager.cpp b/src/core/hle/service/mii/mii_manager.cpp index a5a2a9232..dcfd6b2e2 100644 --- a/src/core/hle/service/mii/mii_manager.cpp +++ b/src/core/hle/service/mii/mii_manager.cpp @@ -130,11 +130,11 @@ Result MiiManager::GetIndex(const DatabaseSessionMetadata& metadata, const CharI } s32 index{}; - Result result = {}; - // FindIndex(index); + const bool is_special = metadata.magic == MiiMagic; + const auto result = database_manager.FindIndex(index, char_info.GetCreateId(), is_special); if (result.IsError()) { - return ResultNotFound; + index = -1; } if (index == -1) { diff --git a/src/core/hle/service/mii/types/char_info.cpp b/src/core/hle/service/mii/types/char_info.cpp index e90124af4..c82186c73 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() + 3; + eyebrow_y = store_data.GetEyebrowY(); nose_type = store_data.GetNoseType(); nose_scale = store_data.GetNoseScale(); nose_y = store_data.GetNoseY(); diff --git a/src/core/hle/service/mii/types/core_data.cpp b/src/core/hle/service/mii/types/core_data.cpp index 465c6293a..1068031e3 100644 --- a/src/core/hle/service/mii/types/core_data.cpp +++ b/src/core/hle/service/mii/types/core_data.cpp @@ -171,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; } } @@ -179,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); 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/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/engines/maxwell_dma.cpp b/src/video_core/engines/maxwell_dma.cpp index da8eab7ee..279f0daa1 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)); 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/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index 35bf80ea3..208e88533 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 diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index d935dd43e..1f9e7acaa 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -590,7 +590,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); @@ -606,6 +606,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; } @@ -1034,15 +1039,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); @@ -1639,7 +1656,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/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 617417040..18185610f 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -76,6 +76,11 @@ 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 { @@ -110,6 +115,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; } @@ -238,6 +245,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, @@ -420,7 +428,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; @@ -774,14 +783,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, diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 488fdd313..8c5355a28 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -45,6 +45,7 @@ VK_DEFINE_HANDLE(VmaAllocator) 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, \ @@ -97,6 +98,7 @@ VK_DEFINE_HANDLE(VmaAllocator) 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) \ @@ -144,6 +146,7 @@ VK_DEFINE_HANDLE(VmaAllocator) #define FOR_EACH_VK_RECOMMENDED_FEATURE(FEATURE_NAME) \ FEATURE_NAME(custom_border_color, customBorderColors) \ 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) \ @@ -359,9 +362,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. @@ -488,6 +496,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; @@ -666,7 +679,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 +743,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. |