From 08fcf41b0a3d4e6066cb72f47c3e1d94bb7fc408 Mon Sep 17 00:00:00 2001
From: James Rowe <jroweboy@gmail.com>
Date: Thu, 17 Jan 2019 00:01:00 -0700
Subject: [PATCH] QT Frontend: Add a Loading screen with progressbar

With shader caches on the horizon, one requirement is to provide visible
feedback for the progress. The shader cache reportedly takes several
minutes to load for large caches that were invalidated, and as such we
should provide a loading screen with progress.

Adds a loading screen widget that will be shown until the first frame of
the game is swapped. This was chosen in case shader caches are not being
used, several games still take more than a few seconds to launch and
could benefit from a loading screen.
---
 CMakeModules/CopyYuzuQt5Deps.cmake |  5 +-
 src/yuzu/CMakeLists.txt            |  7 ++-
 src/yuzu/bootmanager.cpp           | 11 ++++-
 src/yuzu/bootmanager.h             |  3 ++
 src/yuzu/loading_screen.cpp        | 71 +++++++++++++++++++++++++++
 src/yuzu/loading_screen.h          | 50 +++++++++++++++++++
 src/yuzu/loading_screen.ui         | 79 ++++++++++++++++++++++++++++++
 src/yuzu/main.cpp                  | 26 +++++++---
 src/yuzu/main.h                    |  4 +-
 9 files changed, 244 insertions(+), 12 deletions(-)
 create mode 100644 src/yuzu/loading_screen.cpp
 create mode 100644 src/yuzu/loading_screen.h
 create mode 100644 src/yuzu/loading_screen.ui

diff --git a/CMakeModules/CopyYuzuQt5Deps.cmake b/CMakeModules/CopyYuzuQt5Deps.cmake
index 4fef666594..1e9810bba2 100644
--- a/CMakeModules/CopyYuzuQt5Deps.cmake
+++ b/CMakeModules/CopyYuzuQt5Deps.cmake
@@ -45,5 +45,8 @@ function(copy_yuzu_Qt5_deps target_dir)
 
     windows_copy_files(yuzu ${Qt5_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$<CONFIG:Debug>:d>.*)
     windows_copy_files(yuzu ${Qt5_STYLES_DIR} ${STYLES} qwindowsvistastyle$<$<CONFIG:Debug>:d>.*)
-    windows_copy_files(yuzu ${Qt5_IMAGEFORMATS_DIR} ${IMAGEFORMATS} qjpeg$<$<CONFIG:Debug>:d>.*)
+    windows_copy_files(yuzu ${Qt5_IMAGEFORMATS_DIR} ${IMAGEFORMATS}
+        qjpeg$<$<CONFIG:Debug>:d>.*
+        qgif$<$<CONFIG:Debug>:d>.*
+        )
 endfunction(copy_yuzu_Qt5_deps)
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 1f852df4bc..4cab599b4d 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -68,6 +68,8 @@ add_executable(yuzu
     game_list_p.h
     game_list_worker.cpp
     game_list_worker.h
+    loading_screen.cpp
+    loading_screen.h
     hotkeys.cpp
     hotkeys.h
     main.cpp
@@ -102,9 +104,10 @@ set(UIS
     configuration/configure_system.ui
     configuration/configure_touchscreen_advanced.ui
     configuration/configure_web.ui
-    hotkeys.ui
-    main.ui
     compatdb.ui
+    hotkeys.ui
+    loading_screen.ui
+    main.ui
 )
 
 file(GLOB COMPAT_LIST
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp
index 40db7a5e92..f74cb693a4 100644
--- a/src/yuzu/bootmanager.cpp
+++ b/src/yuzu/bootmanager.cpp
@@ -3,9 +3,7 @@
 #include <QKeyEvent>
 #include <QScreen>
 #include <QWindow>
-
 #include <fmt/format.h>
-
 #include "common/microprofile.h"
 #include "common/scm_rev.h"
 #include "core/core.h"
@@ -17,6 +15,7 @@
 #include "video_core/renderer_base.h"
 #include "video_core/video_core.h"
 #include "yuzu/bootmanager.h"
+#include "yuzu/main.h"
 
 EmuThread::EmuThread(GRenderWindow* render_window) : render_window(render_window) {}
 
@@ -114,6 +113,8 @@ GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread)
 
     InputCommon::Init();
     InputCommon::StartJoystickEventHandler();
+    connect(this, &GRenderWindow::FirstFrameDisplayed, static_cast<GMainWindow*>(parent),
+            &GMainWindow::OnLoadComplete);
 }
 
 GRenderWindow::~GRenderWindow() {
@@ -141,6 +142,10 @@ void GRenderWindow::SwapBuffers() {
     child->makeCurrent();
 
     child->swapBuffers();
+    if (!first_frame) {
+        emit FirstFrameDisplayed();
+        first_frame = true;
+    }
 }
 
 void GRenderWindow::MakeCurrent() {
@@ -309,6 +314,8 @@ void GRenderWindow::InitRenderTarget() {
         delete layout();
     }
 
+    first_frame = false;
+
     // TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground,
     // WA_DontShowOnScreen, WA_DeleteOnClose
     QGLFormat fmt;
diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h
index 4e30282155..d1f37e5032 100644
--- a/src/yuzu/bootmanager.h
+++ b/src/yuzu/bootmanager.h
@@ -152,6 +152,7 @@ public slots:
 signals:
     /// Emitted when the window is closed
     void Closed();
+    void FirstFrameDisplayed();
 
 private:
     std::pair<unsigned, unsigned> ScaleTouch(const QPointF pos) const;
@@ -171,6 +172,8 @@ private:
     /// Temporary storage of the screenshot taken
     QImage screenshot_image;
 
+    bool first_frame = false;
+
 protected:
     void showEvent(QShowEvent* event) override;
 };
diff --git a/src/yuzu/loading_screen.cpp b/src/yuzu/loading_screen.cpp
new file mode 100644
index 0000000000..f2d3214f6f
--- /dev/null
+++ b/src/yuzu/loading_screen.cpp
@@ -0,0 +1,71 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QBuffer>
+#include <QByteArray>
+#include <QHBoxLayout>
+#include <QIODevice>
+#include <QImage>
+#include <QLabel>
+#include <QMovie>
+#include <QPainter>
+#include <QPalette>
+#include <QPixmap>
+#include <QProgressBar>
+#include <QStyleOption>
+#include <QWindow>
+#include "common/logging/log.h"
+#include "core/loader/loader.h"
+#include "ui_loading_screen.h"
+#include "yuzu/loading_screen.h"
+
+LoadingScreen::LoadingScreen(QWidget* parent)
+    : QWidget(parent), ui(std::make_unique<Ui::LoadingScreen>()) {
+    ui->setupUi(this);
+    // Progress bar is hidden until we have a use for it.
+    ui->progress_bar->hide();
+}
+
+LoadingScreen::~LoadingScreen() = default;
+
+void LoadingScreen::Prepare(Loader::AppLoader& loader) {
+    std::vector<u8> buffer;
+    if (loader.ReadBanner(buffer) == Loader::ResultStatus::Success) {
+        backing_mem =
+            std::make_unique<QByteArray>(reinterpret_cast<char*>(buffer.data()), buffer.size());
+        backing_buf = std::make_unique<QBuffer>(backing_mem.get());
+        backing_buf->open(QIODevice::ReadOnly);
+        animation = std::make_unique<QMovie>(backing_buf.get(), QByteArray("GIF"));
+        animation->start();
+        ui->banner->setMovie(animation.get());
+        buffer.clear();
+    }
+    if (loader.ReadLogo(buffer) == Loader::ResultStatus::Success) {
+        QPixmap map;
+        map.loadFromData(buffer.data(), buffer.size());
+        ui->logo->setPixmap(map);
+    }
+}
+
+void LoadingScreen::OnLoadProgress(std::size_t value, std::size_t total) {
+    if (total != previous_total) {
+        ui->progress_bar->setMaximum(total);
+        previous_total = total;
+    }
+    ui->progress_bar->setValue(value);
+}
+
+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() {
+    animation.reset();
+    backing_buf.reset();
+    backing_mem.reset();
+}
diff --git a/src/yuzu/loading_screen.h b/src/yuzu/loading_screen.h
new file mode 100644
index 0000000000..ffcaa260df
--- /dev/null
+++ b/src/yuzu/loading_screen.h
@@ -0,0 +1,50 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QWidget>
+
+namespace Loader {
+class AppLoader;
+}
+
+namespace Ui {
+class LoadingScreen;
+}
+
+class QBuffer;
+class QByteArray;
+class QMovie;
+
+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();
+
+    // 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;
+
+    void OnLoadProgress(std::size_t value, std::size_t total);
+
+private:
+    std::unique_ptr<QMovie> animation;
+    std::unique_ptr<QBuffer> backing_buf;
+    std::unique_ptr<QByteArray> backing_mem;
+    std::unique_ptr<Ui::LoadingScreen> ui;
+    std::size_t previous_total = 0;
+};
diff --git a/src/yuzu/loading_screen.ui b/src/yuzu/loading_screen.ui
new file mode 100644
index 0000000000..00579b6709
--- /dev/null
+++ b/src/yuzu/loading_screen.ui
@@ -0,0 +1,79 @@
+<?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="QLabel" name="logo">
+     <property name="text">
+      <string/>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+     </property>
+     <property name="margin">
+      <number>30</number>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QProgressBar" name="progress_bar">
+       <property name="styleSheet">
+        <string notr="true">font-size: 26px;</string>
+       </property>
+       <property name="value">
+        <number>0</number>
+       </property>
+       <property name="format">
+        <string>Loading Shaders %v out of %m</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item alignment="Qt::AlignRight|Qt::AlignBottom">
+    <widget class="QLabel" name="banner">
+     <property name="styleSheet">
+      <string notr="true">background-color: black;</string>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+     <property name="margin">
+      <number>30</number>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index f564de9948..68bfa23ab1 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -92,6 +92,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include "yuzu/game_list.h"
 #include "yuzu/game_list_p.h"
 #include "yuzu/hotkeys.h"
+#include "yuzu/loading_screen.h"
 #include "yuzu/main.h"
 #include "yuzu/ui_settings.h"
 
@@ -411,6 +412,10 @@ void GMainWindow::InitializeWidgets() {
     game_list = new GameList(vfs, this);
     ui.horizontalLayout->addWidget(game_list);
 
+    loading_screen = new LoadingScreen(this);
+    loading_screen->hide();
+    ui.horizontalLayout->addWidget(loading_screen);
+
     // Create status bar
     message_label = new QLabel();
     // Configured separately for left alignment
@@ -897,8 +902,9 @@ void GMainWindow::BootGame(const QString& filename) {
                        .arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc,
                             QString::fromStdString(title_name)));
 
-    render_window->show();
-    render_window->setFocus();
+    loading_screen->Prepare(Core::System::GetInstance().GetAppLoader());
+    loading_screen->show();
+    loading_screen->setFocus();
 
     emulation_running = true;
     if (ui.action_Fullscreen->isChecked()) {
@@ -932,6 +938,8 @@ void GMainWindow::ShutdownGame() {
     ui.action_Load_Amiibo->setEnabled(false);
     ui.action_Capture_Screenshot->setEnabled(false);
     render_window->hide();
+    loading_screen->hide();
+    loading_screen->Clear();
     game_list->show();
     game_list->setFilterFocus();
     setWindowTitle(QString("yuzu %1| %2-%3")
@@ -1505,6 +1513,13 @@ void GMainWindow::OnStopGame() {
     ShutdownGame();
 }
 
+void GMainWindow::OnLoadComplete() {
+    loading_screen->hide();
+    loading_screen->Clear();
+    render_window->show();
+    render_window->setFocus();
+}
+
 void GMainWindow::OnMenuReportCompatibility() {
     if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) {
         CompatDB compatdb{this};
@@ -1771,9 +1786,8 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
             this, tr("Confirm Key Rederivation"),
             tr("You are about to force rederive all of your keys. \nIf you do not know what this "
                "means or what you are doing, \nthis is a potentially destructive action. \nPlease "
-               "make "
-               "sure this is what you want \nand optionally make backups.\n\nThis will delete your "
-               "autogenerated key files and re-run the key derivation module."),
+               "make sure this is what you want \nand optionally make backups.\n\nThis will delete "
+               "your autogenerated key files and re-run the key derivation module."),
             QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel});
 
         if (res == QMessageBox::Cancel)
@@ -1818,7 +1832,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
                     errors +
                     tr("<br><br>You can get all of these and dump all of your games easily by "
                        "following <a href='https://yuzu-emu.org/help/quickstart/'>the "
-                       "quickstart guide</a>. Alternatively, you can use another method of dumping "
+                       "quickstart guide</a>. Alternatively, you can use another method of dumping"
                        "to obtain all of your keys."));
         }
 
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 2d705ad54d..e07c892cf2 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -25,6 +25,7 @@ class GImageInfo;
 class GraphicsBreakPointsWidget;
 class GraphicsSurfaceWidget;
 class GRenderWindow;
+class LoadingScreen;
 class MicroProfileDialog;
 class ProfilerWidget;
 class QLabel;
@@ -109,10 +110,10 @@ signals:
     void WebBrowserFinishedBrowsing();
 
 public slots:
+    void OnLoadComplete();
     void ProfileSelectorSelectProfile();
     void SoftwareKeyboardGetText(const Core::Frontend::SoftwareKeyboardParameters& parameters);
     void SoftwareKeyboardInvokeCheckDialog(std::u16string error_message);
-
     void WebBrowserOpenPage(std::string_view filename, std::string_view arguments);
 
 private:
@@ -212,6 +213,7 @@ private:
 
     GRenderWindow* render_window;
     GameList* game_list;
+    LoadingScreen* loading_screen;
 
     // Status bar elements
     QLabel* message_label = nullptr;