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.
This commit is contained in:
Tillmann Karras
2026-04-03 23:50:59 +01:00
parent 2b6ca92146
commit f06aef4f83
4 changed files with 56 additions and 28 deletions

View File

@@ -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.

View File

@@ -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<void()> update_callback,
void NANDImporter::ImportNANDBin(const std::string& path_to_bin, UpdateCallback update_callback,
const std::function<std::string()>& 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<Type>(entry.mode & 3);
if (type == Type::File)
{
if (m_update_callback(Step::Extracting, m_progress_cur++, m_progress_max))
return false;
std::vector<u8> 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<u8> NANDImporter::GetEntryData(const NANDFSTEntry& entry) const

View File

@@ -23,10 +23,18 @@ public:
NANDImporter();
~NANDImporter();
enum class Step
{
Loading,
Extracting,
};
// Return true to cancel.
using UpdateCallback = std::function<bool(Step step, int cur, int max)>;
// 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<void()> update_callback,
void ImportNANDBin(const std::string& path_to_bin, UpdateCallback update_callback,
const std::function<std::string()>& get_otp_dump_path);
bool ExtractCertificates();
@@ -67,9 +75,10 @@ private:
bool ReadNANDBin(const std::string& path_to_bin,
const std::function<std::string()>& 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<u8> GetEntryData(const NANDFSTEntry& entry) const;
void ExportKeys();
@@ -78,7 +87,9 @@ private:
std::vector<u8> m_nand_keys;
std::unique_ptr<Common::AES::Context> m_aes_ctx;
std::unique_ptr<NANDSuperblock> m_superblock;
std::function<void()> m_update_callback;
UpdateCallback m_update_callback;
u16 m_progress_cur = 0;
u16 m_progress_max = 0;
};
} // namespace DiscIO

View File

@@ -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<void> 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<std::string> keys_file = RunOnObject(this, [this] {