diff --git a/Source/Core/Core/IOS/FS/FileSystem.h b/Source/Core/Core/IOS/FS/FileSystem.h index 581df6fbee..c0f59c2137 100644 --- a/Source/Core/Core/IOS/FS/FileSystem.h +++ b/Source/Core/Core/IOS/FS/FileSystem.h @@ -107,6 +107,33 @@ struct Metadata u16 fst_index; }; +// size of a single cluster in the NAND in bytes +constexpr u16 CLUSTER_SIZE = 16384; + +// total number of clusters available in the NAND +constexpr u16 TOTAL_CLUSTERS = 0x7ec0; + +// number of clusters reserved for bad blocks and similar, not accessible to normal writes +constexpr u16 RESERVED_CLUSTERS = 0x0300; + +// number of clusters actually usable by the file system +constexpr u16 USABLE_CLUSTERS = TOTAL_CLUSTERS - RESERVED_CLUSTERS; + +// size of a single 'block' as defined by the Wii System Menu in clusters +constexpr u16 CLUSTERS_PER_BLOCK = 8; + +// total number of user-accessible blocks in the NAND +constexpr u16 USER_BLOCKS = 2176; + +// total number of user-accessible clusters in the NAND +constexpr u16 USER_CLUSTERS = USER_BLOCKS * CLUSTERS_PER_BLOCK; + +// the inverse of that, the amount of usable clusters reserved for system files +constexpr u16 SYSTEM_CLUSTERS = USABLE_CLUSTERS - USER_CLUSTERS; + +// total number of inodes available in the NAND +constexpr u16 TOTAL_INODES = 0x17ff; + struct NandStats { u32 cluster_size; @@ -124,6 +151,14 @@ struct DirectoryStats u32 used_inodes; }; +// Not a real Wii data struct, but useful for calculating how full the user's NAND is even if it's +// way larger than it should be. +struct ExtendedDirectoryStats +{ + u64 used_clusters; + u64 used_inodes; +}; + struct FileStatus { u32 offset; @@ -252,6 +287,9 @@ public: /// Get usage information about a directory (used cluster and inode counts). virtual Result GetDirectoryStats(const std::string& path) = 0; + /// Like GetDirectoryStats() but not limited to the actual 512 MB NAND limit. + virtual Result GetExtendedDirectoryStats(const std::string& path) = 0; + virtual void SetNandRedirects(std::vector nand_redirects) = 0; }; diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp index f09cadb7c8..a916ffba81 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp @@ -29,21 +29,6 @@ namespace IOS::HLE::FS { constexpr u32 BUFFER_CHUNK_SIZE = 65536; -// size of a single cluster in the NAND -constexpr u16 CLUSTER_SIZE = 16384; - -// total number of clusters available in the NAND -constexpr u16 TOTAL_CLUSTERS = 0x7ec0; - -// number of clusters reserved for bad blocks and similar, not accessible to normal writes -constexpr u16 RESERVED_CLUSTERS = 0x0300; - -// number of clusters actually usable by the file system -constexpr u16 USABLE_CLUSTERS = TOTAL_CLUSTERS - RESERVED_CLUSTERS; - -// total number of inodes available in the NAND -constexpr u16 TOTAL_INODES = 0x17ff; - HostFileSystem::HostFilename HostFileSystem::BuildFilename(const std::string& wii_path) const { for (const auto& redirect : m_nand_redirects) @@ -818,11 +803,24 @@ Result HostFileSystem::GetNandStats() } Result HostFileSystem::GetDirectoryStats(const std::string& wii_path) +{ + const auto result = GetExtendedDirectoryStats(wii_path); + if (!result) + return result.Error(); + + DirectoryStats stats{}; + stats.used_inodes = static_cast(std::min(result->used_inodes, TOTAL_INODES)); + stats.used_clusters = static_cast(std::min(result->used_clusters, USABLE_CLUSTERS)); + return stats; +} + +Result +HostFileSystem::GetExtendedDirectoryStats(const std::string& wii_path) { if (!IsValidPath(wii_path)) return ResultCode::Invalid; - DirectoryStats stats{}; + ExtendedDirectoryStats stats{}; std::string path(BuildFilename(wii_path).host_path); File::FileInfo info(path); if (!info.Exists()) @@ -835,10 +833,8 @@ Result HostFileSystem::GetDirectoryStats(const std::string& wii_ FixupDirectoryEntries(&parent_dir, wii_path == "/"); // add one for the folder itself - stats.used_inodes = static_cast(std::min(1 + parent_dir.size, TOTAL_INODES)); - - const u64 clusters = ComputeUsedClusters(parent_dir); - stats.used_clusters = static_cast(std::min(clusters, USABLE_CLUSTERS)); + stats.used_inodes = 1 + parent_dir.size; + stats.used_clusters = ComputeUsedClusters(parent_dir); } else { diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.h b/Source/Core/Core/IOS/FS/HostBackend/FS.h index 1248c06736..3c68a59495 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.h +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.h @@ -55,6 +55,7 @@ public: Result GetNandStats() override; Result GetDirectoryStats(const std::string& path) override; + Result GetExtendedDirectoryStats(const std::string& path) override; void SetNandRedirects(std::vector nand_redirects) override; diff --git a/Source/Core/Core/WiiUtils.cpp b/Source/Core/Core/WiiUtils.cpp index 30225febfb..92c2c19a0d 100644 --- a/Source/Core/Core/WiiUtils.cpp +++ b/Source/Core/Core/WiiUtils.cpp @@ -961,6 +961,34 @@ static NANDCheckResult CheckNAND(IOS::HLE::Kernel& ios, bool repair) } } + // Get some storage stats. + const auto fs = ios.GetFS(); + const auto root_stats = fs->GetExtendedDirectoryStats("/"); + + // The Wii System Menu's save/channel management only considers a specific subset of the Wii NAND + // user-accessible and will only use those folders when calculating the amount of free blocks it + // displays. This can have weird side-effects where the other parts of the NAND contain more data + // than reserved and it will display free blocks even though there isn't any space left. To avoid + // confusion, report the 'user' and 'system' data separately to the user. + u64 used_clusters_user = 0; + u64 used_inodes_user = 0; + for (std::string user_path : {"/meta", "/ticket", "/title/00010000", "/title/00010001", + "/title/00010003", "/title/00010004", "/title/00010005", + "/title/00010006", "/title/00010007", "/shared2/title"}) + { + const auto dir_stats = fs->GetExtendedDirectoryStats(user_path); + if (dir_stats) + { + used_clusters_user += dir_stats->used_clusters; + used_inodes_user += dir_stats->used_inodes; + } + } + + result.used_clusters_user = used_clusters_user; + result.used_clusters_system = root_stats ? (root_stats->used_clusters - used_clusters_user) : 0; + result.used_inodes_user = used_inodes_user; + result.used_inodes_system = root_stats ? (root_stats->used_inodes - used_inodes_user) : 0; + return result; } diff --git a/Source/Core/Core/WiiUtils.h b/Source/Core/Core/WiiUtils.h index ae2d7c85e0..c8f86d7253 100644 --- a/Source/Core/Core/WiiUtils.h +++ b/Source/Core/Core/WiiUtils.h @@ -101,6 +101,10 @@ struct NANDCheckResult { bool bad = false; std::unordered_set titles_to_remove; + u64 used_clusters_user = 0; + u64 used_clusters_system = 0; + u64 used_inodes_user = 0; + u64 used_inodes_system = 0; }; NANDCheckResult CheckNAND(IOS::HLE::Kernel& ios); bool RepairNAND(IOS::HLE::Kernel& ios); diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index b0989fae66..37562a738c 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -15,6 +15,7 @@ #include #include +#include "Common/Align.h" #include "Common/CommonPaths.h" #include "Common/FileUtil.h" #include "Common/StringUtil.h" @@ -32,6 +33,7 @@ #include "Core/HW/WiiSave.h" #include "Core/HW/Wiimote.h" #include "Core/IOS/ES/ES.h" +#include "Core/IOS/FS/FileSystem.h" #include "Core/IOS/IOS.h" #include "Core/IOS/USB/Bluetooth/BTEmu.h" #include "Core/Movie.h" @@ -1137,7 +1139,42 @@ void MenuBar::CheckNAND() WiiUtils::NANDCheckResult result = WiiUtils::CheckNAND(ios); if (!result.bad) { - ModalMessageBox::information(this, tr("NAND Check"), tr("No issues have been detected.")); + const bool overfull = result.used_clusters_user > IOS::HLE::FS::USER_CLUSTERS || + result.used_clusters_system > IOS::HLE::FS::SYSTEM_CLUSTERS; + const QString user_cluster_message = + tr("The user-accessible part of your NAND contains %1 blocks (%2 KiB) " + "of data, out of an allowed maximum of %3 blocks (%4 KiB).") + .arg(Common::AlignUp(result.used_clusters_user, IOS::HLE::FS::CLUSTERS_PER_BLOCK) / + IOS::HLE::FS::CLUSTERS_PER_BLOCK) + .arg((result.used_clusters_user * IOS::HLE::FS::CLUSTER_SIZE) / 1024) + .arg(IOS::HLE::FS::USER_CLUSTERS / IOS::HLE::FS::CLUSTERS_PER_BLOCK) + .arg((IOS::HLE::FS::USER_CLUSTERS * IOS::HLE::FS::CLUSTER_SIZE) / 1024); + const QString system_cluster_message = + tr("The system-reserved part of your NAND contains %1 blocks (%2 KiB) " + "of data, out of an allowed maximum of %3 blocks (%4 KiB).") + .arg(Common::AlignUp(result.used_clusters_system, IOS::HLE::FS::CLUSTERS_PER_BLOCK) / + IOS::HLE::FS::CLUSTERS_PER_BLOCK) + .arg((result.used_clusters_system * IOS::HLE::FS::CLUSTER_SIZE) / 1024) + .arg(IOS::HLE::FS::SYSTEM_CLUSTERS / IOS::HLE::FS::CLUSTERS_PER_BLOCK) + .arg((IOS::HLE::FS::SYSTEM_CLUSTERS * IOS::HLE::FS::CLUSTER_SIZE) / 1024); + + if (overfull) + { + ModalMessageBox::warning(this, tr("NAND Check"), + QStringLiteral("%1

%2

%3") + .arg(tr("Your NAND contains more data than allowed. Wii " + "software may behave incorrectly or not allow saving.")) + .arg(user_cluster_message) + .arg(system_cluster_message)); + } + else + { + ModalMessageBox::information(this, tr("NAND Check"), + QStringLiteral("%1

%2

%3") + .arg(tr("No issues have been detected.")) + .arg(user_cluster_message) + .arg(system_cluster_message)); + } return; }