Merge branch 'master' into COMBO_BUTTON

This commit is contained in:
ADAS2024 2025-07-27 22:58:12 -04:00 committed by GitHub
commit f2980073a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1918 additions and 210 deletions

View file

@ -17,6 +17,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules")
include(DownloadExternals)
include(CMakeDependentOption)
include(FindPkgConfig)
project(citra LANGUAGES C CXX ASM)
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
@ -481,12 +482,10 @@ if (ENABLE_SDL2 AND USE_SYSTEM_SDL2)
endif()
if (ENABLE_LIBUSB AND USE_SYSTEM_LIBUSB)
include(FindPkgConfig)
find_package(LibUSB)
endif()
if (USE_SYSTEM_SOUNDTOUCH)
include(FindPkgConfig)
find_package(SoundTouch REQUIRED)
add_library(SoundTouch INTERFACE)
target_link_libraries(SoundTouch INTERFACE "${SOUNDTOUCH_LIBRARIES}")

View file

@ -214,8 +214,29 @@ else()
set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_SHARED OFF)
add_subdirectory(zstd/build/cmake EXCLUDE_FROM_ALL)
target_include_directories(libzstd_static INTERFACE $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/externals/zstd/lib>)
target_include_directories(libzstd_static INTERFACE
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/externals/zstd/lib>
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/externals/zstd/lib/common>
)
add_library(zstd_seekable STATIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/zstd/contrib/seekable_format/zstdseek_compress.c>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/zstd/contrib/seekable_format/zstdseek_decompress.c>
)
target_include_directories(zstd_seekable PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/zstd/contrib/seekable_format>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/zstd/lib/common>
)
target_link_libraries(zstd_seekable PUBLIC libzstd_static)
target_link_libraries(libzstd_static INTERFACE zstd_seekable)
add_library(zstd ALIAS libzstd_static)
install(TARGETS zstd_seekable
EXPORT zstdExports
)
endif()
# ENet

View file

@ -52,6 +52,7 @@ enum class BooleanSetting(
COMPRESS_INSTALLED_CIA_CONTENT("compress_cia_installs", Settings.SECTION_STORAGE, false),
ENABLE_COMBO_KEY("enable_combo_key", Settings.SECTION_CONTROLS, true);
override var boolean: Boolean = defaultValue
override val valueAsString: String
@ -82,7 +83,8 @@ enum class BooleanSetting(
CPU_JIT,
ASYNC_CUSTOM_LOADING,
SHADERS_ACCURATE_MUL,
USE_ARTIC_BASE_CONTROLLER
USE_ARTIC_BASE_CONTROLLER,
COMPRESS_INSTALLED_CIA_CONTENT,
)
fun from(key: String): BooleanSetting? =

View file

@ -115,6 +115,7 @@ class Settings {
const val SECTION_STORAGE = "Storage"
const val SECTION_COMBO = "Combo Button"
const val KEY_BUTTON_A = "button_a"
const val KEY_BUTTON_B = "button_b"
const val KEY_BUTTON_X = "button_x"
@ -241,6 +242,7 @@ class Settings {
SECTION_CONTROLS,
SECTION_RENDERER,
SECTION_LAYOUT,
SECTION_STORAGE,
SECTION_UTILITY,
SECTION_AUDIO,
SECTION_DEBUG

View file

@ -569,6 +569,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
BooleanSetting.ALLOW_PLUGIN_LOADER.defaultValue
)
)
add(HeaderSetting(R.string.storage))
add(
SwitchSetting(
BooleanSetting.COMPRESS_INSTALLED_CIA_CONTENT,
R.string.compress_cia_installs,
R.string.compress_cia_installs_description,
BooleanSetting.COMPRESS_INSTALLED_CIA_CONTENT.key,
BooleanSetting.COMPRESS_INSTALLED_CIA_CONTENT.defaultValue
)
)
}
}

View file

@ -796,6 +796,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
true
}
R.id.menu_emulation_button_sliding -> {
showButtonSlidingMenu()
true
}
R.id.menu_emulation_dpad_slide_enable -> {
EmulationMenuSettings.dpadSlide = !EmulationMenuSettings.dpadSlide
true
@ -840,6 +845,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show()
}
private fun showButtonSlidingMenu() {
val editor = preferences.edit()
val buttonSlidingModes = mutableListOf<String>()
buttonSlidingModes.add(getString(R.string.emulation_button_sliding_disabled))
buttonSlidingModes.add(getString(R.string.emulation_button_sliding_enabled))
buttonSlidingModes.add(getString(R.string.emulation_button_sliding_alternative))
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.emulation_button_sliding)
.setSingleChoiceItems(
buttonSlidingModes.toTypedArray(),
EmulationMenuSettings.buttonSlide
) { _: DialogInterface?, which: Int ->
EmulationMenuSettings.buttonSlide = which
}
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
editor.apply()
}
.show()
}
private fun showLandscapeScreenLayoutMenu() {
val popupMenu = PopupMenu(
requireContext(),

View file

@ -96,57 +96,115 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
if (isInEditMode) {
return onTouchWhileEditing(event)
}
var shouldUpdateView = false
var hasActiveButtons = false
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
for (button in overlayButtons) {
if (!button.updateStatus(event, this)) {
continue
if (button.trackId == pointerId) {
hasActiveButtons = true
break
}
if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) {
swapScreen()
}
if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
TurboHelper.toggleTurbo(true)
}
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.id, button.status)
shouldUpdateView = true
}
for (dpad in overlayDpads) {
if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide, this)) {
continue
var hasActiveDpad = false
if (!hasActiveButtons) {
for (dpad in overlayDpads) {
if (dpad.trackId == pointerId) {
hasActiveDpad = true
break
}
}
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.upId, dpad.upStatus)
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.downId,
dpad.downStatus
)
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.leftId,
dpad.leftStatus
)
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.rightId,
dpad.rightStatus
)
shouldUpdateView = true
}
for (joystick in overlayJoysticks) {
if (!joystick.updateStatus(event, this)) {
continue
var hasActiveJoystick = false
if(!hasActiveButtons && !hasActiveDpad){
for (joystick in overlayJoysticks) {
if (joystick.trackId == pointerId) {
hasActiveJoystick = true
break
}
}
}
var shouldUpdateView = false
if(!hasActiveDpad && !hasActiveJoystick) {
for (button in overlayButtons) {
val stateChanged = button.updateStatus(event, hasActiveButtons, this)
if (!stateChanged) {
continue
}
if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) {
swapScreen()
}
else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
TurboHelper.toggleTurbo(true)
}
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
button.id,
button.status
)
shouldUpdateView = true
}
}
if(!hasActiveButtons && !hasActiveJoystick) {
for (dpad in overlayDpads) {
val stateChanged = dpad.updateStatus(
event,
hasActiveDpad,
EmulationMenuSettings.dpadSlide,
this
)
if (!stateChanged) {
continue
}
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.upId,
dpad.upStatus
)
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.downId,
dpad.downStatus
)
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.leftId,
dpad.leftStatus
)
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
dpad.rightId,
dpad.rightStatus
)
shouldUpdateView = true
}
}
if(!hasActiveDpad && !hasActiveButtons) {
for (joystick in overlayJoysticks) {
val stateChanged = joystick.updateStatus(event, hasActiveJoystick, this)
if (!stateChanged) {
continue
}
val axisID = joystick.joystickId
NativeLibrary.onGamePadMoveEvent(
NativeLibrary.TouchScreenDevice,
axisID,
joystick.xAxis,
joystick.yAxis
)
shouldUpdateView = true
}
val axisID = joystick.joystickId
NativeLibrary.onGamePadMoveEvent(
NativeLibrary.TouchScreenDevice,
axisID,
joystick.xAxis,
joystick.yAxis
)
shouldUpdateView = true
}
if (shouldUpdateView) {
@ -157,10 +215,8 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
return true
}
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
val pointerId = event.getPointerId(pointerIndex)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -12,6 +12,20 @@ import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.utils.EmulationMenuSettings
enum class ButtonSlidingMode(val int: Int) {
// Disabled, buttons can only be triggered by pressing them directly.
Disabled(0),
// Additionally to pressing buttons directly, they can be activated and released by sliding into
// and out of their area.
Enabled(1),
// The first button is kept activated until released, further buttons use the simple button
// sliding method.
Alternative(2)
}
/**
* Custom [BitmapDrawable] that is capable
@ -30,6 +44,9 @@ class InputOverlayDrawableButton(
val opacity: Int
) {
var trackId: Int
private var isMotionFirstButton = false // mark the first activated button with the current motion
private var previousTouchX = 0
private var previousTouchY = 0
private var controlPositionX = 0
@ -53,7 +70,8 @@ class InputOverlayDrawableButton(
*
* @return true if value was changed
*/
fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean {
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
val buttonSliding = EmulationMenuSettings.buttonSlide
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
@ -67,23 +85,60 @@ class InputOverlayDrawableButton(
if (!bounds.contains(xPosition, yPosition)) {
return false
}
pressedState = true
trackId = pointerId
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
buttonDown(true, pointerId, overlay)
return true
}
if (isActionUp) {
if (trackId != pointerId) {
return false
}
pressedState = false
trackId = -1
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
buttonUp(overlay)
return true
}
val isActionMoving = motionEvent == MotionEvent.ACTION_MOVE
if (buttonSliding != ButtonSlidingMode.Disabled.int && isActionMoving) {
val inside = bounds.contains(xPosition, yPosition)
if (pressedState) {
// button is already pressed
// check whether we moved out of the button area to update the state
if (inside || trackId != pointerId) {
return false
}
// prevent the first (directly pressed) button to deactivate when sliding off
if (buttonSliding == ButtonSlidingMode.Alternative.int && isMotionFirstButton) {
return false
}
buttonUp(overlay)
return true
} else {
// button was not yet pressed
// check whether we moved into the button area to update the state
if (!inside) {
return false
}
buttonDown(!hasActiveButtons, pointerId, overlay)
return true
}
}
return false
}
private fun buttonDown(firstBtn: Boolean, pointerId: Int, overlay: InputOverlay) {
pressedState = true
isMotionFirstButton = firstBtn
trackId = pointerId
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
private fun buttonUp(overlay: InputOverlay) {
pressedState = false
isMotionFirstButton = false
trackId = -1
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
}
fun onConfigureTouch(event: MotionEvent): Boolean {
val pointerIndex = event.actionIndex
val fingerPositionX = event.getX(pointerIndex).toInt()

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.utils.EmulationMenuSettings
/**
* Custom [BitmapDrawable] that is capable
@ -62,15 +63,19 @@ class InputOverlayDrawableDpad(
trackId = -1
}
fun updateStatus(event: MotionEvent, dpadSlide: Boolean, overlay:InputOverlay): Boolean {
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, dpadSlide: Boolean, overlay: InputOverlay): Boolean {
var isDown = false
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
val pointerId = event.getPointerId(pointerIndex)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
var isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
if (!isActionDown && EmulationMenuSettings.buttonSlide != ButtonSlidingMode.Disabled.int) {
isActionDown = motionEvent == MotionEvent.ACTION_MOVE && !hasActiveButtons
}
val isActionUp =
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
if (isActionDown) {

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -93,14 +93,18 @@ class InputOverlayDrawableJoystick(
currentStateBitmapDrawable.draw(canvas)
}
fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean {
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
val pointerId = event.getPointerId(pointerIndex)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
var isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
if (!isActionDown && EmulationMenuSettings.buttonSlide != ButtonSlidingMode.Disabled.int) {
isActionDown = motionEvent == MotionEvent.ACTION_MOVE && !hasActiveButtons
}
val isActionUp =
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
if (isActionDown) {

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,6 +7,7 @@ package org.citra.citra_emu.utils
import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.overlay.ButtonSlidingMode
object EmulationMenuSettings {
private val preferences =
@ -26,6 +27,13 @@ object EmulationMenuSettings {
.putBoolean("EmulationMenuSettings_DpadSlideEnable", value)
.apply()
}
var buttonSlide: Int
get() = preferences.getInt("EmulationMenuSettings_ButtonSlideMode", ButtonSlidingMode.Disabled.int)
set(value) {
preferences.edit()
.putInt("EmulationMenuSettings_ButtonSlideMode", value)
.apply()
}
var showPerformanceOverlay: Boolean
get() = preferences.getBoolean("EmulationMenuSettings_showPerformanceOverlay", false)

View file

@ -218,6 +218,9 @@ void Config::ReadValues() {
ReadSetting("Layout", Settings::values.custom_portrait_bottom_width);
ReadSetting("Layout", Settings::values.custom_portrait_bottom_height);
// Storage
ReadSetting("Storage", Settings::values.compress_cia_installs);
// Utility
ReadSetting("Utility", Settings::values.dump_textures);
ReadSetting("Utility", Settings::values.custom_textures);

View file

@ -210,6 +210,11 @@ disable_right_eye_render =
# 5: Custom Layout
layout_option =
[Storage]
# Whether to compress the installed CIA contents
# 0 (default): Do not compress, 1: Compress
compress_cia_installs =
# Position of the performance overlay
# 0: Top Left
# 1: Center Top

View file

@ -86,6 +86,10 @@
android:id="@+id/menu_emulation_adjust_opacity"
android:title="@string/emulation_control_opacity" />
<item
android:id="@+id/menu_emulation_button_sliding"
android:title="@string/emulation_button_sliding">
</item>
<group android:checkableBehavior="all">
<item
android:id="@+id/menu_emulation_joystick_rel_center"

View file

@ -219,6 +219,9 @@
<string name="region_mismatch">Region Mismatch Warning</string>
<string name="region_mismatch_emulated">The country setting is not valid for the selected emulated region.</string>
<string name="region_mismatch_console">The country setting is not valid for the current linked console.</string>
<string name="storage">Storage</string>
<string name="compress_cia_installs">Compress installed CIA content</string>
<string name="compress_cia_installs_description">Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</string>
<!-- Camera settings strings -->
<string name="inner_camera">Inner Camera</string>
@ -423,6 +426,10 @@
<string name="emulation_configure_controls">Configure Controls</string>
<string name="emulation_edit_layout">Edit Layout</string>
<string name="emulation_done">Done</string>
<string name="emulation_button_sliding">Button Sliding</string>
<string name="emulation_button_sliding_disabled">Hold originally pressed button</string>
<string name="emulation_button_sliding_enabled">Hold currently pressed button</string>
<string name="emulation_button_sliding_alternative">Hold original and currently pressed button</string>
<string name="emulation_toggle_controls">Toggle Controls</string>
<string name="emulation_control_scale">Adjust Scale</string>
<string name="emulation_control_scale_global">Global Scale</string>

View file

@ -271,6 +271,7 @@ if (ENABLE_VULKAN)
endif()
if (NOT WIN32)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
target_include_directories(citra_qt PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
endif()

View file

@ -92,6 +92,7 @@
#endif
#include "common/settings.h"
#include "common/string_util.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/dumping/backend.h"
#include "core/file_sys/archive_extsavedata.h"
@ -991,6 +992,7 @@ void GMainWindow::ConnectWidgetEvents() {
connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress);
connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport);
connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished);
connect(this, &GMainWindow::CompressFinished, this, &GMainWindow::OnCompressFinished);
connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state,
&MultiplayerState::UpdateThemedIcons);
}
@ -1082,6 +1084,10 @@ void GMainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot);
connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo);
// Tools
connect_menu(ui->action_Compress_ROM_File, &GMainWindow::OnCompressFile);
connect_menu(ui->action_Decompress_ROM_File, &GMainWindow::OnDecompressFile);
// Help
connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder);
connect_menu(ui->action_Open_Log_Folder, []() {
@ -2236,7 +2242,7 @@ void GMainWindow::OnMenuSetUpSystemFiles() {
void GMainWindow::OnMenuInstallCIA() {
QStringList filepaths = QFileDialog::getOpenFileNames(
this, tr("Load Files"), UISettings::values.roms_path,
tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)"));
tr("3DS Installation File (*.cia *.zcia)") + QStringLiteral(";;") + tr("All Files (*.*)"));
if (filepaths.isEmpty()) {
return;
@ -2318,6 +2324,21 @@ void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString
}
}
void GMainWindow::OnCompressFinished(bool is_compress, bool success) {
progress_bar->hide();
progress_bar->setValue(0);
if (!success) {
if (is_compress) {
QMessageBox::critical(this, tr("Error compressing file"),
tr("File compress operation failed, check log for details."));
} else {
QMessageBox::critical(this, tr("Error decompressing file"),
tr("File decompress operation failed, check log for details."));
}
}
}
void GMainWindow::OnCIAInstallFinished() {
progress_bar->hide();
progress_bar->setValue(0);
@ -3025,6 +3046,176 @@ void GMainWindow::OnDumpVideo() {
}
}
void GMainWindow::OnCompressFile() {
// NOTE: Encrypted files SHOULD NEVER be compressed, otherwise the resulting
// compressed file will have very poor compression ratios, due to the high
// entropy caused by encryption. This may cause confusion to the user as they
// will see the files do not compress well and blame the emulator.
//
// This is enforced using the loaders as they already return an error on encryption.
QString filepath = QFileDialog::getOpenFileName(
this, tr("Load 3DS ROM File"), UISettings::values.roms_path,
tr("3DS ROM Files (*.cia *cci *3dsx *cxi)") + QStringLiteral(";;") + tr("All Files (*.*)"));
if (filepath.isEmpty()) {
return;
}
std::string in_path = filepath.toStdString();
// Identify file type
Loader::AppLoader::CompressFileInfo compress_info{};
compress_info.is_supported = false;
size_t frame_size{};
{
auto loader = Loader::GetLoader(in_path);
if (loader) {
compress_info = loader->GetCompressFileInfo();
frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_FRAME_SIZE;
} else {
bool is_compressed = false;
if (Service::AM::CheckCIAToInstall(in_path, is_compressed, true) ==
Service::AM::InstallStatus::Success) {
auto meta_info = Service::AM::GetCIAInfos(in_path);
compress_info.is_supported = true;
compress_info.is_compressed = is_compressed;
compress_info.recommended_compressed_extension = "zcia";
compress_info.recommended_uncompressed_extension = "cia";
compress_info.underlying_magic = std::array<u8, 4>({'C', 'I', 'A', '\0'});
frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_CIA_FRAME_SIZE;
if (meta_info.Succeeded()) {
const auto& meta_info_val = meta_info.Unwrap();
std::vector<u8> value(sizeof(Service::AM::TitleInfo));
memcpy(value.data(), &meta_info_val.first, sizeof(Service::AM::TitleInfo));
compress_info.default_metadata.emplace("titleinfo", value);
if (meta_info_val.second) {
value.resize(sizeof(Loader::SMDH));
memcpy(value.data(), meta_info_val.second.get(), sizeof(Loader::SMDH));
compress_info.default_metadata.emplace("smdh", value);
}
}
}
}
}
if (!compress_info.is_supported) {
QMessageBox::critical(
this, tr("Error compressing file"),
tr("The selected file is not a compatible 3DS ROM format. Make sure you have "
"chosen the right file, and that it is not encrypted."));
return;
}
if (compress_info.is_compressed) {
QMessageBox::warning(this, tr("Error compressing file"),
tr("The selected file is already compressed."));
return;
}
QString out_filter =
tr("3DS Compressed ROM File (*.%1)")
.arg(QString::fromStdString(compress_info.recommended_compressed_extension));
QFileInfo fileinfo(filepath);
QString final_path = fileinfo.path() + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QStringLiteral(".") +
QString::fromStdString(compress_info.recommended_compressed_extension);
filepath = QFileDialog::getSaveFileName(this, tr("Save 3DS Compressed ROM File"), final_path,
out_filter);
if (filepath.isEmpty()) {
return;
}
std::string out_path = filepath.toStdString();
progress_bar->show();
progress_bar->setMaximum(INT_MAX);
(void)QtConcurrent::run([&, in_path, out_path, compress_info, frame_size] {
const auto progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
bool success =
FileUtil::CompressZ3DSFile(in_path, out_path, compress_info.underlying_magic,
frame_size, progress, compress_info.default_metadata);
if (!success) {
FileUtil::Delete(out_path);
}
emit OnCompressFinished(true, success);
});
}
void GMainWindow::OnDecompressFile() {
QString filepath = QFileDialog::getOpenFileName(
this, tr("Load 3DS Compressed ROM File"), UISettings::values.roms_path,
tr("3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi)") + QStringLiteral(";;") +
tr("All Files (*.*)"));
if (filepath.isEmpty()) {
return;
}
std::string in_path = filepath.toStdString();
// Identify file type
Loader::AppLoader::CompressFileInfo compress_info{};
compress_info.is_supported = false;
{
auto loader = Loader::GetLoader(in_path);
if (loader) {
compress_info = loader->GetCompressFileInfo();
} else {
bool is_compressed = false;
if (Service::AM::CheckCIAToInstall(in_path, is_compressed, false) ==
Service::AM::InstallStatus::Success) {
compress_info.is_supported = true;
compress_info.is_compressed = is_compressed;
compress_info.recommended_compressed_extension = "zcia";
compress_info.recommended_uncompressed_extension = "cia";
compress_info.underlying_magic = std::array<u8, 4>({'C', 'I', 'A', '\0'});
}
}
}
if (!compress_info.is_supported) {
QMessageBox::critical(this, tr("Error decompressing file"),
tr("The selected file is not a compatible compressed 3DS ROM format. "
"Make sure you have "
"chosen the right file."));
return;
}
if (!compress_info.is_compressed) {
QMessageBox::warning(this, tr("Error decompressing file"),
tr("The selected file is already decompressed."));
return;
}
QString out_filter =
tr("3DS ROM File (*.%1)")
.arg(QString::fromStdString(compress_info.recommended_uncompressed_extension));
QFileInfo fileinfo(filepath);
QString final_path = fileinfo.path() + QStringLiteral(DIR_SEP) + fileinfo.completeBaseName() +
QStringLiteral(".") +
QString::fromStdString(compress_info.recommended_uncompressed_extension);
filepath = QFileDialog::getSaveFileName(this, tr("Save 3DS ROM File"), final_path, out_filter);
if (filepath.isEmpty()) {
return;
}
std::string out_path = filepath.toStdString();
progress_bar->show();
progress_bar->setMaximum(INT_MAX);
(void)QtConcurrent::run([&, in_path, out_path, compress_info] {
const auto progress = [&](std::size_t written, std::size_t total) {
emit UpdateProgress(written, total);
};
// TODO(PabloMK7): What should we do with the metadata?
bool success = FileUtil::DeCompressZ3DSFile(in_path, out_path, progress);
if (!success) {
FileUtil::Delete(out_path);
}
emit OnCompressFinished(false, success);
});
}
#ifdef _WIN32
void GMainWindow::OnOpenFFmpeg() {
auto filename =
@ -3514,8 +3705,8 @@ static bool IsSingleFileDropEvent(const QMimeData* mime) {
return mime->hasUrls() && mime->urls().length() == 1;
}
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "cxi", "bin", "3dsx",
"app", "elf", "axf"};
static const std::array<std::string, 10> AcceptedExtensions = {
"cci", "cxi", "bin", "3dsx", "app", "elf", "axf", "zcci", "zcxi", "z3dsx"};
static bool IsCorrectFileExtension(const QMimeData* mime) {
const QString& filename = mime->urls().at(0).toLocalFile();

View file

@ -141,6 +141,7 @@ signals:
void UpdateProgress(std::size_t written, std::size_t total);
void CIAInstallReport(Service::AM::InstallStatus status, QString filepath);
void CompressFinished(bool is_compress, bool success);
void CIAInstallFinished();
// Signal that tells widgets to update icons to use the current theme
void UpdateThemedIcons();
@ -248,6 +249,7 @@ private slots:
void OnMenuBootHomeMenu(u32 region);
void OnUpdateProgress(std::size_t written, std::size_t total);
void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
void OnCompressFinished(bool is_compress, bool success);
void OnCIAInstallFinished();
void OnMenuRecentFile();
void OnConfigure();
@ -281,6 +283,8 @@ private slots:
void OnSaveMovie();
void OnCaptureScreenshot();
void OnDumpVideo();
void OnCompressFile();
void OnDecompressFile();
#ifdef _WIN32
void OnOpenFFmpeg();
#endif

View file

@ -475,6 +475,7 @@ void QtConfig::ReadDataStorageValues() {
ReadBasicSetting(Settings::values.use_virtual_sd);
ReadBasicSetting(Settings::values.use_custom_storage);
ReadBasicSetting(Settings::values.compress_cia_installs);
const std::string nand_dir =
ReadSetting(QStringLiteral("nand_directory"), QStringLiteral("")).toString().toStdString();
@ -1045,6 +1046,7 @@ void QtConfig::SaveDataStorageValues() {
WriteBasicSetting(Settings::values.use_virtual_sd);
WriteBasicSetting(Settings::values.use_custom_storage);
WriteBasicSetting(Settings::values.compress_cia_installs);
WriteSetting(QStringLiteral("nand_directory"),
QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)),
QStringLiteral(""));

View file

@ -77,6 +77,7 @@ void ConfigureStorage::SetConfiguration() {
ui->toggle_virtual_sd->setChecked(Settings::values.use_virtual_sd.GetValue());
ui->toggle_custom_storage->setChecked(Settings::values.use_custom_storage.GetValue());
ui->toggle_compress_cia->setChecked(Settings::values.compress_cia_installs.GetValue());
ui->storage_group->setEnabled(!is_powered_on);
}
@ -84,6 +85,7 @@ void ConfigureStorage::SetConfiguration() {
void ConfigureStorage::ApplyConfiguration() {
Settings::values.use_virtual_sd = ui->toggle_virtual_sd->isChecked();
Settings::values.use_custom_storage = ui->toggle_custom_storage->isChecked();
Settings::values.compress_cia_installs = ui->toggle_compress_cia->isChecked();
if (!Settings::values.use_custom_storage) {
FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir,

View file

@ -179,6 +179,20 @@
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="toggle_compress_cia">
<property name="text">
<string>Compress installed CIA content</string>
</property>
<property name="toolTip">
<string>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>

View file

@ -1039,8 +1039,10 @@ void GameList::LoadInterfaceLayout() {
}
const QStringList GameList::supported_file_extensions = {
QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")};
QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app"),
QStringLiteral("z3dsx"), QStringLiteral("zcci"), QStringLiteral("zcxi"),
};
void GameList::RefreshGameDirectory() {
if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {

View file

@ -208,6 +208,9 @@
<addaction name="separator"/>
<addaction name="action_Capture_Screenshot"/>
<addaction name="action_Dump_Video"/>
<addaction name="separator"/>
<addaction name="action_Compress_ROM_File"/>
<addaction name="action_Decompress_ROM_File"/>
</widget>
<widget class="QMenu" name="menu_Help">
<property name="title">
@ -458,6 +461,16 @@
<string>Dump Video</string>
</property>
</action>
<action name="action_Compress_ROM_File">
<property name="text">
<string>Compress ROM File...</string>
</property>
</action>
<action name="action_Decompress_ROM_File">
<property name="text">
<string>Decompress ROM File...</string>
</property>
</action>
<action name="action_View_Lobby">
<property name="enabled">
<bool>true</bool>

View file

@ -213,6 +213,7 @@ void SdlConfig::ReadValues() {
// Data Storage
ReadSetting("Data Storage", Settings::values.use_virtual_sd);
ReadSetting("Data Storage", Settings::values.use_custom_storage);
ReadSetting("Data Storage", Settings::values.compress_cia_installs);
if (Settings::values.use_custom_storage) {
FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir,

View file

@ -1187,7 +1187,7 @@ bool IOFile::SeekImpl(s64 off, int origin) {
return m_good;
}
u64 IOFile::Tell() const {
u64 IOFile::TellImpl() const {
if (IsOpen())
return ftello(m_file);
@ -1224,11 +1224,18 @@ static std::size_t pread(int fd, void* buf, std::size_t count, uint64_t offset)
overlapped.OffsetHigh = static_cast<uint32_t>(offset >> 32);
overlapped.Offset = static_cast<uint32_t>(offset & 0xFFFF'FFFFLL);
LARGE_INTEGER orig, dummy;
// TODO(PabloMK7): This is not fully async, windows being messy again...
// The file pos pointer will be undefined if ReadAt is used in multiple
// threads. Normally not problematic, but worth remembering.
SetFilePointerEx(file, {}, &orig, FILE_CURRENT);
SetLastError(0);
bool ret = ReadFile(file, buf, static_cast<uint32_t>(count), &read_bytes, &overlapped);
DWORD last_error = GetLastError();
SetFilePointerEx(file, orig, &dummy, FILE_BEGIN);
if (!ret && GetLastError() != ERROR_HANDLE_EOF) {
errno = GetLastError();
if (!ret && last_error != ERROR_HANDLE_EOF) {
errno = last_error;
return std::numeric_limits<std::size_t>::max();
}
return read_bytes;

View file

@ -301,7 +301,7 @@ public:
void Swap(IOFile& other) noexcept;
bool Close();
virtual bool Close();
template <typename T>
std::size_t ReadArray(T* data, std::size_t length) {
@ -412,15 +412,15 @@ public:
return WriteImpl(data.data(), data.size(), sizeof(T));
}
[[nodiscard]] bool IsOpen() const {
[[nodiscard]] virtual bool IsOpen() const {
return nullptr != m_file;
}
// m_good is set to false when a read, write or other function fails
[[nodiscard]] bool IsGood() const {
[[nodiscard]] virtual bool IsGood() const {
return m_good;
}
[[nodiscard]] int GetFd() const {
[[nodiscard]] virtual int GetFd() const {
#ifdef ANDROID
return m_fd;
#else
@ -436,13 +436,15 @@ public:
bool Seek(s64 off, int origin) {
return SeekImpl(off, origin);
}
[[nodiscard]] u64 Tell() const;
[[nodiscard]] u64 GetSize() const;
bool Resize(u64 size);
bool Flush();
u64 Tell() const {
return TellImpl();
}
virtual u64 GetSize() const;
virtual bool Resize(u64 size);
virtual bool Flush();
// clear error state
void Clear() {
virtual void Clear() {
m_good = true;
std::clearerr(m_file);
}
@ -451,29 +453,35 @@ public:
return false;
}
const std::string& Filename() const {
virtual bool IsCompressed() {
return false;
}
virtual const std::string& Filename() const {
return filename;
}
protected:
friend struct CryptoIOFileImpl;
virtual bool Open();
virtual std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size);
virtual std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size,
std::size_t offset);
virtual std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size);
virtual bool SeekImpl(s64 off, int origin);
virtual u64 TellImpl() const;
private:
bool Open();
std::FILE* m_file = nullptr;
int m_fd = -1;
bool m_good = true;
std::string filename;
std::string openmode;
u32 flags;
u32 flags = 0;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {

View file

@ -468,6 +468,7 @@ struct Values {
// Data Storage
Setting<bool> use_virtual_sd{true, "use_virtual_sd"};
Setting<bool> use_custom_storage{false, "use_custom_storage"};
Setting<bool> compress_cia_installs{false, "compress_cia_installs"};
// System
SwitchableSetting<s32> region_value{REGION_VALUE_AUTO_SELECT, "region_value"};

View file

@ -1,15 +1,30 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2019 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <chrono>
#include <ctime>
#include <format>
#include <mutex>
#include <sstream>
#include <zstd.h>
#include <zstd/contrib/seekable_format/zstd_seekable.h>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/unique_ptr.hpp>
#include "common/alignment.h"
#include "common/archives.h"
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include "common/zstd_compression.h"
namespace Common::Compression {
std::vector<u8> CompressDataZSTD(std::span<const u8> source, s32 compression_level) {
compression_level = std::clamp(compression_level, ZSTD_minCLevel(), ZSTD_maxCLevel());
const std::size_t max_compressed_size = ZSTD_compressBound(source.size());
@ -71,3 +86,692 @@ std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
}
} // namespace Common::Compression
namespace FileUtil {
template <typename T>
void ReadFromIStream(std::istringstream& s, T* out, size_t out_size) {
s.read(reinterpret_cast<char*>(out), out_size);
}
template <typename T>
void WriteToOStream(std::ostringstream& s, const T* out, size_t out_size) {
s.write(reinterpret_cast<const char*>(out), out_size);
}
Z3DSMetadata::Z3DSMetadata(const std::span<u8>& source_data) {
if (source_data.empty())
return;
std::string buf(reinterpret_cast<const char*>(source_data.data()), source_data.size());
std::istringstream in(buf, std::ios::binary);
u8 version;
ReadFromIStream(in, &version, sizeof(version));
if (version != METADATA_VERSION) {
return;
}
while (!in.eof()) {
Item item;
ReadFromIStream(in, &item, sizeof(Item));
// If end item is reached, stop processing
if (item.type == Item::TYPE_END) {
break;
}
// Only binary type supported for now
if (item.type != Item::TYPE_BINARY) {
in.ignore(static_cast<std::streamsize>(item.name_len) + item.data_len);
continue;
}
std::string name(item.name_len, '\0');
std::vector<u8> data(item.data_len);
ReadFromIStream(in, name.data(), name.size());
ReadFromIStream(in, data.data(), data.size());
items.insert({std::move(name), std::move(data)});
}
}
std::vector<u8> Z3DSMetadata::AsBinary() {
if (items.empty())
return {};
std::ostringstream out;
u8 version = METADATA_VERSION;
WriteToOStream(out, &version, sizeof(u8));
for (const auto& it : items) {
Item item{
.type = Item::TYPE_BINARY,
.name_len = static_cast<u8>(std::min<size_t>(0xFF, it.first.size())),
.data_len = static_cast<u16>(std::min<size_t>(0xFFFF, it.second.size())),
};
WriteToOStream(out, &item, sizeof(item));
WriteToOStream(out, it.first.data(), item.name_len);
WriteToOStream(out, it.second.data(), item.data_len);
}
// Write end item
Item end{};
WriteToOStream(out, &end, sizeof(end));
std::string out_str = out.str();
return std::vector<u8>(out_str.begin(), out_str.end());
}
struct Z3DSWriteIOFile::Z3DSWriteIOFileImpl {
Z3DSWriteIOFileImpl() {}
Z3DSWriteIOFileImpl(size_t frame_size) {
zstd_frame_size = frame_size;
cstream = ZSTD_seekable_createCStream();
size_t init_result = ZSTD_seekable_initCStream(cstream, ZSTD_CLEVEL_DEFAULT, 0,
static_cast<unsigned int>(frame_size));
if (ZSTD_isError(init_result)) {
LOG_ERROR(Common_Filesystem, "ZSTD_seekable_initCStream() error : {}",
ZSTD_getErrorName(init_result));
}
write_header.magic = Z3DSFileHeader::EXPECTED_MAGIC;
write_header.version = Z3DSFileHeader::EXPECTED_VERSION;
write_header.header_size = sizeof(Z3DSFileHeader);
next_input_size_hint = ZSTD_CStreamInSize();
}
bool WriteHeader(IOFile* file) {
file->Seek(0, SEEK_SET);
return file->WriteBytes(&write_header, sizeof(write_header)) == sizeof(write_header);
}
bool WriteMetadata(IOFile* file, const std::span<u8>& data) {
std::array<u8, 0x10> tmp_data{};
size_t total_size = Common::AlignUp(data.size(), 0x10);
write_header.metadata_size = static_cast<u32>(total_size);
size_t res_written = file->WriteBytes(data.data(), data.size());
res_written += file->WriteBytes(tmp_data.data(), total_size - data.size());
return res_written == total_size;
}
size_t Write(IOFile* file, const void* data, std::size_t length) {
size_t ret = length;
const size_t out_size = ZSTD_CStreamOutSize();
const size_t in_size = ZSTD_CStreamInSize();
if (write_buffer.size() < out_size) {
write_buffer.resize(out_size);
}
ZSTD_inBuffer input = {data, length, 0};
while (input.pos < input.size) {
ZSTD_outBuffer output = {write_buffer.data(), write_buffer.size(), 0};
next_input_size_hint = ZSTD_seekable_compressStream(cstream, &output, &input);
if (ZSTD_isError(next_input_size_hint)) {
LOG_ERROR(Common_Filesystem, "ZSTD_seekable_compressStream() error : {}",
ZSTD_getErrorName(next_input_size_hint));
ret = 0;
next_input_size_hint = ZSTD_CStreamInSize();
break;
}
if (next_input_size_hint > in_size) {
next_input_size_hint = in_size;
}
if (file->WriteBytes(static_cast<u8*>(output.dst), output.pos) != output.pos) {
ret = 0;
break;
}
written_compressed += output.pos;
}
return ret;
}
bool Close(IOFile* file, size_t written_uncompressed) {
const size_t out_size = ZSTD_CStreamOutSize();
if (write_buffer.size() < out_size) {
write_buffer.resize(out_size);
}
size_t remaining;
do {
ZSTD_outBuffer output = {write_buffer.data(), write_buffer.size(), 0};
remaining = ZSTD_seekable_endStream(cstream, &output); /* close stream */
if (ZSTD_isError(remaining)) {
LOG_ERROR(Common_Filesystem, "ZSTD_seekable_endStream() error : {}",
ZSTD_getErrorName(remaining));
return false;
}
if (file->WriteBytes(static_cast<u8*>(output.dst), output.pos) != output.pos) {
return false;
}
written_compressed += output.pos;
} while (remaining);
write_header.compressed_size = written_compressed;
write_header.uncompressed_size = written_uncompressed;
ZSTD_seekable_freeCStream(cstream);
return WriteHeader(file);
}
std::vector<u8> write_buffer;
size_t next_input_size_hint = 0;
size_t zstd_frame_size = 0;
u64 written_compressed = 0;
ZSTD_seekable_CStream* cstream{};
Z3DSFileHeader write_header{};
};
Z3DSWriteIOFile::Z3DSWriteIOFile()
: IOFile(), file{std::make_unique<IOFile>()}, impl{std::make_unique<Z3DSWriteIOFileImpl>()} {}
Z3DSWriteIOFile::Z3DSWriteIOFile(std::unique_ptr<IOFile>&& underlying_file,
const std::array<u8, 4>& underlying_magic, size_t frame_size)
: IOFile(), file{std::move(underlying_file)},
impl{std::make_unique<Z3DSWriteIOFileImpl>(frame_size)} {
ASSERT_MSG(!file->IsCompressed(), "Underlying file is already compressed!");
impl->write_header.underlying_magic = underlying_magic;
impl->WriteHeader(file.get());
Metadata().Add("compressor", std::string("Azahar ") + Common::g_build_fullname);
std::time_t tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::tm tm{};
#if defined(_WIN32)
gmtime_s(&tm, &tt);
#else
gmtime_r(&tt, &tm);
#endif
char buf[0x20];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
Metadata().Add("date", buf);
Metadata().Add(
"maxframesize",
std::to_string(frame_size ? frame_size : ZSTD_SEEKABLE_MAX_FRAME_DECOMPRESSED_SIZE));
}
Z3DSWriteIOFile::~Z3DSWriteIOFile() {
this->Close();
}
bool Z3DSWriteIOFile::Close() {
impl->Close(file.get(), written_uncompressed);
return file->Close();
}
u64 Z3DSWriteIOFile::GetSize() const {
return written_uncompressed;
}
bool Z3DSWriteIOFile::Resize(u64 size) {
// Stubbed
UNIMPLEMENTED();
return false;
}
bool Z3DSWriteIOFile::Flush() {
return file->Flush();
}
void Z3DSWriteIOFile::Clear() {
return file->Clear();
}
bool Z3DSWriteIOFile::IsCrypto() {
return file->IsCrypto();
}
const std::string& Z3DSWriteIOFile::Filename() const {
return file->Filename();
}
bool Z3DSWriteIOFile::IsOpen() const {
return file->IsOpen();
}
bool Z3DSWriteIOFile::IsGood() const {
return file->IsGood();
}
int Z3DSWriteIOFile::GetFd() const {
return file->GetFd();
}
bool Z3DSWriteIOFile::Open() {
if (is_serializing) {
return true;
}
// Stubbed
UNIMPLEMENTED();
return false;
}
std::size_t Z3DSWriteIOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) {
// Stubbed
UNIMPLEMENTED();
return 0;
}
std::size_t Z3DSWriteIOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size,
std::size_t offset) {
// Stubbed
UNIMPLEMENTED();
return 0;
}
std::size_t Z3DSWriteIOFile::WriteImpl(const void* data, std::size_t length,
std::size_t data_size) {
if (!metadata_written) {
metadata_written = true;
auto metadata_binary = metadata.AsBinary();
if (!metadata_binary.empty()) {
impl->WriteMetadata(file.get(), metadata_binary);
}
}
size_t ret = impl->Write(file.get(), data, length * data_size);
written_uncompressed += ret;
return ret;
}
bool Z3DSWriteIOFile::SeekImpl(s64 off, int origin) {
if (is_serializing) {
return true;
}
// Stubbed
UNIMPLEMENTED();
return false;
}
u64 Z3DSWriteIOFile::TellImpl() const {
return written_uncompressed;
}
size_t Z3DSWriteIOFile::GetNextWriteHint() {
return impl->next_input_size_hint;
}
template <class Archive>
void Z3DSWriteIOFile::serialize(Archive& ar, const unsigned int) {
is_serializing = true;
ar& boost::serialization::base_object<IOFile>(*this);
ar & file;
ar & written_uncompressed;
ar & metadata_written;
ar & metadata;
Z3DSFileHeader hd;
size_t frame_size;
u64 written_compressed;
if (Archive::is_loading::value) {
ar & hd;
ar & frame_size;
ar & written_compressed;
impl = std::make_unique<Z3DSWriteIOFileImpl>(frame_size);
impl->write_header = hd;
impl->written_compressed = written_compressed;
} else {
ar & impl->write_header;
ar & impl->zstd_frame_size;
ar & impl->written_compressed;
}
is_serializing = false;
}
struct Z3DSReadIOFile::Z3DSReadIOFileImpl {
Z3DSReadIOFileImpl() {}
Z3DSReadIOFileImpl(IOFile* file, bool load_metadata = true) {
curr_file = file;
m_good = file->ReadAtBytes(&header, sizeof(header), 0) == sizeof(header);
m_good &= header.magic == Z3DSFileHeader::EXPECTED_MAGIC &&
header.version == Z3DSFileHeader::EXPECTED_VERSION;
if (!m_good) {
return;
}
if (header.metadata_size && load_metadata) {
std::vector<u8> buff(header.metadata_size);
file->ReadAtBytes(buff.data(), buff.size(), header.header_size);
metadata = Z3DSMetadata(buff);
}
seekable = ZSTD_seekable_create();
ZSTD_seekable_customFile custom_file{
.opaque = this,
.read = [](void* opaque, void* buffer, size_t n) -> int {
return reinterpret_cast<Z3DSReadIOFileImpl*>(opaque)->OnZSTDRead(buffer, n);
},
.seek = [](void* opaque, long long offset, int origin) -> int {
return reinterpret_cast<Z3DSReadIOFileImpl*>(opaque)->OnZSTDSeek(offset, origin);
},
};
size_t init_result = ZSTD_seekable_initAdvanced(seekable, custom_file);
if (ZSTD_isError(init_result)) {
LOG_ERROR(Common_Filesystem, "ZSTD_seekable_initCStream() error : {}",
ZSTD_getErrorName(init_result));
m_good = false;
}
}
int OnZSTDRead(void* buffer, size_t n) {
const size_t read = curr_file->ReadBytes(reinterpret_cast<uint8_t*>(buffer), n);
if (read != n) {
return -1;
}
return 0;
}
int OnZSTDSeek(long long offset, int origin) {
if (origin == SEEK_SET) {
offset += static_cast<long long>(header.metadata_size) + header.header_size;
}
const bool res = curr_file->Seek(offset, origin);
return res ? 0 : -1;
}
size_t Read(void* data, std::size_t length) {
if (!m_good)
return 0;
size_t result = ZSTD_seekable_decompress(seekable, data, length, uncompressed_pos);
if (ZSTD_isError(result)) {
LOG_ERROR(Common_Filesystem, "ZSTD_seekable_decompress() error : {}",
ZSTD_getErrorName(result));
return 0;
}
uncompressed_pos += result;
return result;
}
size_t ReadAt(void* data, std::size_t length, size_t pos) {
if (!m_good)
return 0;
// ReadAt should be thread safe, but seekable compression is not,
// so we are forced to use a lock.
std::scoped_lock lock(read_mutex);
size_t result = ZSTD_seekable_decompress(seekable, data, length, pos);
if (ZSTD_isError(result)) {
LOG_ERROR(Common_Filesystem, "ZSTD_seekable_decompress() error : {}",
ZSTD_getErrorName(result));
return 0;
}
return result;
}
bool Seek(s64 off, int origin) {
s64 start = 0;
switch (origin) {
case SEEK_SET:
start = 0;
break;
case SEEK_CUR:
start = static_cast<s64>(uncompressed_pos);
break;
case SEEK_END:
start = static_cast<s64>(header.uncompressed_size);
break;
default:
return false;
}
s64 new_pos = start + off;
if (new_pos < 0)
return false;
uncompressed_pos = static_cast<u64>(new_pos);
return true;
}
void Close() {
ZSTD_seekable_free(seekable);
}
Z3DSFileHeader header{};
ZSTD_seekable* seekable = nullptr;
bool m_good = true;
IOFile* curr_file = nullptr;
std::mutex read_mutex;
u64 uncompressed_pos = 0;
Z3DSMetadata metadata;
};
std::optional<u32> Z3DSReadIOFile::GetUnderlyingFileMagic(IOFile* underlying_file) {
Z3DSFileHeader header{};
underlying_file->ReadAtBytes(&header, sizeof(header), 0);
if (header.magic != Z3DSFileHeader::EXPECTED_MAGIC ||
header.version != Z3DSFileHeader::EXPECTED_VERSION) {
return std::nullopt;
}
return MakeMagic(header.underlying_magic[0], header.underlying_magic[1],
header.underlying_magic[2], header.underlying_magic[3]);
}
Z3DSReadIOFile::Z3DSReadIOFile()
: IOFile(), file{std::make_unique<IOFile>()}, impl{std::make_unique<Z3DSReadIOFileImpl>()} {}
Z3DSReadIOFile::Z3DSReadIOFile(std::unique_ptr<IOFile>&& underlying_file)
: IOFile(), file{std::move(underlying_file)},
impl{std::make_unique<Z3DSReadIOFileImpl>(file.get())} {
ASSERT_MSG(!file->IsCompressed(), "Underlying file is already compressed!");
}
Z3DSReadIOFile::~Z3DSReadIOFile() {
this->Close();
}
bool Z3DSReadIOFile::Close() {
impl->Close();
return file->Close();
}
u64 Z3DSReadIOFile::GetSize() const {
return impl->header.uncompressed_size;
}
bool Z3DSReadIOFile::Resize(u64 size) {
// Stubbed
UNIMPLEMENTED();
return false;
}
bool Z3DSReadIOFile::Flush() {
return file->Flush();
}
void Z3DSReadIOFile::Clear() {
return file->Clear();
}
bool Z3DSReadIOFile::IsCrypto() {
return file->IsCrypto();
}
const std::string& Z3DSReadIOFile::Filename() const {
return file->Filename();
}
bool Z3DSReadIOFile::IsOpen() const {
return file->IsOpen();
}
bool Z3DSReadIOFile::IsGood() const {
return file->IsGood() && impl->m_good;
}
int Z3DSReadIOFile::GetFd() const {
return file->GetFd();
}
bool Z3DSReadIOFile::Open() {
if (is_serializing) {
return true;
}
// Stubbed
UNIMPLEMENTED();
return false;
}
std::size_t Z3DSReadIOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) {
return impl->Read(data, length * data_size);
}
std::size_t Z3DSReadIOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size,
std::size_t offset) {
return impl->ReadAt(data, length * data_size, offset);
}
std::size_t Z3DSReadIOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) {
// Stubbed
UNIMPLEMENTED();
return 0;
}
bool Z3DSReadIOFile::SeekImpl(s64 off, int origin) {
if (is_serializing) {
return true;
}
return impl->Seek(off, origin);
}
u64 Z3DSReadIOFile::TellImpl() const {
return impl->uncompressed_pos;
}
std::array<u8, 4> Z3DSReadIOFile::GetFileMagic() {
return impl->header.underlying_magic;
}
const Z3DSMetadata& Z3DSReadIOFile::Metadata() {
return impl->metadata;
}
template <class Archive>
void Z3DSReadIOFile::serialize(Archive& ar, const unsigned int) {
is_serializing = true;
ar& boost::serialization::base_object<IOFile>(*this);
ar & file;
if (Archive::is_loading::value) {
impl = std::make_unique<Z3DSReadIOFileImpl>(file.get(), false);
}
ar & impl->uncompressed_pos;
ar & impl->metadata;
is_serializing = false;
}
bool CompressZ3DSFile(const std::string& src_file_name, const std::string& dst_file_name,
const std::array<u8, 4>& underlying_magic, size_t frame_size,
std::function<ProgressCallback>&& update_callback,
std::unordered_map<std::string, std::vector<u8>> metadata) {
IOFile in_file(src_file_name, "rb");
if (!in_file.IsOpen()) {
LOG_ERROR(Common_Filesystem, "Failed to open source file: {}", src_file_name);
return false;
}
std::unique_ptr<IOFile> out_file = std::make_unique<IOFile>(dst_file_name, "wb");
if (!out_file->IsOpen()) {
LOG_ERROR(Common_Filesystem, "Failed to open destination file: {}", dst_file_name);
return false;
}
if (Z3DSReadIOFile::GetUnderlyingFileMagic(&in_file) != std::nullopt) {
LOG_ERROR(Common_Filesystem, "Source file is already compressed, nothing to do: {}",
src_file_name);
return false;
}
Z3DSWriteIOFile out_compress_file(std::move(out_file), underlying_magic, frame_size);
for (auto& it : metadata) {
std::string val_str(it.second.size(), '\0');
memcpy(val_str.data(), it.second.data(), val_str.size());
out_compress_file.Metadata().Add(it.first, val_str);
}
size_t next_chunk = out_compress_file.GetNextWriteHint();
std::vector<u8> buffer(next_chunk);
size_t in_size = in_file.GetSize();
size_t written = 0;
while (written != in_size) {
size_t to_read = ((in_size - written) > next_chunk) ? next_chunk : (in_size - written);
if (buffer.size() < to_read) {
buffer.resize(to_read);
}
if (in_file.ReadBytes(buffer.data(), to_read) != to_read) {
LOG_ERROR(Common_Filesystem, "Failed to read from source file");
return false;
}
if (out_compress_file.WriteBytes(buffer.data(), to_read) != to_read) {
LOG_ERROR(Common_Filesystem, "Failed to write to destination file");
}
written += to_read;
next_chunk = out_compress_file.GetNextWriteHint();
if (update_callback) {
update_callback(written, in_size);
}
}
LOG_INFO(Common_Filesystem, "File {} compressed successfully to {}", src_file_name,
dst_file_name);
return true;
}
bool DeCompressZ3DSFile(const std::string& src_file_name, const std::string& dst_file_name,
std::function<ProgressCallback>&& update_callback) {
std::unique_ptr<IOFile> in_file = std::make_unique<IOFile>(src_file_name, "rb");
if (!in_file->IsOpen()) {
LOG_ERROR(Common_Filesystem, "Failed to open source file: {}", src_file_name);
return false;
}
IOFile out_file(dst_file_name, "wb");
if (!out_file.IsOpen()) {
LOG_ERROR(Common_Filesystem, "Failed to open destination file: {}", dst_file_name);
return false;
}
if (Z3DSReadIOFile::GetUnderlyingFileMagic(in_file.get()) == std::nullopt) {
LOG_ERROR(Common_Filesystem,
"Source file is not compressed or is invalid, nothing to do: {}", src_file_name);
return false;
}
Z3DSReadIOFile in_compress_file(std::move(in_file));
size_t next_chunk = 64 * 1024 * 1024;
std::vector<u8> buffer(next_chunk);
size_t in_size = in_compress_file.GetSize();
size_t written = 0;
while (written != in_size) {
size_t to_read = (in_size - written) > next_chunk ? next_chunk : (in_size - written);
if (buffer.size() < to_read) {
buffer.resize(to_read);
}
if (in_compress_file.ReadBytes(buffer.data(), to_read) != to_read) {
LOG_ERROR(Common_Filesystem, "Failed to read from source file");
return false;
}
if (out_file.WriteBytes(buffer.data(), to_read) != to_read) {
LOG_ERROR(Common_Filesystem, "Failed to write to destination file");
}
written += to_read;
if (update_callback) {
update_callback(written, in_size);
}
}
LOG_INFO(Common_Filesystem, "File {} decompressed successfully to {}", src_file_name,
dst_file_name);
return true;
}
} // namespace FileUtil
SERIALIZE_EXPORT_IMPL(FileUtil::Z3DSReadIOFile);
SERIALIZE_EXPORT_IMPL(FileUtil::Z3DSWriteIOFile);

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2019 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -5,9 +9,14 @@
#pragma once
#include <span>
#include <unordered_map>
#include <vector>
#include <boost/serialization/array.hpp>
#include <boost/serialization/unordered_map.hpp>
#include "common/archives.h"
#include "common/common_types.h"
#include "common/file_util.h"
namespace Common::Compression {
@ -41,3 +50,224 @@ namespace Common::Compression {
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed);
} // namespace Common::Compression
namespace FileUtil {
struct Z3DSFileHeader {
static constexpr std::array<u8, 4> EXPECTED_MAGIC = {'Z', '3', 'D', 'S'};
static constexpr u8 EXPECTED_VERSION = 1;
std::array<u8, 4> magic = EXPECTED_MAGIC;
std::array<u8, 4> underlying_magic{};
u8 version = EXPECTED_VERSION;
u8 reserved = 0;
u16 header_size = 0;
u32 metadata_size = 0;
u64 compressed_size = 0;
u64 uncompressed_size = 0;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & magic;
ar & underlying_magic;
ar & version;
ar & reserved;
ar & header_size;
ar & metadata_size;
ar & compressed_size;
ar & uncompressed_size;
}
};
static_assert(sizeof(Z3DSFileHeader) == 0x20, "Invalid Z3DSFileHeader size");
class Z3DSMetadata {
public:
static constexpr u8 METADATA_VERSION = 1;
Z3DSMetadata() {}
Z3DSMetadata(const std::span<u8>& source_data);
void Add(const std::string& name, const std::span<u8>& data) {
items.insert({name, std::vector<u8>(data.begin(), data.end())});
}
void Add(const std::string& name, const std::string& data) {
items.insert({name, std::vector<u8>(data.begin(), data.end())});
}
std::optional<std::vector<u8>> Get(const std::string& name) const {
auto it = items.find(name);
if (it == items.end()) {
return std::nullopt;
}
return it->second;
}
std::vector<u8> AsBinary();
private:
struct Item {
enum Type : u8 {
TYPE_END = 0,
TYPE_BINARY = 1,
};
Type type{};
u8 name_len{};
u16 data_len{};
};
static_assert(sizeof(Item) == 4);
std::unordered_map<std::string, std::vector<u8>> items;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & items;
}
friend class boost::serialization::access;
};
class Z3DSWriteIOFile : public IOFile {
public:
static constexpr size_t DEFAULT_FRAME_SIZE = 256 * 1024; // 256KiB
static constexpr size_t DEFAULT_CIA_FRAME_SIZE = 32 * 1024 * 1024; // 32MiB
static constexpr size_t MAX_FRAME_SIZE = 0; // Let the lib decide, usually 1GiB
Z3DSWriteIOFile();
Z3DSWriteIOFile(std::unique_ptr<IOFile>&& underlying_file,
const std::array<u8, 4>& underlying_magic, size_t frame_size);
~Z3DSWriteIOFile();
bool Close() override;
u64 GetSize() const override;
bool Resize(u64 size) override;
bool Flush() override;
void Clear() override;
bool IsCrypto() override;
bool IsCompressed() override {
return true;
}
const std::string& Filename() const override;
bool IsOpen() const override;
bool IsGood() const override;
int GetFd() const override;
Z3DSMetadata& Metadata() {
return metadata;
}
size_t GetNextWriteHint();
private:
struct Z3DSWriteIOFileImpl;
bool Open() override;
std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) override;
std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size,
std::size_t offset) override;
std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size) override;
bool SeekImpl(s64 off, int origin) override;
u64 TellImpl() const override;
std::unique_ptr<IOFile> file;
std::unique_ptr<Z3DSWriteIOFileImpl> impl;
u64 written_uncompressed = 0;
bool metadata_written = false;
Z3DSMetadata metadata;
template <class Archive>
void serialize(Archive& ar, const unsigned int);
friend class boost::serialization::access;
bool is_serializing = false;
};
class Z3DSReadIOFile : public IOFile {
public:
static std::optional<u32> GetUnderlyingFileMagic(IOFile* underlying_file);
Z3DSReadIOFile();
Z3DSReadIOFile(std::unique_ptr<IOFile>&& underlying_file);
~Z3DSReadIOFile();
bool Close() override;
u64 GetSize() const override;
bool Resize(u64 size) override;
bool Flush() override;
void Clear() override;
bool IsCrypto() override;
bool IsCompressed() override {
return true;
}
const std::string& Filename() const override;
bool IsOpen() const override;
bool IsGood() const override;
int GetFd() const override;
std::array<u8, 4> GetFileMagic();
const Z3DSMetadata& Metadata();
private:
struct Z3DSReadIOFileImpl;
static constexpr u32 MakeMagic(char a, char b, char c, char d) {
return a | b << 8 | c << 16 | d << 24;
}
bool Open() override;
std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) override;
std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size,
std::size_t offset) override;
std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size) override;
bool SeekImpl(s64 off, int origin) override;
u64 TellImpl() const override;
std::unique_ptr<IOFile> file;
std::unique_ptr<Z3DSReadIOFileImpl> impl;
template <class Archive>
void serialize(Archive& ar, const unsigned int);
friend class boost::serialization::access;
bool is_serializing = false;
};
using ProgressCallback = void(std::size_t, std::size_t);
bool CompressZ3DSFile(const std::string& src_file, const std::string& dst_file,
const std::array<u8, 4>& underlying_magic, size_t frame_size,
std::function<ProgressCallback>&& update_callback = nullptr,
std::unordered_map<std::string, std::vector<u8>> metadata = {});
bool DeCompressZ3DSFile(const std::string& src_file, const std::string& dst_file,
std::function<ProgressCallback>&& update_callback = nullptr);
} // namespace FileUtil
BOOST_CLASS_EXPORT_KEY(FileUtil::Z3DSWriteIOFile)
BOOST_CLASS_EXPORT_KEY(FileUtil::Z3DSReadIOFile)

View file

@ -54,19 +54,29 @@ Loader::ResultStatus CIAContainer::Load(const FileBackend& backend) {
result = LoadMetadata(meta_data);
if (result != Loader::ResultStatus::Success)
return result;
if (cia_header.meta_size >= sizeof(Metadata) + sizeof(Loader::SMDH)) {
std::vector<u8> smdh_data(sizeof(Loader::SMDH));
read_result = backend.Read(GetMetadataOffset() + CIA_METADATA_SIZE,
sizeof(Loader::SMDH), smdh_data.data());
if (read_result.Failed() || *read_result != sizeof(Loader::SMDH))
return Loader::ResultStatus::Error;
result = LoadSMDH(smdh_data);
if (result != Loader::ResultStatus::Success)
return result;
}
}
return Loader::ResultStatus::Success;
}
Loader::ResultStatus CIAContainer::Load(const std::string& filepath) {
FileUtil::IOFile file(filepath, "rb");
if (!file.IsOpen())
Loader::ResultStatus CIAContainer::Load(FileUtil::IOFile* file) {
if (!file->IsOpen())
return Loader::ResultStatus::Error;
// Load CIA Header
std::vector<u8> header_data(sizeof(CIAHeader));
if (file.ReadBytes(header_data.data(), sizeof(CIAHeader)) != sizeof(CIAHeader))
if (file->ReadBytes(header_data.data(), sizeof(CIAHeader)) != sizeof(CIAHeader))
return Loader::ResultStatus::Error;
Loader::ResultStatus result = LoadHeader(header_data);
@ -75,8 +85,8 @@ Loader::ResultStatus CIAContainer::Load(const std::string& filepath) {
// Load Ticket
std::vector<u8> ticket_data(cia_header.tik_size);
file.Seek(GetTicketOffset(), SEEK_SET);
if (file.ReadBytes(ticket_data.data(), cia_header.tik_size) != cia_header.tik_size)
file->Seek(GetTicketOffset(), SEEK_SET);
if (file->ReadBytes(ticket_data.data(), cia_header.tik_size) != cia_header.tik_size)
return Loader::ResultStatus::Error;
result = LoadTicket(ticket_data);
@ -85,8 +95,8 @@ Loader::ResultStatus CIAContainer::Load(const std::string& filepath) {
// Load Title Metadata
std::vector<u8> tmd_data(cia_header.tmd_size);
file.Seek(GetTitleMetadataOffset(), SEEK_SET);
if (file.ReadBytes(tmd_data.data(), cia_header.tmd_size) != cia_header.tmd_size)
file->Seek(GetTitleMetadataOffset(), SEEK_SET);
if (file->ReadBytes(tmd_data.data(), cia_header.tmd_size) != cia_header.tmd_size)
return Loader::ResultStatus::Error;
result = LoadTitleMetadata(tmd_data);
@ -96,13 +106,23 @@ Loader::ResultStatus CIAContainer::Load(const std::string& filepath) {
// Load CIA Metadata
if (cia_header.meta_size) {
std::vector<u8> meta_data(sizeof(Metadata));
file.Seek(GetMetadataOffset(), SEEK_SET);
if (file.ReadBytes(meta_data.data(), sizeof(Metadata)) != sizeof(Metadata))
file->Seek(GetMetadataOffset(), SEEK_SET);
if (file->ReadBytes(meta_data.data(), sizeof(Metadata)) != sizeof(Metadata))
return Loader::ResultStatus::Error;
result = LoadMetadata(meta_data);
if (result != Loader::ResultStatus::Success)
return result;
if (cia_header.meta_size >= sizeof(Metadata) + sizeof(Loader::SMDH)) {
std::vector<u8> smdh_data(sizeof(Loader::SMDH));
file->Seek(GetMetadataOffset() + CIA_METADATA_SIZE, SEEK_SET);
if (file->ReadBytes(smdh_data.data(), sizeof(Loader::SMDH)) != sizeof(Loader::SMDH))
return Loader::ResultStatus::Error;
result = LoadSMDH(smdh_data);
if (result != Loader::ResultStatus::Success)
return result;
}
}
return Loader::ResultStatus::Success;
@ -128,6 +148,11 @@ Loader::ResultStatus CIAContainer::Load(std::span<const u8> file_data) {
result = LoadMetadata(file_data, GetMetadataOffset());
if (result != Loader::ResultStatus::Success)
return result;
if (cia_header.meta_size >= sizeof(Metadata) + sizeof(Loader::SMDH)) {
result = LoadSMDH(file_data, GetMetadataOffset() + CIA_METADATA_SIZE);
if (result != Loader::ResultStatus::Success)
return result;
}
}
return Loader::ResultStatus::Success;
@ -173,6 +198,18 @@ Loader::ResultStatus CIAContainer::LoadMetadata(std::span<const u8> meta_data, s
return Loader::ResultStatus::Success;
}
Loader::ResultStatus CIAContainer::LoadSMDH(std::span<const u8> smdh_data, std::size_t offset) {
if (smdh_data.size() - offset < sizeof(Loader::SMDH)) {
return Loader::ResultStatus::Error;
}
cia_smdh = std::make_unique<Loader::SMDH>();
std::memcpy(cia_smdh.get(), smdh_data.data(), sizeof(Loader::SMDH));
return Loader::ResultStatus::Success;
}
Ticket& CIAContainer::GetTicket() {
return cia_ticket;
}
@ -189,6 +226,10 @@ u32 CIAContainer::GetCoreVersion() const {
return cia_metadata.core_version;
}
const std::unique_ptr<Loader::SMDH>& CIAContainer::GetSMDH() const {
return cia_smdh;
}
u64 CIAContainer::GetCertificateOffset() const {
return Common::AlignUp(cia_header.header_size, CIA_SECTION_ALIGNMENT);
}

View file

@ -9,9 +9,11 @@
#include <span>
#include <string>
#include "common/common_types.h"
#include "common/file_util.h"
#include "common/swap.h"
#include "core/file_sys/ticket.h"
#include "core/file_sys/title_metadata.h"
#include "core/loader/smdh.h"
namespace Loader {
enum class ResultStatus;
@ -62,7 +64,7 @@ class CIAContainer {
public:
// Load whole CIAs outright
Loader::ResultStatus Load(const FileBackend& backend);
Loader::ResultStatus Load(const std::string& filepath);
Loader::ResultStatus Load(FileUtil::IOFile* file);
Loader::ResultStatus Load(std::span<const u8> header_data);
// Load parts of CIAs (for CIAs streamed in)
@ -72,12 +74,14 @@ public:
Loader::ResultStatus LoadTitleMetadata(std::span<const u8> tmd_data, std::size_t offset = 0);
Loader::ResultStatus LoadTitleMetadata(const TitleMetadata& tmd);
Loader::ResultStatus LoadMetadata(std::span<const u8> meta_data, std::size_t offset = 0);
Loader::ResultStatus LoadSMDH(std::span<const u8> smdh_data, std::size_t offset = 0);
const CIAHeader* GetHeader();
Ticket& GetTicket();
const TitleMetadata& GetTitleMetadata() const;
std::array<u64, 0x30>& GetDependencies();
u32 GetCoreVersion() const;
const std::unique_ptr<Loader::SMDH>& GetSMDH() const;
u64 GetCertificateOffset() const;
u64 GetTicketOffset() const;
@ -107,6 +111,7 @@ private:
bool has_header = false;
CIAHeader cia_header;
Metadata cia_metadata;
std::unique_ptr<Loader::SMDH> cia_smdh;
Ticket cia_ticket;
TitleMetadata cia_tmd;
};

View file

@ -10,6 +10,7 @@
#include <cryptopp/sha.h>
#include "common/common_types.h"
#include "common/logging/log.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/file_sys/layered_fs.h"
#include "core/file_sys/ncch_container.h"
@ -137,6 +138,15 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
return Loader::ResultStatus::Success;
}
if (!file->IsOpen()) {
return Loader::ResultStatus::Error;
}
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) != std::nullopt) {
// The file is compressed
file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(file));
}
for (int i = 0; i < 2; i++) {
if (!file->IsOpen()) {
return Loader::ResultStatus::Error;
@ -151,6 +161,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
// Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)...
if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) {
is_ncsd = true;
NCSD_Header ncsd_header;
file->Seek(ncch_offset, SEEK_SET);
file->ReadBytes(&ncsd_header, sizeof(NCSD_Header));
@ -166,9 +177,12 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
// We may be loading a crypto file, try again
if (i == 0) {
file.reset();
file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) != std::nullopt) {
// The file is compressed
file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(file));
}
} else {
return Loader::ResultStatus::ErrorInvalidFormat;
}
@ -179,6 +193,11 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
}
if (!ncch_header.no_crypto) {
// Encrypted NCCH are not supported
return Loader::ResultStatus::ErrorEncrypted;
}
has_header = true;
return Loader::ResultStatus::Success;
}
@ -190,9 +209,18 @@ Loader::ResultStatus NCCHContainer::Load() {
int block_size = kBlockSize;
if (file->IsOpen()) {
size_t file_size;
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) != std::nullopt) {
// The file is compressed
file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(file));
}
size_t file_size;
for (int i = 0; i < 2; i++) {
if (!file->IsOpen()) {
return Loader::ResultStatus::Error;
}
file_size = file->GetSize();
// Reset read pointer in case this file has been read before.
@ -203,6 +231,7 @@ Loader::ResultStatus NCCHContainer::Load() {
// Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)...
if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) {
is_ncsd = true;
NCSD_Header ncsd_header;
file->Seek(ncch_offset, SEEK_SET);
file->ReadBytes(&ncsd_header, sizeof(NCSD_Header));
@ -219,15 +248,25 @@ Loader::ResultStatus NCCHContainer::Load() {
if (i == 0) {
file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) !=
std::nullopt) {
// The file is compressed
file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(file));
}
} else {
return Loader::ResultStatus::ErrorInvalidFormat;
}
} else {
break;
}
}
if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
}
if (file->IsCompressed()) {
LOG_DEBUG(Service_FS, "NCCH file is compressed");
}
has_header = true;
@ -323,12 +362,7 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
return Loader::ResultStatus::Error;
if (file->IsCrypto()) {
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
} else {
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
}
exefs_file = Reopen(file, filepath);
has_exefs = true;
}
@ -366,12 +400,7 @@ Loader::ResultStatus NCCHContainer::LoadOverrides() {
is_tainted = true;
has_exefs = true;
} else {
if (file->IsCrypto()) {
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
} else {
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
}
exefs_file = Reopen(file, filepath);
}
} else if (FileUtil::Exists(exefsdir_override) && FileUtil::IsDirectory(exefsdir_override)) {
is_tainted = true;
@ -607,12 +636,7 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<RomFSReader>& romf
// We reopen the file, to allow its position to be independent from file's
std::unique_ptr<FileUtil::IOFile> romfs_file_inner;
if (file->IsCrypto()) {
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
} else {
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
}
romfs_file_inner = Reopen(file, filepath);
if (!romfs_file_inner->IsOpen())
return Loader::ResultStatus::Error;
@ -742,4 +766,24 @@ bool NCCHContainer::HasExHeader() {
return has_exheader;
}
std::unique_ptr<FileUtil::IOFile> NCCHContainer::Reopen(
const std::unique_ptr<FileUtil::IOFile>& orig_file, const std::string& new_filename) {
const bool is_compressed = orig_file->IsCompressed();
const bool is_crypto = orig_file->IsCrypto();
const std::string filename = new_filename.empty() ? orig_file->Filename() : new_filename;
std::unique_ptr<FileUtil::IOFile> out_file;
if (is_crypto) {
out_file = HW::UniqueData::OpenUniqueCryptoFile(filename, "rb",
HW::UniqueData::UniqueCryptoFileID::NCCH);
} else {
out_file = std::make_unique<FileUtil::IOFile>(filename, "rb");
}
if (is_compressed) {
out_file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(out_file));
}
return out_file;
}
} // namespace FileSys

View file

@ -91,6 +91,10 @@ struct NCCH_Header {
u8 reserved_4[4];
u8 exefs_super_block_hash[0x20];
u8 romfs_super_block_hash[0x20];
u32 GetContentUnitSize() {
return 0x200u * (1u << content_unit_size);
}
};
static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong");
@ -333,16 +337,28 @@ public:
*/
bool HasExHeader();
bool IsNCSD() {
return is_ncsd;
}
bool IsFileCompressed() {
return file->IsCompressed();
}
NCCH_Header ncch_header;
ExeFs_Header exefs_header;
ExHeader_Header exheader_header;
private:
std::unique_ptr<FileUtil::IOFile> Reopen(const std::unique_ptr<FileUtil::IOFile>& orig_file,
const std::string& new_filename = "");
bool has_header = false;
bool has_exheader = false;
bool has_exefs = false;
bool has_romfs = false;
bool is_ncsd = false;
bool is_proto = false;
bool is_tainted = false; // Are there parts of this container being overridden?
bool is_loaded = false;

View file

@ -176,6 +176,17 @@ u64 TitleMetadata::GetContentSizeByIndex(std::size_t index) const {
return tmd_chunks[index].size;
}
u64 TitleMetadata::GetCombinedContentSize(const CIAHeader* header) const {
u64 total_size = 0;
for (auto& chunk : tmd_chunks) {
if (header && !header->IsContentPresent(static_cast<u16>(chunk.index))) {
continue;
}
total_size += chunk.size;
}
return total_size;
}
bool TitleMetadata::GetContentOptional(std::size_t index) const {
return (static_cast<u16>(tmd_chunks[index].type) & FileSys::TMDContentTypeFlag::Optional) != 0;
}

View file

@ -99,6 +99,7 @@ public:
u32 GetContentIDByIndex(std::size_t index) const;
u16 GetContentTypeByIndex(std::size_t index) const;
u64 GetContentSizeByIndex(std::size_t index) const;
u64 GetCombinedContentSize(const CIAHeader* header) const;
bool GetContentOptional(std::size_t index) const;
std::array<u8, 16> GetContentCTRByIndex(std::size_t index) const;
bool HasEncryptedContent(const CIAHeader* header = nullptr) const;

View file

@ -16,6 +16,7 @@
#include "common/hacks/hack_manager.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/file_sys/certificate.h"
#include "core/file_sys/errors.h"
@ -55,16 +56,6 @@ constexpr u8 VARIATION_SYSTEM = 0x02;
constexpr u32 TID_HIGH_UPDATE = 0x0004000E;
constexpr u32 TID_HIGH_DLC = 0x0004008C;
struct TitleInfo {
u64_le tid;
u64_le size;
u16_le version;
u16_le unused;
u32_le type;
};
static_assert(sizeof(TitleInfo) == 0x18, "Title info structure size is wrong");
constexpr u8 OWNERSHIP_DOWNLOADED = 0x01;
constexpr u8 OWNERSHIP_OWNED = 0x02;
@ -105,6 +96,12 @@ NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_conte
file = std::make_unique<FileUtil::IOFile>(out_file, "wb");
}
if (Settings::values.compress_cia_installs) {
std::array<u8, 4> magic = {'N', 'C', 'C', 'H'};
file = std::make_unique<FileUtil::Z3DSWriteIOFile>(
std::move(file), magic, FileUtil::Z3DSWriteIOFile::DEFAULT_FRAME_SIZE);
}
if (!file->IsOpen()) {
is_error = true;
}
@ -116,6 +113,7 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
if (is_not_ncch) {
file->WriteBytes(buffer, length);
return;
}
const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
@ -1061,8 +1059,16 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorFileNotFound;
}
std::unique_ptr<FileUtil::IOFile> in_file = std::make_unique<FileUtil::IOFile>(path, "rb");
bool is_compressed =
FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(in_file.get()) != std::nullopt;
if (is_compressed) {
in_file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(in_file));
}
FileSys::CIAContainer container;
if (container.Load(path) == Loader::ResultStatus::Success) {
if (container.Load(in_file.get()) == Loader::ResultStatus::Success) {
in_file->Seek(0, SEEK_SET);
Service::AM::CIAFile installFile(
Core::System::GetInstance(),
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
@ -1072,18 +1078,12 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorEncrypted;
}
FileUtil::IOFile file(path, "rb");
if (!file.IsOpen()) {
LOG_ERROR(Service_AM, "Could not open CIA file '{}'.", path);
return InstallStatus::ErrorFailedToOpenFile;
}
std::vector<u8> buffer;
buffer.resize(0x10000);
auto file_size = file.GetSize();
auto file_size = in_file->GetSize();
std::size_t total_bytes_read = 0;
while (total_bytes_read != file_size) {
std::size_t bytes_read = file.ReadBytes(buffer.data(), buffer.size());
std::size_t bytes_read = in_file->ReadBytes(buffer.data(), buffer.size());
auto result = installFile.Write(static_cast<u64>(total_bytes_read), bytes_read, true,
false, static_cast<u8*>(buffer.data()));
@ -1128,6 +1128,82 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorInvalid;
}
InstallStatus CheckCIAToInstall(const std::string& path, bool& is_compressed,
bool check_encryption) {
if (!FileUtil::Exists(path)) {
LOG_ERROR(Service_AM, "File {} does not exist!", path);
return InstallStatus::ErrorFileNotFound;
}
std::unique_ptr<FileUtil::IOFile> in_file = std::make_unique<FileUtil::IOFile>(path, "rb");
is_compressed = FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(in_file.get()) != std::nullopt;
if (is_compressed) {
in_file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(in_file));
}
FileSys::CIAContainer container;
if (container.Load(in_file.get()) == Loader::ResultStatus::Success) {
in_file->Seek(0, SEEK_SET);
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
if (check_encryption) {
if (tmd.HasEncryptedContent(container.GetHeader())) {
return InstallStatus::ErrorEncrypted;
}
for (size_t i = 0; i < tmd.GetContentCount(); i++) {
u64 offset = container.GetContentOffset(i);
NCCH_Header ncch;
const auto read = in_file->ReadAtBytes(&ncch, sizeof(ncch), offset);
if (read != sizeof(ncch)) {
return InstallStatus::ErrorInvalid;
}
if (ncch.magic != Loader::MakeMagic('N', 'C', 'C', 'H')) {
return InstallStatus::ErrorInvalid;
}
if (!ncch.no_crypto) {
return InstallStatus::ErrorEncrypted;
}
}
}
return InstallStatus::Success;
}
return InstallStatus::ErrorInvalid;
}
ResultVal<std::pair<TitleInfo, std::unique_ptr<Loader::SMDH>>> GetCIAInfos(
const std::string& path) {
if (!FileUtil::Exists(path)) {
LOG_ERROR(Service_AM, "File {} does not exist!", path);
return ResultUnknown;
}
std::unique_ptr<FileUtil::IOFile> in_file = std::make_unique<FileUtil::IOFile>(path, "rb");
FileSys::CIAContainer container;
if (container.Load(in_file.get()) == Loader::ResultStatus::Success) {
in_file->Seek(0, SEEK_SET);
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
TitleInfo info{};
info.tid = tmd.GetTitleID();
info.version = tmd.GetTitleVersion();
info.size = tmd.GetCombinedContentSize(container.GetHeader());
info.type = tmd.GetTitleType();
const auto& cia_smdh = container.GetSMDH();
std::unique_ptr<Loader::SMDH> smdh{};
if (cia_smdh) {
smdh = std::make_unique<Loader::SMDH>(*cia_smdh);
}
return std::pair<TitleInfo, std::unique_ptr<Loader::SMDH>>(info, std::move(smdh));
}
return ResultUnknown;
}
u64 GetTitleUpdateId(u64 title_id) {
// Real services seem to just discard and replace the whole high word.
return (title_id & 0xFFFFFFFF) | (static_cast<u64>(TID_HIGH_UPDATE) << 32);

View file

@ -105,6 +105,15 @@ struct ImportContentContext {
};
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
struct TitleInfo {
u64_le tid;
u64_le size;
u16_le version;
u16_le unused;
u32_le type;
};
static_assert(sizeof(TitleInfo) == 0x18, "Title info structure size is wrong");
// Title ID valid length
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
@ -359,6 +368,19 @@ private:
InstallStatus InstallCIA(const std::string& path,
std::function<ProgressCallback>&& update_callback = nullptr);
/**
* Checks if the provided path is a valid CIA file
* that can be installed.
* @param path file path of the CIA file to check to install
*/
InstallStatus CheckCIAToInstall(const std::string& path, bool& is_compressed,
bool check_encryption);
/**
* Get CIA metadata information from file.
*/
ResultVal<std::pair<TitleInfo, std::unique_ptr<Loader::SMDH>>> GetCIAInfos(const std::string& path);
/**
* Get the update title ID for a title
* @param titleId the title ID

View file

@ -5,6 +5,7 @@
#include <algorithm>
#include <vector>
#include "common/logging/log.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/hle/kernel/process.h"
#include "core/hle/kernel/resource_limit.h"
@ -94,16 +95,16 @@ static u32 TranslateAddr(u32 addr, const THREEloadinfo* loadinfo, u32* offsets)
using Kernel::CodeSet;
static THREEDSX_Error Load3DSXFile(Core::System& system, FileUtil::IOFile& file, u32 base_addr,
static THREEDSX_Error Load3DSXFile(Core::System& system, FileUtil::IOFile* file, u32 base_addr,
std::shared_ptr<CodeSet>* out_codeset) {
if (!file.IsOpen())
if (!file->IsOpen())
return ERROR_FILE;
// Reset read pointer in case this file has been read before.
file.Seek(0, SEEK_SET);
file->Seek(0, SEEK_SET);
THREEDSX_Header hdr;
if (file.ReadBytes(&hdr, sizeof(hdr)) != sizeof(hdr))
if (file->ReadBytes(&hdr, sizeof(hdr)) != sizeof(hdr))
return ERROR_READ;
THREEloadinfo loadinfo;
@ -129,22 +130,22 @@ static THREEDSX_Error Load3DSXFile(Core::System& system, FileUtil::IOFile& file,
loadinfo.seg_ptrs[2] = loadinfo.seg_ptrs[1] + loadinfo.seg_sizes[1];
// Skip header for future compatibility
file.Seek(hdr.header_size, SEEK_SET);
file->Seek(hdr.header_size, SEEK_SET);
// Read the relocation headers
std::vector<u32> relocs(n_reloc_tables * NUM_SEGMENTS);
for (unsigned int current_segment = 0; current_segment < NUM_SEGMENTS; ++current_segment) {
std::size_t size = n_reloc_tables * sizeof(u32);
if (file.ReadBytes(&relocs[current_segment * n_reloc_tables], size) != size)
if (file->ReadBytes(&relocs[current_segment * n_reloc_tables], size) != size)
return ERROR_READ;
}
// Read the segments
if (file.ReadBytes(loadinfo.seg_ptrs[0], hdr.code_seg_size) != hdr.code_seg_size)
if (file->ReadBytes(loadinfo.seg_ptrs[0], hdr.code_seg_size) != hdr.code_seg_size)
return ERROR_READ;
if (file.ReadBytes(loadinfo.seg_ptrs[1], hdr.rodata_seg_size) != hdr.rodata_seg_size)
if (file->ReadBytes(loadinfo.seg_ptrs[1], hdr.rodata_seg_size) != hdr.rodata_seg_size)
return ERROR_READ;
if (file.ReadBytes(loadinfo.seg_ptrs[2], hdr.data_seg_size - hdr.bss_size) !=
if (file->ReadBytes(loadinfo.seg_ptrs[2], hdr.data_seg_size - hdr.bss_size) !=
hdr.data_seg_size - hdr.bss_size)
return ERROR_READ;
@ -158,7 +159,7 @@ static THREEDSX_Error Load3DSXFile(Core::System& system, FileUtil::IOFile& file,
u32 n_relocs = relocs[current_segment * n_reloc_tables + current_segment_reloc_table];
if (current_segment_reloc_table >= 2) {
// We are not using this table - ignore it because we don't know what it dose
file.Seek(n_relocs * sizeof(THREEDSX_Reloc), SEEK_CUR);
file->Seek(n_relocs * sizeof(THREEDSX_Reloc), SEEK_CUR);
continue;
}
THREEDSX_Reloc reloc_table[RELOCBUFSIZE];
@ -170,7 +171,7 @@ static THREEDSX_Error Load3DSXFile(Core::System& system, FileUtil::IOFile& file,
u32 remaining = std::min(RELOCBUFSIZE, n_relocs);
n_relocs -= remaining;
if (file.ReadBytes(reloc_table, remaining * sizeof(THREEDSX_Reloc)) !=
if (file->ReadBytes(reloc_table, remaining * sizeof(THREEDSX_Reloc)) !=
remaining * sizeof(THREEDSX_Reloc))
return ERROR_READ;
@ -248,13 +249,15 @@ static THREEDSX_Error Load3DSXFile(Core::System& system, FileUtil::IOFile& file,
return ERROR_NONE;
}
FileType AppLoader_THREEDSX::IdentifyType(FileUtil::IOFile& file) {
FileType AppLoader_THREEDSX::IdentifyType(FileUtil::IOFile* file) {
u32 magic;
file.Seek(0, SEEK_SET);
if (1 != file.ReadArray<u32>(&magic, 1))
file->Seek(0, SEEK_SET);
if (1 != file->ReadArray<u32>(&magic, 1))
return FileType::Error;
if (MakeMagic('3', 'D', 'S', 'X') == magic)
if (MakeMagic('3', 'D', 'S', 'X') == magic ||
(MakeMagic('Z', '3', 'D', 'S') == magic &&
FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file) == MakeMagic('3', 'D', 'S', 'X')))
return FileType::THREEDSX;
return FileType::Error;
@ -264,11 +267,15 @@ ResultStatus AppLoader_THREEDSX::Load(std::shared_ptr<Kernel::Process>& process)
if (is_loaded)
return ResultStatus::ErrorAlreadyLoaded;
if (!file.IsOpen())
if (!file->IsOpen())
return ResultStatus::Error;
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) != std::nullopt) {
file = std::make_unique<FileUtil::Z3DSReadIOFile>(std::move(file));
}
std::shared_ptr<CodeSet> codeset;
if (Load3DSXFile(system, file, Memory::PROCESS_IMAGE_VADDR, &codeset) != ERROR_NONE)
if (Load3DSXFile(system, file.get(), Memory::PROCESS_IMAGE_VADDR, &codeset) != ERROR_NONE)
return ResultStatus::Error;
codeset->name = filename;
@ -292,14 +299,14 @@ ResultStatus AppLoader_THREEDSX::Load(std::shared_ptr<Kernel::Process>& process)
}
ResultStatus AppLoader_THREEDSX::ReadRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) {
if (!file.IsOpen())
if (!file->IsOpen())
return ResultStatus::Error;
// Reset read pointer in case this file has been read before.
file.Seek(0, SEEK_SET);
file->Seek(0, SEEK_SET);
THREEDSX_Header hdr;
if (file.ReadBytes(&hdr, sizeof(THREEDSX_Header)) != sizeof(THREEDSX_Header))
if (file->ReadBytes(&hdr, sizeof(THREEDSX_Header)) != sizeof(THREEDSX_Header))
return ResultStatus::Error;
if (hdr.header_size != sizeof(THREEDSX_Header))
@ -308,7 +315,7 @@ ResultStatus AppLoader_THREEDSX::ReadRomFS(std::shared_ptr<FileSys::RomFSReader>
// Check if the 3DSX has a RomFS...
if (hdr.fs_offset != 0) {
u32 romfs_offset = hdr.fs_offset;
u32 romfs_size = static_cast<u32>(file.GetSize()) - hdr.fs_offset;
u32 romfs_size = static_cast<u32>(file->GetSize()) - hdr.fs_offset;
LOG_DEBUG(Loader, "RomFS offset: {:#010X}", romfs_offset);
LOG_DEBUG(Loader, "RomFS size: {:#010X}", romfs_size);
@ -328,15 +335,26 @@ ResultStatus AppLoader_THREEDSX::ReadRomFS(std::shared_ptr<FileSys::RomFSReader>
return ResultStatus::ErrorNotUsed;
}
AppLoader::CompressFileInfo AppLoader_THREEDSX::GetCompressFileInfo() {
CompressFileInfo info;
info.is_supported = true;
info.recommended_compressed_extension = "z3dsx";
info.recommended_uncompressed_extension = "3dsx";
info.underlying_magic = std::array<u8, 4>({'3', 'D', 'S', 'X'});
info.is_compressed =
FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) != std::nullopt;
return info;
}
ResultStatus AppLoader_THREEDSX::ReadIcon(std::vector<u8>& buffer) {
if (!file.IsOpen())
if (!file->IsOpen())
return ResultStatus::Error;
// Reset read pointer in case this file has been read before.
file.Seek(0, SEEK_SET);
file->Seek(0, SEEK_SET);
THREEDSX_Header hdr;
if (file.ReadBytes(&hdr, sizeof(THREEDSX_Header)) != sizeof(THREEDSX_Header))
if (file->ReadBytes(&hdr, sizeof(THREEDSX_Header)) != sizeof(THREEDSX_Header))
return ResultStatus::Error;
if (hdr.header_size != sizeof(THREEDSX_Header))
@ -344,10 +362,10 @@ ResultStatus AppLoader_THREEDSX::ReadIcon(std::vector<u8>& buffer) {
// Check if the 3DSX has a SMDH...
if (hdr.smdh_offset != 0) {
file.Seek(hdr.smdh_offset, SEEK_SET);
file->Seek(hdr.smdh_offset, SEEK_SET);
buffer.resize(hdr.smdh_size);
if (file.ReadBytes(buffer.data(), hdr.smdh_size) != hdr.smdh_size)
if (file->ReadBytes(buffer.data(), hdr.smdh_size) != hdr.smdh_size)
return ResultStatus::Error;
return ResultStatus::Success;

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2014 Dolphin Emulator Project / Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -23,10 +27,10 @@ public:
* @param file FileUtil::IOFile open file
* @return FileType found, or FileType::Error if this loader doesn't know it
*/
static FileType IdentifyType(FileUtil::IOFile& file);
static FileType IdentifyType(FileUtil::IOFile* file);
FileType GetFileType() override {
return IdentifyType(file);
return IdentifyType(file.get());
}
ResultStatus Load(std::shared_ptr<Kernel::Process>& process) override;
@ -35,6 +39,8 @@ public:
ResultStatus ReadRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) override;
CompressFileInfo GetCompressFileInfo() override;
private:
std::string filename;
std::string filepath;

View file

@ -80,7 +80,7 @@ Apploader_Artic::~Apploader_Artic() {
client->Stop();
}
FileType Apploader_Artic::IdentifyType(FileUtil::IOFile& file) {
FileType Apploader_Artic::IdentifyType(FileUtil::IOFile* file) {
return FileType::ARTIC;
}

View file

@ -33,10 +33,10 @@ public:
* @param file FileUtil::IOFile open file
* @return FileType found, or FileType::Error if this loader doesn't know it
*/
static FileType IdentifyType(FileUtil::IOFile& file);
static FileType IdentifyType(FileUtil::IOFile* file);
FileType GetFileType() override {
return IdentifyType(file);
return IdentifyType(file.get());
}
[[nodiscard]] std::span<const u32> GetPreferredRegions() const override {

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -352,10 +356,10 @@ SectionID ElfReader::GetSectionByName(const char* name, int firstSection) const
namespace Loader {
FileType AppLoader_ELF::IdentifyType(FileUtil::IOFile& file) {
FileType AppLoader_ELF::IdentifyType(FileUtil::IOFile* file) {
u32 magic;
file.Seek(0, SEEK_SET);
if (1 != file.ReadArray<u32>(&magic, 1))
file->Seek(0, SEEK_SET);
if (1 != file->ReadArray<u32>(&magic, 1))
return FileType::Error;
if (MakeMagic('\x7f', 'E', 'L', 'F') == magic)
@ -368,15 +372,15 @@ ResultStatus AppLoader_ELF::Load(std::shared_ptr<Kernel::Process>& process) {
if (is_loaded)
return ResultStatus::ErrorAlreadyLoaded;
if (!file.IsOpen())
if (!file->IsOpen())
return ResultStatus::Error;
// Reset read pointer in case this file has been read before.
file.Seek(0, SEEK_SET);
file->Seek(0, SEEK_SET);
std::size_t size = file.GetSize();
std::size_t size = file->GetSize();
std::unique_ptr<u8[]> buffer(new u8[size]);
if (file.ReadBytes(&buffer[0], size) != size)
if (file->ReadBytes(&buffer[0], size) != size)
return ResultStatus::Error;
ElfReader elf_reader(&buffer[0]);

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -22,10 +26,10 @@ public:
* @param file FileUtil::IOFile open file
* @return FileType found, or FileType::Error if this loader doesn't know it
*/
static FileType IdentifyType(FileUtil::IOFile& file);
static FileType IdentifyType(FileUtil::IOFile* file);
FileType GetFileType() override {
return IdentifyType(file);
return IdentifyType(file.get());
}
ResultStatus Load(std::shared_ptr<Kernel::Process>& process) override;

View file

@ -19,7 +19,7 @@ FileType IdentifyFile(FileUtil::IOFile& file) {
FileType type;
#define CHECK_TYPE(loader) \
type = AppLoader_##loader::IdentifyType(file); \
type = AppLoader_##loader::IdentifyType(&file); \
if (FileType::Error != type) \
return type;
@ -48,16 +48,16 @@ FileType GuessFromExtension(const std::string& extension_) {
if (extension == ".elf" || extension == ".axf")
return FileType::ELF;
if (extension == ".cci")
if (extension == ".cci" || extension == ".zcci")
return FileType::CCI;
if (extension == ".cxi" || extension == ".app")
if (extension == ".cxi" || extension == ".app" || extension == ".zcxi")
return FileType::CXI;
if (extension == ".3dsx")
if (extension == ".3dsx" || extension == ".z3dsx")
return FileType::THREEDSX;
if (extension == ".cia")
if (extension == ".cia" || extension == ".zcia")
return FileType::CIA;
return FileType::Unknown;

View file

@ -85,8 +85,17 @@ constexpr u32 MakeMagic(char a, char b, char c, char d) {
/// Interface for loading an application
class AppLoader : NonCopyable {
public:
struct CompressFileInfo {
bool is_supported{};
bool is_compressed{};
std::array<u8, 4> underlying_magic{};
std::string recommended_compressed_extension;
std::string recommended_uncompressed_extension;
std::unordered_map<std::string, std::vector<u8>> default_metadata;
};
explicit AppLoader(Core::System& system_, FileUtil::IOFile&& file)
: system(system_), file(std::move(file)) {}
: system(system_), file(std::make_unique<FileUtil::IOFile>(std::move(file))) {}
virtual ~AppLoader() {}
/**
@ -279,9 +288,15 @@ public:
return false;
}
virtual CompressFileInfo GetCompressFileInfo() {
CompressFileInfo info{};
info.is_supported = false;
return info;
}
protected:
Core::System& system;
FileUtil::IOFile file;
std::unique_ptr<FileUtil::IOFile> file;
bool is_loaded = false;
std::optional<Kernel::MemoryMode> memory_mode_override = std::nullopt;
};

View file

@ -12,6 +12,7 @@
#include "common/settings.h"
#include "common/string_util.h"
#include "common/swap.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/file_sys/ncch_container.h"
#include "core/file_sys/title_metadata.h"
@ -34,10 +35,10 @@ namespace Loader {
using namespace Common::Literals;
static constexpr u64 UPDATE_TID_HIGH = 0x0004000e00000000;
FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile& file) {
FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) {
u32 magic;
file.Seek(0x100, SEEK_SET);
if (1 != file.ReadArray<u32>(&magic, 1))
file->Seek(0x100, SEEK_SET);
if (1 != file->ReadArray<u32>(&magic, 1))
return FileType::Error;
if (MakeMagic('N', 'C', 'S', 'D') == magic)
@ -47,7 +48,7 @@ FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile& file) {
return FileType::CXI;
std::unique_ptr<FileUtil::IOFile> file_crypto = HW::UniqueData::OpenUniqueCryptoFile(
file.Filename(), "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
file->Filename(), "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
file_crypto->Seek(0x100, SEEK_SET);
if (1 != file_crypto->ReadArray<u32>(&magic, 1))
@ -59,6 +60,16 @@ FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile& file) {
if (MakeMagic('N', 'C', 'C', 'H') == magic)
return FileType::CXI;
std::optional<u32> magic_zstd;
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file) != std::nullopt ||
FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file_crypto.get()) != std::nullopt) {
if (MakeMagic('N', 'C', 'S', 'D') == magic_zstd)
return FileType::CCI;
if (MakeMagic('N', 'C', 'C', 'H') == magic_zstd)
return FileType::CXI;
}
return FileType::Error;
}
@ -396,4 +407,34 @@ ResultStatus AppLoader_NCCH::ReadTitle(std::string& title) {
return ResultStatus::Success;
}
AppLoader::CompressFileInfo AppLoader_NCCH::GetCompressFileInfo() {
CompressFileInfo info{};
if (base_ncch.LoadHeader() != ResultStatus::Success) {
info.is_supported = false;
return info;
}
info.is_supported = true;
info.is_compressed = base_ncch.IsFileCompressed();
if (base_ncch.IsNCSD()) {
info.underlying_magic = std::array<u8, 4>({'N', 'C', 'S', 'D'});
info.recommended_compressed_extension = "zcci";
info.recommended_uncompressed_extension = "cci";
} else {
info.underlying_magic = std::array<u8, 4>({'N', 'C', 'C', 'H'});
info.recommended_compressed_extension = "zcxi";
info.recommended_uncompressed_extension = "cxi";
}
std::vector<u8> title_info_vec(sizeof(Service::AM::TitleInfo));
Service::AM::TitleInfo* title_info =
reinterpret_cast<Service::AM::TitleInfo*>(title_info_vec.data());
title_info->tid = base_ncch.ncch_header.program_id;
title_info->version = base_ncch.ncch_header.version;
title_info->size =
base_ncch.ncch_header.content_size * base_ncch.ncch_header.GetContentUnitSize();
title_info->unused = title_info->type = 0;
info.default_metadata.emplace("titleinfo", title_info_vec);
return info;
}
} // namespace Loader

View file

@ -1,4 +1,4 @@
// Copyright 2014 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -17,17 +17,20 @@ class AppLoader_NCCH final : public AppLoader {
public:
AppLoader_NCCH(Core::System& system_, FileUtil::IOFile&& file, const std::string& filepath)
: AppLoader(system_, std::move(file)), base_ncch(filepath), overlay_ncch(&base_ncch),
filepath(filepath) {}
filepath(filepath) {
filetype = IdentifyType(this->file.get());
this->file.reset();
}
/**
* Returns the type of the file
* @param file FileUtil::IOFile open file
* @return FileType found, or FileType::Error if this loader doesn't know it
*/
static FileType IdentifyType(FileUtil::IOFile& file);
static FileType IdentifyType(FileUtil::IOFile* file);
FileType GetFileType() override {
return IdentifyType(file);
return filetype;
}
[[nodiscard]] std::span<const u32> GetPreferredRegions() const override {
@ -71,6 +74,8 @@ public:
ResultStatus ReadTitle(std::string& title) override;
CompressFileInfo GetCompressFileInfo() override;
private:
/**
* Loads .code section into memory for booting
@ -94,6 +99,7 @@ private:
std::vector<u32> preferred_regions;
std::string filepath;
FileType filetype;
};
} // namespace Loader

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -22,6 +22,10 @@ bool IsValidSMDH(std::span<const u8> smdh_data) {
return Loader::MakeMagic('S', 'M', 'D', 'H') == magic;
}
bool SMDH::IsValid() const {
return Loader::MakeMagic('S', 'M', 'D', 'H') == magic;
}
std::vector<u16> SMDH::GetIcon(bool large) const {
u32 size;
const u8* icon_data;

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -77,6 +77,11 @@ struct SMDH {
Visible = 1 << 0,
};
/**
* Checks if SMDH is valid.
*/
bool IsValid() const;
/**
* Gets game icon from SMDH
* @param large If true, returns large icon (48x48), otherwise returns small icon (24x24)

View file

@ -61,22 +61,23 @@ constexpr static std::array<vk::DescriptorSetLayoutBinding, 1> PRESENT_BINDINGS
namespace {
static bool IsLowRefreshRate() {
#ifdef ENABLE_SDL2
const auto sdl_init_status = SDL_Init(SDL_INIT_VIDEO);
if (sdl_init_status < 0) {
LOG_ERROR(Render_Vulkan, "SDL failed to initialize, unable to check refresh rate");
} else {
SDL_DisplayMode cur_display_mode;
SDL_GetCurrentDisplayMode(0, &cur_display_mode); // TODO: Multimonitor handling. -OS
const auto cur_refresh_rate = cur_display_mode.refresh_rate;
SDL_Quit();
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
LOG_ERROR(Render_Vulkan, "SDL video failed to initialize, unable to check refresh rate");
return false;
}
if (cur_refresh_rate < SCREEN_REFRESH_RATE) {
LOG_WARNING(Render_Vulkan,
"Detected refresh rate lower than the emulated 3DS screen: {}hz. FIFO will "
"be disabled",
cur_refresh_rate);
return true;
}
SDL_DisplayMode cur_display_mode;
SDL_GetCurrentDisplayMode(0, &cur_display_mode); // TODO: Multimonitor handling. -OS
const auto cur_refresh_rate = cur_display_mode.refresh_rate;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (cur_refresh_rate < SCREEN_REFRESH_RATE) {
LOG_WARNING(Render_Vulkan,
"Detected refresh rate lower than the emulated 3DS screen: {}hz. FIFO will "
"be disabled",
cur_refresh_rate);
return true;
}
#endif