diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index c75754fce..a461355c5 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -115,6 +115,9 @@ add_executable(citra-qt game_list_worker.h hotkeys.cpp hotkeys.h + loading_screen.cpp + loading_screen.h + loading_screen.ui main.cpp main.h main.ui diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index f28fcdf10..22b6bcdd7 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -46,12 +46,15 @@ void EmuThread::run() { MicroProfileOnThreadCreate("EmuThread"); Frontend::ScopeAcquireContext scope(core_context); + emit LoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); + Core::System::GetInstance().Renderer().Rasterizer()->LoadDiskResources( stop_run, [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { - LOG_DEBUG(Frontend, "Loading stage {} progress {} {}", static_cast<u32>(stage), value, - total); + emit LoadProgress(stage, value, total); }); + emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); + // Holds whether the cpu was running during the last iteration, // so that the DebugModeLeft signal can be emitted before the // next execution step. @@ -127,6 +130,7 @@ OpenGLWindow::~OpenGLWindow() { void OpenGLWindow::Present() { if (!isExposed()) return; + context->makeCurrent(this); VideoCore::g_renderer->TryPresent(100); context->swapBuffers(this); @@ -182,8 +186,8 @@ void OpenGLWindow::exposeEvent(QExposeEvent* event) { QWindow::exposeEvent(event); } -GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread) - : QWidget(parent), emu_thread(emu_thread) { +GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread) + : QWidget(parent_), emu_thread(emu_thread) { setWindowTitle(QStringLiteral("Citra %1 | %2-%3") .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); @@ -192,6 +196,9 @@ GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread) layout->setMargin(0); setLayout(layout); InputCommon::Init(); + + GMainWindow* parent = GetMainWindow(); + connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete); } GRenderWindow::~GRenderWindow() { @@ -206,7 +213,12 @@ void GRenderWindow::DoneCurrent() { core_context->DoneCurrent(); } -void GRenderWindow::PollEvents() {} +void GRenderWindow::PollEvents() { + if (!first_frame) { + first_frame = true; + emit FirstFrameDisplayed(); + } +} // On Qt 5.0+, this correctly gets the size of the framebuffer (pixels). // @@ -363,12 +375,15 @@ void GRenderWindow::resizeEvent(QResizeEvent* event) { void GRenderWindow::InitRenderTarget() { ReleaseRenderTarget(); + first_frame = false; + GMainWindow* parent = GetMainWindow(); QWindow* parent_win_handle = parent ? parent->windowHandle() : nullptr; child_window = new OpenGLWindow(parent_win_handle, this, QOpenGLContext::globalShareContext()); child_window->create(); child_widget = createWindowContainer(child_window, this); child_widget->resize(Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight); + layout()->addWidget(child_widget); core_context = CreateSharedContext(); diff --git a/src/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h index 922e093f9..ab9bd7197 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/bootmanager.h @@ -23,6 +23,10 @@ class QOpenGLContext; class GMainWindow; class GRenderWindow; +namespace VideoCore { +enum class LoadCallbackStage; +} + class GLContext : public Frontend::GraphicsContext { public: explicit GLContext(QOpenGLContext* shared_context); @@ -116,6 +120,8 @@ signals: void DebugModeLeft(); void ErrorThrown(Core::System::ResultStatus, std::string); + + void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); }; class OpenGLWindow : public QWindow { @@ -188,6 +194,11 @@ signals: /// Emitted when the window is closed void Closed(); + /** + * Emitted when the guest first calls SwapBuffers. This is used to hide the loading screen + */ + void FirstFrameDisplayed(); + private: std::pair<u32, u32> ScaleTouch(QPointF pos) const; void TouchBeginEvent(const QTouchEvent* event); @@ -212,6 +223,7 @@ private: /// Temporary storage of the screenshot taken QImage screenshot_image; + bool first_frame = false; protected: void showEvent(QShowEvent* event) override; diff --git a/src/citra_qt/loading_screen.cpp b/src/citra_qt/loading_screen.cpp new file mode 100644 index 000000000..8349245b5 --- /dev/null +++ b/src/citra_qt/loading_screen.cpp @@ -0,0 +1,212 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <unordered_map> +#include <QBuffer> +#include <QByteArray> +#include <QGraphicsOpacityEffect> +#include <QHBoxLayout> +#include <QIODevice> +#include <QImage> +#include <QLabel> +#include <QPainter> +#include <QPalette> +#include <QPixmap> +#include <QProgressBar> +#include <QPropertyAnimation> +#include <QStyleOption> +#include <QTime> +#include <QtConcurrent/QtConcurrentRun> +#include "citra_qt/loading_screen.h" +#include "common/logging/log.h" +#include "core/loader/loader.h" +#include "core/loader/smdh.h" +#include "ui_loading_screen.h" +#include "video_core/rasterizer_interface.h" + +constexpr char PROGRESSBAR_STYLE_PREPARE[] = R"( +QProgressBar {} +QProgressBar::chunk {})"; + +constexpr char PROGRESSBAR_STYLE_DECOMPILE[] = R"( +QProgressBar { + background-color: black; + border: 2px solid white; + border-radius: 4px; + padding: 2px; +} +QProgressBar::chunk { + background-color: #fd8507; + width: 1px; +})"; + +constexpr char PROGRESSBAR_STYLE_BUILD[] = R"( +QProgressBar { + background-color: black; + border: 2px solid white; + border-radius: 4px; + padding: 2px; +} +QProgressBar::chunk { + background-color: #ffe402; + width: 1px; +})"; + +constexpr char PROGRESSBAR_STYLE_COMPLETE[] = R"( +QProgressBar { + background-color: #fd8507; + border: 2px solid white; + border-radius: 4px; + padding: 2px; +} +QProgressBar::chunk { + background-color: #ffe402; +})"; + +// Definitions for the differences in text and styling for each stage +const static std::unordered_map<VideoCore::LoadCallbackStage, const char*> stage_translations{ + {VideoCore::LoadCallbackStage::Prepare, QT_TRANSLATE_NOOP("LoadingScreen", "Loading...")}, + {VideoCore::LoadCallbackStage::Decompile, + QT_TRANSLATE_NOOP("LoadingScreen", "Preparing Shaders %1 / %2")}, + {VideoCore::LoadCallbackStage::Build, + QT_TRANSLATE_NOOP("LoadingScreen", "Loading Shaders %1 / %2")}, + {VideoCore::LoadCallbackStage::Complete, QT_TRANSLATE_NOOP("LoadingScreen", "Launching...")}, +}; +const static std::unordered_map<VideoCore::LoadCallbackStage, const char*> progressbar_style{ + {VideoCore::LoadCallbackStage::Prepare, PROGRESSBAR_STYLE_PREPARE}, + {VideoCore::LoadCallbackStage::Decompile, PROGRESSBAR_STYLE_DECOMPILE}, + {VideoCore::LoadCallbackStage::Build, PROGRESSBAR_STYLE_BUILD}, + {VideoCore::LoadCallbackStage::Complete, PROGRESSBAR_STYLE_COMPLETE}, +}; + +static QPixmap GetQPixmapFromSMDH(std::vector<u8>& smdh_data) { + Loader::SMDH smdh; + memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + bool large = true; + std::vector<u16> icon_data = smdh.GetIcon(large); + const uchar* data = reinterpret_cast<const uchar*>(icon_data.data()); + int size = large ? 48 : 24; + QImage icon(data, size, size, QImage::Format::Format_RGB16); + return QPixmap::fromImage(icon); +} + +LoadingScreen::LoadingScreen(QWidget* parent) + : QWidget(parent), ui(std::make_unique<Ui::LoadingScreen>()), + previous_stage(VideoCore::LoadCallbackStage::Complete) { + ui->setupUi(this); + setMinimumSize(400, 240); + + // 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); + ui->fade_parent->setGraphicsEffect(opacity_effect); + fadeout_animation = std::make_unique<QPropertyAnimation>(opacity_effect, "opacity"); + fadeout_animation->setDuration(500); + fadeout_animation->setStartValue(1); + fadeout_animation->setEndValue(0); + fadeout_animation->setEasingCurve(QEasingCurve::OutBack); + + // After the fade completes, hide the widget and reset the opacity + connect(fadeout_animation.get(), &QPropertyAnimation::finished, [this] { + hide(); + opacity_effect->setOpacity(1); + emit Hidden(); + }); + connect(this, &LoadingScreen::LoadProgress, this, &LoadingScreen::OnLoadProgress, + Qt::QueuedConnection); + qRegisterMetaType<VideoCore::LoadCallbackStage>(); +} + +LoadingScreen::~LoadingScreen() = default; + +void LoadingScreen::Prepare(Loader::AppLoader& loader) { + std::vector<u8> buffer; + // TODO when banner becomes supported, decode it and add it as a movie + + if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) { + QPixmap icon = GetQPixmapFromSMDH(buffer); + ui->icon->setPixmap(icon); + } + std::string title; + if (loader.ReadTitle(title) == Loader::ResultStatus::Success) { + ui->title->setText(QString("Now Loading\n") + QString::fromStdString(title)); + } + eta_shown = false; + OnLoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); +} + +void LoadingScreen::OnLoadComplete() { + fadeout_animation->start(QPropertyAnimation::KeepWhenStopped); +} + +void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, + std::size_t total) { + using namespace std::chrono; + const auto now = high_resolution_clock::now(); + // reset the timer if the stage changes + if (stage != previous_stage) { + ui->progress_bar->setStyleSheet(progressbar_style.at(stage)); + // Hide the progress bar during the prepare stage + if (stage == VideoCore::LoadCallbackStage::Prepare) { + ui->progress_bar->hide(); + } else { + ui->progress_bar->show(); + } + previous_stage = stage; + } + // update the max of the progress bar if the number of shaders change + if (total != previous_total) { + ui->progress_bar->setMaximum(static_cast<int>(total)); + previous_total = total; + } + + // calculate a simple rolling average after the first shader is loaded + if (value > 0) { + rolling_average -= rolling_average / NumberOfDataPoints; + rolling_average += (now - previous_time) / NumberOfDataPoints; + } + + QString estimate; + + // After 25 shader load times were put into the rolling average, determine if the ETA is long + // enough to show it + if (value > NumberOfDataPoints && + (eta_shown || rolling_average * (total - value) > ETABreakPoint)) { + if (!eta_shown) { + eta_shown = true; + } + const auto eta_mseconds = std::chrono::duration_cast<std::chrono::milliseconds>( + rolling_average * (total - value)); + estimate = tr("Estimated Time %1") + .arg(QTime(0, 0, 0, 0) + .addMSecs(std::max<long>(eta_mseconds.count(), 1000)) + .toString(QStringLiteral("mm:ss"))); + } + + // update labels and progress bar + const auto& stg = tr(stage_translations.at(stage)); + if (stage == VideoCore::LoadCallbackStage::Decompile || + stage == VideoCore::LoadCallbackStage::Build) { + ui->stage->setText(stg.arg(value).arg(total)); + } else { + ui->stage->setText(stg); + } + ui->value->setText(estimate); + ui->progress_bar->setValue(static_cast<int>(value)); + previous_time = now; +} + +void LoadingScreen::paintEvent(QPaintEvent* event) { + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QWidget::paintEvent(event); +} + +void LoadingScreen::Clear() {} diff --git a/src/citra_qt/loading_screen.h b/src/citra_qt/loading_screen.h new file mode 100644 index 000000000..59e841f9d --- /dev/null +++ b/src/citra_qt/loading_screen.h @@ -0,0 +1,78 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <chrono> +#include <memory> +#include <QString> +#include <QWidget> + +namespace Loader { +class AppLoader; +} + +namespace Ui { +class LoadingScreen; +} + +namespace VideoCore { +enum class LoadCallbackStage; +} + +class QGraphicsOpacityEffect; +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(); + +private: + std::unique_ptr<Ui::LoadingScreen> ui; + std::size_t previous_total = 0; + VideoCore::LoadCallbackStage previous_stage; + + QGraphicsOpacityEffect* opacity_effect = nullptr; + std::unique_ptr<QPropertyAnimation> fadeout_animation; + + // Variables used to keep track of the current ETA. + // If the rolling_average * shaders_remaining > eta_break_point then we want to display the eta. + // We don't want to always display it since showing an ETA leads people to think its taking + // longer that it is because ETAs are often wrong + static constexpr std::chrono::seconds ETABreakPoint = std::chrono::seconds{10}; + static constexpr std::size_t NumberOfDataPoints = 25; + std::chrono::high_resolution_clock::time_point previous_time; + std::chrono::duration<double> rolling_average = {}; + bool eta_shown = false; +}; + +Q_DECLARE_METATYPE(VideoCore::LoadCallbackStage); diff --git a/src/citra_qt/loading_screen.ui b/src/citra_qt/loading_screen.ui new file mode 100644 index 000000000..769f3a35d --- /dev/null +++ b/src/citra_qt/loading_screen.ui @@ -0,0 +1,188 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LoadingScreen</class> + <widget class="QWidget" name="LoadingScreen"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>746</width> + <height>495</height> + </rect> + </property> + <property name="styleSheet"> + <string notr="true">background-color: rgb(0, 0, 0);</string> + </property> + <layout class="QVBoxLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="fade_parent" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0,0,0,1,0"> + <property name="spacing"> + <number>15</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetNoConstraint</enum> + </property> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="icon"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="margin"> + <number>5</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="title"> + <property name="styleSheet"> + <string notr="true">background-color: black; color: white; +font: 75 20pt "Arial";</string> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter|Qt::AlignBottom"> + <widget class="QLabel" name="stage"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">background-color: black; color: white; +font: 75 20pt "Arial";</string> + </property> + <property name="text"> + <string>Loading Shaders 387 / 1628</string> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter|Qt::AlignTop"> + <widget class="QProgressBar" name="progress_bar"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>500</width> + <height>40</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">QProgressBar { +color: white; +border: 2px solid white; +outline-color: black; +border-radius: 20px; +} +QProgressBar::chunk { +background-color: white; +border-radius: 15px; +}</string> + </property> + <property name="value"> + <number>50</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + <property name="format"> + <string>Loading Shaders %v out of %m</string> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter|Qt::AlignTop"> + <widget class="QLabel" name="value"> + <property name="toolTip"> + <string notr="true"/> + </property> + <property name="styleSheet"> + <string notr="true">background-color: black; color: white; +font: 75 15pt "Arial";</string> + </property> + <property name="text"> + <string>Estimated Time 5m 4s</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 863d0c5b8..33d3d66a1 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -47,6 +47,7 @@ #include "citra_qt/discord.h" #include "citra_qt/game_list.h" #include "citra_qt/hotkeys.h" +#include "citra_qt/loading_screen.h" #include "citra_qt/main.h" #include "citra_qt/multiplayer/state.h" #include "citra_qt/qt_image_interface.h" @@ -222,6 +223,17 @@ void GMainWindow::InitializeWidgets() { ui.horizontalLayout->addWidget(game_list_placeholder); game_list_placeholder->setVisible(false); + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui.horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + } + }); + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room, ui.action_Show_Room); multiplayer_state->setVisible(false); @@ -917,6 +929,9 @@ void GMainWindow::BootGame(const QString& filename) { connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + // Update the GUI registersWidget->OnDebugModeEntered(); if (ui.action_Single_Window_Mode->isChecked()) { @@ -925,8 +940,12 @@ void GMainWindow::BootGame(const QString& filename) { } status_bar_update_timer.start(2000); + // show and hide the render_window to create the context render_window->show(); - render_window->setFocus(); + render_window->hide(); + + loading_screen->Prepare(Core::System::GetInstance().GetAppLoader()); + loading_screen->show(); emulation_running = true; if (ui.action_Fullscreen->isChecked()) { @@ -1003,6 +1022,8 @@ void GMainWindow::ShutdownGame() { ui.action_Advance_Frame->setEnabled(false); ui.action_Capture_Screenshot->setEnabled(false); render_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); if (game_list->isEmpty()) game_list_placeholder->show(); else @@ -1326,6 +1347,10 @@ void GMainWindow::OnStopGame() { ShutdownGame(); } +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); +} + void GMainWindow::OnMenuReportCompatibility() { if (!Settings::values.citra_token.empty() && !Settings::values.citra_username.empty()) { CompatDB compatdb{this}; diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 7ec4fa185..91d7eed1b 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -32,6 +32,7 @@ class GraphicsVertexShaderWidget; class GRenderWindow; class IPCRecorderWidget; class LLEServiceModulesWidget; +class LoadingScreen; class MicroProfileDialog; class MultiplayerState; class ProfilerWidget; @@ -75,6 +76,7 @@ public: public slots: void OnAppFocusStateChanged(Qt::ApplicationState state); + void OnLoadComplete(); signals: @@ -221,6 +223,7 @@ private: GRenderWindow* render_window; GameListPlaceholder* game_list_placeholder; + LoadingScreen* loading_screen; // Status bar elements QProgressBar* progress_bar = nullptr;