QT Frontend: Add disk shader loading progress bar

Until we get a on screen display or async shader loading, we should at
least have some measure of progress in the meantime. This is 90% a port
from the loading screen I made for yuzu, but with a slightly different
changed detection for when to display the ETA. Now we keep track of a
rolling estimate for shader load ETA and only display a ETA if its going
to take longer than 10 seconds.
This commit is contained in:
James Rowe 2020-01-21 18:48:07 -07:00
parent c0df8271bf
commit 961a7b59c9
8 changed files with 542 additions and 6 deletions

View file

@ -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

View file

@ -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();

View file

@ -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;

View file

@ -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() {}

View file

@ -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);

View file

@ -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="0,0,0,1,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::AlignBottom|Qt::AlignHCenter</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 &quot;Arial&quot;;</string>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignTop</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 &quot;Arial&quot;;</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 &quot;Arial&quot;;</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>

View file

@ -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};

View file

@ -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;