Compare commits
1 Commits
android-55
...
android-33
Author | SHA1 | Date | |
---|---|---|---|
3798ffeaa9 |
@ -49,7 +49,7 @@ option(YUZU_TESTS "Compile tests" "${BUILD_TESTING}")
|
||||
|
||||
option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(YUZU_ROOM "Compile LDN room server" ON "NOT ANDROID" OFF)
|
||||
cmake_dependent_option(YUZU_ROOM "Compile LDN room server" ON "NOT ANDROID" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile Windows crash dump (Minidump) support" OFF "WIN32" OFF)
|
||||
|
||||
@ -63,8 +63,6 @@ option(YUZU_DOWNLOAD_TIME_ZONE_DATA "Always download time zone binaries" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "NOT WIN32" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(USE_SYSTEM_MOLTENVK "Use the system MoltenVK lib (instead of the bundled one)" OFF "APPLE" OFF)
|
||||
|
||||
set(DEFAULT_ENABLE_OPENSSL ON)
|
||||
if (ANDROID OR WIN32 OR APPLE)
|
||||
# - Windows defaults to the Schannel backend.
|
||||
@ -524,7 +522,7 @@ if (ENABLE_SDL2)
|
||||
if (YUZU_USE_BUNDLED_SDL2)
|
||||
# Detect toolchain and platform
|
||||
if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND ARCHITECTURE_x86_64)
|
||||
set(SDL2_VER "SDL2-2.28.2")
|
||||
set(SDL2_VER "SDL2-2.28.1")
|
||||
else()
|
||||
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable YUZU_USE_BUNDLED_SDL2 and provide your own.")
|
||||
endif()
|
||||
|
@ -36,21 +36,3 @@ endif()
|
||||
message(STATUS "Using bundled binaries at ${prefix}")
|
||||
set(${prefix_var} "${prefix}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(download_moltenvk_external platform version)
|
||||
set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK")
|
||||
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
||||
if (NOT EXISTS ${MOLTENVK_DIR})
|
||||
if (NOT EXISTS ${MOLTENVK_TAR})
|
||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/${version}/MoltenVK-${platform}.tar
|
||||
${MOLTENVK_TAR} SHOW_PROGRESS)
|
||||
endif()
|
||||
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
|
||||
endif()
|
||||
|
||||
# Add the MoltenVK library path to the prefix so find_library can locate it.
|
||||
list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${platform}")
|
||||
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
5
dist/qt_themes/default/style.qss
vendored
5
dist/qt_themes/default/style.qss
vendored
@ -78,11 +78,6 @@ QPushButton#buttonRefreshDevices {
|
||||
max-height: 21px;
|
||||
}
|
||||
|
||||
QPushButton#button_reset_defaults {
|
||||
min-width: 57px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QWidget#bottomPerGameInput,
|
||||
QWidget#topControllerApplet,
|
||||
QWidget#bottomControllerApplet,
|
||||
|
@ -2228,10 +2228,6 @@ QPushButton#buttonRefreshDevices {
|
||||
padding: 0px 0px;
|
||||
}
|
||||
|
||||
QPushButton#button_reset_defaults {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
QSpinBox#spinboxLStickRange,
|
||||
QSpinBox#spinboxRStickRange,
|
||||
QSpinBox#vibrationSpinPlayer1,
|
||||
|
14
externals/CMakeLists.txt
vendored
14
externals/CMakeLists.txt
vendored
@ -42,11 +42,6 @@ endif()
|
||||
# mbedtls
|
||||
add_subdirectory(mbedtls)
|
||||
target_include_directories(mbedtls PUBLIC ./mbedtls/include)
|
||||
if (NOT MSVC)
|
||||
target_compile_options(mbedcrypto PRIVATE
|
||||
-Wno-unused-but-set-variable
|
||||
-Wno-string-concatenation)
|
||||
endif()
|
||||
|
||||
# MicroProfile
|
||||
add_library(microprofile INTERFACE)
|
||||
@ -99,12 +94,6 @@ if (ENABLE_CUBEB AND NOT TARGET cubeb::cubeb)
|
||||
set(BUILD_TOOLS OFF)
|
||||
add_subdirectory(cubeb)
|
||||
add_library(cubeb::cubeb ALIAS cubeb)
|
||||
if (NOT MSVC)
|
||||
if (TARGET speex)
|
||||
target_compile_options(speex PRIVATE -Wno-sign-compare)
|
||||
endif()
|
||||
target_compile_options(cubeb PRIVATE -Wno-implicit-const-int-float-conversion)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# DiscordRPC
|
||||
@ -162,9 +151,6 @@ endif()
|
||||
if (NOT TARGET LLVM::Demangle)
|
||||
add_library(demangle demangle/ItaniumDemangle.cpp)
|
||||
target_include_directories(demangle PUBLIC ./demangle)
|
||||
if (NOT MSVC)
|
||||
target_compile_options(demangle PRIVATE -Wno-deprecated-declarations) # std::is_pod
|
||||
endif()
|
||||
add_library(LLVM::Demangle ALIAS demangle)
|
||||
endif()
|
||||
|
||||
|
2
externals/SDL
vendored
2
externals/SDL
vendored
Submodule externals/SDL updated: 031912c4b6...116a5344ff
4
externals/ffmpeg/CMakeLists.txt
vendored
4
externals/ffmpeg/CMakeLists.txt
vendored
@ -164,7 +164,7 @@ if (NOT WIN32 AND NOT ANDROID)
|
||||
--enable-decoder=h264
|
||||
--enable-decoder=vp8
|
||||
--enable-decoder=vp9
|
||||
--enable-filter=yadif,scale
|
||||
--enable-filter=yadif
|
||||
--cc="${FFmpeg_CC}"
|
||||
--cxx="${FFmpeg_CXX}"
|
||||
${FFmpeg_HWACCEL_FLAGS}
|
||||
@ -254,7 +254,7 @@ elseif(ANDROID)
|
||||
set(FFmpeg_INCLUDE_DIR "${FFmpeg_INCLUDE_DIR}" PARENT_SCOPE)
|
||||
elseif(WIN32)
|
||||
# Use yuzu FFmpeg binaries
|
||||
set(FFmpeg_EXT_NAME "ffmpeg-6.0")
|
||||
set(FFmpeg_EXT_NAME "ffmpeg-5.1.3")
|
||||
set(FFmpeg_PATH "${CMAKE_BINARY_DIR}/externals/${FFmpeg_EXT_NAME}")
|
||||
download_bundled_external("ffmpeg/" ${FFmpeg_EXT_NAME} "")
|
||||
set(FFmpeg_FOUND YES)
|
||||
|
@ -114,18 +114,15 @@ else()
|
||||
-Wno-attributes
|
||||
-Wno-invalid-offsetof
|
||||
-Wno-unused-parameter
|
||||
)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES Clang) # Clang or AppleClang
|
||||
add_compile_options(
|
||||
-Wno-braced-scalar-init
|
||||
-Wno-unused-private-field
|
||||
-Wno-nullability-completeness
|
||||
-Werror=shadow-uncaptured-local
|
||||
-Werror=implicit-fallthrough
|
||||
-Werror=type-limits
|
||||
)
|
||||
endif()
|
||||
$<$<CXX_COMPILER_ID:Clang>:-Wno-braced-scalar-init>
|
||||
$<$<CXX_COMPILER_ID:Clang>:-Wno-unused-private-field>
|
||||
$<$<CXX_COMPILER_ID:Clang>:-Werror=shadow-uncaptured-local>
|
||||
$<$<CXX_COMPILER_ID:Clang>:-Werror=implicit-fallthrough>
|
||||
$<$<CXX_COMPILER_ID:Clang>:-Werror=type-limits>
|
||||
$<$<CXX_COMPILER_ID:AppleClang>:-Wno-braced-scalar-init>
|
||||
$<$<CXX_COMPILER_ID:AppleClang>:-Wno-unused-private-field>
|
||||
)
|
||||
|
||||
if (ARCHITECTURE_x86_64)
|
||||
add_compile_options("-mcx16")
|
||||
@ -137,7 +134,7 @@ else()
|
||||
endif()
|
||||
|
||||
# GCC bugs
|
||||
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# These diagnostics would be great if they worked, but are just completely broken
|
||||
# and produce bogus errors on external libraries like fmt.
|
||||
add_compile_options(
|
||||
|
@ -95,7 +95,6 @@ android {
|
||||
// builds a release build that doesn't need signing
|
||||
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
|
||||
register("relWithDebInfo") {
|
||||
isDefault = true
|
||||
resValue("string", "app_name_suffixed", "yuzu Debug Release")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = true
|
||||
@ -123,7 +122,6 @@ android {
|
||||
flavorDimensions.add("version")
|
||||
productFlavors {
|
||||
create("mainline") {
|
||||
isDefault = true
|
||||
dimension = "version"
|
||||
buildConfigField("Boolean", "PREMIUM", "false")
|
||||
}
|
||||
@ -162,11 +160,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.create<Delete>("ktlintReset") {
|
||||
delete(File(buildDir.path + File.separator + "intermediates/ktLint"))
|
||||
}
|
||||
|
||||
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
||||
tasks.getByPath("preBuild").dependsOn("ktlintCheck")
|
||||
|
||||
ktlint {
|
||||
|
@ -25,7 +25,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
android:hasFragileUserData="false"
|
||||
android:supportsRtl="true"
|
||||
android:isGame="true"
|
||||
android:appCategory="game"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:banner="@drawable/tv_banner"
|
||||
android:extractNativeLibs="true"
|
||||
@ -56,6 +55,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
|
||||
android:theme="@style/Theme.Yuzu.Main"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||
android:exported="true">
|
||||
@ -66,14 +66,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:mimeType="application/octet-stream"
|
||||
android:scheme="content"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />
|
||||
|
@ -3,25 +3,19 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
|
||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.HomeSetting
|
||||
|
||||
class HomeSettingAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
private val viewLifecycle: LifecycleOwner,
|
||||
var options: List<HomeSetting>
|
||||
) :
|
||||
class HomeSettingAdapter(private val activity: AppCompatActivity, var options: List<HomeSetting>) :
|
||||
RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
|
||||
View.OnClickListener {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
|
||||
@ -85,22 +79,6 @@ class HomeSettingAdapter(
|
||||
binding.optionDescription.alpha = 0.5f
|
||||
binding.optionIcon.alpha = 0.5f
|
||||
}
|
||||
|
||||
option.details.observe(viewLifecycle) { updateOptionDetails(it) }
|
||||
binding.optionDetail.postDelayed(
|
||||
{
|
||||
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.optionDetail.isSelected = true
|
||||
},
|
||||
3000
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateOptionDetails(detailString: String) {
|
||||
if (detailString.isNotEmpty()) {
|
||||
binding.optionDetail.text = detailString
|
||||
binding.optionDetail.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,6 @@ class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List
|
||||
val context = YuzuApplication.appContext
|
||||
binding.textSettingName.text = context.getString(license.titleId)
|
||||
binding.textSettingDescription.text = context.getString(license.descriptionId)
|
||||
binding.textSettingValue.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,19 +5,13 @@ package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.yuzu.yuzu_emu.databinding.PageSetupBinding
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.model.SetupCallback
|
||||
import org.yuzu.yuzu_emu.model.SetupPage
|
||||
import org.yuzu.yuzu_emu.model.StepState
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
|
||||
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
|
||||
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
|
||||
@ -32,7 +26,7 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
|
||||
holder.bind(pages[position])
|
||||
|
||||
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
|
||||
RecyclerView.ViewHolder(binding.root), SetupCallback {
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var page: SetupPage
|
||||
|
||||
init {
|
||||
@ -41,12 +35,6 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
|
||||
|
||||
fun bind(page: SetupPage) {
|
||||
this.page = page
|
||||
|
||||
if (page.stepCompleted.invoke() == StepState.COMPLETE) {
|
||||
binding.buttonAction.visibility = View.INVISIBLE
|
||||
binding.textConfirmation.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
@ -74,15 +62,9 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
|
||||
MaterialButton.ICON_GRAVITY_END
|
||||
}
|
||||
setOnClickListener {
|
||||
page.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
page.buttonAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStepCompleted() {
|
||||
ViewUtils.hideView(binding.buttonAction, 200)
|
||||
ViewUtils.showView(binding.textConfirmation, 200)
|
||||
ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
@ -245,5 +246,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
|
||||
settings.putExtra(ARG_GAME_ID, gameId)
|
||||
context.startActivity(settings)
|
||||
}
|
||||
|
||||
fun launch(
|
||||
context: Context,
|
||||
launcher: ActivityResultLauncher<Intent>,
|
||||
menuTag: String?,
|
||||
gameId: String?
|
||||
) {
|
||||
val settings = Intent(context, SettingsActivity::class.java)
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag)
|
||||
settings.putExtra(ARG_GAME_ID, gameId)
|
||||
launcher.launch(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,11 +207,8 @@ class SettingsAdapter(
|
||||
val sliderBinding = DialogSliderBinding.inflate(inflater)
|
||||
|
||||
textSliderValue = sliderBinding.textValue
|
||||
textSliderValue!!.text = String.format(
|
||||
context.getString(R.string.value_with_units),
|
||||
sliderProgress.toString(),
|
||||
item.units
|
||||
)
|
||||
textSliderValue!!.text = sliderProgress.toString()
|
||||
sliderBinding.textUnits.text = item.units
|
||||
|
||||
sliderBinding.slider.apply {
|
||||
valueFrom = item.min.toFloat()
|
||||
@ -219,11 +216,7 @@ class SettingsAdapter(
|
||||
value = sliderProgress.toFloat()
|
||||
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
|
||||
sliderProgress = value.toInt()
|
||||
textSliderValue!!.text = String.format(
|
||||
context.getString(R.string.value_with_units),
|
||||
sliderProgress.toString(),
|
||||
item.units
|
||||
)
|
||||
textSliderValue!!.text = sliderProgress.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,6 +225,10 @@ class SettingsAdapter(
|
||||
.setView(sliderBinding.root)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
|
||||
.setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
|
||||
sliderBinding.slider.value = item.defaultValue!!.toFloat()
|
||||
onClick(dialog, which)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@ -25,17 +25,12 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
||||
binding.textSettingDescription.setText(item.descriptionId)
|
||||
binding.textSettingDescription.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
val epochTime = setting.value.toLong()
|
||||
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
||||
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
||||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||
binding.textSettingDescription.text = dateFormatter.format(zonedTime)
|
||||
}
|
||||
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
val epochTime = setting.value.toLong()
|
||||
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
||||
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
||||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -23,9 +23,6 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.textSettingValue.visibility = View.GONE
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -5,8 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
|
||||
@ -35,18 +33,4 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
abstract override fun onClick(clicked: View)
|
||||
|
||||
abstract override fun onLongClick(clicked: View): Boolean
|
||||
|
||||
fun setStyle(isEditable: Boolean, binding: ListItemSettingBinding) {
|
||||
val opacity = if (isEditable) 1.0f else 0.5f
|
||||
binding.textSettingName.alpha = opacity
|
||||
binding.textSettingDescription.alpha = opacity
|
||||
binding.textSettingValue.alpha = opacity
|
||||
}
|
||||
|
||||
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
|
||||
binding.switchWidget.isEnabled = isEditable
|
||||
val opacity = if (isEditable) 1.0f else 0.5f
|
||||
binding.textSettingName.alpha = opacity
|
||||
binding.textSettingDescription.alpha = opacity
|
||||
}
|
||||
}
|
||||
|
@ -17,33 +17,28 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item
|
||||
binding.textSettingName.setText(item.nameId)
|
||||
binding.textSettingDescription.visibility = View.VISIBLE
|
||||
if (item.descriptionId != 0) {
|
||||
binding.textSettingDescription.setText(item.descriptionId)
|
||||
binding.textSettingDescription.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
if (item is SingleChoiceSetting) {
|
||||
val resMgr = binding.textSettingValue.context.resources
|
||||
} else if (item is SingleChoiceSetting) {
|
||||
val resMgr = binding.textSettingDescription.context.resources
|
||||
val values = resMgr.getIntArray(item.valuesId)
|
||||
for (i in values.indices) {
|
||||
if (values[i] == item.selectedValue) {
|
||||
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
|
||||
break
|
||||
binding.textSettingDescription.text = resMgr.getStringArray(item.choicesId)[i]
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (item is StringSingleChoiceSetting) {
|
||||
for (i in item.values!!.indices) {
|
||||
if (item.values[i] == item.selectedValue) {
|
||||
binding.textSettingValue.text = item.choices[i]
|
||||
break
|
||||
binding.textSettingDescription.text = item.choices[i]
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -4,7 +4,6 @@
|
||||
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
|
||||
|
||||
import android.view.View
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
|
||||
@ -23,14 +22,6 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
binding.textSettingValue.text = String.format(
|
||||
binding.textSettingValue.context.getString(R.string.value_with_units),
|
||||
setting.selectedValue,
|
||||
setting.units
|
||||
)
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -22,7 +22,6 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.textSettingValue.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -25,12 +25,12 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
|
||||
binding.textSettingDescription.text = ""
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.switchWidget.isChecked = setting.isChecked
|
||||
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked)
|
||||
}
|
||||
binding.switchWidget.isChecked = setting.isChecked
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
binding.switchWidget.isEnabled = setting.isEditable
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
@ -7,11 +7,11 @@ import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@ -19,6 +19,8 @@ import android.util.Rational
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.Insets
|
||||
@ -48,7 +50,6 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
|
||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.overlay.InputOverlay
|
||||
import org.yuzu.yuzu_emu.utils.*
|
||||
|
||||
@ -61,12 +62,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
private var _binding: FragmentEmulationBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val args by navArgs<EmulationFragmentArgs>()
|
||||
|
||||
private lateinit var game: Game
|
||||
val args by navArgs<EmulationFragmentArgs>()
|
||||
|
||||
private var isInFoldableLayout = false
|
||||
|
||||
private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is EmulationActivity) {
|
||||
@ -80,6 +81,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
.collect { updateFoldableLayout(context, it) }
|
||||
}
|
||||
}
|
||||
|
||||
onReturnFromSettings = context.activityResultRegistry.register(
|
||||
"SettingsResult",
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { updateScreenLayout() }
|
||||
} else {
|
||||
throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
|
||||
}
|
||||
@ -91,25 +97,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intentUri: Uri? = requireActivity().intent.data
|
||||
var intentGame: Game? = null
|
||||
if (intentUri != null) {
|
||||
intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
|
||||
GameHelper.getGame(requireActivity().intent.data!!, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
game = if (args.game != null) {
|
||||
args.game!!
|
||||
} else {
|
||||
intentGame ?: error("[EmulationFragment] No bootable game present!")
|
||||
}
|
||||
|
||||
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
||||
retainInstance = true
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
emulationState = EmulationState(game.path)
|
||||
emulationState = EmulationState(args.game.path)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,7 +124,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
updateShowFpsOverlay()
|
||||
|
||||
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
|
||||
game.title
|
||||
args.game.title
|
||||
binding.inGameMenu.setNavigationItemSelectedListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_pause_emulation -> {
|
||||
@ -158,7 +149,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
R.id.menu_settings -> {
|
||||
SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "")
|
||||
SettingsActivity.launch(
|
||||
requireContext(),
|
||||
onReturnFromSettings,
|
||||
SettingsFile.FILE_NAME_CONFIG,
|
||||
""
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
@ -301,11 +297,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
emulationActivity?.let {
|
||||
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
|
||||
Settings.LayoutOption_MobileLandscape ->
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
Settings.LayoutOption_MobilePortrait ->
|
||||
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||
Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,11 +129,7 @@ class HomeSettingsFragment : Fragment() {
|
||||
mainActivity.getGamesDirectory.launch(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
||||
)
|
||||
},
|
||||
{ true },
|
||||
0,
|
||||
0,
|
||||
homeViewModel.gamesDir
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
@ -205,11 +201,7 @@ class HomeSettingsFragment : Fragment() {
|
||||
|
||||
binding.homeSettingsList.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = HomeSettingAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
viewLifecycleOwner,
|
||||
optionsList
|
||||
)
|
||||
adapter = HomeSettingAdapter(requireActivity() as AppCompatActivity, optionsList)
|
||||
}
|
||||
|
||||
setInsets()
|
||||
|
@ -19,7 +19,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.findNavController
|
||||
@ -33,13 +32,10 @@ import org.yuzu.yuzu_emu.adapters.SetupAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.model.SetupCallback
|
||||
import org.yuzu.yuzu_emu.model.SetupPage
|
||||
import org.yuzu.yuzu_emu.model.StepState
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
|
||||
class SetupFragment : Fragment() {
|
||||
private var _binding: FragmentSetupBinding? = null
|
||||
@ -116,22 +112,14 @@ class SetupFragment : Fragment() {
|
||||
0,
|
||||
false,
|
||||
R.string.give_permission,
|
||||
{
|
||||
notificationCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
{ permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) },
|
||||
true,
|
||||
R.string.notification_warning,
|
||||
R.string.notification_warning_description,
|
||||
0,
|
||||
{
|
||||
if (NotificationManagerCompat.from(requireContext())
|
||||
NotificationManagerCompat.from(requireContext())
|
||||
.areNotificationsEnabled()
|
||||
) {
|
||||
StepState.COMPLETE
|
||||
} else {
|
||||
StepState.INCOMPLETE
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -145,22 +133,12 @@ class SetupFragment : Fragment() {
|
||||
R.drawable.ic_add,
|
||||
true,
|
||||
R.string.select_keys,
|
||||
{
|
||||
keyCallback = it
|
||||
getProdKey.launch(arrayOf("*/*"))
|
||||
},
|
||||
{ mainActivity.getProdKey.launch(arrayOf("*/*")) },
|
||||
true,
|
||||
R.string.install_prod_keys_warning,
|
||||
R.string.install_prod_keys_warning_description,
|
||||
R.string.install_prod_keys_warning_help,
|
||||
{
|
||||
val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys")
|
||||
if (file.exists()) {
|
||||
StepState.COMPLETE
|
||||
} else {
|
||||
StepState.INCOMPLETE
|
||||
}
|
||||
}
|
||||
{ File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() }
|
||||
)
|
||||
)
|
||||
add(
|
||||
@ -172,8 +150,9 @@ class SetupFragment : Fragment() {
|
||||
true,
|
||||
R.string.add_games,
|
||||
{
|
||||
gamesDirCallback = it
|
||||
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||
mainActivity.getGamesDirectory.launch(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
||||
)
|
||||
},
|
||||
true,
|
||||
R.string.add_games_warning,
|
||||
@ -184,11 +163,7 @@ class SetupFragment : Fragment() {
|
||||
PreferenceManager.getDefaultSharedPreferences(
|
||||
YuzuApplication.appContext
|
||||
)
|
||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
||||
StepState.COMPLETE
|
||||
} else {
|
||||
StepState.INCOMPLETE
|
||||
}
|
||||
preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -206,13 +181,6 @@ class SetupFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
homeViewModel.shouldPageForward.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
pageForward()
|
||||
homeViewModel.setShouldPageForward(false)
|
||||
}
|
||||
}
|
||||
|
||||
binding.viewPager2.apply {
|
||||
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
|
||||
offscreenPageLimit = 2
|
||||
@ -226,15 +194,15 @@ class SetupFragment : Fragment() {
|
||||
super.onPageSelected(position)
|
||||
|
||||
if (position == 1 && previousPosition == 0) {
|
||||
ViewUtils.showView(binding.buttonNext)
|
||||
ViewUtils.showView(binding.buttonBack)
|
||||
showView(binding.buttonNext)
|
||||
showView(binding.buttonBack)
|
||||
} else if (position == 0 && previousPosition == 1) {
|
||||
ViewUtils.hideView(binding.buttonBack)
|
||||
ViewUtils.hideView(binding.buttonNext)
|
||||
hideView(binding.buttonBack)
|
||||
hideView(binding.buttonNext)
|
||||
} else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
|
||||
ViewUtils.hideView(binding.buttonNext)
|
||||
hideView(binding.buttonNext)
|
||||
} else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
|
||||
ViewUtils.showView(binding.buttonNext)
|
||||
showView(binding.buttonNext)
|
||||
}
|
||||
|
||||
previousPosition = position
|
||||
@ -247,8 +215,7 @@ class SetupFragment : Fragment() {
|
||||
|
||||
// Checks if the user has completed the task on the current page
|
||||
if (currentPage.hasWarning) {
|
||||
val stepState = currentPage.stepCompleted.invoke()
|
||||
if (stepState != StepState.INCOMPLETE) {
|
||||
if (currentPage.taskCompleted.invoke()) {
|
||||
pageForward()
|
||||
return@setOnClickListener
|
||||
}
|
||||
@ -297,15 +264,9 @@ class SetupFragment : Fragment() {
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private lateinit var notificationCallback: SetupCallback
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private val permissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if (it) {
|
||||
notificationCallback.onStepCompleted()
|
||||
}
|
||||
|
||||
if (!it &&
|
||||
!shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
|
||||
) {
|
||||
@ -316,27 +277,6 @@ class SetupFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var keyCallback: SetupCallback
|
||||
|
||||
val getProdKey =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
if (result != null) {
|
||||
if (mainActivity.processKey(result)) {
|
||||
keyCallback.onStepCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var gamesDirCallback: SetupCallback
|
||||
|
||||
val getGamesDirectory =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||
if (result != null) {
|
||||
mainActivity.processGamesDir(result)
|
||||
gamesDirCallback.onStepCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishSetup() {
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
|
||||
.putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
|
||||
@ -344,6 +284,33 @@ class SetupFragment : Fragment() {
|
||||
mainActivity.finishSetup(binding.root.findNavController())
|
||||
}
|
||||
|
||||
private fun showView(view: View) {
|
||||
view.apply {
|
||||
alpha = 0f
|
||||
visibility = View.VISIBLE
|
||||
isClickable = true
|
||||
}.animate().apply {
|
||||
duration = 300
|
||||
alpha(1f)
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun hideView(view: View) {
|
||||
if (view.visibility == View.INVISIBLE) {
|
||||
return
|
||||
}
|
||||
|
||||
view.apply {
|
||||
alpha = 1f
|
||||
isClickable = false
|
||||
}.animate().apply {
|
||||
duration = 300
|
||||
alpha(0f)
|
||||
}.withEndAction {
|
||||
view.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun pageForward() {
|
||||
binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
|
||||
}
|
||||
@ -359,29 +326,15 @@ class SetupFragment : Fragment() {
|
||||
private fun setInsets() =
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
val leftPadding = barInsets.left + cutoutInsets.left
|
||||
val topPadding = barInsets.top + cutoutInsets.top
|
||||
val rightPadding = barInsets.right + cutoutInsets.right
|
||||
val bottomPadding = barInsets.bottom + cutoutInsets.bottom
|
||||
|
||||
if (resources.getBoolean(R.bool.small_layout)) {
|
||||
binding.viewPager2
|
||||
.updatePadding(left = leftPadding, top = topPadding, right = rightPadding)
|
||||
binding.constraintButtons
|
||||
.updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding)
|
||||
} else {
|
||||
binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding)
|
||||
binding.constraintButtons
|
||||
.updatePadding(
|
||||
left = leftPadding,
|
||||
right = rightPadding,
|
||||
bottom = bottomPadding
|
||||
)
|
||||
}
|
||||
view.setPadding(
|
||||
barInsets.left + cutoutInsets.left,
|
||||
barInsets.top + cutoutInsets.top,
|
||||
barInsets.right + cutoutInsets.right,
|
||||
barInsets.bottom + cutoutInsets.bottom
|
||||
)
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,6 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
data class HomeSetting(
|
||||
val titleId: Int,
|
||||
val descriptionId: Int,
|
||||
@ -13,6 +10,5 @@ data class HomeSetting(
|
||||
val onClick: () -> Unit,
|
||||
val isEnabled: () -> Boolean = { true },
|
||||
val disabledTitleId: Int = 0,
|
||||
val disabledMessageId: Int = 0,
|
||||
val details: LiveData<String> = MutableLiveData("")
|
||||
val disabledMessageId: Int = 0
|
||||
)
|
||||
|
@ -3,15 +3,9 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
|
||||
@ -20,17 +14,6 @@ class HomeViewModel : ViewModel() {
|
||||
private val _statusBarShadeVisible = MutableLiveData(true)
|
||||
val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
|
||||
|
||||
private val _shouldPageForward = MutableLiveData(false)
|
||||
val shouldPageForward: LiveData<Boolean> get() = _shouldPageForward
|
||||
|
||||
private val _gamesDir = MutableLiveData(
|
||||
Uri.parse(
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
.getString(GameHelper.KEY_GAME_PATH, "")
|
||||
).path ?: ""
|
||||
)
|
||||
val gamesDir: LiveData<String> get() = _gamesDir
|
||||
|
||||
var navigatedToSetup = false
|
||||
|
||||
init {
|
||||
@ -50,13 +33,4 @@ class HomeViewModel : ViewModel() {
|
||||
}
|
||||
_statusBarShadeVisible.value = visible
|
||||
}
|
||||
|
||||
fun setShouldPageForward(pageForward: Boolean) {
|
||||
_shouldPageForward.value = pageForward
|
||||
}
|
||||
|
||||
fun setGamesDir(activity: FragmentActivity, dir: String) {
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
_gamesDir.value = dir
|
||||
}
|
||||
}
|
||||
|
@ -10,20 +10,10 @@ data class SetupPage(
|
||||
val buttonIconId: Int,
|
||||
val leftAlignedIcon: Boolean,
|
||||
val buttonTextId: Int,
|
||||
val buttonAction: (callback: SetupCallback) -> Unit,
|
||||
val buttonAction: () -> Unit,
|
||||
val hasWarning: Boolean,
|
||||
val warningTitleId: Int = 0,
|
||||
val warningDescriptionId: Int = 0,
|
||||
val warningHelpLinkId: Int = 0,
|
||||
val stepCompleted: () -> StepState = { StepState.UNDEFINED }
|
||||
val taskCompleted: () -> Boolean = { true }
|
||||
)
|
||||
|
||||
interface SetupCallback {
|
||||
fun onStepCompleted()
|
||||
}
|
||||
|
||||
enum class StepState {
|
||||
COMPLETE,
|
||||
INCOMPLETE,
|
||||
UNDEFINED
|
||||
}
|
||||
|
@ -266,81 +266,73 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
|
||||
val getGamesDirectory =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||
if (result != null) {
|
||||
processGamesDir(result)
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
contentResolver.takePersistableUriPermission(
|
||||
result,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
|
||||
// When a new directory is picked, we currently will reset the existing games
|
||||
// database. This effectively means that only one game directory is supported.
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
|
||||
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
||||
.apply()
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.games_dir_selected,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
gamesViewModel.reloadGames(true)
|
||||
}
|
||||
|
||||
fun processGamesDir(result: Uri) {
|
||||
contentResolver.takePersistableUriPermission(
|
||||
result,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
|
||||
// When a new directory is picked, we currently will reset the existing games
|
||||
// database. This effectively means that only one game directory is supported.
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
|
||||
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
||||
.apply()
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.games_dir_selected,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
gamesViewModel.reloadGames(true)
|
||||
homeViewModel.setGamesDir(this, result.path!!)
|
||||
}
|
||||
|
||||
val getProdKey =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
if (result != null) {
|
||||
processKey(result)
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
}
|
||||
|
||||
fun processKey(result: Uri): Boolean {
|
||||
if (FileUtil.getExtension(result) != "keys") {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.reading_keys_failure,
|
||||
R.string.install_prod_keys_failure_extension_description
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
return false
|
||||
}
|
||||
|
||||
contentResolver.takePersistableUriPermission(
|
||||
result,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
|
||||
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
|
||||
if (FileUtil.copyUriToInternalStorage(
|
||||
applicationContext,
|
||||
result,
|
||||
dstPath,
|
||||
"prod.keys"
|
||||
)
|
||||
) {
|
||||
if (NativeLibrary.reloadKeys()) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.install_keys_success,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
gamesViewModel.reloadGames(true)
|
||||
return true
|
||||
} else {
|
||||
if (FileUtil.getExtension(result) != "keys") {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.invalid_keys_error,
|
||||
R.string.install_keys_failure_description,
|
||||
R.string.dumping_keys_quickstart_link
|
||||
R.string.reading_keys_failure,
|
||||
R.string.install_prod_keys_failure_extension_description
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
return false
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
contentResolver.takePersistableUriPermission(
|
||||
result,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
|
||||
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
|
||||
if (FileUtil.copyUriToInternalStorage(
|
||||
applicationContext,
|
||||
result,
|
||||
dstPath,
|
||||
"prod.keys"
|
||||
)
|
||||
) {
|
||||
if (NativeLibrary.reloadKeys()) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.install_keys_success,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
gamesViewModel.reloadGames(true)
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.invalid_keys_error,
|
||||
R.string.install_keys_failure_description,
|
||||
R.string.dumping_keys_quickstart_link
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val getFirmware =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
|
@ -11,7 +11,6 @@ import kotlinx.serialization.json.Json
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||
|
||||
object GameHelper {
|
||||
const val KEY_GAME_PATH = "game_path"
|
||||
@ -30,7 +29,15 @@ object GameHelper {
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
|
||||
addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3)
|
||||
val children = FileUtil.listFiles(context, gamesUri)
|
||||
for (file in children) {
|
||||
if (!file.isDirectory) {
|
||||
// Check that the file has an extension we care about before trying to read out of it.
|
||||
if (Game.extensions.contains(FileUtil.getExtension(file.uri))) {
|
||||
games.add(getGame(file.uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache list of games found on disk
|
||||
val serializedGames = mutableSetOf<String>()
|
||||
@ -45,31 +52,7 @@ object GameHelper {
|
||||
return games.toList()
|
||||
}
|
||||
|
||||
private fun addGamesRecursive(
|
||||
games: MutableList<Game>,
|
||||
files: Array<MinimalDocumentFile>,
|
||||
depth: Int
|
||||
) {
|
||||
if (depth <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
files.forEach {
|
||||
if (it.isDirectory) {
|
||||
addGamesRecursive(
|
||||
games,
|
||||
FileUtil.listFiles(YuzuApplication.appContext, it.uri),
|
||||
depth - 1
|
||||
)
|
||||
} else {
|
||||
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
||||
games.add(getGame(it.uri, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
|
||||
private fun getGame(uri: Uri): Game {
|
||||
val filePath = uri.toString()
|
||||
var name = NativeLibrary.getTitle(filePath)
|
||||
|
||||
@ -94,13 +77,11 @@ object GameHelper {
|
||||
NativeLibrary.isHomebrew(filePath)
|
||||
)
|
||||
|
||||
if (addedToLibrary) {
|
||||
val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
|
||||
if (addedTime == 0L) {
|
||||
preferences.edit()
|
||||
.putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
|
||||
.apply()
|
||||
}
|
||||
val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
|
||||
if (addedTime == 0L) {
|
||||
preferences.edit()
|
||||
.putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
|
||||
.apply()
|
||||
}
|
||||
|
||||
return newGame
|
||||
|
@ -1,35 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.view.View
|
||||
|
||||
object ViewUtils {
|
||||
fun showView(view: View, length: Long = 300) {
|
||||
view.apply {
|
||||
alpha = 0f
|
||||
visibility = View.VISIBLE
|
||||
isClickable = true
|
||||
}.animate().apply {
|
||||
duration = length
|
||||
alpha(1f)
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun hideView(view: View, length: Long = 300) {
|
||||
if (view.visibility == View.INVISIBLE) {
|
||||
return
|
||||
}
|
||||
|
||||
view.apply {
|
||||
alpha = 1f
|
||||
isClickable = false
|
||||
}.animate().apply {
|
||||
duration = length
|
||||
alpha(0f)
|
||||
}.withEndAction {
|
||||
view.visibility = View.INVISIBLE
|
||||
}.start()
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Rational
|
||||
import android.view.SurfaceView
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FixedRatioSurfaceView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@ -21,44 +22,27 @@ class FixedRatioSurfaceView @JvmOverloads constructor(
|
||||
*/
|
||||
fun setAspectRatio(ratio: Rational?) {
|
||||
aspectRatio = ratio?.toFloat() ?: 0f
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val displayWidth: Float = MeasureSpec.getSize(widthMeasureSpec).toFloat()
|
||||
val displayHeight: Float = MeasureSpec.getSize(heightMeasureSpec).toFloat()
|
||||
if (aspectRatio != 0f) {
|
||||
val displayAspect = displayWidth / displayHeight
|
||||
if (displayAspect < aspectRatio) {
|
||||
// Max out width
|
||||
val halfHeight = displayHeight / 2
|
||||
val surfaceHeight = displayWidth / aspectRatio
|
||||
val newTop: Float = halfHeight - (surfaceHeight / 2)
|
||||
val newBottom: Float = halfHeight + (surfaceHeight / 2)
|
||||
super.onMeasure(
|
||||
widthMeasureSpec,
|
||||
MeasureSpec.makeMeasureSpec(
|
||||
newBottom.toInt() - newTop.toInt(),
|
||||
MeasureSpec.EXACTLY
|
||||
)
|
||||
)
|
||||
return
|
||||
} else {
|
||||
// Max out height
|
||||
val halfWidth = displayWidth / 2
|
||||
val surfaceWidth = displayHeight * aspectRatio
|
||||
val newLeft: Float = halfWidth - (surfaceWidth / 2)
|
||||
val newRight: Float = halfWidth + (surfaceWidth / 2)
|
||||
super.onMeasure(
|
||||
MeasureSpec.makeMeasureSpec(
|
||||
newRight.toInt() - newLeft.toInt(),
|
||||
MeasureSpec.EXACTLY
|
||||
),
|
||||
heightMeasureSpec
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||
if (aspectRatio != 0f) {
|
||||
val newWidth: Int
|
||||
val newHeight: Int
|
||||
if (height * aspectRatio < width) {
|
||||
newWidth = (height * aspectRatio).roundToInt()
|
||||
newHeight = height
|
||||
} else {
|
||||
newWidth = width
|
||||
newHeight = (width / aspectRatio).roundToInt()
|
||||
}
|
||||
val left = (width - newWidth) / 2
|
||||
val top = (height - newHeight) / 2
|
||||
setLeftTopRightBottom(left, top, left + newWidth, top + newHeight)
|
||||
} else {
|
||||
setLeftTopRightBottom(0, 0, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/settings_enums.h"
|
||||
#include "core/hle/service/acc/profile_manager.h"
|
||||
#include "input_common/main.h"
|
||||
#include "jni/config.h"
|
||||
@ -145,9 +144,7 @@ void Config::ReadValues() {
|
||||
Service::Account::MAX_USERS - 1);
|
||||
|
||||
// Disable docked mode by default on Android
|
||||
Settings::values.use_docked_mode.SetValue(config->GetBoolean("System", "use_docked_mode", false)
|
||||
? Settings::ConsoleMode::Docked
|
||||
: Settings::ConsoleMode::Handheld);
|
||||
Settings::values.use_docked_mode = config->GetBoolean("System", "use_docked_mode", false);
|
||||
|
||||
const auto rng_seed_enabled = config->GetBoolean("System", "rng_seed_enabled", false);
|
||||
if (rng_seed_enabled) {
|
||||
|
@ -30,7 +30,6 @@
|
||||
#include "core/cpu_manager.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
@ -225,42 +224,6 @@ public:
|
||||
m_system.Renderer().NotifySurfaceChanged();
|
||||
}
|
||||
|
||||
void ConfigureFilesystemProvider(const std::string& filepath) {
|
||||
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto loader = Loader::GetLoader(m_system, file);
|
||||
if (!loader) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto file_type = loader->GetFileType();
|
||||
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
|
||||
return;
|
||||
}
|
||||
|
||||
u64 program_id = 0;
|
||||
const auto res2 = loader->ReadProgramId(program_id);
|
||||
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
|
||||
m_manual_provider->AddEntry(FileSys::TitleType::Application,
|
||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||
program_id, file);
|
||||
} else if (res2 == Loader::ResultStatus::Success &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
const auto nsp = file_type == Loader::FileType::NSP
|
||||
? std::make_shared<FileSys::NSP>(file)
|
||||
: FileSys::XCI{file}.GetSecurePartitionNSP();
|
||||
for (const auto& title : nsp->GetNCAs()) {
|
||||
for (const auto& entry : title.second) {
|
||||
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
||||
entry.second->GetBaseFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
|
||||
@ -291,14 +254,8 @@ public:
|
||||
std::move(android_keyboard), // Software Keyboard
|
||||
nullptr, // Web Browser
|
||||
});
|
||||
|
||||
// Initialize filesystem.
|
||||
m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
|
||||
m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
|
||||
m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
|
||||
m_manual_provider.get());
|
||||
m_system.GetFileSystemController().CreateFactories(*m_vfs);
|
||||
ConfigureFilesystemProvider(filepath);
|
||||
|
||||
// Initialize account manager
|
||||
m_profile_manager = std::make_unique<Service::Account::ProfileManager>();
|
||||
@ -420,7 +377,7 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
return !Settings::IsDockedMode();
|
||||
return !Settings::values.use_docked_mode.GetValue();
|
||||
}
|
||||
|
||||
void SetDeviceType([[maybe_unused]] int index, int type) {
|
||||
@ -532,7 +489,6 @@ private:
|
||||
bool m_is_paused{};
|
||||
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
|
||||
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
|
||||
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
|
||||
|
||||
// GPU driver parameters
|
||||
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/setup_root"
|
||||
@ -8,39 +8,33 @@
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:clipToPadding="false" />
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraint_buttons"
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="8dp">
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/next"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/next"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_back"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/back"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_back"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -21,76 +21,45 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_title"
|
||||
style="@style/TextAppearance.Material3.DisplaySmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_weight="2"
|
||||
tools:text="@string/welcome" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.TitleLarge"
|
||||
android:id="@+id/text_description"
|
||||
style="@style/TextAppearance.Material3.TitleLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/button_action"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_title"
|
||||
app:layout_constraintVertical_weight="2"
|
||||
app:lineHeight="30sp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingHorizontal="32dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="26sp"
|
||||
app:lineHeight="40sp"
|
||||
tools:text="@string/welcome_description" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_confirmation"
|
||||
style="@style/TextAppearance.Material3.TitleLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingBottom="20dp"
|
||||
android:gravity="center"
|
||||
android:textSize="30sp"
|
||||
android:visibility="invisible"
|
||||
android:text="@string/step_complete"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_description"
|
||||
app:layout_constraintVertical_weight="1"
|
||||
app:lineHeight="30sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:textSize="20sp"
|
||||
app:iconGravity="end"
|
||||
app:iconSize="24sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_description"
|
||||
app:iconGravity="end"
|
||||
tools:text="Get started" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -53,23 +53,6 @@
|
||||
android:layout_marginTop="5dp"
|
||||
tools:text="@string/install_prod_keys_description" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:id="@+id/option_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:singleLine="true"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:ellipsize="none"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:layout_marginTop="5dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="/tree/primary:Games" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -5,16 +5,23 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
<TextView
|
||||
android:id="@+id/text_value"
|
||||
style="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/spacing_medlarge"
|
||||
android:layout_marginTop="@dimen/spacing_medlarge"
|
||||
tools:text="75%" />
|
||||
tools:text="75" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_units"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/text_value"
|
||||
android:layout_toEndOf="@+id/text_value"
|
||||
tools:text="%" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/setup_root"
|
||||
@ -8,39 +8,35 @@
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/button_next"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/constraint_buttons"
|
||||
android:layout_alignParentTop="true"
|
||||
android:clipToPadding="false" />
|
||||
android:layout_margin="12dp"
|
||||
android:text="@string/next"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraint_buttons"
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:id="@+id/button_back"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_alignParentBottom="true">
|
||||
android:layout_margin="12dp"
|
||||
android:text="@string/back"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/next"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_back"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
@ -12,40 +11,31 @@
|
||||
android:minHeight="72dp"
|
||||
android:padding="@dimen/spacing_large">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||
android:id="@+id/text_setting_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="viewStart"
|
||||
app:lineHeight="28dp"
|
||||
tools:text="Setting Name" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_name"
|
||||
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="16sp"
|
||||
app:lineHeight="22dp"
|
||||
tools:text="Setting Name" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_description"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/app_disclaimer" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_value"
|
||||
style="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:textAlignment="viewStart"
|
||||
android:textStyle="bold"
|
||||
tools:text="1x" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:id="@+id/text_setting_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignStart="@+id/text_setting_name"
|
||||
android:layout_below="@+id/text_setting_name"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:visibility="visible"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/app_disclaimer" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -21,12 +21,11 @@
|
||||
app:layout_constraintVertical_chainStyle="spread"
|
||||
app:layout_constraintWidth_max="220dp"
|
||||
app:layout_constraintWidth_min="110dp"
|
||||
app:layout_constraintVertical_weight="3"
|
||||
tools:src="@drawable/ic_notification" />
|
||||
app:layout_constraintVertical_weight="3" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_title"
|
||||
style="@style/TextAppearance.Material3.DisplaySmall"
|
||||
style="@style/TextAppearance.Material3.DisplayMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:textAlignment="center"
|
||||
@ -45,42 +44,23 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp"
|
||||
android:textSize="26sp"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/button_action"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_title"
|
||||
app:layout_constraintVertical_weight="2"
|
||||
app:lineHeight="30sp"
|
||||
app:lineHeight="40sp"
|
||||
tools:text="@string/welcome_description" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_confirmation"
|
||||
style="@style/TextAppearance.Material3.TitleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="24dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="30sp"
|
||||
android:visibility="invisible"
|
||||
android:text="@string/step_complete"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_description"
|
||||
app:layout_constraintVertical_weight="1"
|
||||
app:lineHeight="30sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconGravity="end"
|
||||
app:iconSize="24sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -12,9 +12,7 @@
|
||||
tools:layout="@layout/fragment_emulation" >
|
||||
<argument
|
||||
android:name="game"
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game" />
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
||||
|
@ -62,9 +62,7 @@
|
||||
android:label="EmulationActivity">
|
||||
<argument
|
||||
android:name="game"
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
app:argType="org.yuzu.yuzu_emu.model.Game" />
|
||||
</activity>
|
||||
|
||||
<action
|
||||
|
@ -29,7 +29,6 @@
|
||||
<string name="back">Back</string>
|
||||
<string name="add_games">Add Games</string>
|
||||
<string name="add_games_description">Select your games folder</string>
|
||||
<string name="step_complete">Complete!</string>
|
||||
|
||||
<!-- Home strings -->
|
||||
<string name="home_games">Games</string>
|
||||
@ -150,7 +149,6 @@
|
||||
<string name="frame_limit_slider">Limit speed percent</string>
|
||||
<string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
|
||||
<string name="cpu_accuracy">CPU accuracy</string>
|
||||
<string name="value_with_units">%1$s%2$s</string>
|
||||
|
||||
<!-- System settings strings -->
|
||||
<string name="use_docked_mode">Docked Mode</string>
|
||||
|
@ -778,7 +778,7 @@ u32 System::DropVoices(CommandBuffer& command_buffer, u32 estimated_process_time
|
||||
while (i < command_buffer.count) {
|
||||
const auto node_id{cmd->node_id};
|
||||
const auto node_id_type{cmd->node_id >> 28};
|
||||
const auto node_id_base{(cmd->node_id >> 16) & 0xFFF};
|
||||
const auto node_id_base{cmd->node_id & 0xFFF};
|
||||
|
||||
// If the new estimated process time falls below the limit, we're done dropping.
|
||||
if (estimated_process_time <= time_limit) {
|
||||
|
@ -8,7 +8,6 @@
|
||||
#include "audio_core/sink/cubeb_sink.h"
|
||||
#include "audio_core/sink/sink_stream.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "core/core.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
@ -333,38 +332,25 @@ std::vector<std::string> ListCubebSinkDevices(bool capture) {
|
||||
return device_list;
|
||||
}
|
||||
|
||||
namespace {
|
||||
static long TmpDataCallback(cubeb_stream*, void*, const void*, void*, long) {
|
||||
return TargetSampleCount;
|
||||
}
|
||||
static void TmpStateCallback(cubeb_stream*, void*, cubeb_state) {}
|
||||
} // namespace
|
||||
|
||||
bool IsCubebSuitable() {
|
||||
#if !defined(HAVE_CUBEB)
|
||||
return false;
|
||||
#else
|
||||
cubeb* ctx{nullptr};
|
||||
u32 GetCubebLatency() {
|
||||
cubeb* ctx;
|
||||
|
||||
#ifdef _WIN32
|
||||
auto com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
#endif
|
||||
|
||||
// Init cubeb
|
||||
if (cubeb_init(&ctx, "yuzu Latency Getter", nullptr) != CUBEB_OK) {
|
||||
LOG_ERROR(Audio_Sink, "Cubeb failed to init, it is not suitable.");
|
||||
return false;
|
||||
LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
|
||||
// Return a large latency so we choose SDL instead.
|
||||
return 10000u;
|
||||
}
|
||||
|
||||
SCOPE_EXIT({ cubeb_destroy(ctx); });
|
||||
|
||||
#ifdef _WIN32
|
||||
if (SUCCEEDED(com_init_result)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get min latency
|
||||
cubeb_stream_params params{};
|
||||
params.rate = TargetSampleRate;
|
||||
params.channels = 2;
|
||||
@ -375,27 +361,12 @@ bool IsCubebSuitable() {
|
||||
u32 latency{0};
|
||||
const auto latency_error = cubeb_get_min_latency(ctx, ¶ms, &latency);
|
||||
if (latency_error != CUBEB_OK) {
|
||||
LOG_ERROR(Audio_Sink, "Cubeb could not get min latency, it is not suitable.");
|
||||
return false;
|
||||
LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error);
|
||||
latency = TargetSampleCount * 2;
|
||||
}
|
||||
latency = std::max(latency, TargetSampleCount * 2);
|
||||
|
||||
// Test opening a device with standard parameters
|
||||
cubeb_devid output_device{0};
|
||||
cubeb_devid input_device{0};
|
||||
std::string name{"Yuzu test"};
|
||||
cubeb_stream* stream{nullptr};
|
||||
|
||||
if (cubeb_stream_init(ctx, &stream, name.c_str(), input_device, nullptr, output_device, ¶ms,
|
||||
latency, &TmpDataCallback, &TmpStateCallback, nullptr) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Cubeb could not open a device, it is not suitable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cubeb_stream_stop(stream);
|
||||
cubeb_stream_destroy(stream);
|
||||
return true;
|
||||
#endif
|
||||
cubeb_destroy(ctx);
|
||||
return latency;
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
|
@ -97,11 +97,10 @@ private:
|
||||
std::vector<std::string> ListCubebSinkDevices(bool capture);
|
||||
|
||||
/**
|
||||
* Check if this backend is suitable for use.
|
||||
* Checks if enabled, its latency, whether it opens successfully, etc.
|
||||
* Get the reported latency for this sink.
|
||||
*
|
||||
* @return True is this backend is suitable, false otherwise.
|
||||
* @return Minimum latency for this sink.
|
||||
*/
|
||||
bool IsCubebSuitable();
|
||||
u32 GetCubebLatency();
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
|
@ -9,7 +9,6 @@
|
||||
#include "audio_core/sink/sdl2_sink.h"
|
||||
#include "audio_core/sink/sink_stream.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "core/core.h"
|
||||
|
||||
namespace AudioCore::Sink {
|
||||
@ -85,7 +84,6 @@ public:
|
||||
}
|
||||
|
||||
Stop();
|
||||
SDL_ClearQueuedAudio(device);
|
||||
SDL_CloseAudioDevice(device);
|
||||
}
|
||||
|
||||
@ -229,42 +227,8 @@ std::vector<std::string> ListSDLSinkDevices(bool capture) {
|
||||
return device_list;
|
||||
}
|
||||
|
||||
bool IsSDLSuitable() {
|
||||
#if !defined(HAVE_SDL2)
|
||||
return false;
|
||||
#else
|
||||
// Check SDL can init
|
||||
if (!SDL_WasInit(SDL_INIT_AUDIO)) {
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
||||
LOG_ERROR(Audio_Sink, "SDL failed to init, it is not suitable. Error: {}",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We can set any latency frequency we want with SDL, so no need to check that.
|
||||
|
||||
// Check we can open a device with standard parameters
|
||||
SDL_AudioSpec spec;
|
||||
spec.freq = TargetSampleRate;
|
||||
spec.channels = 2u;
|
||||
spec.format = AUDIO_S16SYS;
|
||||
spec.samples = TargetSampleCount * 2;
|
||||
spec.callback = nullptr;
|
||||
spec.userdata = nullptr;
|
||||
|
||||
SDL_AudioSpec obtained;
|
||||
auto device = SDL_OpenAudioDevice(nullptr, false, &spec, &obtained, false);
|
||||
|
||||
if (device == 0) {
|
||||
LOG_ERROR(Audio_Sink, "SDL failed to open a device, it is not suitable. Error: {}",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_CloseAudioDevice(device);
|
||||
return true;
|
||||
#endif
|
||||
u32 GetSDLLatency() {
|
||||
return TargetSampleCount * 2;
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
|
@ -88,11 +88,10 @@ private:
|
||||
std::vector<std::string> ListSDLSinkDevices(bool capture);
|
||||
|
||||
/**
|
||||
* Check if this backend is suitable for use.
|
||||
* Checks if enabled, its latency, whether it opens successfully, etc.
|
||||
* Get the reported latency for this sink.
|
||||
*
|
||||
* @return True is this backend is suitable, false otherwise.
|
||||
* @return Minimum latency for this sink.
|
||||
*/
|
||||
bool IsSDLSuitable();
|
||||
u32 GetSDLLatency();
|
||||
|
||||
} // namespace AudioCore::Sink
|
||||
|
@ -22,7 +22,7 @@ namespace {
|
||||
struct SinkDetails {
|
||||
using FactoryFn = std::unique_ptr<Sink> (*)(std::string_view);
|
||||
using ListDevicesFn = std::vector<std::string> (*)(bool);
|
||||
using SuitableFn = bool (*)();
|
||||
using LatencyFn = u32 (*)();
|
||||
|
||||
/// Name for this sink.
|
||||
Settings::AudioEngine id;
|
||||
@ -30,8 +30,8 @@ struct SinkDetails {
|
||||
FactoryFn factory;
|
||||
/// A method to call to list available devices.
|
||||
ListDevicesFn list_devices;
|
||||
/// Check whether this backend is suitable to be used.
|
||||
SuitableFn is_suitable;
|
||||
/// Method to get the latency of this backend.
|
||||
LatencyFn latency;
|
||||
};
|
||||
|
||||
// sink_details is ordered in terms of desirability, with the best choice at the top.
|
||||
@ -43,7 +43,7 @@ constexpr SinkDetails sink_details[] = {
|
||||
return std::make_unique<CubebSink>(device_id);
|
||||
},
|
||||
&ListCubebSinkDevices,
|
||||
&IsCubebSuitable,
|
||||
&GetCubebLatency,
|
||||
},
|
||||
#endif
|
||||
#ifdef HAVE_SDL2
|
||||
@ -53,17 +53,14 @@ constexpr SinkDetails sink_details[] = {
|
||||
return std::make_unique<SDLSink>(device_id);
|
||||
},
|
||||
&ListSDLSinkDevices,
|
||||
&IsSDLSuitable,
|
||||
&GetSDLLatency,
|
||||
},
|
||||
#endif
|
||||
SinkDetails{
|
||||
Settings::AudioEngine::Null,
|
||||
[](std::string_view device_id) -> std::unique_ptr<Sink> {
|
||||
return std::make_unique<NullSink>(device_id);
|
||||
},
|
||||
[](bool capture) { return std::vector<std::string>{"null"}; },
|
||||
[]() { return true; },
|
||||
},
|
||||
SinkDetails{Settings::AudioEngine::Null,
|
||||
[](std::string_view device_id) -> std::unique_ptr<Sink> {
|
||||
return std::make_unique<NullSink>(device_id);
|
||||
},
|
||||
[](bool capture) { return std::vector<std::string>{"null"}; }, []() { return 0u; }},
|
||||
};
|
||||
|
||||
const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) {
|
||||
@ -75,22 +72,18 @@ const SinkDetails& GetOutputSinkDetails(Settings::AudioEngine sink_id) {
|
||||
auto iter = find_backend(sink_id);
|
||||
|
||||
if (sink_id == Settings::AudioEngine::Auto) {
|
||||
// Auto-select a backend. Use the sink details ordering, preferring cubeb first, checking
|
||||
// that the backend is available and suitable to use.
|
||||
for (auto& details : sink_details) {
|
||||
if (details.is_suitable()) {
|
||||
iter = &details;
|
||||
break;
|
||||
}
|
||||
// Auto-select a backend. Prefer CubeB, but it may report a large minimum latency which
|
||||
// causes audio issues, in that case go with SDL.
|
||||
#if defined(HAVE_CUBEB) && defined(HAVE_SDL2)
|
||||
iter = find_backend(Settings::AudioEngine::Cubeb);
|
||||
if (iter->latency() > TargetSampleCount * 3) {
|
||||
iter = find_backend(Settings::AudioEngine::Sdl2);
|
||||
}
|
||||
#else
|
||||
iter = std::begin(sink_details);
|
||||
#endif
|
||||
LOG_INFO(Service_Audio, "Auto-selecting the {} backend",
|
||||
Settings::CanonicalizeEnum(iter->id));
|
||||
} else {
|
||||
if (iter != std::end(sink_details) && !iter->is_suitable()) {
|
||||
LOG_ERROR(Service_Audio, "Selected backend {} is not suitable, falling back to null",
|
||||
Settings::CanonicalizeEnum(iter->id));
|
||||
iter = find_backend(Settings::AudioEngine::Null);
|
||||
}
|
||||
}
|
||||
|
||||
if (iter == std::end(sink_details)) {
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
@ -11,10 +10,8 @@
|
||||
namespace Common {
|
||||
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
[[nodiscard]] constexpr T AlignUp(T value_, size_t size) {
|
||||
using U = typename std::make_unsigned_t<T>;
|
||||
auto value{static_cast<U>(value_)};
|
||||
requires std::is_unsigned_v<T>
|
||||
[[nodiscard]] constexpr T AlignUp(T value, size_t size) {
|
||||
auto mod{static_cast<T>(value % size)};
|
||||
value -= mod;
|
||||
return static_cast<T>(mod == T{0} ? value : value + size);
|
||||
@ -27,10 +24,8 @@ template <typename T>
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
[[nodiscard]] constexpr T AlignDown(T value_, size_t size) {
|
||||
using U = typename std::make_unsigned_t<T>;
|
||||
const auto value{static_cast<U>(value_)};
|
||||
requires std::is_unsigned_v<T>
|
||||
[[nodiscard]] constexpr T AlignDown(T value, size_t size) {
|
||||
return static_cast<T>(value - value % size);
|
||||
}
|
||||
|
||||
@ -60,30 +55,6 @@ template <typename T, typename U>
|
||||
return (x + (y - 1)) / y;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
[[nodiscard]] constexpr T LeastSignificantOneBit(T x) {
|
||||
return x & ~(x - 1);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
[[nodiscard]] constexpr T ResetLeastSignificantOneBit(T x) {
|
||||
return x & (x - 1);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
[[nodiscard]] constexpr bool IsPowerOfTwo(T x) {
|
||||
return x > 0 && ResetLeastSignificantOneBit(x) == 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
[[nodiscard]] constexpr T FloorPowerOfTwo(T x) {
|
||||
return T{1} << (sizeof(T) * 8 - std::countl_zero(x) - 1);
|
||||
}
|
||||
|
||||
template <typename T, size_t Align = 16>
|
||||
class AlignmentAllocator {
|
||||
public:
|
||||
|
@ -71,10 +71,4 @@ std::vector<u8> DecompressDataLZ4(std::span<const u8> compressed, std::size_t un
|
||||
return uncompressed;
|
||||
}
|
||||
|
||||
int DecompressDataLZ4(void* dst, size_t dst_size, const void* src, size_t src_size) {
|
||||
// This is just a thin wrapper around LZ4.
|
||||
return LZ4_decompress_safe(reinterpret_cast<const char*>(src), reinterpret_cast<char*>(dst),
|
||||
static_cast<int>(src_size), static_cast<int>(dst_size));
|
||||
}
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
@ -56,6 +56,4 @@ namespace Common::Compression {
|
||||
[[nodiscard]] std::vector<u8> DecompressDataLZ4(std::span<const u8> compressed,
|
||||
std::size_t uncompressed_size);
|
||||
|
||||
[[nodiscard]] int DecompressDataLZ4(void* dst, size_t dst_size, const void* src, size_t src_size);
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <version>
|
||||
#include "common/settings_enums.h"
|
||||
#if __cpp_lib_chrono >= 201907L
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
@ -146,10 +145,6 @@ bool IsFastmemEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsDockedMode() {
|
||||
return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked;
|
||||
}
|
||||
|
||||
float Volume() {
|
||||
if (values.audio_muted) {
|
||||
return 0.0f;
|
||||
@ -212,7 +207,9 @@ const char* TranslateCategory(Category category) {
|
||||
return "Miscellaneous";
|
||||
}
|
||||
|
||||
void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo& info) {
|
||||
void UpdateRescalingInfo() {
|
||||
const auto setup = values.resolution_setup.GetValue();
|
||||
auto& info = values.resolution_info;
|
||||
info.downscale = false;
|
||||
switch (setup) {
|
||||
case ResolutionSetup::Res1_2X:
|
||||
@ -272,12 +269,6 @@ void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo& info)
|
||||
info.active = info.up_scale != 1 || info.down_shift != 0;
|
||||
}
|
||||
|
||||
void UpdateRescalingInfo() {
|
||||
const auto setup = values.resolution_setup.GetValue();
|
||||
auto& info = values.resolution_info;
|
||||
TranslateResolutionInfo(setup, info);
|
||||
}
|
||||
|
||||
void RestoreGlobalState(bool is_powered_on) {
|
||||
// If a game is running, DO NOT restore the global settings state
|
||||
if (is_powered_on) {
|
||||
|
@ -379,13 +379,7 @@ struct Values {
|
||||
|
||||
Setting<s32> current_user{linkage, 0, "current_user", Category::System};
|
||||
|
||||
SwitchableSetting<ConsoleMode> use_docked_mode{linkage,
|
||||
ConsoleMode::Docked,
|
||||
"use_docked_mode",
|
||||
Category::System,
|
||||
Specialization::Radio,
|
||||
true,
|
||||
true};
|
||||
SwitchableSetting<bool> use_docked_mode{linkage, true, "use_docked_mode", Category::System};
|
||||
|
||||
// Controls
|
||||
InputSetting<std::array<PlayerInput, 10>> players;
|
||||
@ -525,15 +519,12 @@ bool IsGPULevelHigh();
|
||||
|
||||
bool IsFastmemEnabled();
|
||||
|
||||
bool IsDockedMode();
|
||||
|
||||
float Volume();
|
||||
|
||||
std::string GetTimeZoneString(TimeZone time_zone);
|
||||
|
||||
void LogSettings();
|
||||
|
||||
void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo& info);
|
||||
void UpdateRescalingInfo();
|
||||
|
||||
// Restore the global state of all applicable settings in the Values struct
|
||||
|
@ -1,9 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/settings_common.h"
|
||||
|
||||
namespace Settings {
|
||||
|
@ -56,7 +56,6 @@ enum Specialization : u8 {
|
||||
Scalar = 5, // Values are continuous
|
||||
Countable = 6, // Can be stepped through
|
||||
Paired = 7, // Another setting is associated with this setting
|
||||
Radio = 8, // Setting should be presented in a radio group
|
||||
|
||||
Percentage = (1 << SpecializationAttributeOffset), // Should be represented as a percentage
|
||||
};
|
||||
|
@ -12,8 +12,8 @@ namespace Settings {
|
||||
|
||||
template <typename T>
|
||||
struct EnumMetadata {
|
||||
static std::vector<std::pair<std::string, T>> Canonicalizations();
|
||||
static u32 Index();
|
||||
static constexpr std::vector<std::pair<std::string, T>> Canonicalizations();
|
||||
static constexpr u32 Index();
|
||||
};
|
||||
|
||||
#define PAIR_45(N, X, ...) {#X, N::X} __VA_OPT__(, PAIR_46(N, __VA_ARGS__))
|
||||
@ -66,11 +66,11 @@ struct EnumMetadata {
|
||||
#define ENUM(NAME, ...) \
|
||||
enum class NAME : u32 { __VA_ARGS__ }; \
|
||||
template <> \
|
||||
inline std::vector<std::pair<std::string, NAME>> EnumMetadata<NAME>::Canonicalizations() { \
|
||||
constexpr std::vector<std::pair<std::string, NAME>> EnumMetadata<NAME>::Canonicalizations() { \
|
||||
return {PAIR(NAME, __VA_ARGS__)}; \
|
||||
} \
|
||||
template <> \
|
||||
inline u32 EnumMetadata<NAME>::Index() { \
|
||||
constexpr u32 EnumMetadata<NAME>::Index() { \
|
||||
return __COUNTER__; \
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ enum class AudioEngine : u32 {
|
||||
};
|
||||
|
||||
template <>
|
||||
inline std::vector<std::pair<std::string, AudioEngine>>
|
||||
constexpr std::vector<std::pair<std::string, AudioEngine>>
|
||||
EnumMetadata<AudioEngine>::Canonicalizations() {
|
||||
return {
|
||||
{"auto", AudioEngine::Auto},
|
||||
@ -96,7 +96,7 @@ EnumMetadata<AudioEngine>::Canonicalizations() {
|
||||
}
|
||||
|
||||
template <>
|
||||
inline u32 EnumMetadata<AudioEngine>::Index() {
|
||||
constexpr u32 EnumMetadata<AudioEngine>::Index() {
|
||||
// This is just a sufficiently large number that is more than the number of other enums declared
|
||||
// here
|
||||
return 100;
|
||||
@ -146,10 +146,8 @@ ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum);
|
||||
|
||||
ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch);
|
||||
|
||||
ENUM(ConsoleMode, Handheld, Docked);
|
||||
|
||||
template <typename Type>
|
||||
inline std::string CanonicalizeEnum(Type id) {
|
||||
constexpr std::string CanonicalizeEnum(Type id) {
|
||||
const auto group = EnumMetadata<Type>::Canonicalizations();
|
||||
for (auto& [name, value] : group) {
|
||||
if (value == id) {
|
||||
@ -160,7 +158,7 @@ inline std::string CanonicalizeEnum(Type id) {
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
inline Type ToEnum(const std::string& canonicalization) {
|
||||
constexpr Type ToEnum(const std::string& canonicalization) {
|
||||
const auto group = EnumMetadata<Type>::Canonicalizations();
|
||||
for (auto& [name, value] : group) {
|
||||
if (name == canonicalization) {
|
||||
|
@ -190,7 +190,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string Canonicalize() const override final {
|
||||
[[nodiscard]] std::string constexpr Canonicalize() const override final {
|
||||
if constexpr (std::is_enum_v<Type>) {
|
||||
return CanonicalizeEnum(this->GetValue());
|
||||
} else {
|
||||
@ -256,11 +256,11 @@ public:
|
||||
* @param runtime_modifiable_ Suggests whether this is modifiable while a guest is loaded
|
||||
* @param other_setting_ A second Setting to associate to this one in metadata
|
||||
*/
|
||||
template <typename T = BasicSetting>
|
||||
explicit SwitchableSetting(Linkage& linkage, const Type& default_val, const std::string& name,
|
||||
Category category_, u32 specialization_ = Specialization::Default,
|
||||
bool save_ = true, bool runtime_modifiable_ = false,
|
||||
typename std::enable_if<!ranged, T*>::type other_setting_ = nullptr)
|
||||
BasicSetting* other_setting_ = nullptr)
|
||||
requires(!ranged)
|
||||
: Setting<Type, false>{
|
||||
linkage, default_val, name, category_, specialization_,
|
||||
save_, runtime_modifiable_, other_setting_} {
|
||||
@ -282,12 +282,12 @@ public:
|
||||
* @param runtime_modifiable_ Suggests whether this is modifiable while a guest is loaded
|
||||
* @param other_setting_ A second Setting to associate to this one in metadata
|
||||
*/
|
||||
template <typename T = BasicSetting>
|
||||
explicit SwitchableSetting(Linkage& linkage, const Type& default_val, const Type& min_val,
|
||||
const Type& max_val, const std::string& name, Category category_,
|
||||
u32 specialization_ = Specialization::Default, bool save_ = true,
|
||||
bool runtime_modifiable_ = false,
|
||||
typename std::enable_if<ranged, T*>::type other_setting_ = nullptr)
|
||||
BasicSetting* other_setting_ = nullptr)
|
||||
requires(ranged)
|
||||
: Setting<Type, true>{linkage, default_val, min_val,
|
||||
max_val, name, category_,
|
||||
specialization_, save_, runtime_modifiable_,
|
||||
|
@ -460,6 +460,11 @@ S operator&(const S& i, const swap_struct_t<T, F> v) {
|
||||
return i & v.swap();
|
||||
}
|
||||
|
||||
template <typename S, typename T, typename F>
|
||||
S operator&(const swap_struct_t<T, F> v, const S& i) {
|
||||
return static_cast<S>(v.swap() & i);
|
||||
}
|
||||
|
||||
// Comparison
|
||||
template <typename S, typename T, typename F>
|
||||
bool operator<(const S& p, const swap_struct_t<T, F> v) {
|
||||
|
@ -37,49 +37,6 @@ add_library(core STATIC
|
||||
debugger/gdbstub.h
|
||||
device_memory.cpp
|
||||
device_memory.h
|
||||
file_sys/fssystem/fs_i_storage.h
|
||||
file_sys/fssystem/fssystem_aes_ctr_counter_extended_storage.cpp
|
||||
file_sys/fssystem/fssystem_aes_ctr_counter_extended_storage.h
|
||||
file_sys/fssystem/fssystem_aes_ctr_storage.cpp
|
||||
file_sys/fssystem/fssystem_aes_ctr_storage.h
|
||||
file_sys/fssystem/fssystem_aes_xts_storage.cpp
|
||||
file_sys/fssystem/fssystem_aes_xts_storage.h
|
||||
file_sys/fssystem/fssystem_alignment_matching_storage.h
|
||||
file_sys/fssystem/fssystem_alignment_matching_storage_impl.cpp
|
||||
file_sys/fssystem/fssystem_alignment_matching_storage_impl.h
|
||||
file_sys/fssystem/fssystem_bucket_tree.cpp
|
||||
file_sys/fssystem/fssystem_bucket_tree.h
|
||||
file_sys/fssystem/fssystem_bucket_tree_utils.h
|
||||
file_sys/fssystem/fssystem_compressed_storage.h
|
||||
file_sys/fssystem/fssystem_compression_common.h
|
||||
file_sys/fssystem/fssystem_compression_configuration.cpp
|
||||
file_sys/fssystem/fssystem_compression_configuration.h
|
||||
file_sys/fssystem/fssystem_crypto_configuration.cpp
|
||||
file_sys/fssystem/fssystem_crypto_configuration.h
|
||||
file_sys/fssystem/fssystem_hierarchical_integrity_verification_storage.cpp
|
||||
file_sys/fssystem/fssystem_hierarchical_integrity_verification_storage.h
|
||||
file_sys/fssystem/fssystem_hierarchical_sha256_storage.cpp
|
||||
file_sys/fssystem/fssystem_hierarchical_sha256_storage.h
|
||||
file_sys/fssystem/fssystem_indirect_storage.cpp
|
||||
file_sys/fssystem/fssystem_indirect_storage.h
|
||||
file_sys/fssystem/fssystem_integrity_romfs_storage.cpp
|
||||
file_sys/fssystem/fssystem_integrity_romfs_storage.h
|
||||
file_sys/fssystem/fssystem_integrity_verification_storage.cpp
|
||||
file_sys/fssystem/fssystem_integrity_verification_storage.h
|
||||
file_sys/fssystem/fssystem_memory_resource_buffer_hold_storage.h
|
||||
file_sys/fssystem/fssystem_nca_file_system_driver.cpp
|
||||
file_sys/fssystem/fssystem_nca_file_system_driver.h
|
||||
file_sys/fssystem/fssystem_nca_header.cpp
|
||||
file_sys/fssystem/fssystem_nca_header.h
|
||||
file_sys/fssystem/fssystem_nca_reader.cpp
|
||||
file_sys/fssystem/fssystem_pooled_buffer.cpp
|
||||
file_sys/fssystem/fssystem_pooled_buffer.h
|
||||
file_sys/fssystem/fssystem_sparse_storage.cpp
|
||||
file_sys/fssystem/fssystem_sparse_storage.h
|
||||
file_sys/fssystem/fssystem_switch_storage.h
|
||||
file_sys/fssystem/fssystem_utility.cpp
|
||||
file_sys/fssystem/fssystem_utility.h
|
||||
file_sys/fssystem/fs_types.h
|
||||
file_sys/bis_factory.cpp
|
||||
file_sys/bis_factory.h
|
||||
file_sys/card_image.cpp
|
||||
@ -100,6 +57,8 @@ add_library(core STATIC
|
||||
file_sys/mode.h
|
||||
file_sys/nca_metadata.cpp
|
||||
file_sys/nca_metadata.h
|
||||
file_sys/nca_patch.cpp
|
||||
file_sys/nca_patch.h
|
||||
file_sys/partition_filesystem.cpp
|
||||
file_sys/partition_filesystem.h
|
||||
file_sys/patch_manager.cpp
|
||||
|
@ -263,23 +263,6 @@ void GDBStub::ExecuteCommand(std::string_view packet, std::vector<DebuggerAction
|
||||
|
||||
std::vector<u8> mem(size);
|
||||
if (system.ApplicationMemory().ReadBlock(addr, mem.data(), size)) {
|
||||
// Restore any bytes belonging to replaced instructions.
|
||||
auto it = replaced_instructions.lower_bound(addr);
|
||||
for (; it != replaced_instructions.end() && it->first < addr + size; it++) {
|
||||
// Get the bytes of the instruction we previously replaced.
|
||||
const u32 original_bytes = it->second;
|
||||
|
||||
// Calculate where to start writing to the output buffer.
|
||||
const size_t output_offset = it->first - addr;
|
||||
|
||||
// Calculate how many bytes to write.
|
||||
// The loop condition ensures output_offset < size.
|
||||
const size_t n = std::min<size_t>(size - output_offset, sizeof(u32));
|
||||
|
||||
// Write the bytes to the output buffer.
|
||||
std::memcpy(mem.data() + output_offset, &original_bytes, n);
|
||||
}
|
||||
|
||||
SendReply(Common::HexToString(mem));
|
||||
} else {
|
||||
SendReply(GDB_STUB_REPLY_ERR);
|
||||
|
@ -31,9 +31,13 @@ XCI::XCI(VirtualFile file_, u64 program_id, size_t program_index)
|
||||
: file(std::move(file_)), program_nca_status{Loader::ResultStatus::ErrorXCIMissingProgramNCA},
|
||||
partitions(partition_names.size()),
|
||||
partitions_raw(partition_names.size()), keys{Core::Crypto::KeyManager::Instance()} {
|
||||
const auto header_status = TryReadHeader();
|
||||
if (header_status != Loader::ResultStatus::Success) {
|
||||
status = header_status;
|
||||
if (file->ReadObject(&header) != sizeof(GamecardHeader)) {
|
||||
status = Loader::ResultStatus::ErrorBadXCIHeader;
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.magic != Common::MakeMagic('H', 'E', 'A', 'D')) {
|
||||
status = Loader::ResultStatus::ErrorBadXCIHeader;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -179,9 +183,9 @@ u32 XCI::GetSystemUpdateVersion() {
|
||||
}
|
||||
|
||||
for (const auto& update_file : update->GetFiles()) {
|
||||
NCA nca{update_file};
|
||||
NCA nca{update_file, nullptr, 0};
|
||||
|
||||
if (nca.GetStatus() != Loader::ResultStatus::Success || nca.GetSubdirectories().empty()) {
|
||||
if (nca.GetStatus() != Loader::ResultStatus::Success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -292,7 +296,7 @@ Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto nca = std::make_shared<NCA>(partition_file);
|
||||
auto nca = std::make_shared<NCA>(partition_file, nullptr, 0);
|
||||
if (nca->IsUpdate()) {
|
||||
continue;
|
||||
}
|
||||
@ -312,44 +316,6 @@ Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
Loader::ResultStatus XCI::TryReadHeader() {
|
||||
constexpr size_t CardInitialDataRegionSize = 0x1000;
|
||||
|
||||
// Define the function we'll use to determine if we read a valid header.
|
||||
const auto ReadCardHeader = [&]() {
|
||||
// Ensure we can read the entire header. If we can't, we can't read the card image.
|
||||
if (file->ReadObject(&header) != sizeof(GamecardHeader)) {
|
||||
return Loader::ResultStatus::ErrorBadXCIHeader;
|
||||
}
|
||||
|
||||
// Ensure the header magic matches. If it doesn't, this isn't a card image header.
|
||||
if (header.magic != Common::MakeMagic('H', 'E', 'A', 'D')) {
|
||||
return Loader::ResultStatus::ErrorBadXCIHeader;
|
||||
}
|
||||
|
||||
// We read a card image header.
|
||||
return Loader::ResultStatus::Success;
|
||||
};
|
||||
|
||||
// Try to read the header directly.
|
||||
if (ReadCardHeader() == Loader::ResultStatus::Success) {
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
// Get the size of the file.
|
||||
const size_t card_image_size = file->GetSize();
|
||||
|
||||
// If we are large enough to have a key area, offset past the key area and retry.
|
||||
if (card_image_size >= CardInitialDataRegionSize) {
|
||||
file = std::make_shared<OffsetVfsFile>(file, card_image_size - CardInitialDataRegionSize,
|
||||
CardInitialDataRegionSize);
|
||||
return ReadCardHeader();
|
||||
}
|
||||
|
||||
// We had no header and aren't large enough to have a key area, so this can't be parsed.
|
||||
return Loader::ResultStatus::ErrorBadXCIHeader;
|
||||
}
|
||||
|
||||
u8 XCI::GetFormatVersion() {
|
||||
return GetLogoPartition() == nullptr ? 0x1 : 0x2;
|
||||
}
|
||||
|
@ -128,7 +128,6 @@ public:
|
||||
|
||||
private:
|
||||
Loader::ResultStatus AddNCAFromPartition(XCIPartition part);
|
||||
Loader::ResultStatus TryReadHeader();
|
||||
|
||||
VirtualFile file;
|
||||
GamecardHeader header{};
|
||||
|
@ -12,109 +12,545 @@
|
||||
#include "core/crypto/ctr_encryption_layer.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_patch.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_compression_configuration.h"
|
||||
#include "core/file_sys/fssystem/fssystem_crypto_configuration.h"
|
||||
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
NCA::NCA(VirtualFile file_, const NCA* base_nca)
|
||||
: file(std::move(file_)), keys{Core::Crypto::KeyManager::Instance()} {
|
||||
// Media offsets in headers are stored divided by 512. Mult. by this to get real offset.
|
||||
constexpr u64 MEDIA_OFFSET_MULTIPLIER = 0x200;
|
||||
|
||||
constexpr u64 SECTION_HEADER_SIZE = 0x200;
|
||||
constexpr u64 SECTION_HEADER_OFFSET = 0x400;
|
||||
|
||||
constexpr u32 IVFC_MAX_LEVEL = 6;
|
||||
|
||||
enum class NCASectionFilesystemType : u8 {
|
||||
PFS0 = 0x2,
|
||||
ROMFS = 0x3,
|
||||
};
|
||||
|
||||
struct IVFCLevel {
|
||||
u64_le offset;
|
||||
u64_le size;
|
||||
u32_le block_size;
|
||||
u32_le reserved;
|
||||
};
|
||||
static_assert(sizeof(IVFCLevel) == 0x18, "IVFCLevel has incorrect size.");
|
||||
|
||||
struct IVFCHeader {
|
||||
u32_le magic;
|
||||
u32_le magic_number;
|
||||
INSERT_PADDING_BYTES_NOINIT(8);
|
||||
std::array<IVFCLevel, 6> levels;
|
||||
INSERT_PADDING_BYTES_NOINIT(64);
|
||||
};
|
||||
static_assert(sizeof(IVFCHeader) == 0xE0, "IVFCHeader has incorrect size.");
|
||||
|
||||
struct NCASectionHeaderBlock {
|
||||
INSERT_PADDING_BYTES_NOINIT(3);
|
||||
NCASectionFilesystemType filesystem_type;
|
||||
NCASectionCryptoType crypto_type;
|
||||
INSERT_PADDING_BYTES_NOINIT(3);
|
||||
};
|
||||
static_assert(sizeof(NCASectionHeaderBlock) == 0x8, "NCASectionHeaderBlock has incorrect size.");
|
||||
|
||||
struct NCABucketInfo {
|
||||
u64 table_offset;
|
||||
u64 table_size;
|
||||
std::array<u8, 0x10> table_header;
|
||||
};
|
||||
static_assert(sizeof(NCABucketInfo) == 0x20, "NCABucketInfo has incorrect size.");
|
||||
|
||||
struct NCASparseInfo {
|
||||
NCABucketInfo bucket;
|
||||
u64 physical_offset;
|
||||
u16 generation;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x6);
|
||||
};
|
||||
static_assert(sizeof(NCASparseInfo) == 0x30, "NCASparseInfo has incorrect size.");
|
||||
|
||||
struct NCACompressionInfo {
|
||||
NCABucketInfo bucket;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x8);
|
||||
};
|
||||
static_assert(sizeof(NCACompressionInfo) == 0x28, "NCACompressionInfo has incorrect size.");
|
||||
|
||||
struct NCASectionRaw {
|
||||
NCASectionHeaderBlock header;
|
||||
std::array<u8, 0x138> block_data;
|
||||
std::array<u8, 0x8> section_ctr;
|
||||
NCASparseInfo sparse_info;
|
||||
NCACompressionInfo compression_info;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x60);
|
||||
};
|
||||
static_assert(sizeof(NCASectionRaw) == 0x200, "NCASectionRaw has incorrect size.");
|
||||
|
||||
struct PFS0Superblock {
|
||||
NCASectionHeaderBlock header_block;
|
||||
std::array<u8, 0x20> hash;
|
||||
u32_le size;
|
||||
INSERT_PADDING_BYTES_NOINIT(4);
|
||||
u64_le hash_table_offset;
|
||||
u64_le hash_table_size;
|
||||
u64_le pfs0_header_offset;
|
||||
u64_le pfs0_size;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x1B0);
|
||||
};
|
||||
static_assert(sizeof(PFS0Superblock) == 0x200, "PFS0Superblock has incorrect size.");
|
||||
|
||||
struct RomFSSuperblock {
|
||||
NCASectionHeaderBlock header_block;
|
||||
IVFCHeader ivfc;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x118);
|
||||
};
|
||||
static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size.");
|
||||
|
||||
struct BKTRHeader {
|
||||
u64_le offset;
|
||||
u64_le size;
|
||||
u32_le magic;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x4);
|
||||
u32_le number_entries;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x4);
|
||||
};
|
||||
static_assert(sizeof(BKTRHeader) == 0x20, "BKTRHeader has incorrect size.");
|
||||
|
||||
struct BKTRSuperblock {
|
||||
NCASectionHeaderBlock header_block;
|
||||
IVFCHeader ivfc;
|
||||
INSERT_PADDING_BYTES_NOINIT(0x18);
|
||||
BKTRHeader relocation;
|
||||
BKTRHeader subsection;
|
||||
INSERT_PADDING_BYTES_NOINIT(0xC0);
|
||||
};
|
||||
static_assert(sizeof(BKTRSuperblock) == 0x200, "BKTRSuperblock has incorrect size.");
|
||||
|
||||
union NCASectionHeader {
|
||||
NCASectionRaw raw{};
|
||||
PFS0Superblock pfs0;
|
||||
RomFSSuperblock romfs;
|
||||
BKTRSuperblock bktr;
|
||||
};
|
||||
static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size.");
|
||||
|
||||
static bool IsValidNCA(const NCAHeader& header) {
|
||||
// TODO(DarkLordZach): Add NCA2/NCA0 support.
|
||||
return header.magic == Common::MakeMagic('N', 'C', 'A', '3');
|
||||
}
|
||||
|
||||
NCA::NCA(VirtualFile file_, VirtualFile bktr_base_romfs_, u64 bktr_base_ivfc_offset)
|
||||
: file(std::move(file_)),
|
||||
bktr_base_romfs(std::move(bktr_base_romfs_)), keys{Core::Crypto::KeyManager::Instance()} {
|
||||
if (file == nullptr) {
|
||||
status = Loader::ResultStatus::ErrorNullFile;
|
||||
return;
|
||||
}
|
||||
|
||||
reader = std::make_shared<NcaReader>();
|
||||
if (Result rc =
|
||||
reader->Initialize(file, GetCryptoConfiguration(), GetNcaCompressionConfiguration());
|
||||
R_FAILED(rc)) {
|
||||
if (rc != ResultInvalidNcaSignature) {
|
||||
LOG_ERROR(Loader, "File reader errored out during header read: {:#x}",
|
||||
rc.GetInnerValue());
|
||||
}
|
||||
if (sizeof(NCAHeader) != file->ReadObject(&header)) {
|
||||
LOG_ERROR(Loader, "File reader errored out during header read.");
|
||||
status = Loader::ResultStatus::ErrorBadNCAHeader;
|
||||
return;
|
||||
}
|
||||
|
||||
RightsId rights_id{};
|
||||
reader->GetRightsId(rights_id.data(), rights_id.size());
|
||||
if (rights_id != RightsId{}) {
|
||||
// External decryption key required; provide it here.
|
||||
const auto key_generation = std::max<s32>(reader->GetKeyGeneration(), 1) - 1;
|
||||
|
||||
u128 rights_id_u128;
|
||||
std::memcpy(rights_id_u128.data(), rights_id.data(), sizeof(rights_id));
|
||||
|
||||
auto titlekey =
|
||||
keys.GetKey(Core::Crypto::S128KeyType::Titlekey, rights_id_u128[1], rights_id_u128[0]);
|
||||
if (titlekey == Core::Crypto::Key128{}) {
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekey;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keys.HasKey(Core::Crypto::S128KeyType::Titlekek, key_generation)) {
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekek;
|
||||
return;
|
||||
}
|
||||
|
||||
auto titlekek = keys.GetKey(Core::Crypto::S128KeyType::Titlekek, key_generation);
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(titlekek, Core::Crypto::Mode::ECB);
|
||||
cipher.Transcode(titlekey.data(), titlekey.size(), titlekey.data(),
|
||||
Core::Crypto::Op::Decrypt);
|
||||
|
||||
reader->SetExternalDecryptionKey(titlekey.data(), titlekey.size());
|
||||
if (!HandlePotentialHeaderDecryption()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const s32 fs_count = reader->GetFsCount();
|
||||
NcaFileSystemDriver fs(base_nca ? base_nca->reader : nullptr, reader);
|
||||
std::vector<VirtualFile> filesystems(fs_count);
|
||||
for (s32 i = 0; i < fs_count; i++) {
|
||||
NcaFsHeaderReader header_reader;
|
||||
const Result rc = fs.OpenStorage(&filesystems[i], &header_reader, i);
|
||||
if (R_FAILED(rc)) {
|
||||
LOG_ERROR(Loader, "File reader errored out during read of section {}: {:#x}", i,
|
||||
rc.GetInnerValue());
|
||||
status = Loader::ResultStatus::ErrorBadNCAHeader;
|
||||
return;
|
||||
has_rights_id = std::ranges::any_of(header.rights_id, [](char c) { return c != '\0'; });
|
||||
|
||||
const std::vector<NCASectionHeader> sections = ReadSectionHeaders();
|
||||
is_update = std::ranges::any_of(sections, [](const NCASectionHeader& nca_header) {
|
||||
return nca_header.raw.header.crypto_type == NCASectionCryptoType::BKTR;
|
||||
});
|
||||
|
||||
if (!ReadSections(sections, bktr_base_ivfc_offset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
status = Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
NCA::~NCA() = default;
|
||||
|
||||
bool NCA::CheckSupportedNCA(const NCAHeader& nca_header) {
|
||||
if (nca_header.magic == Common::MakeMagic('N', 'C', 'A', '2')) {
|
||||
status = Loader::ResultStatus::ErrorNCA2;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nca_header.magic == Common::MakeMagic('N', 'C', 'A', '0')) {
|
||||
status = Loader::ResultStatus::ErrorNCA0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NCA::HandlePotentialHeaderDecryption() {
|
||||
if (IsValidNCA(header)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!CheckSupportedNCA(header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NCAHeader dec_header{};
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key256> cipher(
|
||||
keys.GetKey(Core::Crypto::S256KeyType::Header), Core::Crypto::Mode::XTS);
|
||||
cipher.XTSTranscode(&header, sizeof(NCAHeader), &dec_header, 0, 0x200,
|
||||
Core::Crypto::Op::Decrypt);
|
||||
if (IsValidNCA(dec_header)) {
|
||||
header = dec_header;
|
||||
encrypted = true;
|
||||
} else {
|
||||
if (!CheckSupportedNCA(dec_header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header_reader.GetFsType() == NcaFsHeader::FsType::RomFs) {
|
||||
files.push_back(filesystems[i]);
|
||||
romfs = files.back();
|
||||
if (keys.HasKey(Core::Crypto::S256KeyType::Header)) {
|
||||
status = Loader::ResultStatus::ErrorIncorrectHeaderKey;
|
||||
} else {
|
||||
status = Loader::ResultStatus::ErrorMissingHeaderKey;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<NCASectionHeader> NCA::ReadSectionHeaders() const {
|
||||
const std::ptrdiff_t number_sections =
|
||||
std::ranges::count_if(header.section_tables, [](const NCASectionTableEntry& entry) {
|
||||
return entry.media_offset > 0;
|
||||
});
|
||||
|
||||
std::vector<NCASectionHeader> sections(number_sections);
|
||||
const auto length_sections = SECTION_HEADER_SIZE * number_sections;
|
||||
|
||||
if (encrypted) {
|
||||
auto raw = file->ReadBytes(length_sections, SECTION_HEADER_OFFSET);
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key256> cipher(
|
||||
keys.GetKey(Core::Crypto::S256KeyType::Header), Core::Crypto::Mode::XTS);
|
||||
cipher.XTSTranscode(raw.data(), length_sections, sections.data(), 2, SECTION_HEADER_SIZE,
|
||||
Core::Crypto::Op::Decrypt);
|
||||
} else {
|
||||
file->ReadBytes(sections.data(), length_sections, SECTION_HEADER_OFFSET);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
bool NCA::ReadSections(const std::vector<NCASectionHeader>& sections, u64 bktr_base_ivfc_offset) {
|
||||
for (std::size_t i = 0; i < sections.size(); ++i) {
|
||||
const auto& section = sections[i];
|
||||
|
||||
if (section.raw.sparse_info.bucket.table_offset != 0 &&
|
||||
section.raw.sparse_info.bucket.table_size != 0) {
|
||||
LOG_ERROR(Loader, "Sparse NCAs are not supported.");
|
||||
status = Loader::ResultStatus::ErrorSparseNCA;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header_reader.GetFsType() == NcaFsHeader::FsType::PartitionFs) {
|
||||
auto npfs = std::make_shared<PartitionFilesystem>(filesystems[i]);
|
||||
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
|
||||
dirs.push_back(npfs);
|
||||
if (IsDirectoryExeFS(npfs)) {
|
||||
exefs = dirs.back();
|
||||
} else if (IsDirectoryLogoPartition(npfs)) {
|
||||
logo = dirs.back();
|
||||
} else {
|
||||
continue;
|
||||
if (section.raw.compression_info.bucket.table_offset != 0 &&
|
||||
section.raw.compression_info.bucket.table_size != 0) {
|
||||
LOG_ERROR(Loader, "Compressed NCAs are not supported.");
|
||||
status = Loader::ResultStatus::ErrorCompressedNCA;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
|
||||
if (!ReadRomFSSection(section, header.section_tables[i], bktr_base_ivfc_offset)) {
|
||||
return false;
|
||||
}
|
||||
} else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
|
||||
if (!ReadPFS0Section(section, header.section_tables[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NCA::ReadRomFSSection(const NCASectionHeader& section, const NCASectionTableEntry& entry,
|
||||
u64 bktr_base_ivfc_offset) {
|
||||
const std::size_t base_offset = entry.media_offset * MEDIA_OFFSET_MULTIPLIER;
|
||||
ivfc_offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
const std::size_t romfs_offset = base_offset + ivfc_offset;
|
||||
const std::size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
|
||||
auto raw = std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset);
|
||||
auto dec = Decrypt(section, raw, romfs_offset);
|
||||
|
||||
if (dec == nullptr) {
|
||||
if (status != Loader::ResultStatus::Success)
|
||||
return false;
|
||||
if (has_rights_id)
|
||||
status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek;
|
||||
else
|
||||
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.raw.header.crypto_type == NCASectionCryptoType::BKTR) {
|
||||
if (section.bktr.relocation.magic != Common::MakeMagic('B', 'K', 'T', 'R') ||
|
||||
section.bktr.subsection.magic != Common::MakeMagic('B', 'K', 'T', 'R')) {
|
||||
status = Loader::ResultStatus::ErrorBadBKTRHeader;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.bktr.relocation.offset + section.bktr.relocation.size !=
|
||||
section.bktr.subsection.offset) {
|
||||
status = Loader::ResultStatus::ErrorBKTRSubsectionNotAfterRelocation;
|
||||
return false;
|
||||
}
|
||||
|
||||
const u64 size = MEDIA_OFFSET_MULTIPLIER * (entry.media_end_offset - entry.media_offset);
|
||||
if (section.bktr.subsection.offset + section.bktr.subsection.size != size) {
|
||||
status = Loader::ResultStatus::ErrorBKTRSubsectionNotAtEnd;
|
||||
return false;
|
||||
}
|
||||
|
||||
const u64 offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
RelocationBlock relocation_block{};
|
||||
if (dec->ReadObject(&relocation_block, section.bktr.relocation.offset - offset) !=
|
||||
sizeof(RelocationBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadRelocationBlock;
|
||||
return false;
|
||||
}
|
||||
SubsectionBlock subsection_block{};
|
||||
if (dec->ReadObject(&subsection_block, section.bktr.subsection.offset - offset) !=
|
||||
sizeof(RelocationBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadSubsectionBlock;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<RelocationBucketRaw> relocation_buckets_raw(
|
||||
(section.bktr.relocation.size - sizeof(RelocationBlock)) / sizeof(RelocationBucketRaw));
|
||||
if (dec->ReadBytes(relocation_buckets_raw.data(),
|
||||
section.bktr.relocation.size - sizeof(RelocationBlock),
|
||||
section.bktr.relocation.offset + sizeof(RelocationBlock) - offset) !=
|
||||
section.bktr.relocation.size - sizeof(RelocationBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadRelocationBuckets;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<SubsectionBucketRaw> subsection_buckets_raw(
|
||||
(section.bktr.subsection.size - sizeof(SubsectionBlock)) / sizeof(SubsectionBucketRaw));
|
||||
if (dec->ReadBytes(subsection_buckets_raw.data(),
|
||||
section.bktr.subsection.size - sizeof(SubsectionBlock),
|
||||
section.bktr.subsection.offset + sizeof(SubsectionBlock) - offset) !=
|
||||
section.bktr.subsection.size - sizeof(SubsectionBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadSubsectionBuckets;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<RelocationBucket> relocation_buckets(relocation_buckets_raw.size());
|
||||
std::ranges::transform(relocation_buckets_raw, relocation_buckets.begin(),
|
||||
&ConvertRelocationBucketRaw);
|
||||
std::vector<SubsectionBucket> subsection_buckets(subsection_buckets_raw.size());
|
||||
std::ranges::transform(subsection_buckets_raw, subsection_buckets.begin(),
|
||||
&ConvertSubsectionBucketRaw);
|
||||
|
||||
u32 ctr_low;
|
||||
std::memcpy(&ctr_low, section.raw.section_ctr.data(), sizeof(ctr_low));
|
||||
subsection_buckets.back().entries.push_back({section.bktr.relocation.offset, {0}, ctr_low});
|
||||
subsection_buckets.back().entries.push_back({size, {0}, 0});
|
||||
|
||||
std::optional<Core::Crypto::Key128> key;
|
||||
if (encrypted) {
|
||||
if (has_rights_id) {
|
||||
status = Loader::ResultStatus::Success;
|
||||
key = GetTitlekey();
|
||||
if (!key) {
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekey;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
key = GetKeyAreaKey(NCASectionCryptoType::BKTR);
|
||||
if (!key) {
|
||||
status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (header_reader.GetEncryptionType() == NcaFsHeader::EncryptionType::AesCtrEx) {
|
||||
is_update = true;
|
||||
if (bktr_base_romfs == nullptr) {
|
||||
status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto bktr = std::make_shared<BKTR>(
|
||||
bktr_base_romfs, std::make_shared<OffsetVfsFile>(file, romfs_size, base_offset),
|
||||
relocation_block, relocation_buckets, subsection_block, subsection_buckets, encrypted,
|
||||
encrypted ? *key : Core::Crypto::Key128{}, base_offset, bktr_base_ivfc_offset,
|
||||
section.raw.section_ctr);
|
||||
|
||||
// BKTR applies to entire IVFC, so make an offset version to level 6
|
||||
files.push_back(std::make_shared<OffsetVfsFile>(
|
||||
bktr, romfs_size, section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset));
|
||||
} else {
|
||||
files.push_back(std::move(dec));
|
||||
}
|
||||
|
||||
if (is_update && base_nca == nullptr) {
|
||||
status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS;
|
||||
} else {
|
||||
status = Loader::ResultStatus::Success;
|
||||
}
|
||||
romfs = files.back();
|
||||
return true;
|
||||
}
|
||||
|
||||
NCA::~NCA() = default;
|
||||
bool NCA::ReadPFS0Section(const NCASectionHeader& section, const NCASectionTableEntry& entry) {
|
||||
const u64 offset = (static_cast<u64>(entry.media_offset) * MEDIA_OFFSET_MULTIPLIER) +
|
||||
section.pfs0.pfs0_header_offset;
|
||||
const u64 size = MEDIA_OFFSET_MULTIPLIER * (entry.media_end_offset - entry.media_offset);
|
||||
|
||||
auto dec = Decrypt(section, std::make_shared<OffsetVfsFile>(file, size, offset), offset);
|
||||
if (dec != nullptr) {
|
||||
auto npfs = std::make_shared<PartitionFilesystem>(std::move(dec));
|
||||
|
||||
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
|
||||
dirs.push_back(std::move(npfs));
|
||||
if (IsDirectoryExeFS(dirs.back()))
|
||||
exefs = dirs.back();
|
||||
else if (IsDirectoryLogoPartition(dirs.back()))
|
||||
logo = dirs.back();
|
||||
} else {
|
||||
if (has_rights_id)
|
||||
status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek;
|
||||
else
|
||||
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (status != Loader::ResultStatus::Success)
|
||||
return false;
|
||||
if (has_rights_id)
|
||||
status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek;
|
||||
else
|
||||
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
u8 NCA::GetCryptoRevision() const {
|
||||
u8 master_key_id = header.crypto_type;
|
||||
if (header.crypto_type_2 > master_key_id)
|
||||
master_key_id = header.crypto_type_2;
|
||||
if (master_key_id > 0)
|
||||
--master_key_id;
|
||||
return master_key_id;
|
||||
}
|
||||
|
||||
std::optional<Core::Crypto::Key128> NCA::GetKeyAreaKey(NCASectionCryptoType type) const {
|
||||
const auto master_key_id = GetCryptoRevision();
|
||||
|
||||
if (!keys.HasKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, header.key_index)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<u8> key_area(header.key_area.begin(), header.key_area.end());
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(
|
||||
keys.GetKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, header.key_index),
|
||||
Core::Crypto::Mode::ECB);
|
||||
cipher.Transcode(key_area.data(), key_area.size(), key_area.data(), Core::Crypto::Op::Decrypt);
|
||||
|
||||
Core::Crypto::Key128 out{};
|
||||
if (type == NCASectionCryptoType::XTS) {
|
||||
std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin());
|
||||
} else if (type == NCASectionCryptoType::CTR || type == NCASectionCryptoType::BKTR) {
|
||||
std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
|
||||
} else {
|
||||
LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}",
|
||||
type);
|
||||
}
|
||||
|
||||
u128 out_128{};
|
||||
std::memcpy(out_128.data(), out.data(), sizeof(u128));
|
||||
LOG_TRACE(Crypto, "called with crypto_rev={:02X}, kak_index={:02X}, key={:016X}{:016X}",
|
||||
master_key_id, header.key_index, out_128[1], out_128[0]);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<Core::Crypto::Key128> NCA::GetTitlekey() {
|
||||
const auto master_key_id = GetCryptoRevision();
|
||||
|
||||
u128 rights_id{};
|
||||
memcpy(rights_id.data(), header.rights_id.data(), 16);
|
||||
if (rights_id == u128{}) {
|
||||
status = Loader::ResultStatus::ErrorInvalidRightsID;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto titlekey = keys.GetKey(Core::Crypto::S128KeyType::Titlekey, rights_id[1], rights_id[0]);
|
||||
if (titlekey == Core::Crypto::Key128{}) {
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekey;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!keys.HasKey(Core::Crypto::S128KeyType::Titlekek, master_key_id)) {
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekek;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(
|
||||
keys.GetKey(Core::Crypto::S128KeyType::Titlekek, master_key_id), Core::Crypto::Mode::ECB);
|
||||
cipher.Transcode(titlekey.data(), titlekey.size(), titlekey.data(), Core::Crypto::Op::Decrypt);
|
||||
|
||||
return titlekey;
|
||||
}
|
||||
|
||||
VirtualFile NCA::Decrypt(const NCASectionHeader& s_header, VirtualFile in, u64 starting_offset) {
|
||||
if (!encrypted)
|
||||
return in;
|
||||
|
||||
switch (s_header.raw.header.crypto_type) {
|
||||
case NCASectionCryptoType::NONE:
|
||||
LOG_TRACE(Crypto, "called with mode=NONE");
|
||||
return in;
|
||||
case NCASectionCryptoType::CTR:
|
||||
// During normal BKTR decryption, this entire function is skipped. This is for the metadata,
|
||||
// which uses the same CTR as usual.
|
||||
case NCASectionCryptoType::BKTR:
|
||||
LOG_TRACE(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
|
||||
{
|
||||
std::optional<Core::Crypto::Key128> key;
|
||||
if (has_rights_id) {
|
||||
status = Loader::ResultStatus::Success;
|
||||
key = GetTitlekey();
|
||||
if (!key) {
|
||||
if (status == Loader::ResultStatus::Success)
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekey;
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
key = GetKeyAreaKey(NCASectionCryptoType::CTR);
|
||||
if (!key) {
|
||||
status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
auto out = std::make_shared<Core::Crypto::CTREncryptionLayer>(std::move(in), *key,
|
||||
starting_offset);
|
||||
Core::Crypto::CTREncryptionLayer::IVData iv{};
|
||||
for (std::size_t i = 0; i < 8; ++i) {
|
||||
iv[i] = s_header.raw.section_ctr[8 - i - 1];
|
||||
}
|
||||
out->SetIV(iv);
|
||||
return std::static_pointer_cast<VfsFile>(out);
|
||||
}
|
||||
case NCASectionCryptoType::XTS:
|
||||
// TODO(DarkLordZach): Find a test case for XTS-encrypted NCAs
|
||||
default:
|
||||
LOG_ERROR(Crypto, "called with unhandled crypto type={:02X}",
|
||||
s_header.raw.header.crypto_type);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Loader::ResultStatus NCA::GetStatus() const {
|
||||
return status;
|
||||
@ -143,24 +579,21 @@ VirtualDir NCA::GetParentDirectory() const {
|
||||
}
|
||||
|
||||
NCAContentType NCA::GetType() const {
|
||||
return static_cast<NCAContentType>(reader->GetContentType());
|
||||
return header.content_type;
|
||||
}
|
||||
|
||||
u64 NCA::GetTitleId() const {
|
||||
if (is_update) {
|
||||
return reader->GetProgramId() | 0x800;
|
||||
}
|
||||
return reader->GetProgramId();
|
||||
if (is_update || status == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS)
|
||||
return header.title_id | 0x800;
|
||||
return header.title_id;
|
||||
}
|
||||
|
||||
RightsId NCA::GetRightsId() const {
|
||||
RightsId result;
|
||||
reader->GetRightsId(result.data(), result.size());
|
||||
return result;
|
||||
std::array<u8, 16> NCA::GetRightsId() const {
|
||||
return header.rights_id;
|
||||
}
|
||||
|
||||
u32 NCA::GetSDKVersion() const {
|
||||
return reader->GetSdkAddonVersion();
|
||||
return header.sdk_version;
|
||||
}
|
||||
|
||||
bool NCA::IsUpdate() const {
|
||||
@ -179,6 +612,10 @@ VirtualFile NCA::GetBaseFile() const {
|
||||
return file;
|
||||
}
|
||||
|
||||
u64 NCA::GetBaseIVFCOffset() const {
|
||||
return ivfc_offset;
|
||||
}
|
||||
|
||||
VirtualDir NCA::GetLogoPartition() const {
|
||||
return logo;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ enum class ResultStatus : u16;
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class NcaReader;
|
||||
union NCASectionHeader;
|
||||
|
||||
/// Describes the type of content within an NCA archive.
|
||||
enum class NCAContentType : u8 {
|
||||
@ -45,7 +45,41 @@ enum class NCAContentType : u8 {
|
||||
PublicData = 5,
|
||||
};
|
||||
|
||||
using RightsId = std::array<u8, 0x10>;
|
||||
enum class NCASectionCryptoType : u8 {
|
||||
NONE = 1,
|
||||
XTS = 2,
|
||||
CTR = 3,
|
||||
BKTR = 4,
|
||||
};
|
||||
|
||||
struct NCASectionTableEntry {
|
||||
u32_le media_offset;
|
||||
u32_le media_end_offset;
|
||||
INSERT_PADDING_BYTES(0x8);
|
||||
};
|
||||
static_assert(sizeof(NCASectionTableEntry) == 0x10, "NCASectionTableEntry has incorrect size.");
|
||||
|
||||
struct NCAHeader {
|
||||
std::array<u8, 0x100> rsa_signature_1;
|
||||
std::array<u8, 0x100> rsa_signature_2;
|
||||
u32_le magic;
|
||||
u8 is_system;
|
||||
NCAContentType content_type;
|
||||
u8 crypto_type;
|
||||
u8 key_index;
|
||||
u64_le size;
|
||||
u64_le title_id;
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
u32_le sdk_version;
|
||||
u8 crypto_type_2;
|
||||
INSERT_PADDING_BYTES(15);
|
||||
std::array<u8, 0x10> rights_id;
|
||||
std::array<NCASectionTableEntry, 0x4> section_tables;
|
||||
std::array<std::array<u8, 0x20>, 0x4> hash_tables;
|
||||
std::array<u8, 0x40> key_area;
|
||||
INSERT_PADDING_BYTES(0xC0);
|
||||
};
|
||||
static_assert(sizeof(NCAHeader) == 0x400, "NCAHeader has incorrect size.");
|
||||
|
||||
inline bool IsDirectoryExeFS(const VirtualDir& pfs) {
|
||||
// According to switchbrew, an exefs must only contain these two files:
|
||||
@ -63,7 +97,8 @@ inline bool IsDirectoryLogoPartition(const VirtualDir& pfs) {
|
||||
// After construction, use GetStatus to determine if the file is valid and ready to be used.
|
||||
class NCA : public ReadOnlyVfsDirectory {
|
||||
public:
|
||||
explicit NCA(VirtualFile file, const NCA* base_nca = nullptr);
|
||||
explicit NCA(VirtualFile file, VirtualFile bktr_base_romfs = nullptr,
|
||||
u64 bktr_base_ivfc_offset = 0);
|
||||
~NCA() override;
|
||||
|
||||
Loader::ResultStatus GetStatus() const;
|
||||
@ -75,7 +110,7 @@ public:
|
||||
|
||||
NCAContentType GetType() const;
|
||||
u64 GetTitleId() const;
|
||||
RightsId GetRightsId() const;
|
||||
std::array<u8, 0x10> GetRightsId() const;
|
||||
u32 GetSDKVersion() const;
|
||||
bool IsUpdate() const;
|
||||
|
||||
@ -84,9 +119,26 @@ public:
|
||||
|
||||
VirtualFile GetBaseFile() const;
|
||||
|
||||
// Returns the base ivfc offset used in BKTR patching.
|
||||
u64 GetBaseIVFCOffset() const;
|
||||
|
||||
VirtualDir GetLogoPartition() const;
|
||||
|
||||
private:
|
||||
bool CheckSupportedNCA(const NCAHeader& header);
|
||||
bool HandlePotentialHeaderDecryption();
|
||||
|
||||
std::vector<NCASectionHeader> ReadSectionHeaders() const;
|
||||
bool ReadSections(const std::vector<NCASectionHeader>& sections, u64 bktr_base_ivfc_offset);
|
||||
bool ReadRomFSSection(const NCASectionHeader& section, const NCASectionTableEntry& entry,
|
||||
u64 bktr_base_ivfc_offset);
|
||||
bool ReadPFS0Section(const NCASectionHeader& section, const NCASectionTableEntry& entry);
|
||||
|
||||
u8 GetCryptoRevision() const;
|
||||
std::optional<Core::Crypto::Key128> GetKeyAreaKey(NCASectionCryptoType type) const;
|
||||
std::optional<Core::Crypto::Key128> GetTitlekey();
|
||||
VirtualFile Decrypt(const NCASectionHeader& header, VirtualFile in, u64 starting_offset);
|
||||
|
||||
std::vector<VirtualDir> dirs;
|
||||
std::vector<VirtualFile> files;
|
||||
|
||||
@ -94,6 +146,11 @@ private:
|
||||
VirtualDir exefs = nullptr;
|
||||
VirtualDir logo = nullptr;
|
||||
VirtualFile file;
|
||||
VirtualFile bktr_base_romfs;
|
||||
u64 ivfc_offset = 0;
|
||||
|
||||
NCAHeader header{};
|
||||
bool has_rights_id{};
|
||||
|
||||
Loader::ResultStatus status{};
|
||||
|
||||
@ -101,7 +158,6 @@ private:
|
||||
bool is_update = false;
|
||||
|
||||
Core::Crypto::KeyManager& keys;
|
||||
std::shared_ptr<NcaReader> reader;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
@ -17,74 +17,4 @@ constexpr Result ERROR_INVALID_ARGUMENT{ErrorModule::FS, 6001};
|
||||
constexpr Result ERROR_INVALID_OFFSET{ErrorModule::FS, 6061};
|
||||
constexpr Result ERROR_INVALID_SIZE{ErrorModule::FS, 6062};
|
||||
|
||||
constexpr Result ResultUnsupportedSdkVersion{ErrorModule::FS, 50};
|
||||
constexpr Result ResultPartitionNotFound{ErrorModule::FS, 1001};
|
||||
constexpr Result ResultUnsupportedVersion{ErrorModule::FS, 3002};
|
||||
constexpr Result ResultOutOfRange{ErrorModule::FS, 3005};
|
||||
constexpr Result ResultAllocationMemoryFailedInFileSystemBuddyHeapA{ErrorModule::FS, 3294};
|
||||
constexpr Result ResultAllocationMemoryFailedInNcaFileSystemDriverI{ErrorModule::FS, 3341};
|
||||
constexpr Result ResultAllocationMemoryFailedInNcaReaderA{ErrorModule::FS, 3363};
|
||||
constexpr Result ResultAllocationMemoryFailedInAesCtrCounterExtendedStorageA{ErrorModule::FS, 3399};
|
||||
constexpr Result ResultAllocationMemoryFailedInIntegrityRomFsStorageA{ErrorModule::FS, 3412};
|
||||
constexpr Result ResultAllocationMemoryFailedMakeUnique{ErrorModule::FS, 3422};
|
||||
constexpr Result ResultAllocationMemoryFailedAllocateShared{ErrorModule::FS, 3423};
|
||||
constexpr Result ResultInvalidAesCtrCounterExtendedEntryOffset{ErrorModule::FS, 4012};
|
||||
constexpr Result ResultIndirectStorageCorrupted{ErrorModule::FS, 4021};
|
||||
constexpr Result ResultInvalidIndirectEntryOffset{ErrorModule::FS, 4022};
|
||||
constexpr Result ResultInvalidIndirectEntryStorageIndex{ErrorModule::FS, 4023};
|
||||
constexpr Result ResultInvalidIndirectStorageSize{ErrorModule::FS, 4024};
|
||||
constexpr Result ResultInvalidBucketTreeSignature{ErrorModule::FS, 4032};
|
||||
constexpr Result ResultInvalidBucketTreeEntryCount{ErrorModule::FS, 4033};
|
||||
constexpr Result ResultInvalidBucketTreeNodeEntryCount{ErrorModule::FS, 4034};
|
||||
constexpr Result ResultInvalidBucketTreeNodeOffset{ErrorModule::FS, 4035};
|
||||
constexpr Result ResultInvalidBucketTreeEntryOffset{ErrorModule::FS, 4036};
|
||||
constexpr Result ResultInvalidBucketTreeEntrySetOffset{ErrorModule::FS, 4037};
|
||||
constexpr Result ResultInvalidBucketTreeNodeIndex{ErrorModule::FS, 4038};
|
||||
constexpr Result ResultInvalidBucketTreeVirtualOffset{ErrorModule::FS, 4039};
|
||||
constexpr Result ResultRomNcaInvalidPatchMetaDataHashType{ErrorModule::FS, 4084};
|
||||
constexpr Result ResultRomNcaInvalidIntegrityLayerInfoOffset{ErrorModule::FS, 4085};
|
||||
constexpr Result ResultRomNcaInvalidPatchMetaDataHashDataSize{ErrorModule::FS, 4086};
|
||||
constexpr Result ResultRomNcaInvalidPatchMetaDataHashDataOffset{ErrorModule::FS, 4087};
|
||||
constexpr Result ResultRomNcaInvalidPatchMetaDataHashDataHash{ErrorModule::FS, 4088};
|
||||
constexpr Result ResultRomNcaInvalidSparseMetaDataHashType{ErrorModule::FS, 4089};
|
||||
constexpr Result ResultRomNcaInvalidSparseMetaDataHashDataSize{ErrorModule::FS, 4090};
|
||||
constexpr Result ResultRomNcaInvalidSparseMetaDataHashDataOffset{ErrorModule::FS, 4091};
|
||||
constexpr Result ResultRomNcaInvalidSparseMetaDataHashDataHash{ErrorModule::FS, 4091};
|
||||
constexpr Result ResultNcaBaseStorageOutOfRangeB{ErrorModule::FS, 4509};
|
||||
constexpr Result ResultNcaBaseStorageOutOfRangeC{ErrorModule::FS, 4510};
|
||||
constexpr Result ResultNcaBaseStorageOutOfRangeD{ErrorModule::FS, 4511};
|
||||
constexpr Result ResultInvalidNcaSignature{ErrorModule::FS, 4517};
|
||||
constexpr Result ResultNcaFsHeaderHashVerificationFailed{ErrorModule::FS, 4520};
|
||||
constexpr Result ResultInvalidNcaKeyIndex{ErrorModule::FS, 4521};
|
||||
constexpr Result ResultInvalidNcaFsHeaderHashType{ErrorModule::FS, 4522};
|
||||
constexpr Result ResultInvalidNcaFsHeaderEncryptionType{ErrorModule::FS, 4523};
|
||||
constexpr Result ResultInvalidNcaPatchInfoIndirectSize{ErrorModule::FS, 4524};
|
||||
constexpr Result ResultInvalidNcaPatchInfoAesCtrExSize{ErrorModule::FS, 4525};
|
||||
constexpr Result ResultInvalidNcaPatchInfoAesCtrExOffset{ErrorModule::FS, 4526};
|
||||
constexpr Result ResultInvalidNcaHeader{ErrorModule::FS, 4528};
|
||||
constexpr Result ResultInvalidNcaFsHeader{ErrorModule::FS, 4529};
|
||||
constexpr Result ResultNcaBaseStorageOutOfRangeE{ErrorModule::FS, 4530};
|
||||
constexpr Result ResultInvalidHierarchicalSha256BlockSize{ErrorModule::FS, 4532};
|
||||
constexpr Result ResultInvalidHierarchicalSha256LayerCount{ErrorModule::FS, 4533};
|
||||
constexpr Result ResultHierarchicalSha256BaseStorageTooLarge{ErrorModule::FS, 4534};
|
||||
constexpr Result ResultHierarchicalSha256HashVerificationFailed{ErrorModule::FS, 4535};
|
||||
constexpr Result ResultInvalidNcaHierarchicalIntegrityVerificationLayerCount{ErrorModule::FS, 4541};
|
||||
constexpr Result ResultInvalidNcaIndirectStorageOutOfRange{ErrorModule::FS, 4542};
|
||||
constexpr Result ResultInvalidNcaHeader1SignatureKeyGeneration{ErrorModule::FS, 4543};
|
||||
constexpr Result ResultInvalidCompressedStorageSize{ErrorModule::FS, 4547};
|
||||
constexpr Result ResultInvalidNcaMetaDataHashDataSize{ErrorModule::FS, 4548};
|
||||
constexpr Result ResultInvalidNcaMetaDataHashDataHash{ErrorModule::FS, 4549};
|
||||
constexpr Result ResultUnexpectedInCompressedStorageA{ErrorModule::FS, 5324};
|
||||
constexpr Result ResultUnexpectedInCompressedStorageB{ErrorModule::FS, 5325};
|
||||
constexpr Result ResultUnexpectedInCompressedStorageC{ErrorModule::FS, 5326};
|
||||
constexpr Result ResultUnexpectedInCompressedStorageD{ErrorModule::FS, 5327};
|
||||
constexpr Result ResultInvalidArgument{ErrorModule::FS, 6001};
|
||||
constexpr Result ResultInvalidOffset{ErrorModule::FS, 6061};
|
||||
constexpr Result ResultInvalidSize{ErrorModule::FS, 6062};
|
||||
constexpr Result ResultNullptrArgument{ErrorModule::FS, 6063};
|
||||
constexpr Result ResultUnsupportedSetSizeForIndirectStorage{ErrorModule::FS, 6325};
|
||||
constexpr Result ResultUnsupportedWriteForCompressedStorage{ErrorModule::FS, 6387};
|
||||
constexpr Result ResultUnsupportedOperateRangeForCompressedStorage{ErrorModule::FS, 6388};
|
||||
constexpr Result ResultBufferAllocationFailed{ErrorModule::FS, 6705};
|
||||
|
||||
} // namespace FileSys
|
||||
|
@ -1,58 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/overflow.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class IStorage : public VfsFile {
|
||||
public:
|
||||
virtual std::string GetName() const override {
|
||||
return {};
|
||||
}
|
||||
|
||||
virtual VirtualDir GetContainingDirectory() const override {
|
||||
return {};
|
||||
}
|
||||
|
||||
virtual bool IsWritable() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool IsReadable() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool Resize(size_t size) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool Rename(std::string_view name) override {
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline Result CheckAccessRange(s64 offset, s64 size, s64 total_size) {
|
||||
R_UNLESS(offset >= 0, ResultInvalidOffset);
|
||||
R_UNLESS(size >= 0, ResultInvalidSize);
|
||||
R_UNLESS(Common::WrappingAdd(offset, size) >= offset, ResultOutOfRange);
|
||||
R_UNLESS(offset + size <= total_size, ResultOutOfRange);
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
class IReadOnlyStorage : public IStorage {
|
||||
public:
|
||||
virtual bool IsWritable() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,46 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/common_funcs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
struct Int64 {
|
||||
u32 low;
|
||||
u32 high;
|
||||
|
||||
constexpr void Set(s64 v) {
|
||||
this->low = static_cast<u32>((v & static_cast<u64>(0x00000000FFFFFFFFULL)) >> 0);
|
||||
this->high = static_cast<u32>((v & static_cast<u64>(0xFFFFFFFF00000000ULL)) >> 32);
|
||||
}
|
||||
|
||||
constexpr s64 Get() const {
|
||||
return (static_cast<s64>(this->high) << 32) | (static_cast<s64>(this->low));
|
||||
}
|
||||
|
||||
constexpr Int64& operator=(s64 v) {
|
||||
this->Set(v);
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr operator s64() const {
|
||||
return this->Get();
|
||||
}
|
||||
};
|
||||
|
||||
struct HashSalt {
|
||||
static constexpr size_t Size = 32;
|
||||
|
||||
std::array<u8, Size> value;
|
||||
};
|
||||
static_assert(std::is_trivial_v<HashSalt>);
|
||||
static_assert(sizeof(HashSalt) == HashSalt::Size);
|
||||
|
||||
constexpr inline size_t IntegrityMinLayerCount = 2;
|
||||
constexpr inline size_t IntegrityMaxLayerCount = 7;
|
||||
constexpr inline size_t IntegrityLayerCountSave = 5;
|
||||
constexpr inline size_t IntegrityLayerCountSaveDataMeta = 4;
|
||||
|
||||
} // namespace FileSys
|
@ -1,251 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_aes_ctr_counter_extended_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_aes_ctr_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_nca_header.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
class SoftwareDecryptor final : public AesCtrCounterExtendedStorage::IDecryptor {
|
||||
public:
|
||||
virtual void Decrypt(
|
||||
u8* buf, size_t buf_size, const std::array<u8, AesCtrCounterExtendedStorage::KeySize>& key,
|
||||
const std::array<u8, AesCtrCounterExtendedStorage::IvSize>& iv) override final;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result AesCtrCounterExtendedStorage::CreateSoftwareDecryptor(std::unique_ptr<IDecryptor>* out) {
|
||||
std::unique_ptr<IDecryptor> decryptor = std::make_unique<SoftwareDecryptor>();
|
||||
R_UNLESS(decryptor != nullptr, ResultAllocationMemoryFailedInAesCtrCounterExtendedStorageA);
|
||||
*out = std::move(decryptor);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result AesCtrCounterExtendedStorage::Initialize(const void* key, size_t key_size, u32 secure_value,
|
||||
VirtualFile data_storage,
|
||||
VirtualFile table_storage) {
|
||||
// Read and verify the bucket tree header.
|
||||
BucketTree::Header header;
|
||||
table_storage->ReadObject(std::addressof(header), 0);
|
||||
R_TRY(header.Verify());
|
||||
|
||||
// Determine extents.
|
||||
const auto node_storage_size = QueryNodeStorageSize(header.entry_count);
|
||||
const auto entry_storage_size = QueryEntryStorageSize(header.entry_count);
|
||||
const auto node_storage_offset = QueryHeaderStorageSize();
|
||||
const auto entry_storage_offset = node_storage_offset + node_storage_size;
|
||||
|
||||
// Create a software decryptor.
|
||||
std::unique_ptr<IDecryptor> sw_decryptor;
|
||||
R_TRY(CreateSoftwareDecryptor(std::addressof(sw_decryptor)));
|
||||
|
||||
// Initialize.
|
||||
R_RETURN(this->Initialize(
|
||||
key, key_size, secure_value, 0, data_storage,
|
||||
std::make_shared<OffsetVfsFile>(table_storage, node_storage_size, node_storage_offset),
|
||||
std::make_shared<OffsetVfsFile>(table_storage, entry_storage_size, entry_storage_offset),
|
||||
header.entry_count, std::move(sw_decryptor)));
|
||||
}
|
||||
|
||||
Result AesCtrCounterExtendedStorage::Initialize(const void* key, size_t key_size, u32 secure_value,
|
||||
s64 counter_offset, VirtualFile data_storage,
|
||||
VirtualFile node_storage, VirtualFile entry_storage,
|
||||
s32 entry_count,
|
||||
std::unique_ptr<IDecryptor>&& decryptor) {
|
||||
// Validate preconditions.
|
||||
ASSERT(key != nullptr);
|
||||
ASSERT(key_size == KeySize);
|
||||
ASSERT(counter_offset >= 0);
|
||||
ASSERT(decryptor != nullptr);
|
||||
|
||||
// Initialize the bucket tree table.
|
||||
if (entry_count > 0) {
|
||||
R_TRY(
|
||||
m_table.Initialize(node_storage, entry_storage, NodeSize, sizeof(Entry), entry_count));
|
||||
} else {
|
||||
m_table.Initialize(NodeSize, 0);
|
||||
}
|
||||
|
||||
// Set members.
|
||||
m_data_storage = data_storage;
|
||||
std::memcpy(m_key.data(), key, key_size);
|
||||
m_secure_value = secure_value;
|
||||
m_counter_offset = counter_offset;
|
||||
m_decryptor = std::move(decryptor);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void AesCtrCounterExtendedStorage::Finalize() {
|
||||
if (this->IsInitialized()) {
|
||||
m_table.Finalize();
|
||||
m_data_storage = VirtualFile();
|
||||
}
|
||||
}
|
||||
|
||||
Result AesCtrCounterExtendedStorage::GetEntryList(Entry* out_entries, s32* out_entry_count,
|
||||
s32 entry_count, s64 offset, s64 size) {
|
||||
// Validate pre-conditions.
|
||||
ASSERT(offset >= 0);
|
||||
ASSERT(size >= 0);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Clear the out count.
|
||||
R_UNLESS(out_entry_count != nullptr, ResultNullptrArgument);
|
||||
*out_entry_count = 0;
|
||||
|
||||
// Succeed if there's no range.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// If we have an output array, we need it to be non-null.
|
||||
R_UNLESS(out_entries != nullptr || entry_count == 0, ResultNullptrArgument);
|
||||
|
||||
// Check that our range is valid.
|
||||
BucketTree::Offsets table_offsets;
|
||||
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||
|
||||
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||
|
||||
// Find the offset in our tree.
|
||||
BucketTree::Visitor visitor;
|
||||
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||
{
|
||||
const auto entry_offset = visitor.Get<Entry>()->GetOffset();
|
||||
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||
ResultInvalidAesCtrCounterExtendedEntryOffset);
|
||||
}
|
||||
|
||||
// Prepare to loop over entries.
|
||||
const auto end_offset = offset + static_cast<s64>(size);
|
||||
s32 count = 0;
|
||||
|
||||
auto cur_entry = *visitor.Get<Entry>();
|
||||
while (cur_entry.GetOffset() < end_offset) {
|
||||
// Try to write the entry to the out list.
|
||||
if (entry_count != 0) {
|
||||
if (count >= entry_count) {
|
||||
break;
|
||||
}
|
||||
std::memcpy(out_entries + count, std::addressof(cur_entry), sizeof(Entry));
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
// Advance.
|
||||
if (visitor.CanMoveNext()) {
|
||||
R_TRY(visitor.MoveNext());
|
||||
cur_entry = *visitor.Get<Entry>();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the output count.
|
||||
*out_entry_count = count;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
size_t AesCtrCounterExtendedStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||
// Validate preconditions.
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Allow zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
ASSERT(Common::IsAligned(offset, BlockSize));
|
||||
ASSERT(Common::IsAligned(size, BlockSize));
|
||||
|
||||
BucketTree::Offsets table_offsets;
|
||||
ASSERT(R_SUCCEEDED(m_table.GetOffsets(std::addressof(table_offsets))));
|
||||
|
||||
ASSERT(table_offsets.IsInclude(offset, size));
|
||||
|
||||
// Read the data.
|
||||
m_data_storage->Read(buffer, size, offset);
|
||||
|
||||
// Find the offset in our tree.
|
||||
BucketTree::Visitor visitor;
|
||||
ASSERT(R_SUCCEEDED(m_table.Find(std::addressof(visitor), offset)));
|
||||
{
|
||||
const auto entry_offset = visitor.Get<Entry>()->GetOffset();
|
||||
ASSERT(Common::IsAligned(entry_offset, BlockSize));
|
||||
ASSERT(0 <= entry_offset && table_offsets.IsInclude(entry_offset));
|
||||
}
|
||||
|
||||
// Prepare to read in chunks.
|
||||
u8* cur_data = static_cast<u8*>(buffer);
|
||||
auto cur_offset = offset;
|
||||
const auto end_offset = offset + static_cast<s64>(size);
|
||||
|
||||
while (cur_offset < end_offset) {
|
||||
// Get the current entry.
|
||||
const auto cur_entry = *visitor.Get<Entry>();
|
||||
|
||||
// Get and validate the entry's offset.
|
||||
const auto cur_entry_offset = cur_entry.GetOffset();
|
||||
ASSERT(static_cast<size_t>(cur_entry_offset) <= cur_offset);
|
||||
|
||||
// Get and validate the next entry offset.
|
||||
s64 next_entry_offset;
|
||||
if (visitor.CanMoveNext()) {
|
||||
ASSERT(R_SUCCEEDED(visitor.MoveNext()));
|
||||
next_entry_offset = visitor.Get<Entry>()->GetOffset();
|
||||
ASSERT(table_offsets.IsInclude(next_entry_offset));
|
||||
} else {
|
||||
next_entry_offset = table_offsets.end_offset;
|
||||
}
|
||||
ASSERT(Common::IsAligned(next_entry_offset, BlockSize));
|
||||
ASSERT(cur_offset < static_cast<size_t>(next_entry_offset));
|
||||
|
||||
// Get the offset of the entry in the data we read.
|
||||
const auto data_offset = cur_offset - cur_entry_offset;
|
||||
const auto data_size = (next_entry_offset - cur_entry_offset) - data_offset;
|
||||
ASSERT(data_size > 0);
|
||||
|
||||
// Determine how much is left.
|
||||
const auto remaining_size = end_offset - cur_offset;
|
||||
const auto cur_size = static_cast<size_t>(std::min(remaining_size, data_size));
|
||||
ASSERT(cur_size <= size);
|
||||
|
||||
// If necessary, perform decryption.
|
||||
if (cur_entry.encryption_value == Entry::Encryption::Encrypted) {
|
||||
// Make the CTR for the data we're decrypting.
|
||||
const auto counter_offset = m_counter_offset + cur_entry_offset + data_offset;
|
||||
NcaAesCtrUpperIv upper_iv = {
|
||||
.part = {.generation = static_cast<u32>(cur_entry.generation),
|
||||
.secure_value = m_secure_value}};
|
||||
|
||||
std::array<u8, IvSize> iv;
|
||||
AesCtrStorage::MakeIv(iv.data(), IvSize, upper_iv.value, counter_offset);
|
||||
|
||||
// Decrypt.
|
||||
m_decryptor->Decrypt(cur_data, cur_size, m_key, iv);
|
||||
}
|
||||
|
||||
// Advance.
|
||||
cur_data += cur_size;
|
||||
cur_offset += cur_size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void SoftwareDecryptor::Decrypt(u8* buf, size_t buf_size,
|
||||
const std::array<u8, AesCtrCounterExtendedStorage::KeySize>& key,
|
||||
const std::array<u8, AesCtrCounterExtendedStorage::IvSize>& iv) {
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key128, AesCtrCounterExtendedStorage::KeySize> cipher(
|
||||
key, Core::Crypto::Mode::CTR);
|
||||
cipher.SetIV(iv);
|
||||
cipher.Transcode(buf, buf_size, buf, Core::Crypto::Op::Decrypt);
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,114 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "common/literals.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
using namespace Common::Literals;
|
||||
|
||||
class AesCtrCounterExtendedStorage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(AesCtrCounterExtendedStorage);
|
||||
YUZU_NON_MOVEABLE(AesCtrCounterExtendedStorage);
|
||||
|
||||
public:
|
||||
static constexpr size_t BlockSize = 0x10;
|
||||
static constexpr size_t KeySize = 0x10;
|
||||
static constexpr size_t IvSize = 0x10;
|
||||
static constexpr size_t NodeSize = 16_KiB;
|
||||
|
||||
class IDecryptor {
|
||||
public:
|
||||
virtual ~IDecryptor() {}
|
||||
virtual void Decrypt(u8* buf, size_t buf_size, const std::array<u8, KeySize>& key,
|
||||
const std::array<u8, IvSize>& iv) = 0;
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
enum class Encryption : u8 {
|
||||
Encrypted = 0,
|
||||
NotEncrypted = 1,
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(s64)> offset;
|
||||
Encryption encryption_value;
|
||||
std::array<u8, 3> reserved;
|
||||
s32 generation;
|
||||
|
||||
void SetOffset(s64 value) {
|
||||
std::memcpy(this->offset.data(), std::addressof(value), sizeof(s64));
|
||||
}
|
||||
|
||||
s64 GetOffset() const {
|
||||
s64 value;
|
||||
std::memcpy(std::addressof(value), this->offset.data(), sizeof(s64));
|
||||
return value;
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(Entry) == 0x10);
|
||||
static_assert(alignof(Entry) == 4);
|
||||
static_assert(std::is_trivial_v<Entry>);
|
||||
|
||||
public:
|
||||
static constexpr s64 QueryHeaderStorageSize() {
|
||||
return BucketTree::QueryHeaderStorageSize();
|
||||
}
|
||||
|
||||
static constexpr s64 QueryNodeStorageSize(s32 entry_count) {
|
||||
return BucketTree::QueryNodeStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||
}
|
||||
|
||||
static constexpr s64 QueryEntryStorageSize(s32 entry_count) {
|
||||
return BucketTree::QueryEntryStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||
}
|
||||
|
||||
static Result CreateSoftwareDecryptor(std::unique_ptr<IDecryptor>* out);
|
||||
|
||||
public:
|
||||
AesCtrCounterExtendedStorage()
|
||||
: m_table(), m_data_storage(), m_secure_value(), m_counter_offset(), m_decryptor() {}
|
||||
virtual ~AesCtrCounterExtendedStorage() {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
Result Initialize(const void* key, size_t key_size, u32 secure_value, s64 counter_offset,
|
||||
VirtualFile data_storage, VirtualFile node_storage, VirtualFile entry_storage,
|
||||
s32 entry_count, std::unique_ptr<IDecryptor>&& decryptor);
|
||||
void Finalize();
|
||||
|
||||
bool IsInitialized() const {
|
||||
return m_table.IsInitialized();
|
||||
}
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
BucketTree::Offsets offsets;
|
||||
ASSERT(R_SUCCEEDED(m_table.GetOffsets(std::addressof(offsets))));
|
||||
|
||||
return offsets.end_offset;
|
||||
}
|
||||
|
||||
Result GetEntryList(Entry* out_entries, s32* out_entry_count, s32 entry_count, s64 offset,
|
||||
s64 size);
|
||||
|
||||
private:
|
||||
Result Initialize(const void* key, size_t key_size, u32 secure_value, VirtualFile data_storage,
|
||||
VirtualFile table_storage);
|
||||
|
||||
private:
|
||||
mutable BucketTree m_table;
|
||||
VirtualFile m_data_storage;
|
||||
std::array<u8, KeySize> m_key;
|
||||
u32 m_secure_value;
|
||||
s64 m_counter_offset;
|
||||
std::unique_ptr<IDecryptor> m_decryptor;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,129 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/fssystem/fssystem_aes_ctr_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||
#include "core/file_sys/fssystem/fssystem_utility.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
void AesCtrStorage::MakeIv(void* dst, size_t dst_size, u64 upper, s64 offset) {
|
||||
ASSERT(dst != nullptr);
|
||||
ASSERT(dst_size == IvSize);
|
||||
ASSERT(offset >= 0);
|
||||
|
||||
const uintptr_t out_addr = reinterpret_cast<uintptr_t>(dst);
|
||||
|
||||
*reinterpret_cast<u64_be*>(out_addr + 0) = upper;
|
||||
*reinterpret_cast<s64_be*>(out_addr + sizeof(u64)) = static_cast<s64>(offset / BlockSize);
|
||||
}
|
||||
|
||||
AesCtrStorage::AesCtrStorage(VirtualFile base, const void* key, size_t key_size, const void* iv,
|
||||
size_t iv_size)
|
||||
: m_base_storage(std::move(base)) {
|
||||
ASSERT(m_base_storage != nullptr);
|
||||
ASSERT(key != nullptr);
|
||||
ASSERT(iv != nullptr);
|
||||
ASSERT(key_size == KeySize);
|
||||
ASSERT(iv_size == IvSize);
|
||||
|
||||
std::memcpy(m_key.data(), key, KeySize);
|
||||
std::memcpy(m_iv.data(), iv, IvSize);
|
||||
|
||||
m_cipher.emplace(m_key, Core::Crypto::Mode::CTR);
|
||||
}
|
||||
|
||||
size_t AesCtrStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||
// Allow zero-size reads.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Ensure buffer is valid.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// We can only read at block aligned offsets.
|
||||
ASSERT(Common::IsAligned(offset, BlockSize));
|
||||
ASSERT(Common::IsAligned(size, BlockSize));
|
||||
|
||||
// Read the data.
|
||||
m_base_storage->Read(buffer, size, offset);
|
||||
|
||||
// Setup the counter.
|
||||
std::array<u8, IvSize> ctr;
|
||||
std::memcpy(ctr.data(), m_iv.data(), IvSize);
|
||||
AddCounter(ctr.data(), IvSize, offset / BlockSize);
|
||||
|
||||
// Decrypt.
|
||||
m_cipher->SetIV(ctr);
|
||||
m_cipher->Transcode(buffer, size, buffer, Core::Crypto::Op::Decrypt);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t AesCtrStorage::Write(const u8* buffer, size_t size, size_t offset) {
|
||||
// Allow zero-size writes.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Ensure buffer is valid.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// We can only write at block aligned offsets.
|
||||
ASSERT(Common::IsAligned(offset, BlockSize));
|
||||
ASSERT(Common::IsAligned(size, BlockSize));
|
||||
|
||||
// Get a pooled buffer.
|
||||
PooledBuffer pooled_buffer;
|
||||
const bool use_work_buffer = true;
|
||||
if (use_work_buffer) {
|
||||
pooled_buffer.Allocate(size, BlockSize);
|
||||
}
|
||||
|
||||
// Setup the counter.
|
||||
std::array<u8, IvSize> ctr;
|
||||
std::memcpy(ctr.data(), m_iv.data(), IvSize);
|
||||
AddCounter(ctr.data(), IvSize, offset / BlockSize);
|
||||
|
||||
// Loop until all data is written.
|
||||
size_t remaining = size;
|
||||
s64 cur_offset = 0;
|
||||
while (remaining > 0) {
|
||||
// Determine data we're writing and where.
|
||||
const size_t write_size =
|
||||
use_work_buffer ? std::min(pooled_buffer.GetSize(), remaining) : remaining;
|
||||
|
||||
void* write_buf;
|
||||
if (use_work_buffer) {
|
||||
write_buf = pooled_buffer.GetBuffer();
|
||||
} else {
|
||||
write_buf = const_cast<u8*>(buffer);
|
||||
}
|
||||
|
||||
// Encrypt the data.
|
||||
m_cipher->SetIV(ctr);
|
||||
m_cipher->Transcode(buffer, write_size, reinterpret_cast<u8*>(write_buf),
|
||||
Core::Crypto::Op::Encrypt);
|
||||
|
||||
// Write the encrypted data.
|
||||
m_base_storage->Write(reinterpret_cast<u8*>(write_buf), write_size, offset + cur_offset);
|
||||
|
||||
// Advance.
|
||||
cur_offset += write_size;
|
||||
remaining -= write_size;
|
||||
if (remaining > 0) {
|
||||
AddCounter(ctr.data(), IvSize, write_size / BlockSize);
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t AesCtrStorage::GetSize() const {
|
||||
return m_base_storage->GetSize();
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,43 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class AesCtrStorage : public IStorage {
|
||||
YUZU_NON_COPYABLE(AesCtrStorage);
|
||||
YUZU_NON_MOVEABLE(AesCtrStorage);
|
||||
|
||||
public:
|
||||
static constexpr size_t BlockSize = 0x10;
|
||||
static constexpr size_t KeySize = 0x10;
|
||||
static constexpr size_t IvSize = 0x10;
|
||||
|
||||
public:
|
||||
static void MakeIv(void* dst, size_t dst_size, u64 upper, s64 offset);
|
||||
|
||||
public:
|
||||
AesCtrStorage(VirtualFile base, const void* key, size_t key_size, const void* iv,
|
||||
size_t iv_size);
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override;
|
||||
virtual size_t GetSize() const override;
|
||||
|
||||
private:
|
||||
VirtualFile m_base_storage;
|
||||
std::array<u8, KeySize> m_key;
|
||||
std::array<u8, IvSize> m_iv;
|
||||
mutable std::optional<Core::Crypto::AESCipher<Core::Crypto::Key128>> m_cipher;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,112 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fssystem_aes_xts_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||
#include "core/file_sys/fssystem/fssystem_utility.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
void AesXtsStorage::MakeAesXtsIv(void* dst, size_t dst_size, s64 offset, size_t block_size) {
|
||||
ASSERT(dst != nullptr);
|
||||
ASSERT(dst_size == IvSize);
|
||||
ASSERT(offset >= 0);
|
||||
|
||||
const uintptr_t out_addr = reinterpret_cast<uintptr_t>(dst);
|
||||
|
||||
*reinterpret_cast<s64_be*>(out_addr + sizeof(s64)) = offset / block_size;
|
||||
}
|
||||
|
||||
AesXtsStorage::AesXtsStorage(VirtualFile base, const void* key1, const void* key2, size_t key_size,
|
||||
const void* iv, size_t iv_size, size_t block_size)
|
||||
: m_base_storage(std::move(base)), m_block_size(block_size), m_mutex() {
|
||||
ASSERT(m_base_storage != nullptr);
|
||||
ASSERT(key1 != nullptr);
|
||||
ASSERT(key2 != nullptr);
|
||||
ASSERT(iv != nullptr);
|
||||
ASSERT(key_size == KeySize);
|
||||
ASSERT(iv_size == IvSize);
|
||||
ASSERT(Common::IsAligned(m_block_size, AesBlockSize));
|
||||
|
||||
std::memcpy(m_key.data() + 0, key1, KeySize);
|
||||
std::memcpy(m_key.data() + 0x10, key2, KeySize);
|
||||
std::memcpy(m_iv.data(), iv, IvSize);
|
||||
|
||||
m_cipher.emplace(m_key, Core::Crypto::Mode::XTS);
|
||||
}
|
||||
|
||||
size_t AesXtsStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||
// Allow zero-size reads.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Ensure buffer is valid.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// We can only read at block aligned offsets.
|
||||
ASSERT(Common::IsAligned(offset, AesBlockSize));
|
||||
ASSERT(Common::IsAligned(size, AesBlockSize));
|
||||
|
||||
// Read the data.
|
||||
m_base_storage->Read(buffer, size, offset);
|
||||
|
||||
// Setup the counter.
|
||||
std::array<u8, IvSize> ctr;
|
||||
std::memcpy(ctr.data(), m_iv.data(), IvSize);
|
||||
AddCounter(ctr.data(), IvSize, offset / m_block_size);
|
||||
|
||||
// Handle any unaligned data before the start.
|
||||
size_t processed_size = 0;
|
||||
if ((offset % m_block_size) != 0) {
|
||||
// Determine the size of the pre-data read.
|
||||
const size_t skip_size =
|
||||
static_cast<size_t>(offset - Common::AlignDown(offset, m_block_size));
|
||||
const size_t data_size = std::min(size, m_block_size - skip_size);
|
||||
|
||||
// Decrypt into a pooled buffer.
|
||||
{
|
||||
PooledBuffer tmp_buf(m_block_size, m_block_size);
|
||||
ASSERT(tmp_buf.GetSize() >= m_block_size);
|
||||
|
||||
std::memset(tmp_buf.GetBuffer(), 0, skip_size);
|
||||
std::memcpy(tmp_buf.GetBuffer() + skip_size, buffer, data_size);
|
||||
|
||||
m_cipher->SetIV(ctr);
|
||||
m_cipher->Transcode(tmp_buf.GetBuffer(), m_block_size, tmp_buf.GetBuffer(),
|
||||
Core::Crypto::Op::Decrypt);
|
||||
|
||||
std::memcpy(buffer, tmp_buf.GetBuffer() + skip_size, data_size);
|
||||
}
|
||||
|
||||
AddCounter(ctr.data(), IvSize, 1);
|
||||
processed_size += data_size;
|
||||
ASSERT(processed_size == std::min(size, m_block_size - skip_size));
|
||||
}
|
||||
|
||||
// Decrypt aligned chunks.
|
||||
char* cur = reinterpret_cast<char*>(buffer) + processed_size;
|
||||
size_t remaining = size - processed_size;
|
||||
while (remaining > 0) {
|
||||
const size_t cur_size = std::min(m_block_size, remaining);
|
||||
|
||||
m_cipher->SetIV(ctr);
|
||||
m_cipher->Transcode(cur, cur_size, cur, Core::Crypto::Op::Decrypt);
|
||||
|
||||
remaining -= cur_size;
|
||||
cur += cur_size;
|
||||
|
||||
AddCounter(ctr.data(), IvSize, 1);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t AesXtsStorage::GetSize() const {
|
||||
return m_base_storage->GetSize();
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,42 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class AesXtsStorage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(AesXtsStorage);
|
||||
YUZU_NON_MOVEABLE(AesXtsStorage);
|
||||
|
||||
public:
|
||||
static constexpr size_t AesBlockSize = 0x10;
|
||||
static constexpr size_t KeySize = 0x20;
|
||||
static constexpr size_t IvSize = 0x10;
|
||||
|
||||
public:
|
||||
static void MakeAesXtsIv(void* dst, size_t dst_size, s64 offset, size_t block_size);
|
||||
|
||||
public:
|
||||
AesXtsStorage(VirtualFile base, const void* key1, const void* key2, size_t key_size,
|
||||
const void* iv, size_t iv_size, size_t block_size);
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||
virtual size_t GetSize() const override;
|
||||
|
||||
private:
|
||||
VirtualFile m_base_storage;
|
||||
std::array<u8, KeySize> m_key;
|
||||
std::array<u8, IvSize> m_iv;
|
||||
const size_t m_block_size;
|
||||
std::mutex m_mutex;
|
||||
mutable std::optional<Core::Crypto::AESCipher<Core::Crypto::Key256>> m_cipher;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,146 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_alignment_matching_storage_impl.h"
|
||||
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
template <size_t DataAlign_, size_t BufferAlign_>
|
||||
class AlignmentMatchingStorage : public IStorage {
|
||||
YUZU_NON_COPYABLE(AlignmentMatchingStorage);
|
||||
YUZU_NON_MOVEABLE(AlignmentMatchingStorage);
|
||||
|
||||
public:
|
||||
static constexpr size_t DataAlign = DataAlign_;
|
||||
static constexpr size_t BufferAlign = BufferAlign_;
|
||||
|
||||
static constexpr size_t DataAlignMax = 0x200;
|
||||
static_assert(DataAlign <= DataAlignMax);
|
||||
static_assert(Common::IsPowerOfTwo(DataAlign));
|
||||
static_assert(Common::IsPowerOfTwo(BufferAlign));
|
||||
|
||||
private:
|
||||
VirtualFile m_base_storage;
|
||||
s64 m_base_storage_size;
|
||||
|
||||
public:
|
||||
explicit AlignmentMatchingStorage(VirtualFile bs) : m_base_storage(std::move(bs)) {}
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||
// Allocate a work buffer on stack.
|
||||
alignas(DataAlignMax) std::array<char, DataAlign> work_buf;
|
||||
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
s64 bs_size = this->GetSize();
|
||||
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||
|
||||
return AlignmentMatchingStorageImpl::Read(m_base_storage, work_buf.data(), work_buf.size(),
|
||||
DataAlign, BufferAlign, offset, buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||
// Allocate a work buffer on stack.
|
||||
alignas(DataAlignMax) std::array<char, DataAlign> work_buf;
|
||||
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
s64 bs_size = this->GetSize();
|
||||
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||
|
||||
return AlignmentMatchingStorageImpl::Write(m_base_storage, work_buf.data(), work_buf.size(),
|
||||
DataAlign, BufferAlign, offset, buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
return m_base_storage->GetSize();
|
||||
}
|
||||
};
|
||||
|
||||
template <size_t BufferAlign_>
|
||||
class AlignmentMatchingStoragePooledBuffer : public IStorage {
|
||||
YUZU_NON_COPYABLE(AlignmentMatchingStoragePooledBuffer);
|
||||
YUZU_NON_MOVEABLE(AlignmentMatchingStoragePooledBuffer);
|
||||
|
||||
public:
|
||||
static constexpr size_t BufferAlign = BufferAlign_;
|
||||
|
||||
static_assert(Common::IsPowerOfTwo(BufferAlign));
|
||||
|
||||
private:
|
||||
VirtualFile m_base_storage;
|
||||
s64 m_base_storage_size;
|
||||
size_t m_data_align;
|
||||
|
||||
public:
|
||||
explicit AlignmentMatchingStoragePooledBuffer(VirtualFile bs, size_t da)
|
||||
: m_base_storage(std::move(bs)), m_data_align(da) {
|
||||
ASSERT(Common::IsPowerOfTwo(da));
|
||||
}
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
s64 bs_size = this->GetSize();
|
||||
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||
|
||||
// Allocate a pooled buffer.
|
||||
PooledBuffer pooled_buffer;
|
||||
pooled_buffer.AllocateParticularlyLarge(m_data_align, m_data_align);
|
||||
|
||||
return AlignmentMatchingStorageImpl::Read(m_base_storage, pooled_buffer.GetBuffer(),
|
||||
pooled_buffer.GetSize(), m_data_align,
|
||||
BufferAlign, offset, buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
s64 bs_size = this->GetSize();
|
||||
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(offset, size, bs_size)));
|
||||
|
||||
// Allocate a pooled buffer.
|
||||
PooledBuffer pooled_buffer;
|
||||
pooled_buffer.AllocateParticularlyLarge(m_data_align, m_data_align);
|
||||
|
||||
return AlignmentMatchingStorageImpl::Write(m_base_storage, pooled_buffer.GetBuffer(),
|
||||
pooled_buffer.GetSize(), m_data_align,
|
||||
BufferAlign, offset, buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
return m_base_storage->GetSize();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,204 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "core/file_sys/fssystem/fssystem_alignment_matching_storage_impl.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
constexpr size_t GetRoundDownDifference(T x, size_t align) {
|
||||
return static_cast<size_t>(x - Common::AlignDown(x, align));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr size_t GetRoundUpDifference(T x, size_t align) {
|
||||
return static_cast<size_t>(Common::AlignUp(x, align) - x);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
size_t GetRoundUpDifference(T* x, size_t align) {
|
||||
return GetRoundUpDifference(reinterpret_cast<uintptr_t>(x), align);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t AlignmentMatchingStorageImpl::Read(VirtualFile base_storage, char* work_buf,
|
||||
size_t work_buf_size, size_t data_alignment,
|
||||
size_t buffer_alignment, s64 offset, u8* buffer,
|
||||
size_t size) {
|
||||
// Check preconditions.
|
||||
ASSERT(work_buf_size >= data_alignment);
|
||||
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// Determine extents.
|
||||
u8* aligned_core_buffer;
|
||||
s64 core_offset;
|
||||
size_t core_size;
|
||||
size_t buffer_gap;
|
||||
size_t offset_gap;
|
||||
s64 covered_offset;
|
||||
|
||||
const size_t offset_round_up_difference = GetRoundUpDifference(offset, data_alignment);
|
||||
if (Common::IsAligned(reinterpret_cast<uintptr_t>(buffer) + offset_round_up_difference,
|
||||
buffer_alignment)) {
|
||||
aligned_core_buffer = buffer + offset_round_up_difference;
|
||||
|
||||
core_offset = Common::AlignUp(offset, data_alignment);
|
||||
core_size = (size < offset_round_up_difference)
|
||||
? 0
|
||||
: Common::AlignDown(size - offset_round_up_difference, data_alignment);
|
||||
buffer_gap = 0;
|
||||
offset_gap = 0;
|
||||
|
||||
covered_offset = core_size > 0 ? core_offset : offset;
|
||||
} else {
|
||||
const size_t buffer_round_up_difference = GetRoundUpDifference(buffer, buffer_alignment);
|
||||
|
||||
aligned_core_buffer = buffer + buffer_round_up_difference;
|
||||
|
||||
core_offset = Common::AlignDown(offset, data_alignment);
|
||||
core_size = (size < buffer_round_up_difference)
|
||||
? 0
|
||||
: Common::AlignDown(size - buffer_round_up_difference, data_alignment);
|
||||
buffer_gap = buffer_round_up_difference;
|
||||
offset_gap = GetRoundDownDifference(offset, data_alignment);
|
||||
|
||||
covered_offset = offset;
|
||||
}
|
||||
|
||||
// Read the core portion.
|
||||
if (core_size > 0) {
|
||||
base_storage->Read(aligned_core_buffer, core_size, core_offset);
|
||||
|
||||
if (offset_gap != 0 || buffer_gap != 0) {
|
||||
std::memmove(aligned_core_buffer - buffer_gap, aligned_core_buffer + offset_gap,
|
||||
core_size - offset_gap);
|
||||
core_size -= offset_gap;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the head portion.
|
||||
if (offset < covered_offset) {
|
||||
const s64 head_offset = Common::AlignDown(offset, data_alignment);
|
||||
const size_t head_size = static_cast<size_t>(covered_offset - offset);
|
||||
|
||||
ASSERT(GetRoundDownDifference(offset, data_alignment) + head_size <= work_buf_size);
|
||||
|
||||
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, head_offset);
|
||||
std::memcpy(buffer, work_buf + GetRoundDownDifference(offset, data_alignment), head_size);
|
||||
}
|
||||
|
||||
// Handle the tail portion.
|
||||
s64 tail_offset = covered_offset + core_size;
|
||||
size_t remaining_tail_size = static_cast<size_t>((offset + size) - tail_offset);
|
||||
while (remaining_tail_size > 0) {
|
||||
const auto aligned_tail_offset = Common::AlignDown(tail_offset, data_alignment);
|
||||
const auto cur_size =
|
||||
std::min(static_cast<size_t>(aligned_tail_offset + data_alignment - tail_offset),
|
||||
remaining_tail_size);
|
||||
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, aligned_tail_offset);
|
||||
|
||||
ASSERT((tail_offset - offset) + cur_size <= size);
|
||||
ASSERT((tail_offset - aligned_tail_offset) + cur_size <= data_alignment);
|
||||
std::memcpy(reinterpret_cast<char*>(buffer) + (tail_offset - offset),
|
||||
work_buf + (tail_offset - aligned_tail_offset), cur_size);
|
||||
|
||||
remaining_tail_size -= cur_size;
|
||||
tail_offset += cur_size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t AlignmentMatchingStorageImpl::Write(VirtualFile base_storage, char* work_buf,
|
||||
size_t work_buf_size, size_t data_alignment,
|
||||
size_t buffer_alignment, s64 offset, const u8* buffer,
|
||||
size_t size) {
|
||||
// Check preconditions.
|
||||
ASSERT(work_buf_size >= data_alignment);
|
||||
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// Determine extents.
|
||||
const u8* aligned_core_buffer;
|
||||
s64 core_offset;
|
||||
size_t core_size;
|
||||
s64 covered_offset;
|
||||
|
||||
const size_t offset_round_up_difference = GetRoundUpDifference(offset, data_alignment);
|
||||
if (Common::IsAligned(reinterpret_cast<uintptr_t>(buffer) + offset_round_up_difference,
|
||||
buffer_alignment)) {
|
||||
aligned_core_buffer = buffer + offset_round_up_difference;
|
||||
|
||||
core_offset = Common::AlignUp(offset, data_alignment);
|
||||
core_size = (size < offset_round_up_difference)
|
||||
? 0
|
||||
: Common::AlignDown(size - offset_round_up_difference, data_alignment);
|
||||
|
||||
covered_offset = core_size > 0 ? core_offset : offset;
|
||||
} else {
|
||||
aligned_core_buffer = nullptr;
|
||||
|
||||
core_offset = Common::AlignDown(offset, data_alignment);
|
||||
core_size = 0;
|
||||
|
||||
covered_offset = offset;
|
||||
}
|
||||
|
||||
// Write the core portion.
|
||||
if (core_size > 0) {
|
||||
base_storage->Write(aligned_core_buffer, core_size, core_offset);
|
||||
}
|
||||
|
||||
// Handle the head portion.
|
||||
if (offset < covered_offset) {
|
||||
const s64 head_offset = Common::AlignDown(offset, data_alignment);
|
||||
const size_t head_size = static_cast<size_t>(covered_offset - offset);
|
||||
|
||||
ASSERT((offset - head_offset) + head_size <= data_alignment);
|
||||
|
||||
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, head_offset);
|
||||
std::memcpy(work_buf + (offset - head_offset), buffer, head_size);
|
||||
base_storage->Write(reinterpret_cast<u8*>(work_buf), data_alignment, head_offset);
|
||||
}
|
||||
|
||||
// Handle the tail portion.
|
||||
s64 tail_offset = covered_offset + core_size;
|
||||
size_t remaining_tail_size = static_cast<size_t>((offset + size) - tail_offset);
|
||||
while (remaining_tail_size > 0) {
|
||||
ASSERT(static_cast<size_t>(tail_offset - offset) < size);
|
||||
|
||||
const auto aligned_tail_offset = Common::AlignDown(tail_offset, data_alignment);
|
||||
const auto cur_size =
|
||||
std::min(static_cast<size_t>(aligned_tail_offset + data_alignment - tail_offset),
|
||||
remaining_tail_size);
|
||||
|
||||
base_storage->Read(reinterpret_cast<u8*>(work_buf), data_alignment, aligned_tail_offset);
|
||||
std::memcpy(work_buf + GetRoundDownDifference(tail_offset, data_alignment),
|
||||
buffer + (tail_offset - offset), cur_size);
|
||||
base_storage->Write(reinterpret_cast<u8*>(work_buf), data_alignment, aligned_tail_offset);
|
||||
|
||||
remaining_tail_size -= cur_size;
|
||||
tail_offset += cur_size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,21 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class AlignmentMatchingStorageImpl {
|
||||
public:
|
||||
static size_t Read(VirtualFile base_storage, char* work_buf, size_t work_buf_size,
|
||||
size_t data_alignment, size_t buffer_alignment, s64 offset, u8* buffer,
|
||||
size_t size);
|
||||
static size_t Write(VirtualFile base_storage, char* work_buf, size_t work_buf_size,
|
||||
size_t data_alignment, size_t buffer_alignment, s64 offset,
|
||||
const u8* buffer, size_t size);
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,598 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree_utils.h"
|
||||
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
using Node = impl::BucketTreeNode<const s64*>;
|
||||
static_assert(sizeof(Node) == sizeof(BucketTree::NodeHeader));
|
||||
static_assert(std::is_trivial_v<Node>);
|
||||
|
||||
constexpr inline s32 NodeHeaderSize = sizeof(BucketTree::NodeHeader);
|
||||
|
||||
class StorageNode {
|
||||
private:
|
||||
class Offset {
|
||||
public:
|
||||
using difference_type = s64;
|
||||
|
||||
private:
|
||||
s64 m_offset;
|
||||
s32 m_stride;
|
||||
|
||||
public:
|
||||
constexpr Offset(s64 offset, s32 stride) : m_offset(offset), m_stride(stride) {}
|
||||
|
||||
constexpr Offset& operator++() {
|
||||
m_offset += m_stride;
|
||||
return *this;
|
||||
}
|
||||
constexpr Offset operator++(int) {
|
||||
Offset ret(*this);
|
||||
m_offset += m_stride;
|
||||
return ret;
|
||||
}
|
||||
|
||||
constexpr Offset& operator--() {
|
||||
m_offset -= m_stride;
|
||||
return *this;
|
||||
}
|
||||
constexpr Offset operator--(int) {
|
||||
Offset ret(*this);
|
||||
m_offset -= m_stride;
|
||||
return ret;
|
||||
}
|
||||
|
||||
constexpr difference_type operator-(const Offset& rhs) const {
|
||||
return (m_offset - rhs.m_offset) / m_stride;
|
||||
}
|
||||
|
||||
constexpr Offset operator+(difference_type ofs) const {
|
||||
return Offset(m_offset + ofs * m_stride, m_stride);
|
||||
}
|
||||
constexpr Offset operator-(difference_type ofs) const {
|
||||
return Offset(m_offset - ofs * m_stride, m_stride);
|
||||
}
|
||||
|
||||
constexpr Offset& operator+=(difference_type ofs) {
|
||||
m_offset += ofs * m_stride;
|
||||
return *this;
|
||||
}
|
||||
constexpr Offset& operator-=(difference_type ofs) {
|
||||
m_offset -= ofs * m_stride;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr bool operator==(const Offset& rhs) const {
|
||||
return m_offset == rhs.m_offset;
|
||||
}
|
||||
constexpr bool operator!=(const Offset& rhs) const {
|
||||
return m_offset != rhs.m_offset;
|
||||
}
|
||||
|
||||
constexpr s64 Get() const {
|
||||
return m_offset;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
const Offset m_start;
|
||||
const s32 m_count;
|
||||
s32 m_index;
|
||||
|
||||
public:
|
||||
StorageNode(size_t size, s32 count)
|
||||
: m_start(NodeHeaderSize, static_cast<s32>(size)), m_count(count), m_index(-1) {}
|
||||
StorageNode(s64 ofs, size_t size, s32 count)
|
||||
: m_start(NodeHeaderSize + ofs, static_cast<s32>(size)), m_count(count), m_index(-1) {}
|
||||
|
||||
s32 GetIndex() const {
|
||||
return m_index;
|
||||
}
|
||||
|
||||
void Find(const char* buffer, s64 virtual_address) {
|
||||
s32 end = m_count;
|
||||
auto pos = m_start;
|
||||
|
||||
while (end > 0) {
|
||||
auto half = end / 2;
|
||||
auto mid = pos + half;
|
||||
|
||||
s64 offset = 0;
|
||||
std::memcpy(std::addressof(offset), buffer + mid.Get(), sizeof(s64));
|
||||
|
||||
if (offset <= virtual_address) {
|
||||
pos = mid + 1;
|
||||
end -= half + 1;
|
||||
} else {
|
||||
end = half;
|
||||
}
|
||||
}
|
||||
|
||||
m_index = static_cast<s32>(pos - m_start) - 1;
|
||||
}
|
||||
|
||||
Result Find(VirtualFile storage, s64 virtual_address) {
|
||||
s32 end = m_count;
|
||||
auto pos = m_start;
|
||||
|
||||
while (end > 0) {
|
||||
auto half = end / 2;
|
||||
auto mid = pos + half;
|
||||
|
||||
s64 offset = 0;
|
||||
storage->ReadObject(std::addressof(offset), mid.Get());
|
||||
|
||||
if (offset <= virtual_address) {
|
||||
pos = mid + 1;
|
||||
end -= half + 1;
|
||||
} else {
|
||||
end = half;
|
||||
}
|
||||
}
|
||||
|
||||
m_index = static_cast<s32>(pos - m_start) - 1;
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void BucketTree::Header::Format(s32 entry_count_) {
|
||||
ASSERT(entry_count_ >= 0);
|
||||
|
||||
this->magic = Magic;
|
||||
this->version = Version;
|
||||
this->entry_count = entry_count_;
|
||||
this->reserved = 0;
|
||||
}
|
||||
|
||||
Result BucketTree::Header::Verify() const {
|
||||
R_UNLESS(this->magic == Magic, ResultInvalidBucketTreeSignature);
|
||||
R_UNLESS(this->entry_count >= 0, ResultInvalidBucketTreeEntryCount);
|
||||
R_UNLESS(this->version <= Version, ResultUnsupportedVersion);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::NodeHeader::Verify(s32 node_index, size_t node_size, size_t entry_size) const {
|
||||
R_UNLESS(this->index == node_index, ResultInvalidBucketTreeNodeIndex);
|
||||
R_UNLESS(entry_size != 0 && node_size >= entry_size + NodeHeaderSize, ResultInvalidSize);
|
||||
|
||||
const size_t max_entry_count = (node_size - NodeHeaderSize) / entry_size;
|
||||
R_UNLESS(this->count > 0 && static_cast<size_t>(this->count) <= max_entry_count,
|
||||
ResultInvalidBucketTreeNodeEntryCount);
|
||||
R_UNLESS(this->offset >= 0, ResultInvalidBucketTreeNodeOffset);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Initialize(VirtualFile node_storage, VirtualFile entry_storage, size_t node_size,
|
||||
size_t entry_size, s32 entry_count) {
|
||||
// Validate preconditions.
|
||||
ASSERT(entry_size >= sizeof(s64));
|
||||
ASSERT(node_size >= entry_size + sizeof(NodeHeader));
|
||||
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||
ASSERT(!this->IsInitialized());
|
||||
|
||||
// Ensure valid entry count.
|
||||
R_UNLESS(entry_count > 0, ResultInvalidArgument);
|
||||
|
||||
// Allocate node.
|
||||
R_UNLESS(m_node_l1.Allocate(node_size), ResultBufferAllocationFailed);
|
||||
ON_RESULT_FAILURE {
|
||||
m_node_l1.Free(node_size);
|
||||
};
|
||||
|
||||
// Read node.
|
||||
node_storage->Read(reinterpret_cast<u8*>(m_node_l1.Get()), node_size);
|
||||
|
||||
// Verify node.
|
||||
R_TRY(m_node_l1->Verify(0, node_size, sizeof(s64)));
|
||||
|
||||
// Validate offsets.
|
||||
const auto offset_count = GetOffsetCount(node_size);
|
||||
const auto entry_set_count = GetEntrySetCount(node_size, entry_size, entry_count);
|
||||
const auto* const node = m_node_l1.Get<Node>();
|
||||
|
||||
s64 start_offset;
|
||||
if (offset_count < entry_set_count && node->GetCount() < offset_count) {
|
||||
start_offset = *node->GetEnd();
|
||||
} else {
|
||||
start_offset = *node->GetBegin();
|
||||
}
|
||||
const auto end_offset = node->GetEndOffset();
|
||||
|
||||
R_UNLESS(0 <= start_offset && start_offset <= node->GetBeginOffset(),
|
||||
ResultInvalidBucketTreeEntryOffset);
|
||||
R_UNLESS(start_offset < end_offset, ResultInvalidBucketTreeEntryOffset);
|
||||
|
||||
// Set member variables.
|
||||
m_node_storage = node_storage;
|
||||
m_entry_storage = entry_storage;
|
||||
m_node_size = node_size;
|
||||
m_entry_size = entry_size;
|
||||
m_entry_count = entry_count;
|
||||
m_offset_count = offset_count;
|
||||
m_entry_set_count = entry_set_count;
|
||||
|
||||
m_offset_cache.offsets.start_offset = start_offset;
|
||||
m_offset_cache.offsets.end_offset = end_offset;
|
||||
m_offset_cache.is_initialized = true;
|
||||
|
||||
// We succeeded.
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void BucketTree::Initialize(size_t node_size, s64 end_offset) {
|
||||
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||
ASSERT(end_offset > 0);
|
||||
ASSERT(!this->IsInitialized());
|
||||
|
||||
m_node_size = node_size;
|
||||
|
||||
m_offset_cache.offsets.start_offset = 0;
|
||||
m_offset_cache.offsets.end_offset = end_offset;
|
||||
m_offset_cache.is_initialized = true;
|
||||
}
|
||||
|
||||
void BucketTree::Finalize() {
|
||||
if (this->IsInitialized()) {
|
||||
m_node_storage = VirtualFile();
|
||||
m_entry_storage = VirtualFile();
|
||||
m_node_l1.Free(m_node_size);
|
||||
m_node_size = 0;
|
||||
m_entry_size = 0;
|
||||
m_entry_count = 0;
|
||||
m_offset_count = 0;
|
||||
m_entry_set_count = 0;
|
||||
|
||||
m_offset_cache.offsets.start_offset = 0;
|
||||
m_offset_cache.offsets.end_offset = 0;
|
||||
m_offset_cache.is_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
Result BucketTree::Find(Visitor* visitor, s64 virtual_address) {
|
||||
ASSERT(visitor != nullptr);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
R_UNLESS(virtual_address >= 0, ResultInvalidOffset);
|
||||
R_UNLESS(!this->IsEmpty(), ResultOutOfRange);
|
||||
|
||||
BucketTree::Offsets offsets;
|
||||
R_TRY(this->GetOffsets(std::addressof(offsets)));
|
||||
|
||||
R_TRY(visitor->Initialize(this, offsets));
|
||||
|
||||
R_RETURN(visitor->Find(virtual_address));
|
||||
}
|
||||
|
||||
Result BucketTree::InvalidateCache() {
|
||||
// Reset our offsets.
|
||||
m_offset_cache.is_initialized = false;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::EnsureOffsetCache() {
|
||||
// If we already have an offset cache, we're good.
|
||||
R_SUCCEED_IF(m_offset_cache.is_initialized);
|
||||
|
||||
// Acquire exclusive right to edit the offset cache.
|
||||
std::scoped_lock lk(m_offset_cache.mutex);
|
||||
|
||||
// Check again, to be sure.
|
||||
R_SUCCEED_IF(m_offset_cache.is_initialized);
|
||||
|
||||
// Read/verify L1.
|
||||
m_node_storage->Read(reinterpret_cast<u8*>(m_node_l1.Get()), m_node_size);
|
||||
R_TRY(m_node_l1->Verify(0, m_node_size, sizeof(s64)));
|
||||
|
||||
// Get the node.
|
||||
auto* const node = m_node_l1.Get<Node>();
|
||||
|
||||
s64 start_offset;
|
||||
if (m_offset_count < m_entry_set_count && node->GetCount() < m_offset_count) {
|
||||
start_offset = *node->GetEnd();
|
||||
} else {
|
||||
start_offset = *node->GetBegin();
|
||||
}
|
||||
const auto end_offset = node->GetEndOffset();
|
||||
|
||||
R_UNLESS(0 <= start_offset && start_offset <= node->GetBeginOffset(),
|
||||
ResultInvalidBucketTreeEntryOffset);
|
||||
R_UNLESS(start_offset < end_offset, ResultInvalidBucketTreeEntryOffset);
|
||||
|
||||
m_offset_cache.offsets.start_offset = start_offset;
|
||||
m_offset_cache.offsets.end_offset = end_offset;
|
||||
m_offset_cache.is_initialized = true;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::Initialize(const BucketTree* tree, const BucketTree::Offsets& offsets) {
|
||||
ASSERT(tree != nullptr);
|
||||
ASSERT(m_tree == nullptr || m_tree == tree);
|
||||
|
||||
if (m_entry == nullptr) {
|
||||
m_entry = ::operator new(tree->m_entry_size);
|
||||
R_UNLESS(m_entry != nullptr, ResultBufferAllocationFailed);
|
||||
|
||||
m_tree = tree;
|
||||
m_offsets = offsets;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::MoveNext() {
|
||||
R_UNLESS(this->IsValid(), ResultOutOfRange);
|
||||
|
||||
// Invalidate our index, and read the header for the next index.
|
||||
auto entry_index = m_entry_index + 1;
|
||||
if (entry_index == m_entry_set.info.count) {
|
||||
const auto entry_set_index = m_entry_set.info.index + 1;
|
||||
R_UNLESS(entry_set_index < m_entry_set_count, ResultOutOfRange);
|
||||
|
||||
m_entry_index = -1;
|
||||
|
||||
const auto end = m_entry_set.info.end;
|
||||
|
||||
const auto entry_set_size = m_tree->m_node_size;
|
||||
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||
|
||||
m_tree->m_entry_storage->ReadObject(std::addressof(m_entry_set), entry_set_offset);
|
||||
R_TRY(m_entry_set.header.Verify(entry_set_index, entry_set_size, m_tree->m_entry_size));
|
||||
|
||||
R_UNLESS(m_entry_set.info.start == end && m_entry_set.info.start < m_entry_set.info.end,
|
||||
ResultInvalidBucketTreeEntrySetOffset);
|
||||
|
||||
entry_index = 0;
|
||||
} else {
|
||||
m_entry_index = -1;
|
||||
}
|
||||
|
||||
// Read the new entry.
|
||||
const auto entry_size = m_tree->m_entry_size;
|
||||
const auto entry_offset = impl::GetBucketTreeEntryOffset(
|
||||
m_entry_set.info.index, m_tree->m_node_size, entry_size, entry_index);
|
||||
m_tree->m_entry_storage->Read(reinterpret_cast<u8*>(m_entry), entry_size, entry_offset);
|
||||
|
||||
// Note that we changed index.
|
||||
m_entry_index = entry_index;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::MovePrevious() {
|
||||
R_UNLESS(this->IsValid(), ResultOutOfRange);
|
||||
|
||||
// Invalidate our index, and read the header for the previous index.
|
||||
auto entry_index = m_entry_index;
|
||||
if (entry_index == 0) {
|
||||
R_UNLESS(m_entry_set.info.index > 0, ResultOutOfRange);
|
||||
|
||||
m_entry_index = -1;
|
||||
|
||||
const auto start = m_entry_set.info.start;
|
||||
|
||||
const auto entry_set_size = m_tree->m_node_size;
|
||||
const auto entry_set_index = m_entry_set.info.index - 1;
|
||||
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||
|
||||
m_tree->m_entry_storage->ReadObject(std::addressof(m_entry_set), entry_set_offset);
|
||||
R_TRY(m_entry_set.header.Verify(entry_set_index, entry_set_size, m_tree->m_entry_size));
|
||||
|
||||
R_UNLESS(m_entry_set.info.end == start && m_entry_set.info.start < m_entry_set.info.end,
|
||||
ResultInvalidBucketTreeEntrySetOffset);
|
||||
|
||||
entry_index = m_entry_set.info.count;
|
||||
} else {
|
||||
m_entry_index = -1;
|
||||
}
|
||||
|
||||
--entry_index;
|
||||
|
||||
// Read the new entry.
|
||||
const auto entry_size = m_tree->m_entry_size;
|
||||
const auto entry_offset = impl::GetBucketTreeEntryOffset(
|
||||
m_entry_set.info.index, m_tree->m_node_size, entry_size, entry_index);
|
||||
m_tree->m_entry_storage->Read(reinterpret_cast<u8*>(m_entry), entry_size, entry_offset);
|
||||
|
||||
// Note that we changed index.
|
||||
m_entry_index = entry_index;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::Find(s64 virtual_address) {
|
||||
ASSERT(m_tree != nullptr);
|
||||
|
||||
// Get the node.
|
||||
const auto* const node = m_tree->m_node_l1.Get<Node>();
|
||||
R_UNLESS(virtual_address < node->GetEndOffset(), ResultOutOfRange);
|
||||
|
||||
// Get the entry set index.
|
||||
s32 entry_set_index = -1;
|
||||
if (m_tree->IsExistOffsetL2OnL1() && virtual_address < node->GetBeginOffset()) {
|
||||
const auto start = node->GetEnd();
|
||||
const auto end = node->GetBegin() + m_tree->m_offset_count;
|
||||
|
||||
auto pos = std::upper_bound(start, end, virtual_address);
|
||||
R_UNLESS(start < pos, ResultOutOfRange);
|
||||
--pos;
|
||||
|
||||
entry_set_index = static_cast<s32>(pos - start);
|
||||
} else {
|
||||
const auto start = node->GetBegin();
|
||||
const auto end = node->GetEnd();
|
||||
|
||||
auto pos = std::upper_bound(start, end, virtual_address);
|
||||
R_UNLESS(start < pos, ResultOutOfRange);
|
||||
--pos;
|
||||
|
||||
if (m_tree->IsExistL2()) {
|
||||
const auto node_index = static_cast<s32>(pos - start);
|
||||
R_UNLESS(0 <= node_index && node_index < m_tree->m_offset_count,
|
||||
ResultInvalidBucketTreeNodeOffset);
|
||||
|
||||
R_TRY(this->FindEntrySet(std::addressof(entry_set_index), virtual_address, node_index));
|
||||
} else {
|
||||
entry_set_index = static_cast<s32>(pos - start);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the entry set index.
|
||||
R_UNLESS(0 <= entry_set_index && entry_set_index < m_tree->m_entry_set_count,
|
||||
ResultInvalidBucketTreeNodeOffset);
|
||||
|
||||
// Find the entry.
|
||||
R_TRY(this->FindEntry(virtual_address, entry_set_index));
|
||||
|
||||
// Set count.
|
||||
m_entry_set_count = m_tree->m_entry_set_count;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::FindEntrySet(s32* out_index, s64 virtual_address, s32 node_index) {
|
||||
const auto node_size = m_tree->m_node_size;
|
||||
|
||||
PooledBuffer pool(node_size, 1);
|
||||
if (node_size <= pool.GetSize()) {
|
||||
R_RETURN(
|
||||
this->FindEntrySetWithBuffer(out_index, virtual_address, node_index, pool.GetBuffer()));
|
||||
} else {
|
||||
pool.Deallocate();
|
||||
R_RETURN(this->FindEntrySetWithoutBuffer(out_index, virtual_address, node_index));
|
||||
}
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::FindEntrySetWithBuffer(s32* out_index, s64 virtual_address,
|
||||
s32 node_index, char* buffer) {
|
||||
// Calculate node extents.
|
||||
const auto node_size = m_tree->m_node_size;
|
||||
const auto node_offset = (node_index + 1) * static_cast<s64>(node_size);
|
||||
VirtualFile storage = m_tree->m_node_storage;
|
||||
|
||||
// Read the node.
|
||||
storage->Read(reinterpret_cast<u8*>(buffer), node_size, node_offset);
|
||||
|
||||
// Validate the header.
|
||||
NodeHeader header;
|
||||
std::memcpy(std::addressof(header), buffer, NodeHeaderSize);
|
||||
R_TRY(header.Verify(node_index, node_size, sizeof(s64)));
|
||||
|
||||
// Create the node, and find.
|
||||
StorageNode node(sizeof(s64), header.count);
|
||||
node.Find(buffer, virtual_address);
|
||||
R_UNLESS(node.GetIndex() >= 0, ResultInvalidBucketTreeVirtualOffset);
|
||||
|
||||
// Return the index.
|
||||
*out_index = static_cast<s32>(m_tree->GetEntrySetIndex(header.index, node.GetIndex()));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::FindEntrySetWithoutBuffer(s32* out_index, s64 virtual_address,
|
||||
s32 node_index) {
|
||||
// Calculate node extents.
|
||||
const auto node_size = m_tree->m_node_size;
|
||||
const auto node_offset = (node_index + 1) * static_cast<s64>(node_size);
|
||||
VirtualFile storage = m_tree->m_node_storage;
|
||||
|
||||
// Read and validate the header.
|
||||
NodeHeader header;
|
||||
storage->ReadObject(std::addressof(header), node_offset);
|
||||
R_TRY(header.Verify(node_index, node_size, sizeof(s64)));
|
||||
|
||||
// Create the node, and find.
|
||||
StorageNode node(node_offset, sizeof(s64), header.count);
|
||||
R_TRY(node.Find(storage, virtual_address));
|
||||
R_UNLESS(node.GetIndex() >= 0, ResultOutOfRange);
|
||||
|
||||
// Return the index.
|
||||
*out_index = static_cast<s32>(m_tree->GetEntrySetIndex(header.index, node.GetIndex()));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::FindEntry(s64 virtual_address, s32 entry_set_index) {
|
||||
const auto entry_set_size = m_tree->m_node_size;
|
||||
|
||||
PooledBuffer pool(entry_set_size, 1);
|
||||
if (entry_set_size <= pool.GetSize()) {
|
||||
R_RETURN(this->FindEntryWithBuffer(virtual_address, entry_set_index, pool.GetBuffer()));
|
||||
} else {
|
||||
pool.Deallocate();
|
||||
R_RETURN(this->FindEntryWithoutBuffer(virtual_address, entry_set_index));
|
||||
}
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::FindEntryWithBuffer(s64 virtual_address, s32 entry_set_index,
|
||||
char* buffer) {
|
||||
// Calculate entry set extents.
|
||||
const auto entry_size = m_tree->m_entry_size;
|
||||
const auto entry_set_size = m_tree->m_node_size;
|
||||
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||
VirtualFile storage = m_tree->m_entry_storage;
|
||||
|
||||
// Read the entry set.
|
||||
storage->Read(reinterpret_cast<u8*>(buffer), entry_set_size, entry_set_offset);
|
||||
|
||||
// Validate the entry_set.
|
||||
EntrySetHeader entry_set;
|
||||
std::memcpy(std::addressof(entry_set), buffer, sizeof(EntrySetHeader));
|
||||
R_TRY(entry_set.header.Verify(entry_set_index, entry_set_size, entry_size));
|
||||
|
||||
// Create the node, and find.
|
||||
StorageNode node(entry_size, entry_set.info.count);
|
||||
node.Find(buffer, virtual_address);
|
||||
R_UNLESS(node.GetIndex() >= 0, ResultOutOfRange);
|
||||
|
||||
// Copy the data into entry.
|
||||
const auto entry_index = node.GetIndex();
|
||||
const auto entry_offset = impl::GetBucketTreeEntryOffset(0, entry_size, entry_index);
|
||||
std::memcpy(m_entry, buffer + entry_offset, entry_size);
|
||||
|
||||
// Set our entry set/index.
|
||||
m_entry_set = entry_set;
|
||||
m_entry_index = entry_index;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BucketTree::Visitor::FindEntryWithoutBuffer(s64 virtual_address, s32 entry_set_index) {
|
||||
// Calculate entry set extents.
|
||||
const auto entry_size = m_tree->m_entry_size;
|
||||
const auto entry_set_size = m_tree->m_node_size;
|
||||
const auto entry_set_offset = entry_set_index * static_cast<s64>(entry_set_size);
|
||||
VirtualFile storage = m_tree->m_entry_storage;
|
||||
|
||||
// Read and validate the entry_set.
|
||||
EntrySetHeader entry_set;
|
||||
storage->ReadObject(std::addressof(entry_set), entry_set_offset);
|
||||
R_TRY(entry_set.header.Verify(entry_set_index, entry_set_size, entry_size));
|
||||
|
||||
// Create the node, and find.
|
||||
StorageNode node(entry_set_offset, entry_size, entry_set.info.count);
|
||||
R_TRY(node.Find(storage, virtual_address));
|
||||
R_UNLESS(node.GetIndex() >= 0, ResultOutOfRange);
|
||||
|
||||
// Copy the data into entry.
|
||||
const auto entry_index = node.GetIndex();
|
||||
const auto entry_offset =
|
||||
impl::GetBucketTreeEntryOffset(entry_set_offset, entry_size, entry_index);
|
||||
storage->Read(reinterpret_cast<u8*>(m_entry), entry_size, entry_offset);
|
||||
|
||||
// Set our entry set/index.
|
||||
m_entry_set = entry_set;
|
||||
m_entry_index = entry_index;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,416 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/literals.h"
|
||||
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
using namespace Common::Literals;
|
||||
|
||||
class BucketTree {
|
||||
YUZU_NON_COPYABLE(BucketTree);
|
||||
YUZU_NON_MOVEABLE(BucketTree);
|
||||
|
||||
public:
|
||||
static constexpr u32 Magic = Common::MakeMagic('B', 'K', 'T', 'R');
|
||||
static constexpr u32 Version = 1;
|
||||
|
||||
static constexpr size_t NodeSizeMin = 1_KiB;
|
||||
static constexpr size_t NodeSizeMax = 512_KiB;
|
||||
|
||||
public:
|
||||
class Visitor;
|
||||
|
||||
struct Header {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
s32 entry_count;
|
||||
s32 reserved;
|
||||
|
||||
void Format(s32 entry_count);
|
||||
Result Verify() const;
|
||||
};
|
||||
static_assert(std::is_trivial_v<Header>);
|
||||
static_assert(sizeof(Header) == 0x10);
|
||||
|
||||
struct NodeHeader {
|
||||
s32 index;
|
||||
s32 count;
|
||||
s64 offset;
|
||||
|
||||
Result Verify(s32 node_index, size_t node_size, size_t entry_size) const;
|
||||
};
|
||||
static_assert(std::is_trivial_v<NodeHeader>);
|
||||
static_assert(sizeof(NodeHeader) == 0x10);
|
||||
|
||||
struct Offsets {
|
||||
s64 start_offset;
|
||||
s64 end_offset;
|
||||
|
||||
constexpr bool IsInclude(s64 offset) const {
|
||||
return this->start_offset <= offset && offset < this->end_offset;
|
||||
}
|
||||
|
||||
constexpr bool IsInclude(s64 offset, s64 size) const {
|
||||
return size > 0 && this->start_offset <= offset && size <= (this->end_offset - offset);
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<Offsets>);
|
||||
static_assert(sizeof(Offsets) == 0x10);
|
||||
|
||||
struct OffsetCache {
|
||||
Offsets offsets;
|
||||
std::mutex mutex;
|
||||
bool is_initialized;
|
||||
|
||||
OffsetCache() : offsets{-1, -1}, mutex(), is_initialized(false) {}
|
||||
};
|
||||
|
||||
class ContinuousReadingInfo {
|
||||
public:
|
||||
constexpr ContinuousReadingInfo() : m_read_size(), m_skip_count(), m_done() {}
|
||||
|
||||
constexpr void Reset() {
|
||||
m_read_size = 0;
|
||||
m_skip_count = 0;
|
||||
m_done = false;
|
||||
}
|
||||
|
||||
constexpr void SetSkipCount(s32 count) {
|
||||
ASSERT(count >= 0);
|
||||
m_skip_count = count;
|
||||
}
|
||||
constexpr s32 GetSkipCount() const {
|
||||
return m_skip_count;
|
||||
}
|
||||
constexpr bool CheckNeedScan() {
|
||||
return (--m_skip_count) <= 0;
|
||||
}
|
||||
|
||||
constexpr void Done() {
|
||||
m_read_size = 0;
|
||||
m_done = true;
|
||||
}
|
||||
constexpr bool IsDone() const {
|
||||
return m_done;
|
||||
}
|
||||
|
||||
constexpr void SetReadSize(size_t size) {
|
||||
m_read_size = size;
|
||||
}
|
||||
constexpr size_t GetReadSize() const {
|
||||
return m_read_size;
|
||||
}
|
||||
constexpr bool CanDo() const {
|
||||
return m_read_size > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t m_read_size;
|
||||
s32 m_skip_count;
|
||||
bool m_done;
|
||||
};
|
||||
|
||||
private:
|
||||
class NodeBuffer {
|
||||
YUZU_NON_COPYABLE(NodeBuffer);
|
||||
|
||||
public:
|
||||
NodeBuffer() : m_header() {}
|
||||
|
||||
~NodeBuffer() {
|
||||
ASSERT(m_header == nullptr);
|
||||
}
|
||||
|
||||
NodeBuffer(NodeBuffer&& rhs) : m_header(rhs.m_header) {
|
||||
rhs.m_header = nullptr;
|
||||
}
|
||||
|
||||
NodeBuffer& operator=(NodeBuffer&& rhs) {
|
||||
if (this != std::addressof(rhs)) {
|
||||
ASSERT(m_header == nullptr);
|
||||
|
||||
m_header = rhs.m_header;
|
||||
|
||||
rhs.m_header = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Allocate(size_t node_size) {
|
||||
ASSERT(m_header == nullptr);
|
||||
|
||||
m_header = ::operator new(node_size, std::align_val_t{sizeof(s64)});
|
||||
|
||||
// ASSERT(Common::IsAligned(m_header, sizeof(s64)));
|
||||
|
||||
return m_header != nullptr;
|
||||
}
|
||||
|
||||
void Free(size_t node_size) {
|
||||
if (m_header) {
|
||||
::operator delete(m_header, std::align_val_t{sizeof(s64)});
|
||||
m_header = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void FillZero(size_t node_size) const {
|
||||
if (m_header) {
|
||||
std::memset(m_header, 0, node_size);
|
||||
}
|
||||
}
|
||||
|
||||
NodeHeader* Get() const {
|
||||
return reinterpret_cast<NodeHeader*>(m_header);
|
||||
}
|
||||
|
||||
NodeHeader* operator->() const {
|
||||
return this->Get();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* Get() const {
|
||||
static_assert(std::is_trivial_v<T>);
|
||||
static_assert(sizeof(T) == sizeof(NodeHeader));
|
||||
return reinterpret_cast<T*>(m_header);
|
||||
}
|
||||
|
||||
private:
|
||||
void* m_header;
|
||||
};
|
||||
|
||||
private:
|
||||
static constexpr s32 GetEntryCount(size_t node_size, size_t entry_size) {
|
||||
return static_cast<s32>((node_size - sizeof(NodeHeader)) / entry_size);
|
||||
}
|
||||
|
||||
static constexpr s32 GetOffsetCount(size_t node_size) {
|
||||
return static_cast<s32>((node_size - sizeof(NodeHeader)) / sizeof(s64));
|
||||
}
|
||||
|
||||
static constexpr s32 GetEntrySetCount(size_t node_size, size_t entry_size, s32 entry_count) {
|
||||
const s32 entry_count_per_node = GetEntryCount(node_size, entry_size);
|
||||
return Common::DivideUp(entry_count, entry_count_per_node);
|
||||
}
|
||||
|
||||
static constexpr s32 GetNodeL2Count(size_t node_size, size_t entry_size, s32 entry_count) {
|
||||
const s32 offset_count_per_node = GetOffsetCount(node_size);
|
||||
const s32 entry_set_count = GetEntrySetCount(node_size, entry_size, entry_count);
|
||||
|
||||
if (entry_set_count <= offset_count_per_node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const s32 node_l2_count = Common::DivideUp(entry_set_count, offset_count_per_node);
|
||||
ASSERT(node_l2_count <= offset_count_per_node);
|
||||
|
||||
return Common::DivideUp(entry_set_count - (offset_count_per_node - (node_l2_count - 1)),
|
||||
offset_count_per_node);
|
||||
}
|
||||
|
||||
public:
|
||||
BucketTree()
|
||||
: m_node_storage(), m_entry_storage(), m_node_l1(), m_node_size(), m_entry_size(),
|
||||
m_entry_count(), m_offset_count(), m_entry_set_count(), m_offset_cache() {}
|
||||
~BucketTree() {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
Result Initialize(VirtualFile node_storage, VirtualFile entry_storage, size_t node_size,
|
||||
size_t entry_size, s32 entry_count);
|
||||
void Initialize(size_t node_size, s64 end_offset);
|
||||
void Finalize();
|
||||
|
||||
bool IsInitialized() const {
|
||||
return m_node_size > 0;
|
||||
}
|
||||
bool IsEmpty() const {
|
||||
return m_entry_size == 0;
|
||||
}
|
||||
|
||||
Result Find(Visitor* visitor, s64 virtual_address);
|
||||
Result InvalidateCache();
|
||||
|
||||
s32 GetEntryCount() const {
|
||||
return m_entry_count;
|
||||
}
|
||||
|
||||
Result GetOffsets(Offsets* out) {
|
||||
// Ensure we have an offset cache.
|
||||
R_TRY(this->EnsureOffsetCache());
|
||||
|
||||
// Set the output.
|
||||
*out = m_offset_cache.offsets;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
public:
|
||||
static constexpr s64 QueryHeaderStorageSize() {
|
||||
return sizeof(Header);
|
||||
}
|
||||
|
||||
static constexpr s64 QueryNodeStorageSize(size_t node_size, size_t entry_size,
|
||||
s32 entry_count) {
|
||||
ASSERT(entry_size >= sizeof(s64));
|
||||
ASSERT(node_size >= entry_size + sizeof(NodeHeader));
|
||||
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||
ASSERT(entry_count >= 0);
|
||||
|
||||
if (entry_count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (1 + GetNodeL2Count(node_size, entry_size, entry_count)) *
|
||||
static_cast<s64>(node_size);
|
||||
}
|
||||
|
||||
static constexpr s64 QueryEntryStorageSize(size_t node_size, size_t entry_size,
|
||||
s32 entry_count) {
|
||||
ASSERT(entry_size >= sizeof(s64));
|
||||
ASSERT(node_size >= entry_size + sizeof(NodeHeader));
|
||||
ASSERT(NodeSizeMin <= node_size && node_size <= NodeSizeMax);
|
||||
ASSERT(Common::IsPowerOfTwo(node_size));
|
||||
ASSERT(entry_count >= 0);
|
||||
|
||||
if (entry_count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return GetEntrySetCount(node_size, entry_size, entry_count) * static_cast<s64>(node_size);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename EntryType>
|
||||
struct ContinuousReadingParam {
|
||||
s64 offset;
|
||||
size_t size;
|
||||
NodeHeader entry_set;
|
||||
s32 entry_index;
|
||||
Offsets offsets;
|
||||
EntryType entry;
|
||||
};
|
||||
|
||||
private:
|
||||
template <typename EntryType>
|
||||
Result ScanContinuousReading(ContinuousReadingInfo* out_info,
|
||||
const ContinuousReadingParam<EntryType>& param) const;
|
||||
|
||||
bool IsExistL2() const {
|
||||
return m_offset_count < m_entry_set_count;
|
||||
}
|
||||
bool IsExistOffsetL2OnL1() const {
|
||||
return this->IsExistL2() && m_node_l1->count < m_offset_count;
|
||||
}
|
||||
|
||||
s64 GetEntrySetIndex(s32 node_index, s32 offset_index) const {
|
||||
return (m_offset_count - m_node_l1->count) + (m_offset_count * node_index) + offset_index;
|
||||
}
|
||||
|
||||
Result EnsureOffsetCache();
|
||||
|
||||
private:
|
||||
mutable VirtualFile m_node_storage;
|
||||
mutable VirtualFile m_entry_storage;
|
||||
NodeBuffer m_node_l1;
|
||||
size_t m_node_size;
|
||||
size_t m_entry_size;
|
||||
s32 m_entry_count;
|
||||
s32 m_offset_count;
|
||||
s32 m_entry_set_count;
|
||||
OffsetCache m_offset_cache;
|
||||
};
|
||||
|
||||
class BucketTree::Visitor {
|
||||
YUZU_NON_COPYABLE(Visitor);
|
||||
YUZU_NON_MOVEABLE(Visitor);
|
||||
|
||||
public:
|
||||
constexpr Visitor()
|
||||
: m_tree(), m_entry(), m_entry_index(-1), m_entry_set_count(), m_entry_set{} {}
|
||||
~Visitor() {
|
||||
if (m_entry != nullptr) {
|
||||
::operator delete(m_entry, m_tree->m_entry_size);
|
||||
m_tree = nullptr;
|
||||
m_entry = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsValid() const {
|
||||
return m_entry_index >= 0;
|
||||
}
|
||||
bool CanMoveNext() const {
|
||||
return this->IsValid() && (m_entry_index + 1 < m_entry_set.info.count ||
|
||||
m_entry_set.info.index + 1 < m_entry_set_count);
|
||||
}
|
||||
bool CanMovePrevious() const {
|
||||
return this->IsValid() && (m_entry_index > 0 || m_entry_set.info.index > 0);
|
||||
}
|
||||
|
||||
Result MoveNext();
|
||||
Result MovePrevious();
|
||||
|
||||
template <typename EntryType>
|
||||
Result ScanContinuousReading(ContinuousReadingInfo* out_info, s64 offset, size_t size) const;
|
||||
|
||||
const void* Get() const {
|
||||
ASSERT(this->IsValid());
|
||||
return m_entry;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T* Get() const {
|
||||
ASSERT(this->IsValid());
|
||||
return reinterpret_cast<const T*>(m_entry);
|
||||
}
|
||||
|
||||
const BucketTree* GetTree() const {
|
||||
return m_tree;
|
||||
}
|
||||
|
||||
private:
|
||||
Result Initialize(const BucketTree* tree, const BucketTree::Offsets& offsets);
|
||||
|
||||
Result Find(s64 virtual_address);
|
||||
|
||||
Result FindEntrySet(s32* out_index, s64 virtual_address, s32 node_index);
|
||||
Result FindEntrySetWithBuffer(s32* out_index, s64 virtual_address, s32 node_index,
|
||||
char* buffer);
|
||||
Result FindEntrySetWithoutBuffer(s32* out_index, s64 virtual_address, s32 node_index);
|
||||
|
||||
Result FindEntry(s64 virtual_address, s32 entry_set_index);
|
||||
Result FindEntryWithBuffer(s64 virtual_address, s32 entry_set_index, char* buffer);
|
||||
Result FindEntryWithoutBuffer(s64 virtual_address, s32 entry_set_index);
|
||||
|
||||
private:
|
||||
friend class BucketTree;
|
||||
|
||||
union EntrySetHeader {
|
||||
NodeHeader header;
|
||||
struct Info {
|
||||
s32 index;
|
||||
s32 count;
|
||||
s64 end;
|
||||
s64 start;
|
||||
} info;
|
||||
static_assert(std::is_trivial_v<Info>);
|
||||
};
|
||||
static_assert(std::is_trivial_v<EntrySetHeader>);
|
||||
|
||||
const BucketTree* m_tree;
|
||||
BucketTree::Offsets m_offsets;
|
||||
void* m_entry;
|
||||
s32 m_entry_index;
|
||||
s32 m_entry_set_count;
|
||||
EntrySetHeader m_entry_set;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,170 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree_utils.h"
|
||||
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
template <typename EntryType>
|
||||
Result BucketTree::ScanContinuousReading(ContinuousReadingInfo* out_info,
|
||||
const ContinuousReadingParam<EntryType>& param) const {
|
||||
static_assert(std::is_trivial_v<ContinuousReadingParam<EntryType>>);
|
||||
|
||||
// Validate our preconditions.
|
||||
ASSERT(this->IsInitialized());
|
||||
ASSERT(out_info != nullptr);
|
||||
ASSERT(m_entry_size == sizeof(EntryType));
|
||||
|
||||
// Reset the output.
|
||||
out_info->Reset();
|
||||
|
||||
// If there's nothing to read, we're done.
|
||||
R_SUCCEED_IF(param.size == 0);
|
||||
|
||||
// If we're reading a fragment, we're done.
|
||||
R_SUCCEED_IF(param.entry.IsFragment());
|
||||
|
||||
// Validate the first entry.
|
||||
auto entry = param.entry;
|
||||
auto cur_offset = param.offset;
|
||||
R_UNLESS(entry.GetVirtualOffset() <= cur_offset, ResultOutOfRange);
|
||||
|
||||
// Create a pooled buffer for our scan.
|
||||
PooledBuffer pool(m_node_size, 1);
|
||||
char* buffer = nullptr;
|
||||
|
||||
s64 entry_storage_size = m_entry_storage->GetSize();
|
||||
|
||||
// Read the node.
|
||||
if (m_node_size <= pool.GetSize()) {
|
||||
buffer = pool.GetBuffer();
|
||||
const auto ofs = param.entry_set.index * static_cast<s64>(m_node_size);
|
||||
R_UNLESS(m_node_size + ofs <= static_cast<size_t>(entry_storage_size),
|
||||
ResultInvalidBucketTreeNodeEntryCount);
|
||||
|
||||
m_entry_storage->Read(reinterpret_cast<u8*>(buffer), m_node_size, ofs);
|
||||
}
|
||||
|
||||
// Calculate extents.
|
||||
const auto end_offset = cur_offset + static_cast<s64>(param.size);
|
||||
s64 phys_offset = entry.GetPhysicalOffset();
|
||||
|
||||
// Start merge tracking.
|
||||
s64 merge_size = 0;
|
||||
s64 readable_size = 0;
|
||||
bool merged = false;
|
||||
|
||||
// Iterate.
|
||||
auto entry_index = param.entry_index;
|
||||
for (const auto entry_count = param.entry_set.count; entry_index < entry_count; ++entry_index) {
|
||||
// If we're past the end, we're done.
|
||||
if (end_offset <= cur_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate the entry offset.
|
||||
const auto entry_offset = entry.GetVirtualOffset();
|
||||
R_UNLESS(entry_offset <= cur_offset, ResultInvalidIndirectEntryOffset);
|
||||
|
||||
// Get the next entry.
|
||||
EntryType next_entry = {};
|
||||
s64 next_entry_offset;
|
||||
|
||||
if (entry_index + 1 < entry_count) {
|
||||
if (buffer != nullptr) {
|
||||
const auto ofs = impl::GetBucketTreeEntryOffset(0, m_entry_size, entry_index + 1);
|
||||
std::memcpy(std::addressof(next_entry), buffer + ofs, m_entry_size);
|
||||
} else {
|
||||
const auto ofs = impl::GetBucketTreeEntryOffset(param.entry_set.index, m_node_size,
|
||||
m_entry_size, entry_index + 1);
|
||||
m_entry_storage->ReadObject(std::addressof(next_entry), ofs);
|
||||
}
|
||||
|
||||
next_entry_offset = next_entry.GetVirtualOffset();
|
||||
R_UNLESS(param.offsets.IsInclude(next_entry_offset), ResultInvalidIndirectEntryOffset);
|
||||
} else {
|
||||
next_entry_offset = param.entry_set.offset;
|
||||
}
|
||||
|
||||
// Validate the next entry offset.
|
||||
R_UNLESS(cur_offset < next_entry_offset, ResultInvalidIndirectEntryOffset);
|
||||
|
||||
// Determine the much data there is.
|
||||
const auto data_size = next_entry_offset - cur_offset;
|
||||
ASSERT(data_size > 0);
|
||||
|
||||
// Determine how much data we should read.
|
||||
const auto remaining_size = end_offset - cur_offset;
|
||||
const size_t read_size = static_cast<size_t>(std::min(data_size, remaining_size));
|
||||
ASSERT(read_size <= param.size);
|
||||
|
||||
// Update our merge tracking.
|
||||
if (entry.IsFragment()) {
|
||||
// If we can't merge, stop looping.
|
||||
if (EntryType::FragmentSizeMax <= read_size || remaining_size <= data_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, add the current size to the merge size.
|
||||
merge_size += read_size;
|
||||
} else {
|
||||
// If we can't merge, stop looping.
|
||||
if (phys_offset != entry.GetPhysicalOffset()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the size to the readable amount.
|
||||
readable_size += merge_size + read_size;
|
||||
ASSERT(readable_size <= static_cast<s64>(param.size));
|
||||
|
||||
// Update whether we've merged.
|
||||
merged |= merge_size > 0;
|
||||
merge_size = 0;
|
||||
}
|
||||
|
||||
// Advance.
|
||||
cur_offset += read_size;
|
||||
ASSERT(cur_offset <= end_offset);
|
||||
|
||||
phys_offset += next_entry_offset - entry_offset;
|
||||
entry = next_entry;
|
||||
}
|
||||
|
||||
// If we merged, set our readable size.
|
||||
if (merged) {
|
||||
out_info->SetReadSize(static_cast<size_t>(readable_size));
|
||||
}
|
||||
out_info->SetSkipCount(entry_index - param.entry_index);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
template <typename EntryType>
|
||||
Result BucketTree::Visitor::ScanContinuousReading(ContinuousReadingInfo* out_info, s64 offset,
|
||||
size_t size) const {
|
||||
static_assert(std::is_trivial_v<EntryType>);
|
||||
ASSERT(this->IsValid());
|
||||
|
||||
// Create our parameters.
|
||||
ContinuousReadingParam<EntryType> param = {
|
||||
.offset = offset,
|
||||
.size = size,
|
||||
.entry_set = m_entry_set.header,
|
||||
.entry_index = m_entry_index,
|
||||
.offsets{},
|
||||
.entry{},
|
||||
};
|
||||
std::memcpy(std::addressof(param.offsets), std::addressof(m_offsets),
|
||||
sizeof(BucketTree::Offsets));
|
||||
std::memcpy(std::addressof(param.entry), m_entry, sizeof(EntryType));
|
||||
|
||||
// Scan.
|
||||
R_RETURN(m_tree->ScanContinuousReading<EntryType>(out_info, param));
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,110 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||
|
||||
namespace FileSys::impl {
|
||||
|
||||
class SafeValue {
|
||||
public:
|
||||
static s64 GetInt64(const void* ptr) {
|
||||
s64 value;
|
||||
std::memcpy(std::addressof(value), ptr, sizeof(s64));
|
||||
return value;
|
||||
}
|
||||
|
||||
static s64 GetInt64(const s64* ptr) {
|
||||
return GetInt64(static_cast<const void*>(ptr));
|
||||
}
|
||||
|
||||
static s64 GetInt64(const s64& v) {
|
||||
return GetInt64(std::addressof(v));
|
||||
}
|
||||
|
||||
static void SetInt64(void* dst, const void* src) {
|
||||
std::memcpy(dst, src, sizeof(s64));
|
||||
}
|
||||
|
||||
static void SetInt64(void* dst, const s64* src) {
|
||||
return SetInt64(dst, static_cast<const void*>(src));
|
||||
}
|
||||
|
||||
static void SetInt64(void* dst, const s64& v) {
|
||||
return SetInt64(dst, std::addressof(v));
|
||||
}
|
||||
};
|
||||
|
||||
template <typename IteratorType>
|
||||
struct BucketTreeNode {
|
||||
using Header = BucketTree::NodeHeader;
|
||||
|
||||
Header header;
|
||||
|
||||
s32 GetCount() const {
|
||||
return this->header.count;
|
||||
}
|
||||
|
||||
void* GetArray() {
|
||||
return std::addressof(this->header) + 1;
|
||||
}
|
||||
template <typename T>
|
||||
T* GetArray() {
|
||||
return reinterpret_cast<T*>(this->GetArray());
|
||||
}
|
||||
const void* GetArray() const {
|
||||
return std::addressof(this->header) + 1;
|
||||
}
|
||||
template <typename T>
|
||||
const T* GetArray() const {
|
||||
return reinterpret_cast<const T*>(this->GetArray());
|
||||
}
|
||||
|
||||
s64 GetBeginOffset() const {
|
||||
return *this->GetArray<s64>();
|
||||
}
|
||||
s64 GetEndOffset() const {
|
||||
return this->header.offset;
|
||||
}
|
||||
|
||||
IteratorType GetBegin() {
|
||||
return IteratorType(this->GetArray<s64>());
|
||||
}
|
||||
IteratorType GetEnd() {
|
||||
return IteratorType(this->GetArray<s64>()) + this->header.count;
|
||||
}
|
||||
IteratorType GetBegin() const {
|
||||
return IteratorType(this->GetArray<s64>());
|
||||
}
|
||||
IteratorType GetEnd() const {
|
||||
return IteratorType(this->GetArray<s64>()) + this->header.count;
|
||||
}
|
||||
|
||||
IteratorType GetBegin(size_t entry_size) {
|
||||
return IteratorType(this->GetArray(), entry_size);
|
||||
}
|
||||
IteratorType GetEnd(size_t entry_size) {
|
||||
return IteratorType(this->GetArray(), entry_size) + this->header.count;
|
||||
}
|
||||
IteratorType GetBegin(size_t entry_size) const {
|
||||
return IteratorType(this->GetArray(), entry_size);
|
||||
}
|
||||
IteratorType GetEnd(size_t entry_size) const {
|
||||
return IteratorType(this->GetArray(), entry_size) + this->header.count;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr inline s64 GetBucketTreeEntryOffset(s64 entry_set_offset, size_t entry_size,
|
||||
s32 entry_index) {
|
||||
return entry_set_offset + sizeof(BucketTree::NodeHeader) +
|
||||
entry_index * static_cast<s64>(entry_size);
|
||||
}
|
||||
|
||||
constexpr inline s64 GetBucketTreeEntryOffset(s32 entry_set_index, size_t node_size,
|
||||
size_t entry_size, s32 entry_index) {
|
||||
return GetBucketTreeEntryOffset(entry_set_index * static_cast<s64>(node_size), entry_size,
|
||||
entry_index);
|
||||
}
|
||||
|
||||
} // namespace FileSys::impl
|
@ -1,963 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/literals.h"
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||
#include "core/file_sys/fssystem/fssystem_compression_common.h"
|
||||
#include "core/file_sys/fssystem/fssystem_pooled_buffer.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
using namespace Common::Literals;
|
||||
|
||||
class CompressedStorage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(CompressedStorage);
|
||||
YUZU_NON_MOVEABLE(CompressedStorage);
|
||||
|
||||
public:
|
||||
static constexpr size_t NodeSize = 16_KiB;
|
||||
|
||||
struct Entry {
|
||||
s64 virt_offset;
|
||||
s64 phys_offset;
|
||||
CompressionType compression_type;
|
||||
s32 phys_size;
|
||||
|
||||
s64 GetPhysicalSize() const {
|
||||
return this->phys_size;
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<Entry>);
|
||||
static_assert(sizeof(Entry) == 0x18);
|
||||
|
||||
public:
|
||||
static constexpr s64 QueryNodeStorageSize(s32 entry_count) {
|
||||
return BucketTree::QueryNodeStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||
}
|
||||
|
||||
static constexpr s64 QueryEntryStorageSize(s32 entry_count) {
|
||||
return BucketTree::QueryEntryStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||
}
|
||||
|
||||
private:
|
||||
class CompressedStorageCore {
|
||||
YUZU_NON_COPYABLE(CompressedStorageCore);
|
||||
YUZU_NON_MOVEABLE(CompressedStorageCore);
|
||||
|
||||
public:
|
||||
CompressedStorageCore() : m_table(), m_data_storage() {}
|
||||
|
||||
~CompressedStorageCore() {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
public:
|
||||
Result Initialize(VirtualFile data_storage, VirtualFile node_storage,
|
||||
VirtualFile entry_storage, s32 bktr_entry_count, size_t block_size_max,
|
||||
size_t continuous_reading_size_max,
|
||||
GetDecompressorFunction get_decompressor) {
|
||||
// Check pre-conditions.
|
||||
ASSERT(0 < block_size_max);
|
||||
ASSERT(block_size_max <= continuous_reading_size_max);
|
||||
ASSERT(get_decompressor != nullptr);
|
||||
|
||||
// Initialize our entry table.
|
||||
R_TRY(m_table.Initialize(node_storage, entry_storage, NodeSize, sizeof(Entry),
|
||||
bktr_entry_count));
|
||||
|
||||
// Set our other fields.
|
||||
m_block_size_max = block_size_max;
|
||||
m_continuous_reading_size_max = continuous_reading_size_max;
|
||||
m_data_storage = data_storage;
|
||||
m_get_decompressor_function = get_decompressor;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Finalize() {
|
||||
if (this->IsInitialized()) {
|
||||
m_table.Finalize();
|
||||
m_data_storage = VirtualFile();
|
||||
}
|
||||
}
|
||||
|
||||
VirtualFile GetDataStorage() {
|
||||
return m_data_storage;
|
||||
}
|
||||
|
||||
Result GetDataStorageSize(s64* out) {
|
||||
// Check pre-conditions.
|
||||
ASSERT(out != nullptr);
|
||||
|
||||
// Get size.
|
||||
*out = m_data_storage->GetSize();
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
BucketTree& GetEntryTable() {
|
||||
return m_table;
|
||||
}
|
||||
|
||||
Result GetEntryList(Entry* out_entries, s32* out_read_count, s32 max_entry_count,
|
||||
s64 offset, s64 size) {
|
||||
// Check pre-conditions.
|
||||
ASSERT(offset >= 0);
|
||||
ASSERT(size >= 0);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Check that we can output the count.
|
||||
R_UNLESS(out_read_count != nullptr, ResultNullptrArgument);
|
||||
|
||||
// Check that we have anything to read at all.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// Check that either we have a buffer, or this is to determine how many we need.
|
||||
if (max_entry_count != 0) {
|
||||
R_UNLESS(out_entries != nullptr, ResultNullptrArgument);
|
||||
}
|
||||
|
||||
// Get the table offsets.
|
||||
BucketTree::Offsets table_offsets;
|
||||
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||
|
||||
// Validate arguments.
|
||||
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||
|
||||
// Find the offset in our tree.
|
||||
BucketTree::Visitor visitor;
|
||||
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||
{
|
||||
const auto entry_offset = visitor.Get<Entry>()->virt_offset;
|
||||
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||
ResultUnexpectedInCompressedStorageA);
|
||||
}
|
||||
|
||||
// Get the entries.
|
||||
const auto end_offset = offset + size;
|
||||
s32 read_count = 0;
|
||||
while (visitor.Get<Entry>()->virt_offset < end_offset) {
|
||||
// If we should be setting the output, do so.
|
||||
if (max_entry_count != 0) {
|
||||
// Ensure we only read as many entries as we can.
|
||||
if (read_count >= max_entry_count) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the current output entry.
|
||||
out_entries[read_count] = *visitor.Get<Entry>();
|
||||
}
|
||||
|
||||
// Increase the read count.
|
||||
++read_count;
|
||||
|
||||
// If we're at the end, we're done.
|
||||
if (!visitor.CanMoveNext()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Move to the next entry.
|
||||
R_TRY(visitor.MoveNext());
|
||||
}
|
||||
|
||||
// Set the output read count.
|
||||
*out_read_count = read_count;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetSize(s64* out) {
|
||||
// Check pre-conditions.
|
||||
ASSERT(out != nullptr);
|
||||
|
||||
// Get our table offsets.
|
||||
BucketTree::Offsets offsets;
|
||||
R_TRY(m_table.GetOffsets(std::addressof(offsets)));
|
||||
|
||||
// Set the output.
|
||||
*out = offsets.end_offset;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result OperatePerEntry(s64 offset, s64 size, auto f) {
|
||||
// Check pre-conditions.
|
||||
ASSERT(offset >= 0);
|
||||
ASSERT(size >= 0);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Succeed if there's nothing to operate on.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// Get the table offsets.
|
||||
BucketTree::Offsets table_offsets;
|
||||
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||
|
||||
// Validate arguments.
|
||||
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||
|
||||
// Find the offset in our tree.
|
||||
BucketTree::Visitor visitor;
|
||||
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||
{
|
||||
const auto entry_offset = visitor.Get<Entry>()->virt_offset;
|
||||
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||
ResultUnexpectedInCompressedStorageA);
|
||||
}
|
||||
|
||||
// Prepare to operate in chunks.
|
||||
auto cur_offset = offset;
|
||||
const auto end_offset = offset + static_cast<s64>(size);
|
||||
|
||||
while (cur_offset < end_offset) {
|
||||
// Get the current entry.
|
||||
const auto cur_entry = *visitor.Get<Entry>();
|
||||
|
||||
// Get and validate the entry's offset.
|
||||
const auto cur_entry_offset = cur_entry.virt_offset;
|
||||
R_UNLESS(cur_entry_offset <= cur_offset, ResultUnexpectedInCompressedStorageA);
|
||||
|
||||
// Get and validate the next entry offset.
|
||||
s64 next_entry_offset;
|
||||
if (visitor.CanMoveNext()) {
|
||||
R_TRY(visitor.MoveNext());
|
||||
next_entry_offset = visitor.Get<Entry>()->virt_offset;
|
||||
R_UNLESS(table_offsets.IsInclude(next_entry_offset),
|
||||
ResultUnexpectedInCompressedStorageA);
|
||||
} else {
|
||||
next_entry_offset = table_offsets.end_offset;
|
||||
}
|
||||
R_UNLESS(cur_offset < next_entry_offset, ResultUnexpectedInCompressedStorageA);
|
||||
|
||||
// Get the offset of the entry in the data we read.
|
||||
const auto data_offset = cur_offset - cur_entry_offset;
|
||||
const auto data_size = (next_entry_offset - cur_entry_offset);
|
||||
ASSERT(data_size > 0);
|
||||
|
||||
// Determine how much is left.
|
||||
const auto remaining_size = end_offset - cur_offset;
|
||||
const auto cur_size = std::min<s64>(remaining_size, data_size - data_offset);
|
||||
ASSERT(cur_size <= size);
|
||||
|
||||
// Get the data storage size.
|
||||
s64 storage_size = m_data_storage->GetSize();
|
||||
|
||||
// Check that our read remains naively physically in bounds.
|
||||
R_UNLESS(0 <= cur_entry.phys_offset && cur_entry.phys_offset <= storage_size,
|
||||
ResultUnexpectedInCompressedStorageC);
|
||||
|
||||
// If we have any compression, verify that we remain physically in bounds.
|
||||
if (cur_entry.compression_type != CompressionType::None) {
|
||||
R_UNLESS(cur_entry.phys_offset + cur_entry.GetPhysicalSize() <= storage_size,
|
||||
ResultUnexpectedInCompressedStorageC);
|
||||
}
|
||||
|
||||
// Check that block alignment requirements are met.
|
||||
if (CompressionTypeUtility::IsBlockAlignmentRequired(cur_entry.compression_type)) {
|
||||
R_UNLESS(Common::IsAligned(cur_entry.phys_offset, CompressionBlockAlignment),
|
||||
ResultUnexpectedInCompressedStorageA);
|
||||
}
|
||||
|
||||
// Invoke the operator.
|
||||
bool is_continuous = true;
|
||||
R_TRY(
|
||||
f(std::addressof(is_continuous), cur_entry, data_size, data_offset, cur_size));
|
||||
|
||||
// If not continuous, we're done.
|
||||
if (!is_continuous) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance.
|
||||
cur_offset += cur_size;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
public:
|
||||
using ReadImplFunction = std::function<Result(void*, size_t)>;
|
||||
using ReadFunction = std::function<Result(size_t, const ReadImplFunction&)>;
|
||||
|
||||
public:
|
||||
Result Read(s64 offset, s64 size, const ReadFunction& read_func) {
|
||||
// Check pre-conditions.
|
||||
ASSERT(offset >= 0);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Succeed immediately, if we have nothing to read.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// Declare read lambda.
|
||||
constexpr int EntriesCountMax = 0x80;
|
||||
struct Entries {
|
||||
CompressionType compression_type;
|
||||
u32 gap_from_prev;
|
||||
u32 physical_size;
|
||||
u32 virtual_size;
|
||||
};
|
||||
std::array<Entries, EntriesCountMax> entries;
|
||||
s32 entry_count = 0;
|
||||
Entry prev_entry = {
|
||||
.virt_offset = -1,
|
||||
.phys_offset{},
|
||||
.compression_type{},
|
||||
.phys_size{},
|
||||
};
|
||||
bool will_allocate_pooled_buffer = false;
|
||||
s64 required_access_physical_offset = 0;
|
||||
s64 required_access_physical_size = 0;
|
||||
|
||||
auto PerformRequiredRead = [&]() -> Result {
|
||||
// If there are no entries, we have nothing to do.
|
||||
R_SUCCEED_IF(entry_count == 0);
|
||||
|
||||
// Get the remaining size in a convenient form.
|
||||
const size_t total_required_size =
|
||||
static_cast<size_t>(required_access_physical_size);
|
||||
|
||||
// Perform the read based on whether we need to allocate a buffer.
|
||||
if (will_allocate_pooled_buffer) {
|
||||
// Allocate a pooled buffer.
|
||||
PooledBuffer pooled_buffer;
|
||||
if (pooled_buffer.GetAllocatableSizeMax() >= total_required_size) {
|
||||
pooled_buffer.Allocate(total_required_size, m_block_size_max);
|
||||
} else {
|
||||
pooled_buffer.AllocateParticularlyLarge(
|
||||
std::min<size_t>(
|
||||
total_required_size,
|
||||
PooledBuffer::GetAllocatableParticularlyLargeSizeMax()),
|
||||
m_block_size_max);
|
||||
}
|
||||
|
||||
// Read each of the entries.
|
||||
for (s32 entry_idx = 0; entry_idx < entry_count; ++entry_idx) {
|
||||
// Determine the current read size.
|
||||
bool will_use_pooled_buffer = false;
|
||||
const size_t cur_read_size = [&]() -> size_t {
|
||||
if (const size_t target_entry_size =
|
||||
static_cast<size_t>(entries[entry_idx].physical_size) +
|
||||
static_cast<size_t>(entries[entry_idx].gap_from_prev);
|
||||
target_entry_size <= pooled_buffer.GetSize()) {
|
||||
// We'll be using the pooled buffer.
|
||||
will_use_pooled_buffer = true;
|
||||
|
||||
// Determine how much we can read.
|
||||
const size_t max_size = std::min<size_t>(
|
||||
required_access_physical_size, pooled_buffer.GetSize());
|
||||
|
||||
size_t read_size = 0;
|
||||
for (auto n = entry_idx; n < entry_count; ++n) {
|
||||
const size_t cur_entry_size =
|
||||
static_cast<size_t>(entries[n].physical_size) +
|
||||
static_cast<size_t>(entries[n].gap_from_prev);
|
||||
if (read_size + cur_entry_size > max_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
read_size += cur_entry_size;
|
||||
}
|
||||
|
||||
return read_size;
|
||||
} else {
|
||||
// If we don't fit, we must be uncompressed.
|
||||
ASSERT(entries[entry_idx].compression_type ==
|
||||
CompressionType::None);
|
||||
|
||||
// We can perform the whole of an uncompressed read directly.
|
||||
return entries[entry_idx].virtual_size;
|
||||
}
|
||||
}();
|
||||
|
||||
// Perform the read based on whether or not we'll use the pooled buffer.
|
||||
if (will_use_pooled_buffer) {
|
||||
// Read the compressed data into the pooled buffer.
|
||||
auto* const buffer = pooled_buffer.GetBuffer();
|
||||
m_data_storage->Read(reinterpret_cast<u8*>(buffer), cur_read_size,
|
||||
required_access_physical_offset);
|
||||
|
||||
// Decompress the data.
|
||||
size_t buffer_offset;
|
||||
for (buffer_offset = 0;
|
||||
entry_idx < entry_count &&
|
||||
((static_cast<size_t>(entries[entry_idx].physical_size) +
|
||||
static_cast<size_t>(entries[entry_idx].gap_from_prev)) == 0 ||
|
||||
buffer_offset < cur_read_size);
|
||||
buffer_offset += entries[entry_idx++].physical_size) {
|
||||
// Advance by the relevant gap.
|
||||
buffer_offset += entries[entry_idx].gap_from_prev;
|
||||
|
||||
const auto compression_type = entries[entry_idx].compression_type;
|
||||
switch (compression_type) {
|
||||
case CompressionType::None: {
|
||||
// Check that we can remain within bounds.
|
||||
ASSERT(buffer_offset + entries[entry_idx].virtual_size <=
|
||||
cur_read_size);
|
||||
|
||||
// Perform no decompression.
|
||||
R_TRY(read_func(
|
||||
entries[entry_idx].virtual_size,
|
||||
[&](void* dst, size_t dst_size) -> Result {
|
||||
// Check that the size is valid.
|
||||
ASSERT(dst_size == entries[entry_idx].virtual_size);
|
||||
|
||||
// We have no compression, so just copy the data
|
||||
// out.
|
||||
std::memcpy(dst, buffer + buffer_offset,
|
||||
entries[entry_idx].virtual_size);
|
||||
R_SUCCEED();
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
case CompressionType::Zeros: {
|
||||
// Check that we can remain within bounds.
|
||||
ASSERT(buffer_offset <= cur_read_size);
|
||||
|
||||
// Zero the memory.
|
||||
R_TRY(read_func(
|
||||
entries[entry_idx].virtual_size,
|
||||
[&](void* dst, size_t dst_size) -> Result {
|
||||
// Check that the size is valid.
|
||||
ASSERT(dst_size == entries[entry_idx].virtual_size);
|
||||
|
||||
// The data is zeroes, so zero the buffer.
|
||||
std::memset(dst, 0, entries[entry_idx].virtual_size);
|
||||
R_SUCCEED();
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Check that we can remain within bounds.
|
||||
ASSERT(buffer_offset + entries[entry_idx].physical_size <=
|
||||
cur_read_size);
|
||||
|
||||
// Get the decompressor.
|
||||
const auto decompressor =
|
||||
this->GetDecompressor(compression_type);
|
||||
R_UNLESS(decompressor != nullptr,
|
||||
ResultUnexpectedInCompressedStorageB);
|
||||
|
||||
// Decompress the data.
|
||||
R_TRY(read_func(entries[entry_idx].virtual_size,
|
||||
[&](void* dst, size_t dst_size) -> Result {
|
||||
// Check that the size is valid.
|
||||
ASSERT(dst_size ==
|
||||
entries[entry_idx].virtual_size);
|
||||
|
||||
// Perform the decompression.
|
||||
R_RETURN(decompressor(
|
||||
dst, entries[entry_idx].virtual_size,
|
||||
buffer + buffer_offset,
|
||||
entries[entry_idx].physical_size));
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we processed the correct amount of data.
|
||||
ASSERT(buffer_offset == cur_read_size);
|
||||
} else {
|
||||
// Account for the gap from the previous entry.
|
||||
required_access_physical_offset += entries[entry_idx].gap_from_prev;
|
||||
required_access_physical_size -= entries[entry_idx].gap_from_prev;
|
||||
|
||||
// We don't need the buffer (as the data is uncompressed), so just
|
||||
// execute the read.
|
||||
R_TRY(
|
||||
read_func(cur_read_size, [&](void* dst, size_t dst_size) -> Result {
|
||||
// Check that the size is valid.
|
||||
ASSERT(dst_size == cur_read_size);
|
||||
|
||||
// Perform the read.
|
||||
m_data_storage->Read(reinterpret_cast<u8*>(dst), cur_read_size,
|
||||
required_access_physical_offset);
|
||||
|
||||
R_SUCCEED();
|
||||
}));
|
||||
}
|
||||
|
||||
// Advance on.
|
||||
required_access_physical_offset += cur_read_size;
|
||||
required_access_physical_size -= cur_read_size;
|
||||
}
|
||||
|
||||
// Verify that we have nothing remaining to read.
|
||||
ASSERT(required_access_physical_size == 0);
|
||||
|
||||
R_SUCCEED();
|
||||
} else {
|
||||
// We don't need a buffer, so just execute the read.
|
||||
R_TRY(read_func(total_required_size, [&](void* dst, size_t dst_size) -> Result {
|
||||
// Check that the size is valid.
|
||||
ASSERT(dst_size == total_required_size);
|
||||
|
||||
// Perform the read.
|
||||
m_data_storage->Read(reinterpret_cast<u8*>(dst), total_required_size,
|
||||
required_access_physical_offset);
|
||||
|
||||
R_SUCCEED();
|
||||
}));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
R_TRY(this->OperatePerEntry(
|
||||
offset, size,
|
||||
[&](bool* out_continuous, const Entry& entry, s64 virtual_data_size,
|
||||
s64 data_offset, s64 read_size) -> Result {
|
||||
// Determine the physical extents.
|
||||
s64 physical_offset, physical_size;
|
||||
if (CompressionTypeUtility::IsRandomAccessible(entry.compression_type)) {
|
||||
physical_offset = entry.phys_offset + data_offset;
|
||||
physical_size = read_size;
|
||||
} else {
|
||||
physical_offset = entry.phys_offset;
|
||||
physical_size = entry.GetPhysicalSize();
|
||||
}
|
||||
|
||||
// If we have a pending data storage operation, perform it if we have to.
|
||||
const s64 required_access_physical_end =
|
||||
required_access_physical_offset + required_access_physical_size;
|
||||
if (required_access_physical_size > 0) {
|
||||
const bool required_by_gap =
|
||||
!(required_access_physical_end <= physical_offset &&
|
||||
physical_offset <= Common::AlignUp(required_access_physical_end,
|
||||
CompressionBlockAlignment));
|
||||
const bool required_by_continuous_size =
|
||||
((physical_size + physical_offset) - required_access_physical_end) +
|
||||
required_access_physical_size >
|
||||
static_cast<s64>(m_continuous_reading_size_max);
|
||||
const bool required_by_entry_count = entry_count == EntriesCountMax;
|
||||
if (required_by_gap || required_by_continuous_size ||
|
||||
required_by_entry_count) {
|
||||
// Check that our planned access is sane.
|
||||
ASSERT(!will_allocate_pooled_buffer ||
|
||||
required_access_physical_size <=
|
||||
static_cast<s64>(m_continuous_reading_size_max));
|
||||
|
||||
// Perform the required read.
|
||||
const Result rc = PerformRequiredRead();
|
||||
if (R_FAILED(rc)) {
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
// Reset our requirements.
|
||||
prev_entry.virt_offset = -1;
|
||||
required_access_physical_size = 0;
|
||||
entry_count = 0;
|
||||
will_allocate_pooled_buffer = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check that we're within bounds on entries.
|
||||
ASSERT(entry_count < EntriesCountMax);
|
||||
|
||||
// Determine if a buffer allocation is needed.
|
||||
if (entry.compression_type != CompressionType::None ||
|
||||
(prev_entry.virt_offset >= 0 &&
|
||||
entry.virt_offset - prev_entry.virt_offset !=
|
||||
entry.phys_offset - prev_entry.phys_offset)) {
|
||||
will_allocate_pooled_buffer = true;
|
||||
}
|
||||
|
||||
// If we need to access the data storage, update our required access parameters.
|
||||
if (CompressionTypeUtility::IsDataStorageAccessRequired(
|
||||
entry.compression_type)) {
|
||||
// If the data is compressed, ensure the access is sane.
|
||||
if (entry.compression_type != CompressionType::None) {
|
||||
R_UNLESS(data_offset == 0, ResultInvalidOffset);
|
||||
R_UNLESS(virtual_data_size == read_size, ResultInvalidSize);
|
||||
R_UNLESS(entry.GetPhysicalSize() <= static_cast<s64>(m_block_size_max),
|
||||
ResultUnexpectedInCompressedStorageD);
|
||||
}
|
||||
|
||||
// Update the required access parameters.
|
||||
s64 gap_from_prev;
|
||||
if (required_access_physical_size > 0) {
|
||||
gap_from_prev = physical_offset - required_access_physical_end;
|
||||
} else {
|
||||
gap_from_prev = 0;
|
||||
required_access_physical_offset = physical_offset;
|
||||
}
|
||||
required_access_physical_size += physical_size + gap_from_prev;
|
||||
|
||||
// Create an entry to access the data storage.
|
||||
entries[entry_count++] = {
|
||||
.compression_type = entry.compression_type,
|
||||
.gap_from_prev = static_cast<u32>(gap_from_prev),
|
||||
.physical_size = static_cast<u32>(physical_size),
|
||||
.virtual_size = static_cast<u32>(read_size),
|
||||
};
|
||||
} else {
|
||||
// Verify that we're allowed to be operating on the non-data-storage-access
|
||||
// type.
|
||||
R_UNLESS(entry.compression_type == CompressionType::Zeros,
|
||||
ResultUnexpectedInCompressedStorageB);
|
||||
|
||||
// If we have entries, create a fake entry for the zero region.
|
||||
if (entry_count != 0) {
|
||||
// We need to have a physical size.
|
||||
R_UNLESS(entry.GetPhysicalSize() != 0,
|
||||
ResultUnexpectedInCompressedStorageD);
|
||||
|
||||
// Create a fake entry.
|
||||
entries[entry_count++] = {
|
||||
.compression_type = CompressionType::Zeros,
|
||||
.gap_from_prev = 0,
|
||||
.physical_size = 0,
|
||||
.virtual_size = static_cast<u32>(read_size),
|
||||
};
|
||||
} else {
|
||||
// We have no entries, so we can just perform the read.
|
||||
const Result rc =
|
||||
read_func(static_cast<size_t>(read_size),
|
||||
[&](void* dst, size_t dst_size) -> Result {
|
||||
// Check the space we should zero is correct.
|
||||
ASSERT(dst_size == static_cast<size_t>(read_size));
|
||||
|
||||
// Zero the memory.
|
||||
std::memset(dst, 0, read_size);
|
||||
R_SUCCEED();
|
||||
});
|
||||
if (R_FAILED(rc)) {
|
||||
R_THROW(rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the previous entry.
|
||||
prev_entry = entry;
|
||||
|
||||
// We're continuous.
|
||||
*out_continuous = true;
|
||||
R_SUCCEED();
|
||||
}));
|
||||
|
||||
// If we still have a pending access, perform it.
|
||||
if (required_access_physical_size != 0) {
|
||||
R_TRY(PerformRequiredRead());
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
DecompressorFunction GetDecompressor(CompressionType type) const {
|
||||
// Check that we can get a decompressor for the type.
|
||||
if (CompressionTypeUtility::IsUnknownType(type)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Get the decompressor.
|
||||
return m_get_decompressor_function(type);
|
||||
}
|
||||
|
||||
bool IsInitialized() const {
|
||||
return m_table.IsInitialized();
|
||||
}
|
||||
|
||||
private:
|
||||
size_t m_block_size_max;
|
||||
size_t m_continuous_reading_size_max;
|
||||
BucketTree m_table;
|
||||
VirtualFile m_data_storage;
|
||||
GetDecompressorFunction m_get_decompressor_function;
|
||||
};
|
||||
|
||||
class CacheManager {
|
||||
YUZU_NON_COPYABLE(CacheManager);
|
||||
YUZU_NON_MOVEABLE(CacheManager);
|
||||
|
||||
private:
|
||||
struct AccessRange {
|
||||
s64 virtual_offset;
|
||||
s64 virtual_size;
|
||||
u32 physical_size;
|
||||
bool is_block_alignment_required;
|
||||
|
||||
s64 GetEndVirtualOffset() const {
|
||||
return this->virtual_offset + this->virtual_size;
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<AccessRange>);
|
||||
|
||||
public:
|
||||
CacheManager() = default;
|
||||
|
||||
public:
|
||||
Result Initialize(s64 storage_size, size_t cache_size_0, size_t cache_size_1,
|
||||
size_t max_cache_entries) {
|
||||
// Set our fields.
|
||||
m_storage_size = storage_size;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Read(CompressedStorageCore& core, s64 offset, void* buffer, size_t size) {
|
||||
// If we have nothing to read, succeed.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// Check that we have a buffer to read into.
|
||||
R_UNLESS(buffer != nullptr, ResultNullptrArgument);
|
||||
|
||||
// Check that the read is in bounds.
|
||||
R_UNLESS(offset <= m_storage_size, ResultInvalidOffset);
|
||||
|
||||
// Determine how much we can read.
|
||||
const size_t read_size = std::min<size_t>(size, m_storage_size - offset);
|
||||
|
||||
// Create head/tail ranges.
|
||||
AccessRange head_range = {};
|
||||
AccessRange tail_range = {};
|
||||
bool is_tail_set = false;
|
||||
|
||||
// Operate to determine the head range.
|
||||
R_TRY(core.OperatePerEntry(
|
||||
offset, 1,
|
||||
[&](bool* out_continuous, const Entry& entry, s64 virtual_data_size,
|
||||
s64 data_offset, s64 data_read_size) -> Result {
|
||||
// Set the head range.
|
||||
head_range = {
|
||||
.virtual_offset = entry.virt_offset,
|
||||
.virtual_size = virtual_data_size,
|
||||
.physical_size = static_cast<u32>(entry.phys_size),
|
||||
.is_block_alignment_required =
|
||||
CompressionTypeUtility::IsBlockAlignmentRequired(
|
||||
entry.compression_type),
|
||||
};
|
||||
|
||||
// If required, set the tail range.
|
||||
if (static_cast<s64>(offset + read_size) <=
|
||||
entry.virt_offset + virtual_data_size) {
|
||||
tail_range = {
|
||||
.virtual_offset = entry.virt_offset,
|
||||
.virtual_size = virtual_data_size,
|
||||
.physical_size = static_cast<u32>(entry.phys_size),
|
||||
.is_block_alignment_required =
|
||||
CompressionTypeUtility::IsBlockAlignmentRequired(
|
||||
entry.compression_type),
|
||||
};
|
||||
is_tail_set = true;
|
||||
}
|
||||
|
||||
// We only want to determine the head range, so we're not continuous.
|
||||
*out_continuous = false;
|
||||
R_SUCCEED();
|
||||
}));
|
||||
|
||||
// If necessary, determine the tail range.
|
||||
if (!is_tail_set) {
|
||||
R_TRY(core.OperatePerEntry(
|
||||
offset + read_size - 1, 1,
|
||||
[&](bool* out_continuous, const Entry& entry, s64 virtual_data_size,
|
||||
s64 data_offset, s64 data_read_size) -> Result {
|
||||
// Set the tail range.
|
||||
tail_range = {
|
||||
.virtual_offset = entry.virt_offset,
|
||||
.virtual_size = virtual_data_size,
|
||||
.physical_size = static_cast<u32>(entry.phys_size),
|
||||
.is_block_alignment_required =
|
||||
CompressionTypeUtility::IsBlockAlignmentRequired(
|
||||
entry.compression_type),
|
||||
};
|
||||
|
||||
// We only want to determine the tail range, so we're not continuous.
|
||||
*out_continuous = false;
|
||||
R_SUCCEED();
|
||||
}));
|
||||
}
|
||||
|
||||
// Begin performing the accesses.
|
||||
s64 cur_offset = offset;
|
||||
size_t cur_size = read_size;
|
||||
char* cur_dst = static_cast<char*>(buffer);
|
||||
|
||||
// Determine our alignment.
|
||||
const bool head_unaligned = head_range.is_block_alignment_required &&
|
||||
(cur_offset != head_range.virtual_offset ||
|
||||
static_cast<s64>(cur_size) < head_range.virtual_size);
|
||||
const bool tail_unaligned = [&]() -> bool {
|
||||
if (tail_range.is_block_alignment_required) {
|
||||
if (static_cast<s64>(cur_size + cur_offset) ==
|
||||
tail_range.GetEndVirtualOffset()) {
|
||||
return false;
|
||||
} else if (!head_unaligned) {
|
||||
return true;
|
||||
} else {
|
||||
return head_range.GetEndVirtualOffset() <
|
||||
static_cast<s64>(cur_size + cur_offset);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}();
|
||||
|
||||
// Determine start/end offsets.
|
||||
const s64 start_offset =
|
||||
head_range.is_block_alignment_required ? head_range.virtual_offset : cur_offset;
|
||||
const s64 end_offset = tail_range.is_block_alignment_required
|
||||
? tail_range.GetEndVirtualOffset()
|
||||
: cur_offset + cur_size;
|
||||
|
||||
// Perform the read.
|
||||
bool is_burst_reading = false;
|
||||
R_TRY(core.Read(
|
||||
start_offset, end_offset - start_offset,
|
||||
[&](size_t size_buffer_required,
|
||||
const CompressedStorageCore::ReadImplFunction& read_impl) -> Result {
|
||||
// Determine whether we're burst reading.
|
||||
const AccessRange* unaligned_range = nullptr;
|
||||
if (!is_burst_reading) {
|
||||
// Check whether we're using head, tail, or none as unaligned.
|
||||
if (head_unaligned && head_range.virtual_offset <= cur_offset &&
|
||||
cur_offset < head_range.GetEndVirtualOffset()) {
|
||||
unaligned_range = std::addressof(head_range);
|
||||
} else if (tail_unaligned && tail_range.virtual_offset <= cur_offset &&
|
||||
cur_offset < tail_range.GetEndVirtualOffset()) {
|
||||
unaligned_range = std::addressof(tail_range);
|
||||
} else {
|
||||
is_burst_reading = true;
|
||||
}
|
||||
}
|
||||
ASSERT((is_burst_reading ^ (unaligned_range != nullptr)));
|
||||
|
||||
// Perform reading by burst, or not.
|
||||
if (is_burst_reading) {
|
||||
// Check that the access is valid for burst reading.
|
||||
ASSERT(size_buffer_required <= cur_size);
|
||||
|
||||
// Perform the read.
|
||||
Result rc = read_impl(cur_dst, size_buffer_required);
|
||||
if (R_FAILED(rc)) {
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
// Advance.
|
||||
cur_dst += size_buffer_required;
|
||||
cur_offset += size_buffer_required;
|
||||
cur_size -= size_buffer_required;
|
||||
|
||||
// Determine whether we're going to continue burst reading.
|
||||
const s64 offset_aligned =
|
||||
tail_unaligned ? tail_range.virtual_offset : end_offset;
|
||||
ASSERT(cur_offset <= offset_aligned);
|
||||
|
||||
if (offset_aligned <= cur_offset) {
|
||||
is_burst_reading = false;
|
||||
}
|
||||
} else {
|
||||
// We're not burst reading, so we have some unaligned range.
|
||||
ASSERT(unaligned_range != nullptr);
|
||||
|
||||
// Check that the size is correct.
|
||||
ASSERT(size_buffer_required ==
|
||||
static_cast<size_t>(unaligned_range->virtual_size));
|
||||
|
||||
// Get a pooled buffer for our read.
|
||||
PooledBuffer pooled_buffer;
|
||||
pooled_buffer.Allocate(size_buffer_required, size_buffer_required);
|
||||
|
||||
// Perform read.
|
||||
Result rc = read_impl(pooled_buffer.GetBuffer(), size_buffer_required);
|
||||
if (R_FAILED(rc)) {
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
// Copy the data we read to the destination.
|
||||
const size_t skip_size = cur_offset - unaligned_range->virtual_offset;
|
||||
const size_t copy_size = std::min<size_t>(
|
||||
cur_size, unaligned_range->GetEndVirtualOffset() - cur_offset);
|
||||
|
||||
std::memcpy(cur_dst, pooled_buffer.GetBuffer() + skip_size, copy_size);
|
||||
|
||||
// Advance.
|
||||
cur_dst += copy_size;
|
||||
cur_offset += copy_size;
|
||||
cur_size -= copy_size;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
s64 m_storage_size = 0;
|
||||
};
|
||||
|
||||
public:
|
||||
CompressedStorage() = default;
|
||||
virtual ~CompressedStorage() {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
Result Initialize(VirtualFile data_storage, VirtualFile node_storage, VirtualFile entry_storage,
|
||||
s32 bktr_entry_count, size_t block_size_max,
|
||||
size_t continuous_reading_size_max, GetDecompressorFunction get_decompressor,
|
||||
size_t cache_size_0, size_t cache_size_1, s32 max_cache_entries) {
|
||||
// Initialize our core.
|
||||
R_TRY(m_core.Initialize(data_storage, node_storage, entry_storage, bktr_entry_count,
|
||||
block_size_max, continuous_reading_size_max, get_decompressor));
|
||||
|
||||
// Get our core size.
|
||||
s64 core_size = 0;
|
||||
R_TRY(m_core.GetSize(std::addressof(core_size)));
|
||||
|
||||
// Initialize our cache manager.
|
||||
R_TRY(m_cache_manager.Initialize(core_size, cache_size_0, cache_size_1, max_cache_entries));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Finalize() {
|
||||
m_core.Finalize();
|
||||
}
|
||||
|
||||
VirtualFile GetDataStorage() {
|
||||
return m_core.GetDataStorage();
|
||||
}
|
||||
|
||||
Result GetDataStorageSize(s64* out) {
|
||||
R_RETURN(m_core.GetDataStorageSize(out));
|
||||
}
|
||||
|
||||
Result GetEntryList(Entry* out_entries, s32* out_read_count, s32 max_entry_count, s64 offset,
|
||||
s64 size) {
|
||||
R_RETURN(m_core.GetEntryList(out_entries, out_read_count, max_entry_count, offset, size));
|
||||
}
|
||||
|
||||
BucketTree& GetEntryTable() {
|
||||
return m_core.GetEntryTable();
|
||||
}
|
||||
|
||||
public:
|
||||
virtual size_t GetSize() const override {
|
||||
s64 ret{};
|
||||
m_core.GetSize(&ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||
if (R_SUCCEEDED(m_cache_manager.Read(m_core, offset, buffer, size))) {
|
||||
return size;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
mutable CompressedStorageCore m_core;
|
||||
mutable CacheManager m_cache_manager;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,43 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
enum class CompressionType : u8 {
|
||||
None = 0,
|
||||
Zeros = 1,
|
||||
Two = 2,
|
||||
Lz4 = 3,
|
||||
Unknown = 4,
|
||||
};
|
||||
|
||||
using DecompressorFunction = Result (*)(void*, size_t, const void*, size_t);
|
||||
using GetDecompressorFunction = DecompressorFunction (*)(CompressionType);
|
||||
|
||||
constexpr s64 CompressionBlockAlignment = 0x10;
|
||||
|
||||
namespace CompressionTypeUtility {
|
||||
|
||||
constexpr bool IsBlockAlignmentRequired(CompressionType type) {
|
||||
return type != CompressionType::None && type != CompressionType::Zeros;
|
||||
}
|
||||
|
||||
constexpr bool IsDataStorageAccessRequired(CompressionType type) {
|
||||
return type != CompressionType::Zeros;
|
||||
}
|
||||
|
||||
constexpr bool IsRandomAccessible(CompressionType type) {
|
||||
return type == CompressionType::None;
|
||||
}
|
||||
|
||||
constexpr bool IsUnknownType(CompressionType type) {
|
||||
return type >= CompressionType::Unknown;
|
||||
}
|
||||
|
||||
} // namespace CompressionTypeUtility
|
||||
|
||||
} // namespace FileSys
|
@ -1,36 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/lz4_compression.h"
|
||||
#include "core/file_sys/fssystem/fssystem_compression_configuration.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
Result DecompressLz4(void* dst, size_t dst_size, const void* src, size_t src_size) {
|
||||
auto result = Common::Compression::DecompressDataLZ4(dst, dst_size, src, src_size);
|
||||
R_UNLESS(static_cast<size_t>(result) == dst_size, ResultUnexpectedInCompressedStorageC);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
constexpr DecompressorFunction GetNcaDecompressorFunction(CompressionType type) {
|
||||
switch (type) {
|
||||
case CompressionType::Lz4:
|
||||
return DecompressLz4;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const NcaCompressionConfiguration& GetNcaCompressionConfiguration() {
|
||||
static const NcaCompressionConfiguration configuration = {
|
||||
.get_decompressor = GetNcaDecompressorFunction,
|
||||
};
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,12 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
const NcaCompressionConfiguration& GetNcaCompressionConfiguration();
|
||||
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/fssystem/fssystem_crypto_configuration.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
void GenerateKey(void* dst_key, size_t dst_key_size, const void* src_key, size_t src_key_size,
|
||||
s32 key_type) {
|
||||
if (key_type == static_cast<s32>(KeyType::ZeroKey)) {
|
||||
std::memset(dst_key, 0, dst_key_size);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key_type == static_cast<s32>(KeyType::InvalidKey) ||
|
||||
key_type < static_cast<s32>(KeyType::ZeroKey) ||
|
||||
key_type >= static_cast<s32>(KeyType::NcaExternalKey)) {
|
||||
std::memset(dst_key, 0xFF, dst_key_size);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& instance = Core::Crypto::KeyManager::Instance();
|
||||
|
||||
if (key_type == static_cast<s32>(KeyType::NcaHeaderKey1) ||
|
||||
key_type == static_cast<s32>(KeyType::NcaHeaderKey2)) {
|
||||
const s32 key_index = static_cast<s32>(KeyType::NcaHeaderKey2) == key_type;
|
||||
const auto key = instance.GetKey(Core::Crypto::S256KeyType::Header);
|
||||
std::memcpy(dst_key, key.data() + key_index * 0x10, std::min(dst_key_size, key.size() / 2));
|
||||
return;
|
||||
}
|
||||
|
||||
const s32 key_generation =
|
||||
std::max(key_type / NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount, 1) - 1;
|
||||
const s32 key_index = key_type % NcaCryptoConfiguration::KeyAreaEncryptionKeyIndexCount;
|
||||
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(
|
||||
instance.GetKey(Core::Crypto::S128KeyType::KeyArea, key_generation, key_index),
|
||||
Core::Crypto::Mode::ECB);
|
||||
cipher.Transcode(reinterpret_cast<const u8*>(src_key), src_key_size,
|
||||
reinterpret_cast<u8*>(dst_key), Core::Crypto::Op::Decrypt);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const NcaCryptoConfiguration& GetCryptoConfiguration() {
|
||||
static const NcaCryptoConfiguration configuration = {
|
||||
.header_1_sign_key_moduli{},
|
||||
.header_1_sign_key_public_exponent{},
|
||||
.key_area_encryption_key_source{},
|
||||
.header_encryption_key_source{},
|
||||
.header_encrypted_encryption_keys{},
|
||||
.generate_key = GenerateKey,
|
||||
.verify_sign1{},
|
||||
.is_plaintext_header_available{},
|
||||
.is_available_sw_key{},
|
||||
};
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,12 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_nca_file_system_driver.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
const NcaCryptoConfiguration& GetCryptoConfiguration();
|
||||
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_hierarchical_integrity_verification_storage.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
HierarchicalIntegrityVerificationStorage::HierarchicalIntegrityVerificationStorage()
|
||||
: m_data_size(-1) {
|
||||
for (size_t i = 0; i < MaxLayers - 1; i++) {
|
||||
m_verify_storages[i] = std::make_shared<IntegrityVerificationStorage>();
|
||||
}
|
||||
}
|
||||
|
||||
Result HierarchicalIntegrityVerificationStorage::Initialize(
|
||||
const HierarchicalIntegrityVerificationInformation& info,
|
||||
HierarchicalStorageInformation storage, int max_data_cache_entries, int max_hash_cache_entries,
|
||||
s8 buffer_level) {
|
||||
// Validate preconditions.
|
||||
ASSERT(IntegrityMinLayerCount <= info.max_layers && info.max_layers <= IntegrityMaxLayerCount);
|
||||
|
||||
// Set member variables.
|
||||
m_max_layers = info.max_layers;
|
||||
|
||||
// Initialize the top level verification storage.
|
||||
m_verify_storages[0]->Initialize(storage[HierarchicalStorageInformation::MasterStorage],
|
||||
storage[HierarchicalStorageInformation::Layer1Storage],
|
||||
static_cast<s64>(1) << info.info[0].block_order, HashSize,
|
||||
false);
|
||||
|
||||
// Ensure we don't leak state if further initialization goes wrong.
|
||||
ON_RESULT_FAILURE {
|
||||
m_verify_storages[0]->Finalize();
|
||||
m_data_size = -1;
|
||||
};
|
||||
|
||||
// Initialize the top level buffer storage.
|
||||
m_buffer_storages[0] = m_verify_storages[0];
|
||||
R_UNLESS(m_buffer_storages[0] != nullptr, ResultAllocationMemoryFailedAllocateShared);
|
||||
|
||||
// Prepare to initialize the level storages.
|
||||
s32 level = 0;
|
||||
|
||||
// Ensure we don't leak state if further initialization goes wrong.
|
||||
ON_RESULT_FAILURE_2 {
|
||||
m_verify_storages[level + 1]->Finalize();
|
||||
for (; level > 0; --level) {
|
||||
m_buffer_storages[level].reset();
|
||||
m_verify_storages[level]->Finalize();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the level storages.
|
||||
for (; level < m_max_layers - 3; ++level) {
|
||||
// Initialize the verification storage.
|
||||
auto buffer_storage =
|
||||
std::make_shared<OffsetVfsFile>(m_buffer_storages[level], info.info[level].size, 0);
|
||||
m_verify_storages[level + 1]->Initialize(
|
||||
std::move(buffer_storage), storage[level + 2],
|
||||
static_cast<s64>(1) << info.info[level + 1].block_order,
|
||||
static_cast<s64>(1) << info.info[level].block_order, false);
|
||||
|
||||
// Initialize the buffer storage.
|
||||
m_buffer_storages[level + 1] = m_verify_storages[level + 1];
|
||||
R_UNLESS(m_buffer_storages[level + 1] != nullptr,
|
||||
ResultAllocationMemoryFailedAllocateShared);
|
||||
}
|
||||
|
||||
// Initialize the final level storage.
|
||||
{
|
||||
// Initialize the verification storage.
|
||||
auto buffer_storage =
|
||||
std::make_shared<OffsetVfsFile>(m_buffer_storages[level], info.info[level].size, 0);
|
||||
m_verify_storages[level + 1]->Initialize(
|
||||
std::move(buffer_storage), storage[level + 2],
|
||||
static_cast<s64>(1) << info.info[level + 1].block_order,
|
||||
static_cast<s64>(1) << info.info[level].block_order, true);
|
||||
|
||||
// Initialize the buffer storage.
|
||||
m_buffer_storages[level + 1] = m_verify_storages[level + 1];
|
||||
R_UNLESS(m_buffer_storages[level + 1] != nullptr,
|
||||
ResultAllocationMemoryFailedAllocateShared);
|
||||
}
|
||||
|
||||
// Set the data size.
|
||||
m_data_size = info.info[level + 1].size;
|
||||
|
||||
// We succeeded.
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void HierarchicalIntegrityVerificationStorage::Finalize() {
|
||||
if (m_data_size >= 0) {
|
||||
m_data_size = 0;
|
||||
|
||||
for (s32 level = m_max_layers - 2; level >= 0; --level) {
|
||||
m_buffer_storages[level].reset();
|
||||
m_verify_storages[level]->Finalize();
|
||||
}
|
||||
|
||||
m_data_size = -1;
|
||||
}
|
||||
}
|
||||
|
||||
size_t HierarchicalIntegrityVerificationStorage::Read(u8* buffer, size_t size,
|
||||
size_t offset) const {
|
||||
// Validate preconditions.
|
||||
ASSERT(m_data_size >= 0);
|
||||
|
||||
// Succeed if zero-size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// Read the data.
|
||||
return m_buffer_storages[m_max_layers - 2]->Read(buffer, size, offset);
|
||||
}
|
||||
|
||||
size_t HierarchicalIntegrityVerificationStorage::GetSize() const {
|
||||
return m_data_size;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,164 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/fssystem/fs_types.h"
|
||||
#include "core/file_sys/fssystem/fssystem_alignment_matching_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_integrity_verification_storage.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||
Int64 offset;
|
||||
Int64 size;
|
||||
s32 block_order;
|
||||
std::array<u8, 4> reserved;
|
||||
};
|
||||
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationLevelInformation>);
|
||||
static_assert(sizeof(HierarchicalIntegrityVerificationLevelInformation) == 0x18);
|
||||
static_assert(alignof(HierarchicalIntegrityVerificationLevelInformation) == 0x4);
|
||||
|
||||
struct HierarchicalIntegrityVerificationInformation {
|
||||
u32 max_layers;
|
||||
std::array<HierarchicalIntegrityVerificationLevelInformation, IntegrityMaxLayerCount - 1> info;
|
||||
HashSalt seed;
|
||||
|
||||
s64 GetLayeredHashSize() const {
|
||||
return this->info[this->max_layers - 2].offset;
|
||||
}
|
||||
|
||||
s64 GetDataOffset() const {
|
||||
return this->info[this->max_layers - 2].offset;
|
||||
}
|
||||
|
||||
s64 GetDataSize() const {
|
||||
return this->info[this->max_layers - 2].size;
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationInformation>);
|
||||
|
||||
struct HierarchicalIntegrityVerificationMetaInformation {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u32 master_hash_size;
|
||||
HierarchicalIntegrityVerificationInformation level_hash_info;
|
||||
};
|
||||
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationMetaInformation>);
|
||||
|
||||
struct HierarchicalIntegrityVerificationSizeSet {
|
||||
s64 control_size;
|
||||
s64 master_hash_size;
|
||||
std::array<s64, IntegrityMaxLayerCount - 2> layered_hash_sizes;
|
||||
};
|
||||
static_assert(std::is_trivial_v<HierarchicalIntegrityVerificationSizeSet>);
|
||||
|
||||
class HierarchicalIntegrityVerificationStorage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(HierarchicalIntegrityVerificationStorage);
|
||||
YUZU_NON_MOVEABLE(HierarchicalIntegrityVerificationStorage);
|
||||
|
||||
public:
|
||||
using GenerateRandomFunction = void (*)(void* dst, size_t size);
|
||||
|
||||
class HierarchicalStorageInformation {
|
||||
public:
|
||||
enum {
|
||||
MasterStorage = 0,
|
||||
Layer1Storage = 1,
|
||||
Layer2Storage = 2,
|
||||
Layer3Storage = 3,
|
||||
Layer4Storage = 4,
|
||||
Layer5Storage = 5,
|
||||
DataStorage = 6,
|
||||
};
|
||||
|
||||
private:
|
||||
std::array<VirtualFile, DataStorage + 1> m_storages;
|
||||
|
||||
public:
|
||||
void SetMasterHashStorage(VirtualFile s) {
|
||||
m_storages[MasterStorage] = s;
|
||||
}
|
||||
void SetLayer1HashStorage(VirtualFile s) {
|
||||
m_storages[Layer1Storage] = s;
|
||||
}
|
||||
void SetLayer2HashStorage(VirtualFile s) {
|
||||
m_storages[Layer2Storage] = s;
|
||||
}
|
||||
void SetLayer3HashStorage(VirtualFile s) {
|
||||
m_storages[Layer3Storage] = s;
|
||||
}
|
||||
void SetLayer4HashStorage(VirtualFile s) {
|
||||
m_storages[Layer4Storage] = s;
|
||||
}
|
||||
void SetLayer5HashStorage(VirtualFile s) {
|
||||
m_storages[Layer5Storage] = s;
|
||||
}
|
||||
void SetDataStorage(VirtualFile s) {
|
||||
m_storages[DataStorage] = s;
|
||||
}
|
||||
|
||||
VirtualFile& operator[](s32 index) {
|
||||
ASSERT(MasterStorage <= index && index <= DataStorage);
|
||||
return m_storages[index];
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
HierarchicalIntegrityVerificationStorage();
|
||||
virtual ~HierarchicalIntegrityVerificationStorage() override {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
Result Initialize(const HierarchicalIntegrityVerificationInformation& info,
|
||||
HierarchicalStorageInformation storage, int max_data_cache_entries,
|
||||
int max_hash_cache_entries, s8 buffer_level);
|
||||
void Finalize();
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||
virtual size_t GetSize() const override;
|
||||
|
||||
bool IsInitialized() const {
|
||||
return m_data_size >= 0;
|
||||
}
|
||||
|
||||
s64 GetL1HashVerificationBlockSize() const {
|
||||
return m_verify_storages[m_max_layers - 2]->GetBlockSize();
|
||||
}
|
||||
|
||||
VirtualFile GetL1HashStorage() {
|
||||
return std::make_shared<OffsetVfsFile>(
|
||||
m_buffer_storages[m_max_layers - 3],
|
||||
Common::DivideUp(m_data_size, this->GetL1HashVerificationBlockSize()), 0);
|
||||
}
|
||||
|
||||
public:
|
||||
static constexpr s8 GetDefaultDataCacheBufferLevel(u32 max_layers) {
|
||||
return static_cast<s8>(16 + max_layers - 2);
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr s64 HashSize = 256 / 8;
|
||||
static constexpr size_t MaxLayers = IntegrityMaxLayerCount;
|
||||
|
||||
private:
|
||||
static GenerateRandomFunction s_generate_random;
|
||||
|
||||
static void SetGenerateRandomFunction(GenerateRandomFunction func) {
|
||||
s_generate_random = func;
|
||||
}
|
||||
|
||||
private:
|
||||
friend struct HierarchicalIntegrityVerificationMetaInformation;
|
||||
|
||||
private:
|
||||
std::array<std::shared_ptr<IntegrityVerificationStorage>, MaxLayers - 1> m_verify_storages;
|
||||
std::array<VirtualFile, MaxLayers - 1> m_buffer_storages;
|
||||
s64 m_data_size;
|
||||
s32 m_max_layers;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,80 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "core/file_sys/fssystem/fssystem_hierarchical_sha256_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
s32 Log2(s32 value) {
|
||||
ASSERT(value > 0);
|
||||
ASSERT(Common::IsPowerOfTwo(value));
|
||||
|
||||
s32 log = 0;
|
||||
while ((value >>= 1) > 0) {
|
||||
++log;
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result HierarchicalSha256Storage::Initialize(VirtualFile* base_storages, s32 layer_count,
|
||||
size_t htbs, void* hash_buf, size_t hash_buf_size) {
|
||||
// Validate preconditions.
|
||||
ASSERT(layer_count == LayerCount);
|
||||
ASSERT(Common::IsPowerOfTwo(htbs));
|
||||
ASSERT(hash_buf != nullptr);
|
||||
|
||||
// Set size tracking members.
|
||||
m_hash_target_block_size = static_cast<s32>(htbs);
|
||||
m_log_size_ratio = Log2(m_hash_target_block_size / HashSize);
|
||||
|
||||
// Get the base storage size.
|
||||
m_base_storage_size = base_storages[2]->GetSize();
|
||||
{
|
||||
auto size_guard = SCOPE_GUARD({ m_base_storage_size = 0; });
|
||||
R_UNLESS(m_base_storage_size <= static_cast<s64>(HashSize)
|
||||
<< m_log_size_ratio << m_log_size_ratio,
|
||||
ResultHierarchicalSha256BaseStorageTooLarge);
|
||||
size_guard.Cancel();
|
||||
}
|
||||
|
||||
// Set hash buffer tracking members.
|
||||
m_base_storage = base_storages[2];
|
||||
m_hash_buffer = static_cast<char*>(hash_buf);
|
||||
m_hash_buffer_size = hash_buf_size;
|
||||
|
||||
// Read the master hash.
|
||||
std::array<u8, HashSize> master_hash{};
|
||||
base_storages[0]->ReadObject(std::addressof(master_hash));
|
||||
|
||||
// Read and validate the data being hashed.
|
||||
s64 hash_storage_size = base_storages[1]->GetSize();
|
||||
ASSERT(Common::IsAligned(hash_storage_size, HashSize));
|
||||
ASSERT(hash_storage_size <= m_hash_target_block_size);
|
||||
ASSERT(hash_storage_size <= static_cast<s64>(m_hash_buffer_size));
|
||||
|
||||
base_storages[1]->Read(reinterpret_cast<u8*>(m_hash_buffer),
|
||||
static_cast<size_t>(hash_storage_size), 0);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
size_t HierarchicalSha256Storage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||
// Succeed if zero-size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate that we have a buffer to read into.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// Read the data.
|
||||
return m_base_storage->Read(buffer, size, offset);
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,44 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class HierarchicalSha256Storage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(HierarchicalSha256Storage);
|
||||
YUZU_NON_MOVEABLE(HierarchicalSha256Storage);
|
||||
|
||||
public:
|
||||
static constexpr s32 LayerCount = 3;
|
||||
static constexpr size_t HashSize = 256 / 8;
|
||||
|
||||
public:
|
||||
HierarchicalSha256Storage() : m_mutex() {}
|
||||
|
||||
Result Initialize(VirtualFile* base_storages, s32 layer_count, size_t htbs, void* hash_buf,
|
||||
size_t hash_buf_size);
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
return m_base_storage->GetSize();
|
||||
}
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t length, size_t offset) const override;
|
||||
|
||||
private:
|
||||
VirtualFile m_base_storage;
|
||||
s64 m_base_storage_size;
|
||||
char* m_hash_buffer;
|
||||
size_t m_hash_buffer_size;
|
||||
s32 m_hash_target_block_size;
|
||||
s32 m_log_size_ratio;
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,119 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fssystem_indirect_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
Result IndirectStorage::Initialize(VirtualFile table_storage) {
|
||||
// Read and verify the bucket tree header.
|
||||
BucketTree::Header header;
|
||||
table_storage->ReadObject(std::addressof(header));
|
||||
R_TRY(header.Verify());
|
||||
|
||||
// Determine extents.
|
||||
const auto node_storage_size = QueryNodeStorageSize(header.entry_count);
|
||||
const auto entry_storage_size = QueryEntryStorageSize(header.entry_count);
|
||||
const auto node_storage_offset = QueryHeaderStorageSize();
|
||||
const auto entry_storage_offset = node_storage_offset + node_storage_size;
|
||||
|
||||
// Initialize.
|
||||
R_RETURN(this->Initialize(
|
||||
std::make_shared<OffsetVfsFile>(table_storage, node_storage_size, node_storage_offset),
|
||||
std::make_shared<OffsetVfsFile>(table_storage, entry_storage_size, entry_storage_offset),
|
||||
header.entry_count));
|
||||
}
|
||||
|
||||
void IndirectStorage::Finalize() {
|
||||
if (this->IsInitialized()) {
|
||||
m_table.Finalize();
|
||||
for (auto i = 0; i < StorageCount; i++) {
|
||||
m_data_storage[i] = VirtualFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result IndirectStorage::GetEntryList(Entry* out_entries, s32* out_entry_count, s32 entry_count,
|
||||
s64 offset, s64 size) {
|
||||
// Validate pre-conditions.
|
||||
ASSERT(offset >= 0);
|
||||
ASSERT(size >= 0);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Clear the out count.
|
||||
R_UNLESS(out_entry_count != nullptr, ResultNullptrArgument);
|
||||
*out_entry_count = 0;
|
||||
|
||||
// Succeed if there's no range.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// If we have an output array, we need it to be non-null.
|
||||
R_UNLESS(out_entries != nullptr || entry_count == 0, ResultNullptrArgument);
|
||||
|
||||
// Check that our range is valid.
|
||||
BucketTree::Offsets table_offsets;
|
||||
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||
|
||||
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||
|
||||
// Find the offset in our tree.
|
||||
BucketTree::Visitor visitor;
|
||||
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||
{
|
||||
const auto entry_offset = visitor.Get<Entry>()->GetVirtualOffset();
|
||||
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||
ResultInvalidIndirectEntryOffset);
|
||||
}
|
||||
|
||||
// Prepare to loop over entries.
|
||||
const auto end_offset = offset + static_cast<s64>(size);
|
||||
s32 count = 0;
|
||||
|
||||
auto cur_entry = *visitor.Get<Entry>();
|
||||
while (cur_entry.GetVirtualOffset() < end_offset) {
|
||||
// Try to write the entry to the out list.
|
||||
if (entry_count != 0) {
|
||||
if (count >= entry_count) {
|
||||
break;
|
||||
}
|
||||
std::memcpy(out_entries + count, std::addressof(cur_entry), sizeof(Entry));
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
// Advance.
|
||||
if (visitor.CanMoveNext()) {
|
||||
R_TRY(visitor.MoveNext());
|
||||
cur_entry = *visitor.Get<Entry>();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the output count.
|
||||
*out_entry_count = count;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
size_t IndirectStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||
// Validate pre-conditions.
|
||||
ASSERT(this->IsInitialized());
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// Succeed if there's nothing to read.
|
||||
if (size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const_cast<IndirectStorage*>(this)->OperatePerEntry<true, true>(
|
||||
offset, size,
|
||||
[=](VirtualFile storage, s64 data_offset, s64 cur_offset, s64 cur_size) -> Result {
|
||||
storage->Read(reinterpret_cast<u8*>(buffer) + (cur_offset - offset),
|
||||
static_cast<size_t>(cur_size), data_offset);
|
||||
R_SUCCEED();
|
||||
});
|
||||
|
||||
return size;
|
||||
}
|
||||
} // namespace FileSys
|
@ -1,294 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree.h"
|
||||
#include "core/file_sys/fssystem/fssystem_bucket_tree_template_impl.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class IndirectStorage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(IndirectStorage);
|
||||
YUZU_NON_MOVEABLE(IndirectStorage);
|
||||
|
||||
public:
|
||||
static constexpr s32 StorageCount = 2;
|
||||
static constexpr size_t NodeSize = 16_KiB;
|
||||
|
||||
struct Entry {
|
||||
std::array<u8, sizeof(s64)> virt_offset;
|
||||
std::array<u8, sizeof(s64)> phys_offset;
|
||||
s32 storage_index;
|
||||
|
||||
void SetVirtualOffset(const s64& ofs) {
|
||||
std::memcpy(this->virt_offset.data(), std::addressof(ofs), sizeof(s64));
|
||||
}
|
||||
|
||||
s64 GetVirtualOffset() const {
|
||||
s64 offset;
|
||||
std::memcpy(std::addressof(offset), this->virt_offset.data(), sizeof(s64));
|
||||
return offset;
|
||||
}
|
||||
|
||||
void SetPhysicalOffset(const s64& ofs) {
|
||||
std::memcpy(this->phys_offset.data(), std::addressof(ofs), sizeof(s64));
|
||||
}
|
||||
|
||||
s64 GetPhysicalOffset() const {
|
||||
s64 offset;
|
||||
std::memcpy(std::addressof(offset), this->phys_offset.data(), sizeof(s64));
|
||||
return offset;
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<Entry>);
|
||||
static_assert(sizeof(Entry) == 0x14);
|
||||
|
||||
struct EntryData {
|
||||
s64 virt_offset;
|
||||
s64 phys_offset;
|
||||
s32 storage_index;
|
||||
|
||||
void Set(const Entry& entry) {
|
||||
this->virt_offset = entry.GetVirtualOffset();
|
||||
this->phys_offset = entry.GetPhysicalOffset();
|
||||
this->storage_index = entry.storage_index;
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<EntryData>);
|
||||
|
||||
public:
|
||||
IndirectStorage() : m_table(), m_data_storage() {}
|
||||
virtual ~IndirectStorage() {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
Result Initialize(VirtualFile table_storage);
|
||||
void Finalize();
|
||||
|
||||
bool IsInitialized() const {
|
||||
return m_table.IsInitialized();
|
||||
}
|
||||
|
||||
Result Initialize(VirtualFile node_storage, VirtualFile entry_storage, s32 entry_count) {
|
||||
R_RETURN(
|
||||
m_table.Initialize(node_storage, entry_storage, NodeSize, sizeof(Entry), entry_count));
|
||||
}
|
||||
|
||||
void SetStorage(s32 idx, VirtualFile storage) {
|
||||
ASSERT(0 <= idx && idx < StorageCount);
|
||||
m_data_storage[idx] = storage;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void SetStorage(s32 idx, T storage, s64 offset, s64 size) {
|
||||
ASSERT(0 <= idx && idx < StorageCount);
|
||||
m_data_storage[idx] = std::make_shared<OffsetVfsFile>(storage, size, offset);
|
||||
}
|
||||
|
||||
Result GetEntryList(Entry* out_entries, s32* out_entry_count, s32 entry_count, s64 offset,
|
||||
s64 size);
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
BucketTree::Offsets offsets{};
|
||||
m_table.GetOffsets(std::addressof(offsets));
|
||||
|
||||
return offsets.end_offset;
|
||||
}
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||
|
||||
public:
|
||||
static constexpr s64 QueryHeaderStorageSize() {
|
||||
return BucketTree::QueryHeaderStorageSize();
|
||||
}
|
||||
|
||||
static constexpr s64 QueryNodeStorageSize(s32 entry_count) {
|
||||
return BucketTree::QueryNodeStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||
}
|
||||
|
||||
static constexpr s64 QueryEntryStorageSize(s32 entry_count) {
|
||||
return BucketTree::QueryEntryStorageSize(NodeSize, sizeof(Entry), entry_count);
|
||||
}
|
||||
|
||||
protected:
|
||||
BucketTree& GetEntryTable() {
|
||||
return m_table;
|
||||
}
|
||||
|
||||
VirtualFile& GetDataStorage(s32 index) {
|
||||
ASSERT(0 <= index && index < StorageCount);
|
||||
return m_data_storage[index];
|
||||
}
|
||||
|
||||
template <bool ContinuousCheck, bool RangeCheck, typename F>
|
||||
Result OperatePerEntry(s64 offset, s64 size, F func);
|
||||
|
||||
private:
|
||||
struct ContinuousReadingEntry {
|
||||
static constexpr size_t FragmentSizeMax = 4_KiB;
|
||||
|
||||
IndirectStorage::Entry entry;
|
||||
|
||||
s64 GetVirtualOffset() const {
|
||||
return this->entry.GetVirtualOffset();
|
||||
}
|
||||
|
||||
s64 GetPhysicalOffset() const {
|
||||
return this->entry.GetPhysicalOffset();
|
||||
}
|
||||
|
||||
bool IsFragment() const {
|
||||
return this->entry.storage_index != 0;
|
||||
}
|
||||
};
|
||||
static_assert(std::is_trivial_v<ContinuousReadingEntry>);
|
||||
|
||||
private:
|
||||
mutable BucketTree m_table;
|
||||
std::array<VirtualFile, StorageCount> m_data_storage;
|
||||
};
|
||||
|
||||
template <bool ContinuousCheck, bool RangeCheck, typename F>
|
||||
Result IndirectStorage::OperatePerEntry(s64 offset, s64 size, F func) {
|
||||
// Validate preconditions.
|
||||
ASSERT(offset >= 0);
|
||||
ASSERT(size >= 0);
|
||||
ASSERT(this->IsInitialized());
|
||||
|
||||
// Succeed if there's nothing to operate on.
|
||||
R_SUCCEED_IF(size == 0);
|
||||
|
||||
// Get the table offsets.
|
||||
BucketTree::Offsets table_offsets;
|
||||
R_TRY(m_table.GetOffsets(std::addressof(table_offsets)));
|
||||
|
||||
// Validate arguments.
|
||||
R_UNLESS(table_offsets.IsInclude(offset, size), ResultOutOfRange);
|
||||
|
||||
// Find the offset in our tree.
|
||||
BucketTree::Visitor visitor;
|
||||
R_TRY(m_table.Find(std::addressof(visitor), offset));
|
||||
{
|
||||
const auto entry_offset = visitor.Get<Entry>()->GetVirtualOffset();
|
||||
R_UNLESS(0 <= entry_offset && table_offsets.IsInclude(entry_offset),
|
||||
ResultInvalidIndirectEntryOffset);
|
||||
}
|
||||
|
||||
// Prepare to operate in chunks.
|
||||
auto cur_offset = offset;
|
||||
const auto end_offset = offset + static_cast<s64>(size);
|
||||
BucketTree::ContinuousReadingInfo cr_info;
|
||||
|
||||
while (cur_offset < end_offset) {
|
||||
// Get the current entry.
|
||||
const auto cur_entry = *visitor.Get<Entry>();
|
||||
|
||||
// Get and validate the entry's offset.
|
||||
const auto cur_entry_offset = cur_entry.GetVirtualOffset();
|
||||
R_UNLESS(cur_entry_offset <= cur_offset, ResultInvalidIndirectEntryOffset);
|
||||
|
||||
// Validate the storage index.
|
||||
R_UNLESS(0 <= cur_entry.storage_index && cur_entry.storage_index < StorageCount,
|
||||
ResultInvalidIndirectEntryStorageIndex);
|
||||
|
||||
// If we need to check the continuous info, do so.
|
||||
if constexpr (ContinuousCheck) {
|
||||
// Scan, if we need to.
|
||||
if (cr_info.CheckNeedScan()) {
|
||||
R_TRY(visitor.ScanContinuousReading<ContinuousReadingEntry>(
|
||||
std::addressof(cr_info), cur_offset,
|
||||
static_cast<size_t>(end_offset - cur_offset)));
|
||||
}
|
||||
|
||||
// Process a base storage entry.
|
||||
if (cr_info.CanDo()) {
|
||||
// Ensure that we can process.
|
||||
R_UNLESS(cur_entry.storage_index == 0, ResultInvalidIndirectEntryStorageIndex);
|
||||
|
||||
// Ensure that we remain within range.
|
||||
const auto data_offset = cur_offset - cur_entry_offset;
|
||||
const auto cur_entry_phys_offset = cur_entry.GetPhysicalOffset();
|
||||
const auto cur_size = static_cast<s64>(cr_info.GetReadSize());
|
||||
|
||||
// If we should, verify the range.
|
||||
if constexpr (RangeCheck) {
|
||||
// Get the current data storage's size.
|
||||
s64 cur_data_storage_size = m_data_storage[0]->GetSize();
|
||||
|
||||
R_UNLESS(0 <= cur_entry_phys_offset &&
|
||||
cur_entry_phys_offset <= cur_data_storage_size,
|
||||
ResultInvalidIndirectEntryOffset);
|
||||
R_UNLESS(cur_entry_phys_offset + data_offset + cur_size <=
|
||||
cur_data_storage_size,
|
||||
ResultInvalidIndirectStorageSize);
|
||||
}
|
||||
|
||||
// Operate.
|
||||
R_TRY(func(m_data_storage[0], cur_entry_phys_offset + data_offset, cur_offset,
|
||||
cur_size));
|
||||
|
||||
// Mark as done.
|
||||
cr_info.Done();
|
||||
}
|
||||
}
|
||||
|
||||
// Get and validate the next entry offset.
|
||||
s64 next_entry_offset;
|
||||
if (visitor.CanMoveNext()) {
|
||||
R_TRY(visitor.MoveNext());
|
||||
next_entry_offset = visitor.Get<Entry>()->GetVirtualOffset();
|
||||
R_UNLESS(table_offsets.IsInclude(next_entry_offset), ResultInvalidIndirectEntryOffset);
|
||||
} else {
|
||||
next_entry_offset = table_offsets.end_offset;
|
||||
}
|
||||
R_UNLESS(cur_offset < next_entry_offset, ResultInvalidIndirectEntryOffset);
|
||||
|
||||
// Get the offset of the entry in the data we read.
|
||||
const auto data_offset = cur_offset - cur_entry_offset;
|
||||
const auto data_size = (next_entry_offset - cur_entry_offset);
|
||||
ASSERT(data_size > 0);
|
||||
|
||||
// Determine how much is left.
|
||||
const auto remaining_size = end_offset - cur_offset;
|
||||
const auto cur_size = std::min<s64>(remaining_size, data_size - data_offset);
|
||||
ASSERT(cur_size <= size);
|
||||
|
||||
// Operate, if we need to.
|
||||
bool needs_operate;
|
||||
if constexpr (!ContinuousCheck) {
|
||||
needs_operate = true;
|
||||
} else {
|
||||
needs_operate = !cr_info.IsDone() || cur_entry.storage_index != 0;
|
||||
}
|
||||
|
||||
if (needs_operate) {
|
||||
const auto cur_entry_phys_offset = cur_entry.GetPhysicalOffset();
|
||||
|
||||
if constexpr (RangeCheck) {
|
||||
// Get the current data storage's size.
|
||||
s64 cur_data_storage_size = m_data_storage[cur_entry.storage_index]->GetSize();
|
||||
|
||||
// Ensure that we remain within range.
|
||||
R_UNLESS(0 <= cur_entry_phys_offset &&
|
||||
cur_entry_phys_offset <= cur_data_storage_size,
|
||||
ResultIndirectStorageCorrupted);
|
||||
R_UNLESS(cur_entry_phys_offset + data_offset + cur_size <= cur_data_storage_size,
|
||||
ResultIndirectStorageCorrupted);
|
||||
}
|
||||
|
||||
R_TRY(func(m_data_storage[cur_entry.storage_index], cur_entry_phys_offset + data_offset,
|
||||
cur_offset, cur_size));
|
||||
}
|
||||
|
||||
cur_offset += cur_size;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,30 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_integrity_romfs_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
Result IntegrityRomFsStorage::Initialize(
|
||||
HierarchicalIntegrityVerificationInformation level_hash_info, Hash master_hash,
|
||||
HierarchicalIntegrityVerificationStorage::HierarchicalStorageInformation storage_info,
|
||||
int max_data_cache_entries, int max_hash_cache_entries, s8 buffer_level) {
|
||||
// Set master hash.
|
||||
m_master_hash = master_hash;
|
||||
m_master_hash_storage = std::make_shared<ArrayVfsFile<sizeof(Hash)>>(m_master_hash.value);
|
||||
R_UNLESS(m_master_hash_storage != nullptr,
|
||||
ResultAllocationMemoryFailedInIntegrityRomFsStorageA);
|
||||
|
||||
// Set the master hash storage.
|
||||
storage_info[0] = m_master_hash_storage;
|
||||
|
||||
// Initialize our integrity storage.
|
||||
R_RETURN(m_integrity_storage.Initialize(level_hash_info, storage_info, max_data_cache_entries,
|
||||
max_hash_cache_entries, buffer_level));
|
||||
}
|
||||
|
||||
void IntegrityRomFsStorage::Finalize() {
|
||||
m_integrity_storage.Finalize();
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,42 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/fssystem/fssystem_hierarchical_integrity_verification_storage.h"
|
||||
#include "core/file_sys/fssystem/fssystem_nca_header.h"
|
||||
#include "core/file_sys/vfs_vector.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
constexpr inline size_t IntegrityLayerCountRomFs = 7;
|
||||
constexpr inline size_t IntegrityHashLayerBlockSize = 16_KiB;
|
||||
|
||||
class IntegrityRomFsStorage : public IReadOnlyStorage {
|
||||
public:
|
||||
IntegrityRomFsStorage() {}
|
||||
virtual ~IntegrityRomFsStorage() override {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
Result Initialize(
|
||||
HierarchicalIntegrityVerificationInformation level_hash_info, Hash master_hash,
|
||||
HierarchicalIntegrityVerificationStorage::HierarchicalStorageInformation storage_info,
|
||||
int max_data_cache_entries, int max_hash_cache_entries, s8 buffer_level);
|
||||
void Finalize();
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||
return m_integrity_storage.Read(buffer, size, offset);
|
||||
}
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
return m_integrity_storage.GetSize();
|
||||
}
|
||||
|
||||
private:
|
||||
HierarchicalIntegrityVerificationStorage m_integrity_storage;
|
||||
Hash m_master_hash;
|
||||
std::shared_ptr<ArrayVfsFile<sizeof(Hash)>> m_master_hash_storage;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,91 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "core/file_sys/fssystem/fssystem_integrity_verification_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
constexpr inline u32 ILog2(u32 val) {
|
||||
ASSERT(val > 0);
|
||||
return static_cast<u32>((sizeof(u32) * 8) - 1 - std::countl_zero<u32>(val));
|
||||
}
|
||||
|
||||
void IntegrityVerificationStorage::Initialize(VirtualFile hs, VirtualFile ds, s64 verif_block_size,
|
||||
s64 upper_layer_verif_block_size, bool is_real_data) {
|
||||
// Validate preconditions.
|
||||
ASSERT(verif_block_size >= HashSize);
|
||||
|
||||
// Set storages.
|
||||
m_hash_storage = hs;
|
||||
m_data_storage = ds;
|
||||
|
||||
// Set verification block sizes.
|
||||
m_verification_block_size = verif_block_size;
|
||||
m_verification_block_order = ILog2(static_cast<u32>(verif_block_size));
|
||||
ASSERT(m_verification_block_size == 1ll << m_verification_block_order);
|
||||
|
||||
// Set upper layer block sizes.
|
||||
upper_layer_verif_block_size = std::max(upper_layer_verif_block_size, HashSize);
|
||||
m_upper_layer_verification_block_size = upper_layer_verif_block_size;
|
||||
m_upper_layer_verification_block_order = ILog2(static_cast<u32>(upper_layer_verif_block_size));
|
||||
ASSERT(m_upper_layer_verification_block_size == 1ll << m_upper_layer_verification_block_order);
|
||||
|
||||
// Validate sizes.
|
||||
{
|
||||
s64 hash_size = m_hash_storage->GetSize();
|
||||
s64 data_size = m_data_storage->GetSize();
|
||||
ASSERT(((hash_size / HashSize) * m_verification_block_size) >= data_size);
|
||||
}
|
||||
|
||||
// Set data.
|
||||
m_is_real_data = is_real_data;
|
||||
}
|
||||
|
||||
void IntegrityVerificationStorage::Finalize() {
|
||||
m_hash_storage = VirtualFile();
|
||||
m_data_storage = VirtualFile();
|
||||
}
|
||||
|
||||
size_t IntegrityVerificationStorage::Read(u8* buffer, size_t size, size_t offset) const {
|
||||
// Succeed if zero size.
|
||||
if (size == 0) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
ASSERT(buffer != nullptr);
|
||||
|
||||
// Validate the offset.
|
||||
s64 data_size = m_data_storage->GetSize();
|
||||
ASSERT(offset <= static_cast<size_t>(data_size));
|
||||
|
||||
// Validate the access range.
|
||||
ASSERT(R_SUCCEEDED(IStorage::CheckAccessRange(
|
||||
offset, size, Common::AlignUp(data_size, static_cast<size_t>(m_verification_block_size)))));
|
||||
|
||||
// Determine the read extents.
|
||||
size_t read_size = size;
|
||||
if (static_cast<s64>(offset + read_size) > data_size) {
|
||||
// Determine the padding sizes.
|
||||
s64 padding_offset = data_size - offset;
|
||||
size_t padding_size = static_cast<size_t>(
|
||||
m_verification_block_size - (padding_offset & (m_verification_block_size - 1)));
|
||||
ASSERT(static_cast<s64>(padding_size) < m_verification_block_size);
|
||||
|
||||
// Clear the padding.
|
||||
std::memset(static_cast<u8*>(buffer) + padding_offset, 0, padding_size);
|
||||
|
||||
// Set the new in-bounds size.
|
||||
read_size = static_cast<size_t>(data_size - offset);
|
||||
}
|
||||
|
||||
// Perform the read.
|
||||
return m_data_storage->Read(buffer, read_size, offset);
|
||||
}
|
||||
|
||||
size_t IntegrityVerificationStorage::GetSize() const {
|
||||
return m_data_storage->GetSize();
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -1,65 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
#include "core/file_sys/fssystem/fs_types.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class IntegrityVerificationStorage : public IReadOnlyStorage {
|
||||
YUZU_NON_COPYABLE(IntegrityVerificationStorage);
|
||||
YUZU_NON_MOVEABLE(IntegrityVerificationStorage);
|
||||
|
||||
public:
|
||||
static constexpr s64 HashSize = 256 / 8;
|
||||
|
||||
struct BlockHash {
|
||||
std::array<u8, HashSize> hash;
|
||||
};
|
||||
static_assert(std::is_trivial_v<BlockHash>);
|
||||
|
||||
public:
|
||||
IntegrityVerificationStorage()
|
||||
: m_verification_block_size(0), m_verification_block_order(0),
|
||||
m_upper_layer_verification_block_size(0), m_upper_layer_verification_block_order(0) {}
|
||||
virtual ~IntegrityVerificationStorage() override {
|
||||
this->Finalize();
|
||||
}
|
||||
|
||||
void Initialize(VirtualFile hs, VirtualFile ds, s64 verif_block_size,
|
||||
s64 upper_layer_verif_block_size, bool is_real_data);
|
||||
void Finalize();
|
||||
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override;
|
||||
virtual size_t GetSize() const override;
|
||||
|
||||
s64 GetBlockSize() const {
|
||||
return m_verification_block_size;
|
||||
}
|
||||
|
||||
private:
|
||||
static void SetValidationBit(BlockHash* hash) {
|
||||
ASSERT(hash != nullptr);
|
||||
hash->hash[HashSize - 1] |= 0x80;
|
||||
}
|
||||
|
||||
static bool IsValidationBit(const BlockHash* hash) {
|
||||
ASSERT(hash != nullptr);
|
||||
return (hash->hash[HashSize - 1] & 0x80) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
VirtualFile m_hash_storage;
|
||||
VirtualFile m_data_storage;
|
||||
s64 m_verification_block_size;
|
||||
s64 m_verification_block_order;
|
||||
s64 m_upper_layer_verification_block_size;
|
||||
s64 m_upper_layer_verification_block_order;
|
||||
bool m_is_real_data;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
@ -1,61 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/fssystem/fs_i_storage.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class MemoryResourceBufferHoldStorage : public IStorage {
|
||||
YUZU_NON_COPYABLE(MemoryResourceBufferHoldStorage);
|
||||
YUZU_NON_MOVEABLE(MemoryResourceBufferHoldStorage);
|
||||
|
||||
public:
|
||||
MemoryResourceBufferHoldStorage(VirtualFile storage, size_t buffer_size)
|
||||
: m_storage(std::move(storage)), m_buffer(::operator new(buffer_size)),
|
||||
m_buffer_size(buffer_size) {}
|
||||
|
||||
virtual ~MemoryResourceBufferHoldStorage() {
|
||||
// If we have a buffer, deallocate it.
|
||||
if (m_buffer != nullptr) {
|
||||
::operator delete(m_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsValid() const {
|
||||
return m_buffer != nullptr;
|
||||
}
|
||||
void* GetBuffer() const {
|
||||
return m_buffer;
|
||||
}
|
||||
|
||||
public:
|
||||
virtual size_t Read(u8* buffer, size_t size, size_t offset) const override {
|
||||
// Check pre-conditions.
|
||||
ASSERT(m_storage != nullptr);
|
||||
|
||||
return m_storage->Read(buffer, size, offset);
|
||||
}
|
||||
|
||||
virtual size_t GetSize() const override {
|
||||
// Check pre-conditions.
|
||||
ASSERT(m_storage != nullptr);
|
||||
|
||||
return m_storage->GetSize();
|
||||
}
|
||||
|
||||
virtual size_t Write(const u8* buffer, size_t size, size_t offset) override {
|
||||
// Check pre-conditions.
|
||||
ASSERT(m_storage != nullptr);
|
||||
|
||||
return m_storage->Write(buffer, size, offset);
|
||||
}
|
||||
|
||||
private:
|
||||
VirtualFile m_storage;
|
||||
void* m_buffer;
|
||||
size_t m_buffer_size;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user