From 20331a77cecb760c9d5d64bb838b3d0951a9e9f4 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 30 Oct 2021 20:44:25 +0200 Subject: [PATCH] Qt: multithreaded trophy icon refresh --- rpcs3/Loader/TROPUSR.cpp | 10 +- rpcs3/Loader/TROPUSR.h | 10 +- rpcs3/rpcs3qt/trophy_manager_dialog.cpp | 160 +++++++++++++++++------- rpcs3/rpcs3qt/trophy_manager_dialog.h | 10 ++ 4 files changed, 132 insertions(+), 58 deletions(-) diff --git a/rpcs3/Loader/TROPUSR.cpp b/rpcs3/Loader/TROPUSR.cpp index 7043093ffc..df2c47445e 100644 --- a/rpcs3/Loader/TROPUSR.cpp +++ b/rpcs3/Loader/TROPUSR.cpp @@ -204,12 +204,12 @@ bool TROPUSRLoader::Generate(const std::string& filepath, const std::string& con return Save(filepath); } -u32 TROPUSRLoader::GetTrophiesCount() +u32 TROPUSRLoader::GetTrophiesCount() const { return ::size32(m_table6); } -u32 TROPUSRLoader::GetUnlockedTrophiesCount() +u32 TROPUSRLoader::GetUnlockedTrophiesCount() const { u32 count = 0; for (const auto& trophy : m_table6) @@ -296,7 +296,7 @@ u32 TROPUSRLoader::GetUnlockedPlatinumID(u32 trophy_id, const std::string& confi return pid; } -u32 TROPUSRLoader::GetTrophyGrade(u32 id) +u32 TROPUSRLoader::GetTrophyGrade(u32 id) const { if (id >= m_table4.size()) { @@ -307,7 +307,7 @@ u32 TROPUSRLoader::GetTrophyGrade(u32 id) return m_table4[id].trophy_grade; // Let's assume the trophies are stored ordered } -u32 TROPUSRLoader::GetTrophyUnlockState(u32 id) +u32 TROPUSRLoader::GetTrophyUnlockState(u32 id) const { if (id >= m_table6.size()) { @@ -318,7 +318,7 @@ u32 TROPUSRLoader::GetTrophyUnlockState(u32 id) return m_table6[id].trophy_state; // Let's assume the trophies are stored ordered } -u64 TROPUSRLoader::GetTrophyTimestamp(u32 id) +u64 TROPUSRLoader::GetTrophyTimestamp(u32 id) const { if (id >= m_table6.size()) { diff --git a/rpcs3/Loader/TROPUSR.h b/rpcs3/Loader/TROPUSR.h index bc6f6af88e..efc668ba41 100644 --- a/rpcs3/Loader/TROPUSR.h +++ b/rpcs3/Loader/TROPUSR.h @@ -89,14 +89,14 @@ public: virtual load_result Load(const std::string& filepath, const std::string& configpath); virtual bool Save(const std::string& filepath); - virtual u32 GetTrophiesCount(); - virtual u32 GetUnlockedTrophiesCount(); + virtual u32 GetTrophiesCount() const; + virtual u32 GetUnlockedTrophiesCount() const; virtual u32 GetUnlockedPlatinumID(u32 trophy_id, const std::string& config_path); - virtual u32 GetTrophyGrade(u32 id); - virtual u32 GetTrophyUnlockState(u32 id); - virtual u64 GetTrophyTimestamp(u32 id); + virtual u32 GetTrophyGrade(u32 id) const; + virtual u32 GetTrophyUnlockState(u32 id) const; + virtual u64 GetTrophyTimestamp(u32 id) const; virtual bool UnlockTrophy(u32 id, u64 timestamp1, u64 timestamp2); }; diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index 819de0efdd..a44ab9e395 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -111,6 +111,7 @@ trophy_manager_dialog::trophy_manager_dialog(std::shared_ptr gui_s m_trophy_table->setHorizontalHeaderLabels(QStringList{ tr("Icon"), tr("Name"), tr("Description"), tr("Type"), tr("Status"), tr("ID"), tr("Platinum Relevant") }); m_trophy_table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); m_trophy_table->horizontalHeader()->setStretchLastSection(true); + m_trophy_table->horizontalHeader()->setSectionResizeMode(TrophyColumns::Icon, QHeaderView::Fixed); m_trophy_table->verticalHeader()->setVisible(false); m_trophy_table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); m_trophy_table->setContextMenuPolicy(Qt::CustomContextMenu); @@ -338,6 +339,24 @@ trophy_manager_dialog::trophy_manager_dialog(std::shared_ptr gui_s m_game_combo->setCurrentText(item->text()); }); + connect(&m_trophy_repaint_watcher, &QFutureWatcher::finished, this, &trophy_manager_dialog::ReadjustTrophyTable); + connect(&m_trophy_repaint_watcher, &QFutureWatcher::resultReadyAt, this, [this](int index) + { + if (QTableWidgetItem* icon_item = m_trophy_table->item(index, TrophyColumns::Icon)) + { + icon_item->setData(Qt::DecorationRole, m_trophy_repaint_watcher.resultAt(index)); + } + }); + + connect(&m_game_repaint_watcher, &QFutureWatcher::finished, this, &trophy_manager_dialog::ReadjustGameTable); + connect(&m_game_repaint_watcher, &QFutureWatcher::resultReadyAt, this, [this](int index) + { + if (QTableWidgetItem* icon_item = m_game_table->item(index, GameColumns::GameIcon)) + { + icon_item->setData(Qt::DecorationRole, m_game_repaint_watcher.resultAt(index)); + } + }); + RepaintUI(true); StartTrophyLoadThreads(); @@ -486,12 +505,33 @@ QPixmap trophy_manager_dialog::GetResizedGameIcon(int index) const QTableWidgetItem* item = m_game_table->item(index, GameColumns::GameIcon); if (!item) { - return QPixmap(); + QPixmap placeholder(m_game_icon_size); + placeholder.fill(Qt::transparent); + return placeholder; } - const QPixmap icon = item->data(Qt::UserRole).value(); + + QPixmap icon; + + if (!item->data(GameUserRole::GamePixmapLoaded).toBool()) + { + // Load game icon + const int trophy_index = item->data(GameUserRole::GameIndex).toInt(); + const std::string icon_path = m_trophies_db[trophy_index]->path + "ICON0.PNG"; + if (!icon.load(qstr(icon_path))) + { + gui_log.warning("Could not load trophy game icon from path %s", icon_path); + } + item->setData(GameUserRole::GamePixmapLoaded, true); + item->setData(GameUserRole::GamePixmap, icon); + } + else + { + icon = item->data(GameUserRole::GamePixmap).value(); + } + const qreal dpr = devicePixelRatioF(); - QPixmap new_icon = QPixmap(icon.size() * dpr); + QPixmap new_icon(icon.size() * dpr); new_icon.setDevicePixelRatio(dpr); new_icon.fill(m_game_icon_color); @@ -511,26 +551,34 @@ void trophy_manager_dialog::ResizeGameIcons() if (m_game_combo->count() <= 0) return; + if (m_game_repaint_watcher.isRunning()) + { + m_game_repaint_watcher.cancel(); + m_game_repaint_watcher.waitForFinished(); + } + + QPixmap placeholder(m_game_icon_size); + placeholder.fill(Qt::transparent); + QList indices; for (int i = 0; i < m_game_table->rowCount(); ++i) + { indices.append(i); + if (QTableWidgetItem* icon_item = m_game_table->item(i, GameColumns::GameIcon)) + { + icon_item->setData(Qt::DecorationRole, placeholder); + } + } + const std::function get_scaled = [this](const int& i) { return GetResizedGameIcon(i); }; - // NOTE: Due to a Qt bug in Qt 5.15.2, QtConcurrent::blockingMapped has a high risk of deadlocking. So let's just use QtConcurrent::mapped. - const QList scaled = QtConcurrent::mapped(indices, get_scaled).results(); - - for (int i = 0; i < m_game_table->rowCount() && i < scaled.count(); ++i) - { - QTableWidgetItem* icon_item = m_game_table->item(i, TrophyColumns::Icon); - if (icon_item) - icon_item->setData(Qt::DecorationRole, scaled[i]); - } - ReadjustGameTable(); + + m_game_repaint_watcher.setFuture(QtConcurrent::mapped(indices, get_scaled)); } void trophy_manager_dialog::ResizeTrophyIcons() @@ -538,10 +586,19 @@ void trophy_manager_dialog::ResizeTrophyIcons() if (m_game_combo->count() <= 0) return; + if (m_trophy_repaint_watcher.isRunning()) + { + m_trophy_repaint_watcher.cancel(); + m_trophy_repaint_watcher.waitForFinished(); + } + const int db_pos = m_game_combo->currentData().toInt(); const qreal dpr = devicePixelRatioF(); const int new_height = m_icon_height * dpr; + QPixmap placeholder(m_icon_height, m_icon_height); + placeholder.fill(Qt::transparent); + QList trophy_ids; for (int i = 0; i < m_trophy_table->rowCount(); ++i) { @@ -549,11 +606,20 @@ void trophy_manager_dialog::ResizeTrophyIcons() { trophy_ids.append(item->text().toInt()); } + + if (QTableWidgetItem* icon_item = m_trophy_table->item(i, TrophyColumns::Icon)) + { + icon_item->setData(Qt::DecorationRole, placeholder); + } } - const std::function get_scaled = [this, data = m_trophies_db.at(db_pos).get(), dpr, new_height](const int& trophy_id) + ReadjustTrophyTable(); + + const std::function get_scaled = [this, data = m_trophies_db.at(db_pos).get(), dpr, new_height](const int& trophy_id) -> QPixmap { QPixmap icon; + + if (data) { std::scoped_lock lock(m_trophies_db_mtx); if (data->trophy_images.contains(trophy_id)) @@ -570,7 +636,7 @@ void trophy_manager_dialog::ResizeTrophyIcons() } } - QPixmap new_icon = QPixmap(icon.size() * dpr); + QPixmap new_icon(icon.size() * dpr); new_icon.setDevicePixelRatio(dpr); new_icon.fill(m_game_icon_color); @@ -585,18 +651,7 @@ void trophy_manager_dialog::ResizeTrophyIcons() return new_icon.scaledToHeight(new_height, Qt::SmoothTransformation); }; - - // NOTE: Due to a Qt bug in Qt 5.15.2, QtConcurrent::blockingMapped has a high risk of deadlocking. So let's just use QtConcurrent::mapped. - const QList scaled = QtConcurrent::mapped(trophy_ids, get_scaled).results(); - - for (int i = 0; i < m_trophy_table->rowCount() && i < scaled.count(); ++i) - { - QTableWidgetItem* icon_item = m_trophy_table->item(i, TrophyColumns::Icon); - if (icon_item) - icon_item->setData(Qt::DecorationRole, scaled.at(i)); - } - - ReadjustTrophyTable(); + m_trophy_repaint_watcher.setFuture(QtConcurrent::mapped(trophy_ids, get_scaled)); } void trophy_manager_dialog::ApplyFilter() @@ -605,10 +660,10 @@ void trophy_manager_dialog::ApplyFilter() return; const int db_pos = m_game_combo->currentData().toInt(); - if (db_pos + 0u >= m_trophies_db.size() || !m_trophies_db[db_pos]) + if (db_pos < 0 || static_cast(db_pos) >= m_trophies_db.size() || !m_trophies_db[db_pos]) return; - const auto trop_usr = m_trophies_db[db_pos]->trop_usr.get(); + const TROPUSRLoader* trop_usr = m_trophies_db[db_pos]->trop_usr.get(); if (!trop_usr) return; @@ -679,6 +734,16 @@ void trophy_manager_dialog::ShowContextMenu(const QPoint& pos) void trophy_manager_dialog::StartTrophyLoadThreads() { + if (m_game_repaint_watcher.isRunning()) + { + m_game_repaint_watcher.waitForFinished(); + } + + if (m_trophy_repaint_watcher.isRunning()) + { + m_trophy_repaint_watcher.waitForFinished(); + } + m_trophies_db.clear(); const QString trophy_path = qstr(vfs::get(m_trophy_dir)); @@ -738,6 +803,12 @@ void trophy_manager_dialog::StartTrophyLoadThreads() void trophy_manager_dialog::PopulateGameTable() { + if (m_game_repaint_watcher.isRunning()) + { + m_game_repaint_watcher.cancel(); + m_game_repaint_watcher.waitForFinished(); + } + m_game_table->setSortingEnabled(false); // Disable sorting before using setItem calls m_game_table->clearContents(); @@ -749,20 +820,8 @@ void trophy_manager_dialog::PopulateGameTable() for (usz i = 0; i < m_trophies_db.size(); ++i) indices.append(static_cast(i)); - const std::function get_icon = [this](const int& i) - { - // Load game icon - QPixmap icon; - const std::string icon_path = m_trophies_db[i]->path + "ICON0.PNG"; - if (!icon.load(qstr(icon_path))) - { - gui_log.warning("Could not load trophy game icon from path %s", icon_path); - } - return icon; - }; - - // NOTE: Due to a Qt bug in Qt 5.15.2, QtConcurrent::blockingMapped has a high risk of deadlocking. So let's just use QtConcurrent::mapped. - const QList icons = QtConcurrent::mapped(indices, get_icon).results(); + QPixmap placeholder(m_game_icon_size); + placeholder.fill(Qt::transparent); for (int i = 0; i < indices.count(); ++i) { @@ -773,8 +832,10 @@ void trophy_manager_dialog::PopulateGameTable() const QString name = qstr(m_trophies_db[i]->game_name).simplified(); custom_table_widget_item* icon_item = new custom_table_widget_item; - if (icons.count() > i) - icon_item->setData(Qt::UserRole, icons[i]); + icon_item->setData(Qt::DecorationRole, placeholder); + icon_item->setData(GameUserRole::GameIndex, i); + icon_item->setData(GameUserRole::GamePixmapLoaded, false); + icon_item->setData(GameUserRole::GamePixmap, QPixmap()); m_game_table->setItem(i, GameColumns::GameIcon, icon_item); m_game_table->setItem(i, GameColumns::GameName, new custom_table_widget_item(name)); @@ -785,10 +846,10 @@ void trophy_manager_dialog::PopulateGameTable() m_game_combo->model()->sort(0, Qt::AscendingOrder); - ResizeGameIcons(); - m_game_table->setSortingEnabled(true); // Enable sorting only after using setItem calls + ResizeGameIcons(); + gui::utils::resize_combo_box_view(m_game_combo); } @@ -821,6 +882,9 @@ void trophy_manager_dialog::PopulateTrophyTable() return; } + QPixmap placeholder(m_icon_height, m_icon_height); + placeholder.fill(Qt::transparent); + int i = 0; for (std::shared_ptr n = trophy_base->GetChildren(); n; n = n->GetNext()) { @@ -876,6 +940,7 @@ void trophy_manager_dialog::PopulateTrophyTable() custom_table_widget_item* icon_item = new custom_table_widget_item(); icon_item->setData(Qt::UserRole, hidden, true); + icon_item->setData(Qt::DecorationRole, placeholder); custom_table_widget_item* type_item = new custom_table_widget_item(trophy_type); type_item->setData(Qt::UserRole, static_cast(details.trophyGrade), true); @@ -920,7 +985,6 @@ void trophy_manager_dialog::ReadjustTrophyTable() const // Resize and fixate icon column m_trophy_table->resizeColumnToContents(TrophyColumns::Icon); - m_trophy_table->horizontalHeader()->setSectionResizeMode(TrophyColumns::Icon, QHeaderView::Fixed); // Shorten the last section to remove horizontal scrollbar if possible m_trophy_table->resizeColumnToContents(TrophyColumns::Count - 1); diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.h b/rpcs3/rpcs3qt/trophy_manager_dialog.h index 4fc0189656..c866ba2b02 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.h +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,13 @@ enum GameColumns GameColumnsCount }; +enum GameUserRole +{ + GameIndex = Qt::UserRole, + GamePixmapLoaded, + GamePixmap +}; + class trophy_manager_dialog : public QWidget { Q_OBJECT @@ -103,6 +111,8 @@ private: QSplitter* m_splitter; //! Contains the game and trophy tables game_list* m_trophy_table; //! UI element to display trophy stuff. QTableWidget* m_game_table; //! UI element to display games. + QFutureWatcher m_game_repaint_watcher; + QFutureWatcher m_trophy_repaint_watcher; bool m_show_hidden_trophies = false; bool m_show_unlocked_trophies = true;