Compare commits

..

20 Commits

Author SHA1 Message Date
0be509b7c4 Android #79 2023-09-23 00:57:38 +00:00
33e2dce715 Merge pull request #11572 from t895/import-heuristic
android: Adjust valid user data check
2023-09-22 14:53:39 -04:00
f3bc7354b1 android: Adjust valid user data check 2023-09-22 12:05:44 -04:00
bd5ae33153 Merge pull request #11561 from german77/hle_applet
am: mii_edit: Implement DB operations
2023-09-22 09:56:14 -04:00
16f1592e50 Merge pull request #11557 from GPUCode/brr-format
renderer_vulkan: Correct component order for A4B4G4R4_UNORM
2023-09-22 09:56:04 -04:00
fda08cbbb0 Merge pull request #11563 from Kelebek1/dma_regs
Fix DMA engine register offsets
2023-09-22 09:55:54 -04:00
a57ca3fb66 am: mii_edit: Implement DB operations 2023-09-21 18:21:39 -06:00
c619199bb4 Merge pull request #11564 from t895/overlay-inset-fix
android: Update androidx window library to 1.2.0-beta03
2023-09-21 19:15:36 -04:00
703bf7cfce android: Update androidx window library to 1.2.0-beta03
Fixes an issue with the input overlay on certain devices where the controls would appear offscreen.
2023-09-21 17:36:14 -04:00
4f69be8169 Fix DMA engine register offsets 2023-09-21 20:21:00 +01:00
9e9cb28471 Merge pull request #11555 from yuzu-emu/revert-11551-allow-save-imports-always
Revert "android: Allow save imports always"
2023-09-21 09:21:19 -04:00
2ffea42ec8 Merge pull request #11553 from rkfg/pfs-fix
pfs: Fix reading filenames past the buffer end
2023-09-21 09:21:08 -04:00
4a59dc2947 renderer_vulkan: Correct component order for A4B4G4R4_UNORM 2023-09-21 15:33:44 +03:00
c644c1a90a Revert "android: Allow save imports always" 2023-09-21 02:57:28 -04:00
753bc3a448 pfs: Fix reading filenames past the buffer end 2023-09-21 05:12:05 +03:00
c708643972 Merge pull request #11551 from t895/allow-save-imports-always
android: Allow save imports always
2023-09-20 18:32:31 -04:00
a85325f56a android: Remove unused strings related to the save manager 2023-09-20 15:01:03 -04:00
bdb4fd208f android: Allow importing saves even if no saves are found
Exporting still won't be allowed on an empty save directory.
2023-09-20 15:00:34 -04:00
1fae4a01a8 Merge pull request #11543 from t895/import-export-user-data
android: Add import/export buttons for user data
2023-09-20 10:17:24 -04:00
1e740df9b8 android: Add import/export buttons for user data 2023-09-19 15:54:47 -04:00
29 changed files with 510 additions and 145 deletions

View File

@ -214,7 +214,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
implementation("io.coil-kt:coil:2.2.2") implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1") 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("org.ini4j:ini4j:0.5.4")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

View File

@ -49,6 +49,7 @@ class HomeSettingAdapter(
holder.option.onClick.invoke() holder.option.onClick.invoke()
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
activity,
titleId = holder.option.disabledTitleId, titleId = holder.option.disabledTitleId,
descriptionId = holder.option.disabledMessageId descriptionId = holder.option.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG) ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)

View File

@ -26,6 +26,7 @@ import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
class AboutFragment : Fragment() { class AboutFragment : Fragment() {
private var _binding: FragmentAboutBinding? = null 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.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }

View File

@ -187,6 +187,7 @@ class ImportExportSavesFragment : DialogFragment() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (!validZip) { if (!validZip) {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.save_file_invalid_zip_structure, titleId = R.string.save_file_invalid_zip_structure,
descriptionId = R.string.save_file_invalid_zip_structure_description descriptionId = R.string.save_file_invalid_zip_structure_description
).show(activity.supportFragmentManager, MessageDialogFragment.TAG) ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -18,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.model.TaskViewModel
@ -28,19 +30,27 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE) val titleId = requireArguments().getInt(TITLE)
val cancellable = requireArguments().getBoolean(CANCELLABLE)
binding = DialogProgressBarBinding.inflate(layoutInflater) binding = DialogProgressBarBinding.inflate(layoutInflater)
binding.progressBar.isIndeterminate = true binding.progressBar.isIndeterminate = true
val dialog = MaterialAlertDialogBuilder(requireContext()) val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(titleId) .setTitle(titleId)
.setView(binding.root) .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) { if (!taskViewModel.isRunning.value) {
taskViewModel.runTask() taskViewModel.runTask()
} }
return dialog return alertDialog
} }
override fun onCreateView( override fun onCreateView(
@ -53,21 +63,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.apply {
repeatOnLifecycle(Lifecycle.State.CREATED) { launch {
taskViewModel.isComplete.collect { repeatOnLifecycle(Lifecycle.State.CREATED) {
if (it) { taskViewModel.isComplete.collect {
dismiss() if (it) {
when (val result = taskViewModel.result.value) { dismiss()
is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG) when (val result = taskViewModel.result.value) {
.show() is String -> Toast.makeText(
requireContext(),
result,
Toast.LENGTH_LONG
).show()
is MessageDialogFragment -> result.show( is MessageDialogFragment -> result.show(
requireActivity().supportFragmentManager, requireActivity().supportFragmentManager,
MessageDialogFragment.TAG 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" const val TAG = "IndeterminateProgressDialogFragment"
private const val TITLE = "Title" private const val TITLE = "Title"
private const val CANCELLABLE = "Cancellable"
fun newInstance( fun newInstance(
activity: AppCompatActivity, activity: AppCompatActivity,
titleId: Int, titleId: Int,
cancellable: Boolean = false,
task: () -> Any task: () -> Any
): IndeterminateProgressDialogFragment { ): IndeterminateProgressDialogFragment {
val dialog = IndeterminateProgressDialogFragment() val dialog = IndeterminateProgressDialogFragment()
val args = Bundle() val args = Bundle()
ViewModelProvider(activity)[TaskViewModel::class.java].task = task ViewModelProvider(activity)[TaskViewModel::class.java].task = task
args.putInt(TITLE, titleId) args.putInt(TITLE, titleId)
args.putBoolean(CANCELLABLE, cancellable)
dialog.arguments = args dialog.arguments = args
return dialog return dialog
} }

View File

@ -4,14 +4,21 @@
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment 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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.model.MessageDialogViewModel
class MessageDialogFragment : DialogFragment() { class MessageDialogFragment : DialogFragment() {
private val messageDialogViewModel: MessageDialogViewModel by activityViewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE_ID) val titleId = requireArguments().getInt(TITLE_ID)
val titleString = requireArguments().getString(TITLE_STRING)!! val titleString = requireArguments().getString(TITLE_STRING)!!
@ -37,6 +44,12 @@ class MessageDialogFragment : DialogFragment() {
return dialog.show() return dialog.show()
} }
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
messageDialogViewModel.dismissAction.invoke()
messageDialogViewModel.clear()
}
private fun openLink(link: String) { private fun openLink(link: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(intent) startActivity(intent)
@ -52,11 +65,13 @@ class MessageDialogFragment : DialogFragment() {
private const val HELP_LINK = "Link" private const val HELP_LINK = "Link"
fun newInstance( fun newInstance(
activity: FragmentActivity,
titleId: Int = 0, titleId: Int = 0,
titleString: String = "", titleString: String = "",
descriptionId: Int = 0, descriptionId: Int = 0,
descriptionString: String = "", descriptionString: String = "",
helpLinkId: Int = 0 helpLinkId: Int = 0,
dismissAction: () -> Unit = {}
): MessageDialogFragment { ): MessageDialogFragment {
val dialog = MessageDialogFragment() val dialog = MessageDialogFragment()
val bundle = Bundle() val bundle = Bundle()
@ -67,6 +82,8 @@ class MessageDialogFragment : DialogFragment() {
putString(DESCRIPTION_STRING, descriptionString) putString(DESCRIPTION_STRING, descriptionString)
putInt(HELP_LINK, helpLinkId) putInt(HELP_LINK, helpLinkId)
} }
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
dismissAction
dialog.arguments = bundle dialog.arguments = bundle
return dialog return dialog
} }

View File

@ -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 = {}
}
}

View File

@ -20,12 +20,20 @@ class TaskViewModel : ViewModel() {
val isRunning: StateFlow<Boolean> get() = _isRunning val isRunning: StateFlow<Boolean> get() = _isRunning
private val _isRunning = MutableStateFlow(false) private val _isRunning = MutableStateFlow(false)
val cancelled: StateFlow<Boolean> get() = _cancelled
private val _cancelled = MutableStateFlow(false)
lateinit var task: () -> Any lateinit var task: () -> Any
fun clear() { fun clear() {
_result.value = Any() _result.value = Any()
_isComplete.value = false _isComplete.value = false
_isRunning.value = false _isRunning.value = false
_cancelled.value = false
}
fun setCancelled(value: Boolean) {
_cancelled.value = value
} }
fun runTask() { fun runTask() {

View File

@ -46,13 +46,21 @@ import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class MainActivity : AppCompatActivity(), ThemeProvider { class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val homeViewModel: HomeViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels()
private val taskViewModel: TaskViewModel by viewModels()
override var themeId: Int = 0 override var themeId: Int = 0
@ -307,6 +315,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
fun processKey(result: Uri): Boolean { fun processKey(result: Uri): Boolean {
if (FileUtil.getExtension(result) != "keys") { if (FileUtil.getExtension(result) != "keys") {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
this,
titleId = R.string.reading_keys_failure, titleId = R.string.reading_keys_failure,
descriptionId = R.string.install_prod_keys_failure_extension_description descriptionId = R.string.install_prod_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG) ).show(supportFragmentManager, MessageDialogFragment.TAG)
@ -336,6 +345,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return true return true
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
this,
titleId = R.string.invalid_keys_error, titleId = R.string.invalid_keys_error,
descriptionId = R.string.install_keys_failure_description, descriptionId = R.string.install_keys_failure_description,
helpLinkId = R.string.dumping_keys_quickstart_link helpLinkId = R.string.dumping_keys_quickstart_link
@ -376,6 +386,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
this,
titleId = R.string.firmware_installed_failure, titleId = R.string.firmware_installed_failure,
descriptionId = R.string.firmware_installed_failure_description descriptionId = R.string.firmware_installed_failure_description
) )
@ -395,7 +406,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
IndeterminateProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.newInstance(
this, this,
R.string.firmware_installing, R.string.firmware_installing,
task task = task
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
@ -407,6 +418,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (FileUtil.getExtension(result) != "bin") { if (FileUtil.getExtension(result) != "bin") {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
this,
titleId = R.string.reading_keys_failure, titleId = R.string.reading_keys_failure,
descriptionId = R.string.install_amiibo_keys_failure_extension_description descriptionId = R.string.install_amiibo_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG) ).show(supportFragmentManager, MessageDialogFragment.TAG)
@ -434,6 +446,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
).show() ).show()
} else { } else {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
this,
titleId = R.string.invalid_keys_error, titleId = R.string.invalid_keys_error,
descriptionId = R.string.install_keys_failure_description, descriptionId = R.string.install_keys_failure_description,
helpLinkId = R.string.dumping_keys_quickstart_link helpLinkId = R.string.dumping_keys_quickstart_link
@ -583,12 +596,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
installResult.append(separator) installResult.append(separator)
} }
return@newInstance MessageDialogFragment.newInstance( return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_failure, titleId = R.string.install_game_content_failure,
descriptionString = installResult.toString().trim(), descriptionString = installResult.toString().trim(),
helpLinkId = R.string.install_game_content_help_link helpLinkId = R.string.install_game_content_help_link
) )
} else { } else {
return@newInstance MessageDialogFragment.newInstance( return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_success, titleId = R.string.install_game_content_success,
descriptionString = installResult.toString().trim() descriptionString = installResult.toString().trim()
) )
@ -596,4 +611,111 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.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))
stream.write(file.readBytes())
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)
}
} }

View File

@ -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>

View File

@ -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>

View File

@ -1,24 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> android:id="@+id/progress_bar"
android:layout_width="match_parent"
<com.google.android.material.progressindicator.LinearProgressIndicator android:layout_height="wrap_content"
android:id="@+id/progress_bar" android:padding="24dp"
android:layout_width="match_parent" app:trackCornerRadius="4dp" />
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>

View File

@ -176,6 +176,67 @@
</LinearLayout> </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">
<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 <com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -128,6 +128,15 @@
<string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string> <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="licenses_description">Projects that make yuzu for Android possible</string>
<string name="build">Build</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="support_link">https://discord.gg/u77vRWY</string>
<string name="website_link">https://yuzu-emu.org/</string> <string name="website_link">https://yuzu-emu.org/</string>
<string name="github_link">https://github.com/yuzu-emu</string> <string name="github_link">https://github.com/yuzu-emu</string>
@ -215,6 +224,9 @@
<string name="auto">Auto</string> <string name="auto">Auto</string>
<string name="submit">Submit</string> <string name="submit">Submit</string>
<string name="string_null">Null</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 --> <!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string> <string name="select_gpu_driver">Select GPU driver</string>

View File

@ -47,6 +47,7 @@ PartitionFilesystem::PartitionFilesystem(VirtualFile file) {
// Actually read in now... // Actually read in now...
std::vector<u8> file_data = file->ReadBytes(metadata_size); std::vector<u8> file_data = file->ReadBytes(metadata_size);
const std::size_t total_size = file_data.size(); const std::size_t total_size = file_data.size();
file_data.push_back(0);
if (total_size != metadata_size) { if (total_size != metadata_size) {
status = Loader::ResultStatus::ErrorIncorrectPFSFileSize; status = Loader::ResultStatus::ErrorIncorrectPFSFileSize;

View File

@ -7,7 +7,9 @@
#include "core/frontend/applets/mii_edit.h" #include "core/frontend/applets/mii_edit.h"
#include "core/hle/service/am/am.h" #include "core/hle/service/am/am.h"
#include "core/hle/service/am/applets/applet_mii_edit.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/mii/mii_manager.h"
#include "core/hle/service/sm/sm.h"
namespace Service::AM::Applets { namespace Service::AM::Applets {
@ -56,6 +58,12 @@ void MiiEdit::Initialize() {
sizeof(MiiEditAppletInputV4)); sizeof(MiiEditAppletInputV4));
break; 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 { bool MiiEdit::TransactionComplete() const {
@ -78,22 +86,46 @@ void MiiEdit::Execute() {
// This is a default stub for each of the MiiEdit applet modes. // This is a default stub for each of the MiiEdit applet modes.
switch (applet_input_common.applet_mode) { switch (applet_input_common.applet_mode) {
case MiiEditAppletMode::ShowMiiEdit: case MiiEditAppletMode::ShowMiiEdit:
case MiiEditAppletMode::AppendMii:
case MiiEditAppletMode::AppendMiiImage: case MiiEditAppletMode::AppendMiiImage:
case MiiEditAppletMode::UpdateMiiImage: case MiiEditAppletMode::UpdateMiiImage:
MiiEditOutput(MiiEditResult::Success, 0); MiiEditOutput(MiiEditResult::Success, 0);
break; break;
case MiiEditAppletMode::CreateMii: case MiiEditAppletMode::AppendMii: {
case MiiEditAppletMode::EditMii: {
Mii::CharInfo char_info{};
Mii::StoreData store_data{}; Mii::StoreData store_data{};
store_data.BuildBase(Mii::Gender::Male); store_data.BuildRandom(Mii::Age::All, Mii::Gender::All, Mii::Race::All);
char_info.SetFromStoreData(store_data); 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{ const MiiEditCharInfo edit_char_info{
.mii_info{applet_input_common.applet_mode == MiiEditAppletMode::EditMii .mii_info{char_info},
? applet_input_v4.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); MiiEditOutputForCharInfoEditing(MiiEditResult::Success, edit_char_info);

View File

@ -11,6 +11,11 @@ namespace Core {
class System; class System;
} // namespace Core } // namespace Core
namespace Service::Mii {
struct DatabaseSessionMetadata;
class MiiManager;
} // namespace Service::Mii
namespace Service::AM::Applets { namespace Service::AM::Applets {
class MiiEdit final : public Applet { class MiiEdit final : public Applet {
@ -40,6 +45,8 @@ private:
MiiEditAppletInputV4 applet_input_v4{}; MiiEditAppletInputV4 applet_input_v4{};
bool is_complete{false}; bool is_complete{false};
std::shared_ptr<Mii::MiiManager> manager = nullptr;
Mii::DatabaseSessionMetadata metadata{};
}; };
} // namespace Service::AM::Applets } // namespace Service::AM::Applets

View File

@ -18,8 +18,10 @@ namespace Service::Mii {
class IDatabaseService final : public ServiceFramework<IDatabaseService> { class IDatabaseService final : public ServiceFramework<IDatabaseService> {
public: public:
explicit IDatabaseService(Core::System& system_, bool is_system_) explicit IDatabaseService(Core::System& system_, std::shared_ptr<MiiManager> mii_manager,
: ServiceFramework{system_, "IDatabaseService"}, is_system{is_system_} { bool is_system_)
: ServiceFramework{system_, "IDatabaseService"}, manager{mii_manager}, is_system{
is_system_} {
// clang-format off // clang-format off
static const FunctionInfo functions[] = { static const FunctionInfo functions[] = {
{0, &IDatabaseService::IsUpdated, "IsUpdated"}, {0, &IDatabaseService::IsUpdated, "IsUpdated"},
@ -54,7 +56,7 @@ public:
RegisterHandlers(functions); RegisterHandlers(functions);
manager.Initialize(metadata); manager->Initialize(metadata);
} }
private: private:
@ -64,7 +66,7 @@ private:
LOG_DEBUG(Service_Mii, "called with source_flag={}", source_flag); 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}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -74,7 +76,7 @@ private:
void IsFullDatabase(HLERequestContext& ctx) { void IsFullDatabase(HLERequestContext& ctx) {
LOG_DEBUG(Service_Mii, "called"); LOG_DEBUG(Service_Mii, "called");
const bool is_full_database = manager.IsFullDatabase(); const bool is_full_database = manager->IsFullDatabase();
IPC::ResponseBuilder rb{ctx, 3}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -85,7 +87,7 @@ private:
IPC::RequestParser rp{ctx}; IPC::RequestParser rp{ctx};
const auto source_flag{rp.PopRaw<SourceFlag>()}; 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); LOG_DEBUG(Service_Mii, "called with source_flag={}, mii_count={}", source_flag, mii_count);
@ -101,7 +103,7 @@ private:
u32 mii_count{}; u32 mii_count{};
std::vector<CharInfoElement> char_info_elements(output_size); 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) { if (mii_count != 0) {
ctx.WriteBuffer(char_info_elements); ctx.WriteBuffer(char_info_elements);
@ -122,7 +124,7 @@ private:
u32 mii_count{}; u32 mii_count{};
std::vector<CharInfo> char_info(output_size); 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) { if (mii_count != 0) {
ctx.WriteBuffer(char_info); ctx.WriteBuffer(char_info);
@ -144,7 +146,7 @@ private:
LOG_INFO(Service_Mii, "called with source_flag={}", source_flag); LOG_INFO(Service_Mii, "called with source_flag={}", source_flag);
CharInfo new_char_info{}; 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()) { if (result.IsFailure()) {
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
rb.Push(result); rb.Push(result);
@ -183,7 +185,7 @@ private:
} }
CharInfo char_info{}; 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)}; IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -203,7 +205,7 @@ private:
} }
CharInfo char_info{}; CharInfo char_info{};
manager.BuildDefault(char_info, index); manager->BuildDefault(char_info, index);
IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -217,7 +219,7 @@ private:
u32 mii_count{}; u32 mii_count{};
std::vector<StoreDataElement> store_data_elements(output_size); 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) { if (mii_count != 0) {
ctx.WriteBuffer(store_data_elements); ctx.WriteBuffer(store_data_elements);
@ -238,7 +240,7 @@ private:
u32 mii_count{}; u32 mii_count{};
std::vector<StoreData> store_data(output_size); 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) { if (mii_count != 0) {
ctx.WriteBuffer(store_data); ctx.WriteBuffer(store_data);
@ -266,7 +268,7 @@ private:
StoreData new_store_data{}; StoreData new_store_data{};
if (result.IsSuccess()) { 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()) { if (result.IsFailure()) {
@ -288,7 +290,7 @@ private:
LOG_INFO(Service_Mii, "called with create_id={}, is_special={}", LOG_INFO(Service_Mii, "called with create_id={}, is_special={}",
create_id.FormattedString(), 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}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -309,14 +311,14 @@ private:
} }
if (result.IsSuccess()) { 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)) { if (new_index < 0 || new_index >= static_cast<s32>(count)) {
result = ResultInvalidArgument; result = ResultInvalidArgument;
} }
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
result = manager.Move(metadata, new_index, create_id); result = manager->Move(metadata, new_index, create_id);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -336,7 +338,7 @@ private:
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
result = manager.AddOrReplace(metadata, store_data); result = manager->AddOrReplace(metadata, store_data);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -356,7 +358,7 @@ private:
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
result = manager.Delete(metadata, create_id); result = manager->Delete(metadata, create_id);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -376,7 +378,7 @@ private:
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
result = manager.DestroyFile(metadata); result = manager->DestroyFile(metadata);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -396,7 +398,7 @@ private:
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
result = manager.DeleteFile(); result = manager->DeleteFile();
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -416,7 +418,7 @@ private:
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
result = manager.Format(metadata); result = manager->Format(metadata);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -434,7 +436,7 @@ private:
} }
if (result.IsSuccess()) { if (result.IsSuccess()) {
is_broken_with_clear_flag = manager.IsBrokenWithClearFlag(metadata); is_broken_with_clear_flag = manager->IsBrokenWithClearFlag(metadata);
} }
IPC::ResponseBuilder rb{ctx, 3}; IPC::ResponseBuilder rb{ctx, 3};
@ -449,7 +451,7 @@ private:
LOG_DEBUG(Service_Mii, "called"); LOG_DEBUG(Service_Mii, "called");
s32 index{}; s32 index{};
const auto result = manager.GetIndex(metadata, info, index); const auto result = manager->GetIndex(metadata, info, index);
IPC::ResponseBuilder rb{ctx, 3}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(result); rb.Push(result);
@ -462,7 +464,7 @@ private:
LOG_INFO(Service_Mii, "called, interface_version={:08X}", interface_version); 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}; IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -475,7 +477,7 @@ private:
LOG_INFO(Service_Mii, "called"); LOG_INFO(Service_Mii, "called");
CharInfo char_info{}; 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)}; IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)};
rb.Push(result); rb.Push(result);
@ -489,7 +491,7 @@ private:
LOG_INFO(Service_Mii, "called"); LOG_INFO(Service_Mii, "called");
CharInfo char_info{}; 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)}; IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)};
rb.Push(result); rb.Push(result);
@ -503,7 +505,7 @@ private:
LOG_INFO(Service_Mii, "called"); LOG_INFO(Service_Mii, "called");
CoreData core_data{}; 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)}; IPC::ResponseBuilder rb{ctx, 2 + sizeof(CoreData) / sizeof(u32)};
rb.Push(result); rb.Push(result);
@ -516,41 +518,46 @@ private:
LOG_INFO(Service_Mii, "called"); 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}; IPC::ResponseBuilder rb{ctx, 2};
rb.Push(result); rb.Push(result);
} }
MiiManager manager{}; std::shared_ptr<MiiManager> manager = nullptr;
DatabaseSessionMetadata metadata{}; DatabaseSessionMetadata metadata{};
bool is_system{}; bool is_system{};
}; };
class MiiDBModule final : public ServiceFramework<MiiDBModule> { MiiDBModule::MiiDBModule(Core::System& system_, const char* name_,
public: std::shared_ptr<MiiManager> mii_manager, bool is_system_)
explicit MiiDBModule(Core::System& system_, const char* name_, bool is_system_) : ServiceFramework{system_, name_}, manager{mii_manager}, is_system{is_system_} {
: ServiceFramework{system_, name_}, is_system{is_system_} { // clang-format off
// clang-format off static const FunctionInfo functions[] = {
static const FunctionInfo functions[] = { {0, &MiiDBModule::GetDatabaseService, "GetDatabaseService"},
{0, &MiiDBModule::GetDatabaseService, "GetDatabaseService"}, };
}; // clang-format on
// clang-format on
RegisterHandlers(functions); RegisterHandlers(functions);
if (manager == nullptr) {
manager = std::make_shared<MiiManager>();
} }
}
private: MiiDBModule::~MiiDBModule() = default;
void GetDatabaseService(HLERequestContext& ctx) {
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<IDatabaseService>(system, is_system);
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> { class MiiImg final : public ServiceFramework<MiiImg> {
public: public:
@ -596,11 +603,12 @@ private:
void LoopProcess(Core::System& system) { void LoopProcess(Core::System& system) {
auto server_manager = std::make_unique<ServerManager>(system); auto server_manager = std::make_unique<ServerManager>(system);
std::shared_ptr<MiiManager> manager = nullptr;
server_manager->RegisterNamedService("mii:e", server_manager->RegisterNamedService(
std::make_shared<MiiDBModule>(system, "mii:e", true)); "mii:e", std::make_shared<MiiDBModule>(system, "mii:e", manager, true));
server_manager->RegisterNamedService("mii:u", server_manager->RegisterNamedService(
std::make_shared<MiiDBModule>(system, "mii:u", false)); "mii:u", std::make_shared<MiiDBModule>(system, "mii:u", manager, false));
server_manager->RegisterNamedService("miiimg", std::make_shared<MiiImg>(system)); server_manager->RegisterNamedService("miiimg", std::make_shared<MiiImg>(system));
ServerManager::RunServer(std::move(server_manager)); ServerManager::RunServer(std::move(server_manager));
} }

View File

@ -3,11 +3,29 @@
#pragma once #pragma once
#include "core/hle/service/service.h"
namespace Core { namespace Core {
class System; class System;
} }
namespace Service::Mii { 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); void LoopProcess(Core::System& system);

View File

@ -130,11 +130,11 @@ Result MiiManager::GetIndex(const DatabaseSessionMetadata& metadata, const CharI
} }
s32 index{}; s32 index{};
Result result = {}; const bool is_special = metadata.magic == MiiMagic;
// FindIndex(index); const auto result = database_manager.FindIndex(index, char_info.GetCreateId(), is_special);
if (result.IsError()) { if (result.IsError()) {
return ResultNotFound; index = -1;
} }
if (index == -1) { if (index == -1) {

View File

@ -37,7 +37,7 @@ void CharInfo::SetFromStoreData(const StoreData& store_data) {
eyebrow_aspect = store_data.GetEyebrowAspect(); eyebrow_aspect = store_data.GetEyebrowAspect();
eyebrow_rotate = store_data.GetEyebrowRotate(); eyebrow_rotate = store_data.GetEyebrowRotate();
eyebrow_x = store_data.GetEyebrowX(); eyebrow_x = store_data.GetEyebrowX();
eyebrow_y = store_data.GetEyebrowY() + 3; eyebrow_y = store_data.GetEyebrowY();
nose_type = store_data.GetNoseType(); nose_type = store_data.GetNoseType();
nose_scale = store_data.GetNoseScale(); nose_scale = store_data.GetNoseScale();
nose_y = store_data.GetNoseY(); nose_y = store_data.GetNoseY();

View File

@ -171,7 +171,7 @@ void CoreData::BuildRandom(Age age, Gender gender, Race race) {
u8 glasses_type{}; u8 glasses_type{};
while (glasses_type_start < glasses_type_info.values[glasses_type]) { while (glasses_type_start < glasses_type_info.values[glasses_type]) {
if (++glasses_type >= glasses_type_info.values_count) { if (++glasses_type >= glasses_type_info.values_count) {
ASSERT(false); glasses_type = 0;
break; break;
} }
} }
@ -179,6 +179,7 @@ void CoreData::BuildRandom(Age age, Gender gender, Race race) {
SetGlassType(static_cast<GlassType>(glasses_type)); SetGlassType(static_cast<GlassType>(glasses_type));
SetGlassColor(RawData::GetGlassColorFromVer3(0)); SetGlassColor(RawData::GetGlassColorFromVer3(0));
SetGlassScale(4); SetGlassScale(4);
SetGlassY(static_cast<u8>(axis_y + 10));
SetMoleType(MoleType::None); SetMoleType(MoleType::None);
SetMoleScale(4); SetMoleScale(4);

View File

@ -1716,18 +1716,18 @@ const std::array<RandomMiiData4, 18> RandomMiiMouthType{
const std::array<RandomMiiData2, 3> RandomMiiGlassType{ const std::array<RandomMiiData2, 3> RandomMiiGlassType{
RandomMiiData2{ RandomMiiData2{
.arg_1 = 0, .arg_1 = 0,
.values_count = 9, .values_count = 4,
.values = {90, 94, 96, 100, 0, 0, 0, 0, 0}, .values = {90, 94, 96, 100},
}, },
RandomMiiData2{ RandomMiiData2{
.arg_1 = 1, .arg_1 = 1,
.values_count = 9, .values_count = 8,
.values = {83, 86, 90, 93, 94, 96, 98, 100, 0}, .values = {83, 86, 90, 93, 94, 96, 98, 100},
}, },
RandomMiiData2{ RandomMiiData2{
.arg_1 = 2, .arg_1 = 2,
.values_count = 9, .values_count = 8,
.values = {78, 83, 0, 93, 0, 0, 98, 100, 0}, .values = {78, 83, 0, 93, 0, 0, 98, 100},
}, },
}; };

View File

@ -109,10 +109,11 @@ void MaxwellDMA::Launch() {
const bool is_const_a_dst = regs.remap_const.dst_x == RemapConst::Swizzle::CONST_A; 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) { if (regs.launch_dma.remap_enable != 0 && is_const_a_dst) {
ASSERT(regs.remap_const.component_size_minus_one == 3); 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)); 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::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, memory_manager.WriteBlockUnsafe(regs.offset_out,
reinterpret_cast<u8*>(read_buffer.data()), reinterpret_cast<u8*>(read_buffer.data()),
regs.line_length_in * sizeof(u32)); regs.line_length_in * sizeof(u32));

View File

@ -214,14 +214,15 @@ public:
NO_WRITE = 6, NO_WRITE = 6,
}; };
PackedGPUVAddr address; u32 remap_consta_value;
u32 remap_constb_value;
union { union {
BitField<0, 12, u32> dst_components_raw;
BitField<0, 3, Swizzle> dst_x; BitField<0, 3, Swizzle> dst_x;
BitField<4, 3, Swizzle> dst_y; BitField<4, 3, Swizzle> dst_y;
BitField<8, 3, Swizzle> dst_z; BitField<8, 3, Swizzle> dst_z;
BitField<12, 3, Swizzle> dst_w; BitField<12, 3, Swizzle> dst_w;
BitField<0, 12, u32> dst_components_raw;
BitField<16, 2, u32> component_size_minus_one; BitField<16, 2, u32> component_size_minus_one;
BitField<20, 2, u32> num_src_components_minus_one; BitField<20, 2, u32> num_src_components_minus_one;
BitField<24, 2, u32> num_dst_components_minus_one; BitField<24, 2, u32> num_dst_components_minus_one;
@ -274,55 +275,57 @@ private:
struct Regs { struct Regs {
union { union {
struct { struct {
u32 reserved[0x40]; INSERT_PADDING_BYTES_NOINIT(0x100);
u32 nop; u32 nop;
u32 reserved01[0xf]; INSERT_PADDING_BYTES_NOINIT(0x3C);
u32 pm_trigger; u32 pm_trigger;
u32 reserved02[0x3f]; INSERT_PADDING_BYTES_NOINIT(0xFC);
Semaphore semaphore; Semaphore semaphore;
u32 reserved03[0x2]; INSERT_PADDING_BYTES_NOINIT(0x8);
RenderEnable render_enable; RenderEnable render_enable;
PhysMode src_phys_mode; PhysMode src_phys_mode;
PhysMode dst_phys_mode; PhysMode dst_phys_mode;
u32 reserved04[0x26]; INSERT_PADDING_BYTES_NOINIT(0x98);
LaunchDMA launch_dma; LaunchDMA launch_dma;
u32 reserved05[0x3f]; INSERT_PADDING_BYTES_NOINIT(0xFC);
PackedGPUVAddr offset_in; PackedGPUVAddr offset_in;
PackedGPUVAddr offset_out; PackedGPUVAddr offset_out;
s32 pitch_in; s32 pitch_in;
s32 pitch_out; s32 pitch_out;
u32 line_length_in; u32 line_length_in;
u32 line_count; u32 line_count;
u32 reserved06[0xb6]; INSERT_PADDING_BYTES_NOINIT(0x2E0);
u32 remap_consta_value;
u32 remap_constb_value;
RemapConst remap_const; RemapConst remap_const;
DMA::Parameters dst_params; DMA::Parameters dst_params;
u32 reserved07[0x1]; INSERT_PADDING_BYTES_NOINIT(0x4);
DMA::Parameters src_params; DMA::Parameters src_params;
u32 reserved08[0x275]; INSERT_PADDING_BYTES_NOINIT(0x9D4);
u32 pm_trigger_end; u32 pm_trigger_end;
u32 reserved09[0x3ba]; INSERT_PADDING_BYTES_NOINIT(0xEE8);
}; };
std::array<u32, NUM_REGS> reg_array; std::array<u32, NUM_REGS> reg_array;
}; };
} regs{}; } regs{};
static_assert(sizeof(Regs) == NUM_REGS * 4);
#define ASSERT_REG_POSITION(field_name, position) \ #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") "Field " #field_name " has invalid position")
ASSERT_REG_POSITION(launch_dma, 0xC0); ASSERT_REG_POSITION(semaphore, 0x240);
ASSERT_REG_POSITION(offset_in, 0x100); ASSERT_REG_POSITION(render_enable, 0x254);
ASSERT_REG_POSITION(offset_out, 0x102); ASSERT_REG_POSITION(src_phys_mode, 0x260);
ASSERT_REG_POSITION(pitch_in, 0x104); ASSERT_REG_POSITION(launch_dma, 0x300);
ASSERT_REG_POSITION(pitch_out, 0x105); ASSERT_REG_POSITION(offset_in, 0x400);
ASSERT_REG_POSITION(line_length_in, 0x106); ASSERT_REG_POSITION(offset_out, 0x408);
ASSERT_REG_POSITION(line_count, 0x107); ASSERT_REG_POSITION(pitch_in, 0x410);
ASSERT_REG_POSITION(remap_const, 0x1C0); ASSERT_REG_POSITION(pitch_out, 0x414);
ASSERT_REG_POSITION(dst_params, 0x1C3); ASSERT_REG_POSITION(line_length_in, 0x418);
ASSERT_REG_POSITION(src_params, 0x1CA); 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 #undef ASSERT_REG_POSITION
}; };

View File

@ -185,7 +185,7 @@ struct FormatTuple {
{VK_FORMAT_BC2_SRGB_BLOCK}, // BC2_SRGB {VK_FORMAT_BC2_SRGB_BLOCK}, // BC2_SRGB
{VK_FORMAT_BC3_SRGB_BLOCK}, // BC3_SRGB {VK_FORMAT_BC3_SRGB_BLOCK}, // BC3_SRGB
{VK_FORMAT_BC7_SRGB_BLOCK}, // BC7_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_R4G4_UNORM_PACK8}, // G4R4_UNORM
{VK_FORMAT_ASTC_4x4_SRGB_BLOCK}, // ASTC_2D_4X4_SRGB {VK_FORMAT_ASTC_4x4_SRGB_BLOCK}, // ASTC_2D_4X4_SRGB
{VK_FORMAT_ASTC_8x8_SRGB_BLOCK}, // ASTC_2D_8X8_SRGB {VK_FORMAT_ASTC_8x8_SRGB_BLOCK}, // ASTC_2D_8X8_SRGB

View File

@ -600,7 +600,7 @@ void CopyBufferToImage(vk::CommandBuffer cmdbuf, VkBuffer src_buffer, VkImage im
} }
void TryTransformSwizzleIfNeeded(PixelFormat format, std::array<SwizzleSource, 4>& swizzle, void TryTransformSwizzleIfNeeded(PixelFormat format, std::array<SwizzleSource, 4>& swizzle,
bool emulate_bgr565) { bool emulate_bgr565, bool emulate_a4b4g4r4) {
switch (format) { switch (format) {
case PixelFormat::A1B5G5R5_UNORM: case PixelFormat::A1B5G5R5_UNORM:
std::ranges::transform(swizzle, swizzle.begin(), SwapBlueRed); std::ranges::transform(swizzle, swizzle.begin(), SwapBlueRed);
@ -616,6 +616,11 @@ void TryTransformSwizzleIfNeeded(PixelFormat format, std::array<SwizzleSource, 4
case PixelFormat::G4R4_UNORM: case PixelFormat::G4R4_UNORM:
std::ranges::transform(swizzle, swizzle.begin(), SwapGreenRed); std::ranges::transform(swizzle, swizzle.begin(), SwapGreenRed);
break; break;
case PixelFormat::A4B4G4R4_UNORM:
if (emulate_a4b4g4r4) {
std::ranges::reverse(swizzle);
}
break;
default: default:
break; break;
} }
@ -1649,7 +1654,8 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
}; };
if (!info.IsRenderTarget()) { if (!info.IsRenderTarget()) {
swizzle = info.Swizzle(); 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) { if ((aspect_mask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0) {
std::ranges::transform(swizzle, swizzle.begin(), ConvertGreenRed); std::ranges::transform(swizzle, swizzle.begin(), ConvertGreenRed);
} }

View File

@ -76,6 +76,11 @@ constexpr std::array VK_FORMAT_R32G32B32_SFLOAT{
VK_FORMAT_UNDEFINED, VK_FORMAT_UNDEFINED,
}; };
constexpr std::array VK_FORMAT_A4B4G4R4_UNORM_PACK16{
VK_FORMAT_R4G4B4A4_UNORM_PACK16,
VK_FORMAT_UNDEFINED,
};
} // namespace Alternatives } // namespace Alternatives
enum class NvidiaArchitecture { enum class NvidiaArchitecture {
@ -110,6 +115,8 @@ constexpr const VkFormat* GetFormatAlternatives(VkFormat format) {
return Alternatives::R8G8B8_SSCALED.data(); return Alternatives::R8G8B8_SSCALED.data();
case VK_FORMAT_R32G32B32_SFLOAT: case VK_FORMAT_R32G32B32_SFLOAT:
return Alternatives::VK_FORMAT_R32G32B32_SFLOAT.data(); return Alternatives::VK_FORMAT_R32G32B32_SFLOAT.data();
case VK_FORMAT_A4B4G4R4_UNORM_PACK16_EXT:
return Alternatives::VK_FORMAT_A4B4G4R4_UNORM_PACK16.data();
default: default:
return nullptr; return nullptr;
} }
@ -238,6 +245,7 @@ std::unordered_map<VkFormat, VkFormatProperties> GetFormatProperties(vk::Physica
VK_FORMAT_R32_SINT, VK_FORMAT_R32_SINT,
VK_FORMAT_R32_UINT, VK_FORMAT_R32_UINT,
VK_FORMAT_R4G4B4A4_UNORM_PACK16, VK_FORMAT_R4G4B4A4_UNORM_PACK16,
VK_FORMAT_A4B4G4R4_UNORM_PACK16_EXT,
VK_FORMAT_R4G4_UNORM_PACK8, VK_FORMAT_R4G4_UNORM_PACK8,
VK_FORMAT_R5G5B5A1_UNORM_PACK16, VK_FORMAT_R5G5B5A1_UNORM_PACK16,
VK_FORMAT_R5G6B5_UNORM_PACK16, VK_FORMAT_R5G6B5_UNORM_PACK16,

View File

@ -45,6 +45,7 @@ VK_DEFINE_HANDLE(VmaAllocator)
FEATURE(EXT, ExtendedDynamicState, EXTENDED_DYNAMIC_STATE, extended_dynamic_state) \ FEATURE(EXT, ExtendedDynamicState, EXTENDED_DYNAMIC_STATE, extended_dynamic_state) \
FEATURE(EXT, ExtendedDynamicState2, EXTENDED_DYNAMIC_STATE_2, extended_dynamic_state2) \ FEATURE(EXT, ExtendedDynamicState2, EXTENDED_DYNAMIC_STATE_2, extended_dynamic_state2) \
FEATURE(EXT, ExtendedDynamicState3, EXTENDED_DYNAMIC_STATE_3, extended_dynamic_state3) \ 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, IndexTypeUint8, INDEX_TYPE_UINT8, index_type_uint8) \
FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \ FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \
FEATURE(EXT, PrimitiveTopologyListRestart, PRIMITIVE_TOPOLOGY_LIST_RESTART, \ 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_EXTENSION_NAME) \
EXTENSION_NAME(VK_EXT_EXTENDED_DYNAMIC_STATE_2_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_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_LINE_RASTERIZATION_EXTENSION_NAME) \
EXTENSION_NAME(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) \ EXTENSION_NAME(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) \
EXTENSION_NAME(VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_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) \ #define FOR_EACH_VK_RECOMMENDED_FEATURE(FEATURE_NAME) \
FEATURE_NAME(custom_border_color, customBorderColors) \ FEATURE_NAME(custom_border_color, customBorderColors) \
FEATURE_NAME(extended_dynamic_state, extendedDynamicState) \ FEATURE_NAME(extended_dynamic_state, extendedDynamicState) \
FEATURE_NAME(format_a4b4g4r4, formatA4B4G4R4) \
FEATURE_NAME(index_type_uint8, indexTypeUint8) \ FEATURE_NAME(index_type_uint8, indexTypeUint8) \
FEATURE_NAME(primitive_topology_list_restart, primitiveTopologyListRestart) \ FEATURE_NAME(primitive_topology_list_restart, primitiveTopologyListRestart) \
FEATURE_NAME(provoking_vertex, provokingVertexLast) \ FEATURE_NAME(provoking_vertex, provokingVertexLast) \
@ -488,6 +491,11 @@ public:
return extensions.extended_dynamic_state3; 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. /// Returns true if the device supports VK_EXT_extended_dynamic_state3.
bool IsExtExtendedDynamicState3BlendingSupported() const { bool IsExtExtendedDynamicState3BlendingSupported() const {
return dynamic_state3_blending; return dynamic_state3_blending;