From f06aef4f8382d258e16bac2ea8a19aa48dda72a0 Mon Sep 17 00:00:00 2001 From: Tillmann Karras Date: Fri, 3 Apr 2026 23:50:59 +0100 Subject: [PATCH] Improve NAND import progress dialog Now with cancel button and an actual progress bar. For simplicity, we do two passes on the progress bar, one for loading the NAND into memory and one for extracting it. The user directory is likely on an SSD, making the extraction pass invisibly fast. --- Source/Android/jni/WiiUtils.cpp | 8 +++---- Source/Core/DiscIO/NANDImporter.cpp | 35 +++++++++++++++++++--------- Source/Core/DiscIO/NANDImporter.h | 17 +++++++++++--- Source/Core/DolphinQt/MainWindow.cpp | 24 +++++++++++-------- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/Source/Android/jni/WiiUtils.cpp b/Source/Android/jni/WiiUtils.cpp index 77ef3e3f7f..995f0d05b6 100644 --- a/Source/Android/jni/WiiUtils.cpp +++ b/Source/Android/jni/WiiUtils.cpp @@ -102,10 +102,10 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_utils_WiiUtils_importNANDB return DiscIO::NANDImporter().ImportNANDBin( path, - [] { - // This callback gets called every now and then in case we want to update the GUI. However, - // we have no way of knowing what the current progress is, so we can't do anything - // especially useful. DolphinQt chooses to show the elapsed time, for reference. + [](DiscIO::NANDImporter::Step, int, int) { + // This callback gets called every now and then in case we want to update the GUI. + // TODO + return false; }, [] { // This callback gets called if the NAND file does not have decryption keys appended to it. diff --git a/Source/Core/DiscIO/NANDImporter.cpp b/Source/Core/DiscIO/NANDImporter.cpp index 54122d0626..f4c94bbb2c 100644 --- a/Source/Core/DiscIO/NANDImporter.cpp +++ b/Source/Core/DiscIO/NANDImporter.cpp @@ -23,8 +23,7 @@ NANDImporter::NANDImporter() : m_nand_root(File::GetUserPath(D_WIIROOT_IDX)) } NANDImporter::~NANDImporter() = default; -void NANDImporter::ImportNANDBin(const std::string& path_to_bin, - std::function update_callback, +void NANDImporter::ImportNANDBin(const std::string& path_to_bin, UpdateCallback update_callback, const std::function& get_otp_dump_path) { m_update_callback = std::move(update_callback); @@ -35,8 +34,13 @@ void NANDImporter::ImportNANDBin(const std::string& path_to_bin, return; ExportKeys(); - ProcessEntry(0, ""); + + if (!ExtractFiles()) + return; + ExtractCertificates(); + + std::ignore = m_update_callback(Step::Extracting, m_progress_cur, m_progress_max); } bool NANDImporter::ReadNANDBin(const std::string& path_to_bin, @@ -60,10 +64,8 @@ bool NANDImporter::ReadNANDBin(const std::string& path_to_bin, for (size_t i = 0; i < NAND_TOTAL_BLOCKS; i++) { - // Instead of updating on every cycle, we only update every 1000 cycles for a balance between - // not updating fast enough vs updating too fast - if (i % 1000 == 0) - m_update_callback(); + if (m_update_callback(Step::Loading, int(i), int(NAND_TOTAL_BLOCKS))) + return false; file.ReadBytes(&m_nand[i * NAND_BLOCK_SIZE], NAND_BLOCK_SIZE); @@ -121,6 +123,14 @@ bool NANDImporter::FindSuperblock() return true; } +bool NANDImporter::ExtractFiles() +{ + constexpr u16 CLUSTER_CHAIN_END = 0xFFFB; + m_progress_cur = 0; + m_progress_max = std::ranges::count(m_superblock->fat, CLUSTER_CHAIN_END); + return ProcessEntry(0, ""); +} + std::string NANDImporter::GetPath(const NANDFSTEntry& entry, const std::string& parent_path) { std::string name(entry.name, strnlen(entry.name, sizeof(NANDFSTEntry::name))); @@ -131,25 +141,26 @@ std::string NANDImporter::GetPath(const NANDFSTEntry& entry, const std::string& return parent_path + '/' + name; } -void NANDImporter::ProcessEntry(u16 entry_number, const std::string& parent_path) +bool NANDImporter::ProcessEntry(u16 entry_number, const std::string& parent_path) { while (entry_number != 0xffff) { if (entry_number >= m_superblock->fst.size()) { ERROR_LOG_FMT(DISCIO, "FST entry number {} out of range", entry_number); - return; + return false; } const NANDFSTEntry entry = m_superblock->fst[entry_number]; const std::string path = GetPath(entry, parent_path); INFO_LOG_FMT(DISCIO, "Entry: {} Path: {}", entry, path); - m_update_callback(); Type type = static_cast(entry.mode & 3); if (type == Type::File) { + if (m_update_callback(Step::Extracting, m_progress_cur++, m_progress_max)) + return false; std::vector data = GetEntryData(entry); File::IOFile file(m_nand_root + path, "wb"); file.WriteBytes(data.data(), data.size()); @@ -157,7 +168,8 @@ void NANDImporter::ProcessEntry(u16 entry_number, const std::string& parent_path else if (type == Type::Directory) { File::CreateDir(m_nand_root + path); - ProcessEntry(entry.sub, path); + if (!ProcessEntry(entry.sub, path)) + return false; } else { @@ -166,6 +178,7 @@ void NANDImporter::ProcessEntry(u16 entry_number, const std::string& parent_path entry_number = entry.sib; } + return true; } std::vector NANDImporter::GetEntryData(const NANDFSTEntry& entry) const diff --git a/Source/Core/DiscIO/NANDImporter.h b/Source/Core/DiscIO/NANDImporter.h index 164d009f5a..f22f043709 100644 --- a/Source/Core/DiscIO/NANDImporter.h +++ b/Source/Core/DiscIO/NANDImporter.h @@ -23,10 +23,18 @@ public: NANDImporter(); ~NANDImporter(); + enum class Step + { + Loading, + Extracting, + }; + // Return true to cancel. + using UpdateCallback = std::function; + // Extract a NAND image to the configured NAND root. // If the associated OTP/SEEPROM dump (keys.bin) is not included in the image, // get_otp_dump_path will be called to get a path to it. - void ImportNANDBin(const std::string& path_to_bin, std::function update_callback, + void ImportNANDBin(const std::string& path_to_bin, UpdateCallback update_callback, const std::function& get_otp_dump_path); bool ExtractCertificates(); @@ -67,9 +75,10 @@ private: bool ReadNANDBin(const std::string& path_to_bin, const std::function& get_otp_dump_path); bool FindSuperblock(); + bool ExtractFiles(); std::string GetPath(const NANDFSTEntry& entry, const std::string& parent_path); std::string FormatDebugString(const NANDFSTEntry& entry); - void ProcessEntry(u16 entry_number, const std::string& parent_path); + bool ProcessEntry(u16 entry_number, const std::string& parent_path); std::vector GetEntryData(const NANDFSTEntry& entry) const; void ExportKeys(); @@ -78,7 +87,9 @@ private: std::vector m_nand_keys; std::unique_ptr m_aes_ctx; std::unique_ptr m_superblock; - std::function m_update_callback; + UpdateCallback m_update_callback; + u16 m_progress_cur = 0; + u16 m_progress_max = 0; }; } // namespace DiscIO diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index c3f5b4148c..2713157a74 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -1881,20 +1881,24 @@ void MainWindow::OnImportNANDBackup() return; ParallelProgressDialog dialog(this); - dialog.GetRaw()->setMinimum(0); - dialog.GetRaw()->setMaximum(0); - dialog.GetRaw()->setLabelText(tr("Importing NAND backup")); - dialog.GetRaw()->setCancelButton(nullptr); - - auto beginning = QDateTime::currentDateTime().toMSecsSinceEpoch(); + dialog.GetRaw()->setWindowTitle(tr("Importing NAND backup")); std::future result = std::async(std::launch::async, [&] { DiscIO::NANDImporter().ImportNANDBin( file.toStdString(), - [&dialog, beginning] { - dialog.SetLabelText( - tr("Importing NAND backup\n Time elapsed: %1s") - .arg((QDateTime::currentDateTime().toMSecsSinceEpoch() - beginning) / 1000)); + [&dialog](DiscIO::NANDImporter::Step step, u32 cur, u32 max) { + switch (step) + { + case DiscIO::NANDImporter::Step::Loading: + dialog.SetLabelText(tr("Loading NAND...")); + break; + case DiscIO::NANDImporter::Step::Extracting: + dialog.SetLabelText(tr("Extracting NAND...")); + break; + } + dialog.SetValue(cur); + dialog.SetMaximum(max); + return dialog.WasCanceled(); }, [this] { std::optional keys_file = RunOnObject(this, [this] {