diff --git a/src/citron/configuration/configure_filesystem.cpp b/src/citron/configuration/configure_filesystem.cpp
index bab26c910..13634f328 100644
--- a/src/citron/configuration/configure_filesystem.cpp
+++ b/src/citron/configuration/configure_filesystem.cpp
@@ -71,6 +71,7 @@ void ConfigureFilesystem::SetConfiguration() {
ui->dump_nso->setChecked(Settings::values.dump_nso.GetValue());
ui->cache_game_list->setChecked(UISettings::values.cache_game_list.GetValue());
ui->prompt_for_autoloader->setChecked(UISettings::values.prompt_for_autoloader.GetValue());
+ ui->backup_saves_to_nand->setChecked(Settings::values.backup_saves_to_nand.GetValue());
// NCA Scanning Toggle
ui->scan_nca->setChecked(UISettings::values.scan_nca.GetValue());
@@ -102,6 +103,7 @@ void ConfigureFilesystem::ApplyConfiguration() {
Settings::values.dump_nso = ui->dump_nso->isChecked();
UISettings::values.cache_game_list = ui->cache_game_list->isChecked();
UISettings::values.prompt_for_autoloader = ui->prompt_for_autoloader->isChecked();
+ Settings::values.backup_saves_to_nand.SetValue(ui->backup_saves_to_nand->isChecked());
// NCA Scanning Toggle
UISettings::values.scan_nca = ui->scan_nca->isChecked();
diff --git a/src/citron/configuration/configure_filesystem.ui b/src/citron/configuration/configure_filesystem.ui
index 18d085c9a..be236c794 100644
--- a/src/citron/configuration/configure_filesystem.ui
+++ b/src/citron/configuration/configure_filesystem.ui
@@ -87,6 +87,7 @@
- Cache Game List Metadata
- Reset Metadata Cache
- Scan for .nca files (Advanced: Significantly slows down scanning)
+ - Allow Backup Saves to NAND if using Custom Save Path
diff --git a/src/common/settings.h b/src/common/settings.h
index 567ee6b3e..cb7a1df37 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -684,8 +684,9 @@ struct Values {
// Key: build_id (hex string), Value: set of disabled cheat names
std::map> disabled_cheats;
- // Custom Save Paths
+ // Custom Save Paths (with backups)
std::map custom_save_paths;
+ Setting backup_saves_to_nand{linkage, false, "backup_saves_to_nand", Category::DataStorage};
};
extern Values values;
diff --git a/src/core/file_sys/directory_save_data_filesystem.cpp b/src/core/file_sys/directory_save_data_filesystem.cpp
index 402025d56..cdd7ce72e 100644
--- a/src/core/file_sys/directory_save_data_filesystem.cpp
+++ b/src/core/file_sys/directory_save_data_filesystem.cpp
@@ -4,6 +4,7 @@
#include
#include
#include "common/logging/log.h"
+#include "common/settings.h"
#include "core/file_sys/errors.h"
#include "core/file_sys/directory_save_data_filesystem.h"
@@ -16,8 +17,9 @@ constexpr int RetryWaitTimeMs = 100;
} // Anonymous namespace
-DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem)
- : base_fs(std::move(base_filesystem)), extra_data_accessor(base_fs), journaling_enabled(true),
+DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem)
+ : base_fs(std::move(base_filesystem)), backup_fs(std::move(backup_filesystem)),
+ extra_data_accessor(base_fs), journaling_enabled(true),
open_writable_files(0) {}
DirectorySaveDataFileSystem::~DirectorySaveDataFileSystem() = default;
@@ -126,6 +128,25 @@ Result DirectorySaveDataFileSystem::Commit() {
// Update cached committed_dir reference
committed_dir = base_fs->GetSubdirectory(CommittedDirectoryName);
+ if (Settings::values.backup_saves_to_nand.GetValue() && backup_fs != nullptr) {
+ LOG_INFO(Service_FS, "Dual-Save: Backing up custom save to NAND...");
+
+ // 1. Find or Create the '0' (Committed) folder in the NAND
+ auto nand_committed = backup_fs->GetSubdirectory(CommittedDirectoryName);
+ if (nand_committed == nullptr) {
+ nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName);
+ }
+
+ if (nand_committed != nullptr) {
+ // 2. Wipe whatever old backup was there
+ backup_fs->DeleteSubdirectoryRecursive(CommittedDirectoryName);
+ nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName);
+
+ // 3. Copy the fresh data from our 'working' area to the NAND '0' folder
+ CopyDirectoryRecursively(nand_committed, working_dir);
+ }
+ }
+
LOG_INFO(Service_FS, "Save data committed successfully");
return ResultSuccess;
}
diff --git a/src/core/file_sys/directory_save_data_filesystem.h b/src/core/file_sys/directory_save_data_filesystem.h
index eedcddc6e..954841cdd 100644
--- a/src/core/file_sys/directory_save_data_filesystem.h
+++ b/src/core/file_sys/directory_save_data_filesystem.h
@@ -18,7 +18,8 @@ namespace FileSys {
/// Uses /0 (committed) and /1 (working) directories for journaling
class DirectorySaveDataFileSystem {
public:
- explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem);
+ // optional directory here for backup
+ explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem = nullptr);
~DirectorySaveDataFileSystem();
/// Initialize the journaling filesystem
@@ -54,6 +55,7 @@ private:
Result RetryFinitelyForTargetLocked(std::function operation);
VirtualDir base_fs;
+ VirtualDir backup_fs; // This will store the NAND path
VirtualDir working_dir;
VirtualDir committed_dir;
SaveDataExtraDataAccessor extra_data_accessor;
diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp
index b14b52c28..3ca2e0a6d 100644
--- a/src/core/file_sys/savedata_factory.cpp
+++ b/src/core/file_sys/savedata_factory.cpp
@@ -7,8 +7,10 @@
#include "common/assert.h"
#include "common/common_types.h"
#include "common/logging/log.h"
+#include "common/settings.h"
#include "common/uuid.h"
#include "core/core.h"
+#include "core/file_sys/directory_save_data_filesystem.h"
#include "core/file_sys/errors.h"
#include "core/file_sys/savedata_extra_data_accessor.h"
#include "core/file_sys/savedata_factory.h"
@@ -58,8 +60,9 @@ std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u
} // Anonymous namespace
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
- VirtualDir save_directory_)
- : system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
+ VirtualDir save_directory_, VirtualDir backup_directory_)
+ : system{system_}, program_id{program_id_}, dir{std::move(save_directory_)},
+ backup_dir{std::move(backup_directory_)} {
// Delete all temporary storages
// On hardware, it is expected that temporary storage be empty at first use.
dir->DeleteSubdirectoryRecursive("temp");
@@ -100,7 +103,6 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut
}
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
-
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
meta.user_id, meta.system_save_data_id);
@@ -324,4 +326,41 @@ Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData&
return ResultSuccess;
}
+void SaveDataFactory::DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const {
+ LOG_INFO(Common, "Dual-Save: Backup process initiated for Program ID: {:016X}", program_id);
+
+ if (!Settings::values.backup_saves_to_nand.GetValue()) {
+ LOG_INFO(Common, "Dual-Save: Backup skipped (Setting is OFF)");
+ return;
+ }
+
+ if (backup_dir == nullptr) {
+ LOG_ERROR(Common, "Dual-Save: Backup failed (NAND directory is NULL)");
+ return;
+ }
+
+ if (custom_dir == nullptr) {
+ LOG_ERROR(Common, "Dual-Save: Backup failed (Source Custom directory is NULL)");
+ return;
+ }
+
+ const auto nand_path = GetFullPath(program_id, backup_dir, space, meta.type, meta.program_id,
+ meta.user_id, meta.system_save_data_id);
+
+ auto nand_out = backup_dir->CreateDirectoryRelative(nand_path);
+ if (nand_out != nullptr) {
+ LOG_INFO(Common, "Dual-Save: Mirroring files to NAND: {}", nand_path);
+
+ // Clear the old backup
+ nand_out->CleanSubdirectoryRecursive(".");
+
+ // Perform the copy
+ VfsRawCopyD(custom_dir, nand_out);
+
+ LOG_INFO(Common, "Dual-Save: NAND Backup successful.");
+ } else {
+ LOG_ERROR(Common, "Dual-Save: Could not create/access NAND backup path!");
+ }
+}
+
} // namespace FileSys
diff --git a/src/core/file_sys/savedata_factory.h b/src/core/file_sys/savedata_factory.h
index 33a713359..54250515d 100644
--- a/src/core/file_sys/savedata_factory.h
+++ b/src/core/file_sys/savedata_factory.h
@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -27,7 +28,7 @@ using ProgramId = u64;
class SaveDataFactory {
public:
explicit SaveDataFactory(Core::System& system_, ProgramId program_id_,
- VirtualDir save_directory_);
+ VirtualDir save_directory_, VirtualDir backup_directory_ = nullptr);
~SaveDataFactory();
VirtualDir Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const;
@@ -54,11 +55,13 @@ public:
const SaveDataAttribute& attribute) const;
void SetAutoCreate(bool state);
+ void DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const;
private:
Core::System& system;
ProgramId program_id;
VirtualDir dir;
+ VirtualDir backup_dir; // This will hold the NAND path
bool auto_create{true};
};
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp
index 1a3637fda..edb7d6ede 100644
--- a/src/core/hle/service/filesystem/filesystem.cpp
+++ b/src/core/hle/service/filesystem/filesystem.cpp
@@ -431,7 +431,12 @@ std::shared_ptr FileSystemController::CreateSaveDataFa
if (!custom_path_str.empty() && Common::FS::IsDir(custom_path)) {
LOG_INFO(Service_FS, "Using custom save path for program_id={:016X}: {}", program_id, custom_path_str);
auto custom_save_directory = vfs->OpenDirectory(custom_path_str, rw_mode);
- return std::make_shared(system, program_id, std::move(custom_save_directory));
+
+ // Fetch the default NAND directory to act as the backup location
+ auto nand_directory = vfs->OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode);
+
+ return std::make_shared(
+ system, program_id, std::move(custom_save_directory), std::move(nand_directory));
}
}
diff --git a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp
index b4cd22d60..c21285a96 100644
--- a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp
+++ b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp
@@ -1,7 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/string_util.h"
+#include "common/settings.h"
#include "core/file_sys/fssrv/fssrv_sf_path.h"
#include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/filesystem/fsp/fs_i_directory.h"
@@ -10,10 +12,16 @@
namespace Service::FileSystem {
-IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_)
- : ServiceFramework{system_, "IFileSystem"}, backend{std::make_unique(
- dir_)},
- size_getter{std::move(size_getter_)} {
+IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_,
+ std::shared_ptr factory_,
+ FileSys::SaveDataSpaceId space_id_, FileSys::SaveDataAttribute attribute_)
+ : ServiceFramework{system_, "IFileSystem"},
+ backend{std::make_unique(dir_)},
+ size_getter{std::move(size_getter_)},
+ content_dir{std::move(dir_)},
+ save_factory{std::move(factory_)},
+ save_space{space_id_},
+ save_attr{attribute_} {
static const FunctionInfo functions[] = {
{0, D<&IFileSystem::CreateFile>, "CreateFile"},
{1, D<&IFileSystem::DeleteFile>, "DeleteFile"},
@@ -124,11 +132,15 @@ Result IFileSystem::GetEntryType(
}
Result IFileSystem::Commit() {
- LOG_DEBUG(Service_FS, "called");
+ Result res = backend->Commit();
+ if (res != ResultSuccess) return res;
- // Based on LibHac DirectorySaveDataFileSystem::DoCommit
- // The backend FSA layer should handle the actual commit logic
- R_RETURN(backend->Commit());
+ if (save_factory && Settings::values.backup_saves_to_nand.GetValue()) {
+ LOG_INFO(Common, "IFileSystem: Commit detected, triggering NAND backup...");
+ save_factory->DoNandBackup(save_space, save_attr, content_dir);
+ }
+
+ R_RETURN(res);
}
Result IFileSystem::GetFreeSpaceSize(
diff --git a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h
index dd069f36f..ee9588581 100644
--- a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h
+++ b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h
@@ -1,10 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_funcs.h"
#include "core/file_sys/fs_filesystem.h"
+#include "core/file_sys/fs_save_data_types.h"
#include "core/file_sys/fsa/fs_i_filesystem.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/hle/service/cmif_types.h"
@@ -23,7 +25,10 @@ class IDirectory;
class IFileSystem final : public ServiceFramework {
public:
- explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_);
+ explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_,
+ std::shared_ptr factory_ = nullptr,
+ FileSys::SaveDataSpaceId space_id_ = {},
+ FileSys::SaveDataAttribute attribute_ = {});
Result CreateFile(const InLargeData path, s32 option,
s64 size);
@@ -55,6 +60,10 @@ public:
private:
std::unique_ptr backend;
SizeGetter size_getter;
+ FileSys::VirtualDir content_dir;
+ std::shared_ptr save_factory;
+ FileSys::SaveDataSpaceId save_space;
+ FileSys::SaveDataAttribute save_attr;
};
} // namespace Service::FileSystem
diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp
index 70cd31926..32c79af63 100644
--- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp
+++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp
@@ -280,8 +280,9 @@ Result FSP_SRV::OpenSaveDataFileSystem(OutInterface out_interface,
break;
}
- *out_interface =
- std::make_shared(system, std::move(dir), SizeGetter::FromStorageId(fsc, id));
+ *out_interface = std::make_shared(
+ system, std::move(dir), SizeGetter::FromStorageId(fsc, id),
+ save_data_controller->GetFactory(), space_id, attribute);
R_SUCCEED();
}
diff --git a/src/core/hle/service/filesystem/save_data_controller.h b/src/core/hle/service/filesystem/save_data_controller.h
index 2117f9adb..61be15465 100644
--- a/src/core/hle/service/filesystem/save_data_controller.h
+++ b/src/core/hle/service/filesystem/save_data_controller.h
@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
+// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -38,6 +39,10 @@ public:
FileSys::SaveDataSpaceId space,
const FileSys::SaveDataAttribute& attribute);
+ std::shared_ptr GetFactory() const {
+ return factory;
+ }
+
void SetAutoCreate(bool state);
private:
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp
index 6148d3310..8b9b85ba3 100644
--- a/src/frontend_common/config.cpp
+++ b/src/frontend_common/config.cpp
@@ -283,6 +283,7 @@ void Config::ReadDataStorageValues() {
FS::SetCitronPath(FS::CitronPath::TASDir, ReadStringSetting(std::string("tas_directory")));
ReadCategory(Settings::Category::DataStorage);
+ Settings::values.backup_saves_to_nand = ReadBooleanSetting(std::string("backup_saves_to_nand"), false);
EndGroup();
}
@@ -636,6 +637,7 @@ void Config::SaveDataStorageValues() {
std::make_optional(FS::GetCitronPathString(FS::CitronPath::TASDir)));
WriteCategory(Settings::Category::DataStorage);
+ WriteBooleanSetting(std::string("backup_saves_to_nand"), Settings::values.backup_saves_to_nand.GetValue());
EndGroup();
}