Qt: Use QtAsyncTaskWithProgress for dump verification

This commit is contained in:
Stenzek
2025-11-22 23:57:33 +10:00
parent 3f882cf254
commit d1519aa097
4 changed files with 142 additions and 118 deletions

View File

@@ -445,146 +445,158 @@ void GameSummaryWidget::onComputeHashClicked()
return; return;
} }
Error error; m_ui.computeHashes->setEnabled(false);
std::unique_ptr<CDImage> image = CDImage::Open(m_path.c_str(), false, &error);
QtAsyncTaskWithProgress::create(this, TRANSLATE_SV("GameSummaryWidget", "Verifying Image"), {}, true, 1, 0, 0.0f,
[this, path = m_path](ProgressCallback* progress) {
Error error;
CDImageHasher::TrackHashes track_hashes;
const bool result = computeImageHash(path, track_hashes, progress, &error);
const bool cancelled = (!result && progress->IsCancelled());
return
[this, track_hashes = std::move(track_hashes), error = std::move(error), result,
cancelled]() { processHashResults(track_hashes, result, cancelled, error); };
});
}
bool GameSummaryWidget::computeImageHash(const std::string& path, CDImageHasher::TrackHashes& track_hashes,
ProgressCallback* const progress, Error* const error) const
{
std::unique_ptr<CDImage> image = CDImage::Open(m_path.c_str(), false, error);
if (!image) if (!image)
return false;
track_hashes.reserve(image->GetTrackCount());
progress->SetProgressRange(image->GetTrackCount());
for (u32 track = 0; track < image->GetTrackCount(); track++)
{ {
QtUtils::MessageBoxCritical(QtUtils::GetRootWidget(this), tr("Image Open Failed"), progress->SetProgressValue(track);
QString::fromStdString(error.GetDescription())); progress->PushState();
CDImageHasher::Hash hash;
if (!CDImageHasher::GetTrackHash(image.get(), static_cast<u8>(track + 1), &hash, progress, error))
{
progress->PopState();
return false;
}
track_hashes.emplace_back(hash);
progress->PopState();
}
return true;
}
void GameSummaryWidget::processHashResults(const CDImageHasher::TrackHashes& track_hashes, bool result, bool cancelled,
const Error& error)
{
m_ui.computeHashes->setEnabled(true);
if (!result)
{
if (!cancelled)
{
QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Hash Calculation Failed"),
QString::fromStdString(error.GetDescription()));
}
return; return;
} }
QtModalProgressCallback progress_callback(this);
progress_callback.SetCancellable(true);
progress_callback.SetProgressRange(image->GetTrackCount());
progress_callback.MakeVisible();
std::vector<CDImageHasher::Hash> track_hashes;
track_hashes.reserve(image->GetTrackCount());
// Calculate hashes
bool calculate_hash_success = true;
for (u8 track = 1; track <= image->GetTrackCount(); track++)
{
progress_callback.SetProgressValue(track - 1);
progress_callback.PushState();
CDImageHasher::Hash hash;
if (!CDImageHasher::GetTrackHash(image.get(), track, &hash, &progress_callback, &error))
{
progress_callback.PopState();
if (progress_callback.IsCancelled())
return;
QtUtils::MessageBoxCritical(QtUtils::GetRootWidget(this), tr("Hash Calculation Failed"),
QString::fromStdString(error.GetDescription()));
calculate_hash_success = false;
break;
}
track_hashes.emplace_back(hash);
QTreeWidgetItem* const row = m_ui.tracks->topLevelItem(track - 1);
row->setText(4, QString::fromStdString(CDImageHasher::HashToString(hash)));
progress_callback.PopState();
}
// Verify hashes against gamedb // Verify hashes against gamedb
std::vector<bool> verification_results(image->GetTrackCount(), false); std::vector<bool> verification_results(track_hashes.size(), false);
if (calculate_hash_success)
std::string found_revision;
std::string found_serial;
m_redump_search_keyword = CDImageHasher::HashToString(track_hashes.front());
// Verification strategy used:
// 1. First, find all matches for the data track
// If none are found, fail verification for all tracks
// 2. For each data track match, try to match all audio tracks
// If all match, assume this revision. Else, try other revisions,
// and accept the one with the most matches.
const GameDatabase::TrackHashesMap& hashes_map = GameDatabase::GetTrackHashesMap();
auto data_track_matches = hashes_map.equal_range(track_hashes[0]);
if (data_track_matches.first != data_track_matches.second)
{ {
std::string found_revision; auto best_data_match = data_track_matches.second;
std::string found_serial; for (auto iter = data_track_matches.first; iter != data_track_matches.second; ++iter)
m_redump_search_keyword = CDImageHasher::HashToString(track_hashes.front());
progress_callback.SetStatusText(TRANSLATE("GameSummaryWidget", "Verifying hashes..."));
progress_callback.SetProgressValue(image->GetTrackCount());
// Verification strategy used:
// 1. First, find all matches for the data track
// If none are found, fail verification for all tracks
// 2. For each data track match, try to match all audio tracks
// If all match, assume this revision. Else, try other revisions,
// and accept the one with the most matches.
const GameDatabase::TrackHashesMap& hashes_map = GameDatabase::GetTrackHashesMap();
auto data_track_matches = hashes_map.equal_range(track_hashes[0]);
if (data_track_matches.first != data_track_matches.second)
{ {
auto best_data_match = data_track_matches.second; std::vector<bool> current_verification_results(track_hashes.size(), false);
for (auto iter = data_track_matches.first; iter != data_track_matches.second; ++iter) const auto& data_track_attribs = iter->second;
current_verification_results[0] = true; // Data track already matched
for (auto audio_tracks_iter = std::next(track_hashes.begin()); audio_tracks_iter != track_hashes.end();
++audio_tracks_iter)
{ {
std::vector<bool> current_verification_results(image->GetTrackCount(), false); auto audio_track_matches = hashes_map.equal_range(*audio_tracks_iter);
const auto& data_track_attribs = iter->second; for (auto audio_iter = audio_track_matches.first; audio_iter != audio_track_matches.second; ++audio_iter)
current_verification_results[0] = true; // Data track already matched
for (auto audio_tracks_iter = std::next(track_hashes.begin()); audio_tracks_iter != track_hashes.end();
++audio_tracks_iter)
{ {
auto audio_track_matches = hashes_map.equal_range(*audio_tracks_iter); // If audio track comes from the same revision and code as the data track, "pass" it
for (auto audio_iter = audio_track_matches.first; audio_iter != audio_track_matches.second; ++audio_iter) if (audio_iter->second == data_track_attribs)
{
// If audio track comes from the same revision and code as the data track, "pass" it
if (audio_iter->second == data_track_attribs)
{
current_verification_results[std::distance(track_hashes.begin(), audio_tracks_iter)] = true;
break;
}
}
}
const auto old_matches_count = std::count(verification_results.begin(), verification_results.end(), true);
const auto new_matches_count =
std::count(current_verification_results.begin(), current_verification_results.end(), true);
if (new_matches_count > old_matches_count)
{
best_data_match = iter;
verification_results = current_verification_results;
// If all elements got matched, early out
if (new_matches_count >= static_cast<ptrdiff_t>(verification_results.size()))
{ {
current_verification_results[std::distance(track_hashes.begin(), audio_tracks_iter)] = true;
break; break;
} }
} }
} }
found_revision = best_data_match->second.revision_str; const auto old_matches_count = std::count(verification_results.begin(), verification_results.end(), true);
found_serial = best_data_match->second.serial; const auto new_matches_count =
} std::count(current_verification_results.begin(), current_verification_results.end(), true);
QString text; if (new_matches_count > old_matches_count)
if (!found_revision.empty())
text = tr("Revision: %1").arg(found_revision.empty() ? tr("N/A") : QString::fromStdString(found_revision));
if (found_serial != m_dialog->getGameSerial())
{
if (found_serial.empty())
{ {
text = tr("No known dump found that matches this hash."); best_data_match = iter;
} verification_results = current_verification_results;
else // If all elements got matched, early out
{ if (new_matches_count >= static_cast<ptrdiff_t>(verification_results.size()))
const QString mismatch_str = tr("Serial Mismatch: %1 vs %2") {
.arg(QString::fromStdString(found_serial)) break;
.arg(QString::fromStdString(m_dialog->getGameSerial())); }
if (!text.isEmpty())
text = QStringLiteral("%1 | %2").arg(mismatch_str).arg(text);
else
text = mismatch_str;
} }
} }
setRevisionText(text); found_revision = best_data_match->second.revision_str;
found_serial = best_data_match->second.serial;
} }
for (u8 track = 0; track < image->GetTrackCount(); track++) QString text;
if (!found_revision.empty())
text = tr("Revision: %1").arg(found_revision.empty() ? tr("N/A") : QString::fromStdString(found_revision));
if (found_serial != m_dialog->getGameSerial())
{ {
QTreeWidgetItem* const row = m_ui.tracks->topLevelItem(track); if (found_serial.empty())
{
text = tr("No known dump found that matches this hash.");
}
else
{
const QString mismatch_str = tr("Serial Mismatch: %1 vs %2")
.arg(QString::fromStdString(found_serial))
.arg(QString::fromStdString(m_dialog->getGameSerial()));
if (!text.isEmpty())
text = QStringLiteral("%1 | %2").arg(mismatch_str).arg(text);
else
text = mismatch_str;
}
}
setRevisionText(text);
// update in ui
for (size_t i = 0; i < track_hashes.size(); i++)
{
QTreeWidgetItem* const row = m_ui.tracks->topLevelItem(static_cast<int>(i));
row->setText(4, QString::fromStdString(CDImageHasher::HashToString(track_hashes[i])));
QBrush brush; QBrush brush;
if (verification_results[track]) if (verification_results[i])
{ {
brush = QColor(0, 200, 0); brush = QColor(0, 200, 0);
row->setText(5, QString::fromUtf8(u8"\u2713")); row->setText(5, QString::fromUtf8(u8"\u2713"));

View File

@@ -1,8 +1,12 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> // SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0 // SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once #pragma once
#include "util/cd_image_hasher.h"
#include "common/types.h" #include "common/types.h"
#include <QtWidgets/QWidget> #include <QtWidgets/QWidget>
#include "ui_gamesummarywidget.h" #include "ui_gamesummarywidget.h"
@@ -13,6 +17,7 @@ namespace GameList {
struct Entry; struct Entry;
} }
class ProgressCallback;
class SettingsWindow; class SettingsWindow;
class GameSummaryWidget : public QWidget class GameSummaryWidget : public QWidget
@@ -40,6 +45,11 @@ private:
void onEditInputProfileClicked(); void onEditInputProfileClicked();
void onComputeHashClicked(); void onComputeHashClicked();
bool computeImageHash(const std::string& path, CDImageHasher::TrackHashes& track_hashes,
ProgressCallback* const progress, Error* const error) const;
void processHashResults(const CDImageHasher::TrackHashes& track_hashes, bool result, bool cancelled,
const Error& error);
Ui::GameSummaryWidget m_ui; Ui::GameSummaryWidget m_ui;
SettingsWindow* m_dialog; SettingsWindow* m_dialog;

View File

@@ -27,8 +27,6 @@ bool CDImageHasher::ReadIndex(CDImage* image, u8 track, u8 index, MD5Digest* dig
const u32 index_length = image->GetTrackIndexLength(track, index); const u32 index_length = image->GetTrackIndexLength(track, index);
const u32 update_interval = std::max<u32>(index_length / 100u, 1u); const u32 update_interval = std::max<u32>(index_length / 100u, 1u);
progress_callback->FormatStatusText(TRANSLATE_FS("CDImageHasher", "Computing hash for Track {}/Index {}..."), track,
index);
progress_callback->SetProgressRange(index_length); progress_callback->SetProgressRange(index_length);
if (!image->Seek(index_start)) if (!image->Seek(index_start))
@@ -67,6 +65,7 @@ bool CDImageHasher::ReadTrack(CDImage* image, u8 track, MD5Digest* digest, Progr
progress_callback->PushState(); progress_callback->PushState();
const bool dataTrack = track == 1; const bool dataTrack = track == 1;
progress_callback->FormatStatusText(TRANSLATE_FS("CDImageHasher", "Computing hash for Track {}..."), track);
progress_callback->SetProgressRange(dataTrack ? 1 : 2); progress_callback->SetProgressRange(dataTrack ? 1 : 2);
u8 progress = 0; u8 progress = 0;

View File

@@ -8,6 +8,7 @@
#include <array> #include <array>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
class CDImage; class CDImage;
class Error; class Error;
@@ -16,6 +17,8 @@ class ProgressCallback;
namespace CDImageHasher { namespace CDImageHasher {
using Hash = std::array<u8, 16>; using Hash = std::array<u8, 16>;
using TrackHashes = std::vector<Hash>;
std::string HashToString(const Hash& hash); std::string HashToString(const Hash& hash);
std::optional<Hash> HashFromString(std::string_view str); std::optional<Hash> HashFromString(std::string_view str);