feat(fs): Cross-Compatible Emulator Save Pathing w/ Custom Save Paths

Signed-off-by: Collecting <collecting@noreply.localhost>
This commit is contained in:
Collecting
2025-12-31 03:51:44 +00:00
parent 1820ed7815
commit 4f4b3a9360

View File

@@ -17,9 +17,15 @@ constexpr int RetryWaitTimeMs = 100;
} // Anonymous namespace
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),
// Updated constructor to accept mirror_filesystem
DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem,
VirtualDir backup_filesystem,
VirtualDir mirror_filesystem)
: base_fs(std::move(base_filesystem)),
backup_fs(std::move(backup_filesystem)),
mirror_fs(std::move(mirror_filesystem)),
extra_data_accessor(base_fs),
journaling_enabled(true),
open_writable_files(0) {}
DirectorySaveDataFileSystem::~DirectorySaveDataFileSystem() = default;
@@ -88,19 +94,15 @@ Result DirectorySaveDataFileSystem::Commit() {
std::scoped_lock lk{mutex};
if (!journaling_enabled) {
// Non-journaling: just commit extra data
return extra_data_accessor.CommitExtraDataWithTimeStamp(
std::chrono::system_clock::now().time_since_epoch().count());
}
// Check that all writable files are closed
if (open_writable_files > 0) {
LOG_ERROR(Service_FS, "Cannot commit: {} writable files still open", open_writable_files);
return ResultWriteModeFileNotClosed;
}
// Atomic commit process (based on LibHac lines 572-622)
// 1. Rename committed → synchronizing (backup old version)
auto committed = base_fs->GetSubdirectory(CommittedDirectoryName);
if (committed != nullptr) {
if (!committed->Rename(SynchronizingDirectoryName)) {
@@ -108,41 +110,34 @@ Result DirectorySaveDataFileSystem::Commit() {
}
}
// 2. Copy working → synchronizing (prepare new commit)
R_TRY(SynchronizeDirectory(SynchronizingDirectoryName, ModifiedDirectoryName));
// 3. Commit extra data with updated timestamp
R_TRY(extra_data_accessor.CommitExtraDataWithTimeStamp(
std::chrono::system_clock::now().time_since_epoch().count()));
// 4. Rename synchronizing → committed (make it permanent)
auto sync_dir = base_fs->GetSubdirectory(SynchronizingDirectoryName);
if (sync_dir == nullptr) {
return ResultPathNotFound;
}
if (!sync_dir->Rename(CommittedDirectoryName)) {
if (sync_dir == nullptr || !sync_dir->Rename(CommittedDirectoryName)) {
return ResultPermissionDenied;
}
// 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...");
// Now that the NAND is safely updated, we push the changes back to Ryujinx/Eden
if (mirror_fs != nullptr) {
LOG_INFO(Service_FS, "Mirroring: Pushing changes back to external source...");
// 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);
}
// Use SmartSyncToMirror instead of CleanSubdirectoryRecursive
// working_dir contains the data that was just successfully committed
SmartSyncToMirror(mirror_fs, working_dir);
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
LOG_INFO(Service_FS, "Mirroring: External sync successful.");
}
// Standard backup only if Mirroring is NOT active
else if (Settings::values.backup_saves_to_nand.GetValue() && backup_fs != nullptr) {
LOG_INFO(Service_FS, "Dual-Save: Backing up to NAND...");
backup_fs->DeleteSubdirectoryRecursive(CommittedDirectoryName);
auto nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName);
if (nand_committed) {
CopyDirectoryRecursively(nand_committed, working_dir);
}
}
@@ -167,8 +162,6 @@ Result DirectorySaveDataFileSystem::Rollback() {
}
bool DirectorySaveDataFileSystem::HasUncommittedChanges() const {
// For now, assume any write means uncommitted changes
// A full implementation would compare directory contents
return open_writable_files > 0;
}
@@ -224,8 +217,7 @@ Result DirectorySaveDataFileSystem::CopyDirectoryRecursively(VirtualDir dest, Vi
return ResultSuccess;
}
Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(
std::function<Result()> operation) {
Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(std::function<Result()> operation) {
int remaining_retries = MaxRetryCount;
while (true) {
@@ -235,7 +227,6 @@ Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(
return ResultSuccess;
}
// Only retry on TargetLocked error
if (result != ResultTargetLocked) {
return result;
}
@@ -249,4 +240,28 @@ Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(
}
}
void DirectorySaveDataFileSystem::SmartSyncToMirror(VirtualDir mirror_dest, VirtualDir citron_source) {
// Citron: Extra safety check for valid pointers and writable permissions
if (mirror_dest == nullptr || citron_source == nullptr || !mirror_dest->IsWritable()) {
return;
}
// Sync files from Citron back to the Mirror
for (const auto& c_file : citron_source->GetFiles()) {
auto m_file = mirror_dest->CreateFile(c_file->GetName());
if (m_file) {
m_file->WriteBytes(c_file->ReadAllBytes());
}
}
// Recursively handle subfolders (like 'private', 'extra', etc)
for (const auto& c_subdir : citron_source->GetSubdirectories()) {
auto m_subdir = mirror_dest->GetDirectoryRelative(c_subdir->GetName());
if (m_subdir == nullptr) {
m_subdir = mirror_dest->CreateSubdirectory(c_subdir->GetName());
}
SmartSyncToMirror(m_subdir, c_subdir);
}
}
} // namespace FileSys