diff --git a/src/citron/loading_screen.cpp b/src/citron/loading_screen.cpp index 99b697c39..f2b9f7a3f 100644 --- a/src/citron/loading_screen.cpp +++ b/src/citron/loading_screen.cpp @@ -1,54 +1,18 @@ // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include +#include "citron/loading_screen.h" #include -#include -#include #include -#include #include #include +#include +#include "citron/theme.h" #include "core/frontend/framebuffer_layout.h" #include "core/loader/loader.h" #include "ui_loading_screen.h" #include "video_core/rasterizer_interface.h" -#include "citron/loading_screen.h" - -// Mingw seems to not have QMovie at all. If QMovie is missing then use a single frame instead of an -// showing the full animation -#if !CITRON_QT_MOVIE_MISSING -#include -#endif - -constexpr char PROGRESSBAR_STYLE_PREPARE[] = R"( -QProgressBar {} -QProgressBar::chunk {})"; - -constexpr char PROGRESSBAR_STYLE_BUILD[] = R"( -QProgressBar { - background-color: black; - border: 2px solid white; - border-radius: 4px; - padding: 2px; -} -QProgressBar::chunk { - background-color: #ff3c28; - width: 1px; -})"; - -constexpr char PROGRESSBAR_STYLE_COMPLETE[] = R"( -QProgressBar { - background-color: #0ab9e6; - border: 2px solid white; - border-radius: 4px; - padding: 2px; -} -QProgressBar::chunk { - background-color: #ff3c28; -})"; LoadingScreen::LoadingScreen(QWidget* parent) : QWidget(parent), ui(std::make_unique()), @@ -56,132 +20,188 @@ LoadingScreen::LoadingScreen(QWidget* parent) ui->setupUi(this); setMinimumSize(Layout::MinimumSize::Width, Layout::MinimumSize::Height); - // Create a fade out effect to hide this loading screen widget. - // When fading opacity, it will fade to the parent widgets background color, which is why we - // create an internal widget named fade_widget that we use the effect on, while keeping the - // loading screen widget's background color black. This way we can create a fade to black effect - opacity_effect = new QGraphicsOpacityEffect(this); - opacity_effect->setOpacity(1); + opacity_effect = new QGraphicsOpacityEffect(ui->fade_parent); ui->fade_parent->setGraphicsEffect(opacity_effect); - fadeout_animation = std::make_unique(opacity_effect, "opacity"); - fadeout_animation->setDuration(500); - fadeout_animation->setStartValue(1); - fadeout_animation->setEndValue(0); - fadeout_animation->setEasingCurve(QEasingCurve::OutBack); + fadeout_animation = new QPropertyAnimation(opacity_effect, "opacity", this); + fadeout_animation->setDuration(400); + fadeout_animation->setEasingCurve(QEasingCurve::OutQuad); + fadeout_animation->setStartValue(1.0); + fadeout_animation->setEndValue(0.0); - // After the fade completes, hide the widget and reset the opacity - connect(fadeout_animation.get(), &QPropertyAnimation::finished, [this] { + connect(fadeout_animation, &QPropertyAnimation::finished, this, [this] { hide(); - opacity_effect->setOpacity(1); + opacity_effect->setOpacity(1.0); emit Hidden(); }); + + loading_text_animation_timer = new QTimer(this); + connect(loading_text_animation_timer, &QTimer::timeout, this, &LoadingScreen::UpdateLoadingText); + connect(this, &LoadingScreen::LoadProgress, this, &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); qRegisterMetaType(); - - stage_translations = { - {VideoCore::LoadCallbackStage::Prepare, tr("Loading...")}, - {VideoCore::LoadCallbackStage::Build, tr("Loading Shaders %1 / %2")}, - {VideoCore::LoadCallbackStage::Complete, tr("Launching...")}, - }; - progressbar_style = { - {VideoCore::LoadCallbackStage::Prepare, PROGRESSBAR_STYLE_PREPARE}, - {VideoCore::LoadCallbackStage::Build, PROGRESSBAR_STYLE_BUILD}, - {VideoCore::LoadCallbackStage::Complete, PROGRESSBAR_STYLE_COMPLETE}, - }; } -LoadingScreen::~LoadingScreen() = default; +LoadingScreen::~LoadingScreen() { + loading_text_animation_timer->stop(); +} void LoadingScreen::Prepare(Loader::AppLoader& loader) { + QPixmap game_icon_pixmap; std::vector buffer; - if (loader.ReadBanner(buffer) == Loader::ResultStatus::Success) { -#ifdef CITRON_QT_MOVIE_MISSING - QPixmap map; - map.loadFromData(buffer.data(), buffer.size()); - ui->banner->setPixmap(map); -#else - backing_mem = std::make_unique(reinterpret_cast(buffer.data()), - static_cast(buffer.size())); - backing_buf = std::make_unique(backing_mem.get()); - backing_buf->open(QIODevice::ReadOnly); - animation = std::make_unique(backing_buf.get(), QByteArray()); - animation->start(); - ui->banner->setMovie(animation.get()); -#endif - buffer.clear(); + if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) { + game_icon_pixmap.loadFromData(buffer.data(), static_cast(buffer.size())); + } else { + game_icon_pixmap = QPixmap(QStringLiteral(":/icons/scalable/actions/games.svg")); } - if (loader.ReadLogo(buffer) == Loader::ResultStatus::Success) { - QPixmap map; - map.loadFromData(buffer.data(), static_cast(buffer.size())); - ui->logo->setPixmap(map); + + if (!game_icon_pixmap.isNull()) { + QPixmap rounded_pixmap(game_icon_pixmap.size()); + rounded_pixmap.fill(Qt::transparent); + QPainter painter(&rounded_pixmap); + painter.setRenderHint(QPainter::Antialiasing); + QPainterPath path; + const int radius = game_icon_pixmap.width() / 6; + path.addRoundedRect(rounded_pixmap.rect(), radius, radius); + painter.setClipPath(path); + painter.drawPixmap(0, 0, game_icon_pixmap); + ui->game_icon->setPixmap(rounded_pixmap.scaled(ui->game_icon->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } else { + ui->game_icon->setPixmap(game_icon_pixmap); + } + + std::string title; + if (loader.ReadTitle(title) == Loader::ResultStatus::Success && !title.empty()) { + stage_translations = { + {VideoCore::LoadCallbackStage::Prepare, tr("Loading %1").arg(QString::fromStdString(title))}, + {VideoCore::LoadCallbackStage::Build, tr("Loading %1").arg(QString::fromStdString(title))}, + {VideoCore::LoadCallbackStage::Complete, tr("Launching...")}, + }; + } else { + stage_translations = { + {VideoCore::LoadCallbackStage::Prepare, tr("Loading Game...")}, + {VideoCore::LoadCallbackStage::Build, tr("Loading Game...")}, + {VideoCore::LoadCallbackStage::Complete, tr("Launching...")}, + }; } slow_shader_compile_start = false; OnLoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); } +void LoadingScreen::showEvent(QShowEvent* event) { + opacity_effect->setOpacity(0.0); + auto fade_in = new QPropertyAnimation(opacity_effect, "opacity", this); + fade_in->setDuration(400); + fade_in->setStartValue(0.0); + fade_in->setEndValue(1.0); + fade_in->setEasingCurve(QEasingCurve::OutQuad); + fade_in->start(QAbstractAnimation::DeleteWhenStopped); + + QWidget::showEvent(event); +} + +void LoadingScreen::UpdateLoadingText() { + int dot_count = ui->stage->text().count(QLatin1Char('.')); + QString new_text = base_loading_text; + if (dot_count >= 3) { + // Cycle back to no dots + } else { + new_text.append(QString(dot_count + 1, QLatin1Char('.'))); + } + ui->stage->setText(new_text); +} + void LoadingScreen::OnLoadComplete() { - fadeout_animation->start(QPropertyAnimation::KeepWhenStopped); + loading_text_animation_timer->stop(); + fadeout_animation->start(); } void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { using namespace std::chrono; const auto now = steady_clock::now(); - // reset the timer if the stage changes + if (stage != previous_stage) { - ui->progress_bar->setStyleSheet(QString::fromUtf8(progressbar_style[stage])); - // Hide the progress bar during the prepare stage - if (stage == VideoCore::LoadCallbackStage::Prepare) { - ui->progress_bar->hide(); - } else { - ui->progress_bar->show(); + QString style; + switch (stage) { + case VideoCore::LoadCallbackStage::Build: + style = QString::fromUtf8(R"( + QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; } + QProgressBar::chunk { background-color: %1; border-radius: 4px; } + )").arg(Theme::GetAccentColor()); + break; + case VideoCore::LoadCallbackStage::Complete: + style = QString::fromUtf8(R"( + QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; } + QProgressBar::chunk { background-color: %1; border-radius: 4px; } + )").arg(Theme::GetAccentColor()); + break; + default: + style = QStringLiteral(""); + break; } + ui->shader_progress_bar->setStyleSheet(style); + ui->progress_bar->setStyleSheet(style); + + base_loading_text = stage_translations[stage]; + const QFontMetrics metrics(ui->stage->font()); + const int max_width = metrics.horizontalAdvance(base_loading_text + QLatin1String("...")); + ui->stage->setFixedWidth(max_width); + ui->stage->setText(base_loading_text); + + if (stage == VideoCore::LoadCallbackStage::Complete) { + loading_text_animation_timer->stop(); + } else { + loading_text_animation_timer->start(500); + } + + ui->progress_bar->setVisible(stage == VideoCore::LoadCallbackStage::Complete); + ui->shader_widget->setVisible(stage == VideoCore::LoadCallbackStage::Build); + previous_stage = stage; - // reset back to fast shader compiling since the stage changed slow_shader_compile_start = false; } - // update the max of the progress bar if the number of shaders change - if (total != previous_total) { - ui->progress_bar->setMaximum(static_cast(total)); - previous_total = total; - } - // Reset the progress bar ranges if compilation is done + if (stage == VideoCore::LoadCallbackStage::Complete) { ui->progress_bar->setRange(0, 0); } - QString estimate; - // If there's a drastic slowdown in the rate, then display an estimate - if (now - previous_time > milliseconds{50} || slow_shader_compile_start) { - if (!slow_shader_compile_start) { - slow_shader_start = steady_clock::now(); - slow_shader_compile_start = true; - slow_shader_first_value = value; - } - // only calculate an estimate time after a second has passed since stage change - const auto diff = duration_cast(now - slow_shader_start); - if (diff > seconds{1}) { - const auto eta_mseconds = - static_cast(static_cast(total - slow_shader_first_value) / - (value - slow_shader_first_value) * diff.count()); - estimate = - tr("Estimated Time %1") - .arg(QTime(0, 0, 0, 0) - .addMSecs(std::max(eta_mseconds - diff.count() + 1000, 1000)) - .toString(QStringLiteral("mm:ss"))); - } - } - - // update labels and progress bar if (stage == VideoCore::LoadCallbackStage::Build) { - ui->stage->setText(stage_translations[stage].arg(value).arg(total)); - } else { - ui->stage->setText(stage_translations[stage]); + if (total != previous_total) { + ui->shader_progress_bar->setMaximum(static_cast(total)); + previous_total = total; + } + ui->shader_progress_bar->setValue(static_cast(value)); + + QString estimate; + if (now - previous_time > milliseconds{50} || slow_shader_compile_start) { + if (!slow_shader_compile_start) { + slow_shader_start = steady_clock::now(); + slow_shader_compile_start = true; + slow_shader_first_value = value; + } + const auto diff = duration_cast(now - slow_shader_start); + if (diff > seconds{1} && (value - slow_shader_first_value > 0)) { + const auto eta_mseconds = + static_cast(static_cast(total - slow_shader_first_value) / + (value - slow_shader_first_value) * diff.count()); + estimate = + tr("ETA: %1") + .arg(QTime(0, 0, 0, 0) + .addMSecs(std::max(eta_mseconds - diff.count(), 0)) + .toString(QStringLiteral("mm:ss"))); + } + } + + ui->shader_stage_label->setText(tr("Building Shaders...")); + + if (!estimate.isEmpty()) { + ui->shader_value_label->setText(QStringLiteral("%1 / %2 (%3)").arg(value).arg(total).arg(estimate)); + } else { + ui->shader_value_label->setText(QStringLiteral("%1 / %2").arg(value).arg(total)); + } } - ui->value->setText(estimate); - ui->progress_bar->setValue(static_cast(value)); previous_time = now; } @@ -190,13 +210,9 @@ void LoadingScreen::paintEvent(QPaintEvent* event) { opt.initFrom(this); QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - QWidget::paintEvent(event); } void LoadingScreen::Clear() { -#ifndef CITRON_QT_MOVIE_MISSING - animation.reset(); - backing_buf.reset(); - backing_mem.reset(); -#endif + ui->game_icon->clear(); + loading_text_animation_timer->stop(); } diff --git a/src/citron/loading_screen.h b/src/citron/loading_screen.h index dae3c4c6a..479ddaa4f 100644 --- a/src/citron/loading_screen.h +++ b/src/citron/loading_screen.h @@ -1,88 +1,67 @@ // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include +#include +#include +#include #include +#include #include #include -#if !QT_CONFIG(movie) -#define CITRON_QT_MOVIE_MISSING 1 -#endif +class QGraphicsOpacityEffect; +class QPropertyAnimation; namespace Loader { class AppLoader; } - namespace Ui { class LoadingScreen; } - namespace VideoCore { enum class LoadCallbackStage; } -class QBuffer; -class QByteArray; -class QGraphicsOpacityEffect; -class QMovie; -class QPropertyAnimation; - class LoadingScreen : public QWidget { Q_OBJECT public: explicit LoadingScreen(QWidget* parent = nullptr); - ~LoadingScreen(); - /// Call before showing the loading screen to load the widgets with the logo and banner for the - /// currently loaded application. void Prepare(Loader::AppLoader& loader); - - /// After the loading screen is hidden, the owner of this class can call this to clean up any - /// used resources such as the logo and banner. void Clear(); - - /// Slot used to update the status of the progress bar void OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); - - /// Hides the LoadingScreen with a fade out effect void OnLoadComplete(); - - // In order to use a custom widget with a stylesheet, you need to override the paintEvent - // See https://wiki.qt.io/How_to_Change_the_Background_Color_of_QWidget void paintEvent(QPaintEvent* event) override; signals: void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); - /// Signals that this widget is completely hidden now and should be replaced with the other - /// widget void Hidden(); +protected: + void showEvent(QShowEvent* event) override; + +private slots: + void UpdateLoadingText(); + private: -#ifndef CITRON_QT_MOVIE_MISSING - std::unique_ptr animation; - std::unique_ptr backing_buf; - std::unique_ptr backing_mem; -#endif std::unique_ptr ui; std::size_t previous_total = 0; VideoCore::LoadCallbackStage previous_stage; QGraphicsOpacityEffect* opacity_effect = nullptr; - std::unique_ptr fadeout_animation; + QPropertyAnimation* fadeout_animation = nullptr; + QTimer* loading_text_animation_timer = nullptr; - // Definitions for the differences in text and styling for each stage - std::unordered_map progressbar_style; std::unordered_map stage_translations; + QString base_loading_text; - // newly generated shaders are added to the end of the file, so when loading and compiling - // shaders, it will start quickly but end slow if new shaders were added since previous launch. - // These variables are used to detect the change in speed so we can generate an ETA bool slow_shader_compile_start = false; std::chrono::steady_clock::time_point slow_shader_start; std::chrono::steady_clock::time_point previous_time; diff --git a/src/citron/loading_screen.ui b/src/citron/loading_screen.ui index 820b47536..aabc55376 100644 --- a/src/citron/loading_screen.ui +++ b/src/citron/loading_screen.ui @@ -6,142 +6,49 @@ 0 0 - 746 - 495 + 1280 + 720 background-color: rgb(0, 0, 0); - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - + + + + + Qt::Horizontal + + + + 40 + 20 + + + + - + + background-color: transparent; + + - 0 + 30 - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - 30 - - - - - - 15 + + + + 256 + 256 + - - QLayout::SetNoConstraint - - - - - - 0 - 0 - - - - background-color: black; color: white; -font: 75 20pt "Arial"; - - - Loading Shaders 387 / 1628 - - - - - - - - 0 - 0 - - - - - 500 - 40 - - - - QProgressBar { -color: white; -border: 2px solid white; -outline-color: black; -border-radius: 20px; -} -QProgressBar::chunk { -background-color: white; -border-radius: 15px; -} - - - 50 - - - false - - - Loading Shaders %v out of %m - - - - - - - - - - background-color: black; color: white; -font: 75 15pt "Arial"; - - - Estimated Time 5m 4s - - - - - - - - - background-color: black; + + + 256 + 256 + @@ -149,14 +56,160 @@ font: 75 15pt "Arial"; Qt::AlignCenter - - 30 - + + + + + + + 10 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + background-color: transparent; color: white; + font: 75 24pt "Arial"; + + + Loading... + + + Qt::AlignVCenter|Qt::AlignLeft + + + + + + + + 0 + 12 + + + + + 16777215 + 12 + + + + 0 + + + false + + + + + + + + 5 + + + 0 + + + 15 + + + 0 + + + 0 + + + + + background-color: transparent; color: white; + font: 18pt "Arial"; + + + Building Shaders + + + + + + + + 0 + 8 + + + + + 16777215 + 8 + + + + 0 + + + false + + + + + + + background-color: transparent; color: #cccccc; + font: 14pt "Arial"; + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + +