08fcf41b0a
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.
382 lines
12 KiB
C++
382 lines
12 KiB
C++
#include <QApplication>
|
|
#include <QHBoxLayout>
|
|
#include <QKeyEvent>
|
|
#include <QScreen>
|
|
#include <QWindow>
|
|
#include <fmt/format.h>
|
|
#include "common/microprofile.h"
|
|
#include "common/scm_rev.h"
|
|
#include "core/core.h"
|
|
#include "core/frontend/framebuffer_layout.h"
|
|
#include "core/settings.h"
|
|
#include "input_common/keyboard.h"
|
|
#include "input_common/main.h"
|
|
#include "input_common/motion_emu.h"
|
|
#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) {}
|
|
|
|
void EmuThread::run() {
|
|
if (!Settings::values.use_multi_core) {
|
|
// Single core mode must acquire OpenGL context for entire emulation session
|
|
render_window->MakeCurrent();
|
|
}
|
|
|
|
MicroProfileOnThreadCreate("EmuThread");
|
|
|
|
stop_run = false;
|
|
|
|
// holds whether the cpu was running during the last iteration,
|
|
// so that the DebugModeLeft signal can be emitted before the
|
|
// next execution step
|
|
bool was_active = false;
|
|
while (!stop_run) {
|
|
if (running) {
|
|
if (!was_active)
|
|
emit DebugModeLeft();
|
|
|
|
Core::System::ResultStatus result = Core::System::GetInstance().RunLoop();
|
|
if (result != Core::System::ResultStatus::Success) {
|
|
this->SetRunning(false);
|
|
emit ErrorThrown(result, Core::System::GetInstance().GetStatusDetails());
|
|
}
|
|
|
|
was_active = running || exec_step;
|
|
if (!was_active && !stop_run)
|
|
emit DebugModeEntered();
|
|
} else if (exec_step) {
|
|
if (!was_active)
|
|
emit DebugModeLeft();
|
|
|
|
exec_step = false;
|
|
Core::System::GetInstance().SingleStep();
|
|
emit DebugModeEntered();
|
|
yieldCurrentThread();
|
|
|
|
was_active = false;
|
|
} else {
|
|
std::unique_lock<std::mutex> lock(running_mutex);
|
|
running_cv.wait(lock, [this] { return IsRunning() || exec_step || stop_run; });
|
|
}
|
|
}
|
|
|
|
// Shutdown the core emulation
|
|
Core::System::GetInstance().Shutdown();
|
|
|
|
#if MICROPROFILE_ENABLED
|
|
MicroProfileOnThreadExit();
|
|
#endif
|
|
|
|
render_window->moveContext();
|
|
}
|
|
|
|
// This class overrides paintEvent and resizeEvent to prevent the GUI thread from stealing GL
|
|
// context.
|
|
// The corresponding functionality is handled in EmuThread instead
|
|
class GGLWidgetInternal : public QGLWidget {
|
|
public:
|
|
GGLWidgetInternal(QGLFormat fmt, GRenderWindow* parent)
|
|
: QGLWidget(fmt, parent), parent(parent) {}
|
|
|
|
void paintEvent(QPaintEvent* ev) override {
|
|
if (do_painting) {
|
|
QPainter painter(this);
|
|
}
|
|
}
|
|
|
|
void resizeEvent(QResizeEvent* ev) override {
|
|
parent->OnClientAreaResized(ev->size().width(), ev->size().height());
|
|
parent->OnFramebufferSizeChanged();
|
|
}
|
|
|
|
void DisablePainting() {
|
|
do_painting = false;
|
|
}
|
|
void EnablePainting() {
|
|
do_painting = true;
|
|
}
|
|
|
|
private:
|
|
GRenderWindow* parent;
|
|
bool do_painting;
|
|
};
|
|
|
|
GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread)
|
|
: QWidget(parent), child(nullptr), emu_thread(emu_thread) {
|
|
|
|
setWindowTitle(QStringLiteral("yuzu %1 | %2-%3")
|
|
.arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc));
|
|
setAttribute(Qt::WA_AcceptTouchEvents);
|
|
|
|
InputCommon::Init();
|
|
InputCommon::StartJoystickEventHandler();
|
|
connect(this, &GRenderWindow::FirstFrameDisplayed, static_cast<GMainWindow*>(parent),
|
|
&GMainWindow::OnLoadComplete);
|
|
}
|
|
|
|
GRenderWindow::~GRenderWindow() {
|
|
InputCommon::Shutdown();
|
|
}
|
|
|
|
void GRenderWindow::moveContext() {
|
|
DoneCurrent();
|
|
|
|
// If the thread started running, move the GL Context to the new thread. Otherwise, move it
|
|
// back.
|
|
auto thread = (QThread::currentThread() == qApp->thread() && emu_thread != nullptr)
|
|
? emu_thread
|
|
: qApp->thread();
|
|
child->context()->moveToThread(thread);
|
|
}
|
|
|
|
void GRenderWindow::SwapBuffers() {
|
|
// In our multi-threaded QGLWidget use case we shouldn't need to call `makeCurrent`,
|
|
// since we never call `doneCurrent` in this thread.
|
|
// However:
|
|
// - The Qt debug runtime prints a bogus warning on the console if `makeCurrent` wasn't called
|
|
// since the last time `swapBuffers` was executed;
|
|
// - On macOS, if `makeCurrent` isn't called explicitely, resizing the buffer breaks.
|
|
child->makeCurrent();
|
|
|
|
child->swapBuffers();
|
|
if (!first_frame) {
|
|
emit FirstFrameDisplayed();
|
|
first_frame = true;
|
|
}
|
|
}
|
|
|
|
void GRenderWindow::MakeCurrent() {
|
|
child->makeCurrent();
|
|
}
|
|
|
|
void GRenderWindow::DoneCurrent() {
|
|
child->doneCurrent();
|
|
}
|
|
|
|
void GRenderWindow::PollEvents() {}
|
|
|
|
// On Qt 5.0+, this correctly gets the size of the framebuffer (pixels).
|
|
//
|
|
// Older versions get the window size (density independent pixels),
|
|
// and hence, do not support DPI scaling ("retina" displays).
|
|
// The result will be a viewport that is smaller than the extent of the window.
|
|
void GRenderWindow::OnFramebufferSizeChanged() {
|
|
// Screen changes potentially incur a change in screen DPI, hence we should update the
|
|
// framebuffer size
|
|
qreal pixelRatio = windowPixelRatio();
|
|
unsigned width = child->QPaintDevice::width() * pixelRatio;
|
|
unsigned height = child->QPaintDevice::height() * pixelRatio;
|
|
UpdateCurrentFramebufferLayout(width, height);
|
|
}
|
|
|
|
void GRenderWindow::BackupGeometry() {
|
|
geometry = ((QGLWidget*)this)->saveGeometry();
|
|
}
|
|
|
|
void GRenderWindow::RestoreGeometry() {
|
|
// We don't want to back up the geometry here (obviously)
|
|
QWidget::restoreGeometry(geometry);
|
|
}
|
|
|
|
void GRenderWindow::restoreGeometry(const QByteArray& geometry) {
|
|
// Make sure users of this class don't need to deal with backing up the geometry themselves
|
|
QWidget::restoreGeometry(geometry);
|
|
BackupGeometry();
|
|
}
|
|
|
|
QByteArray GRenderWindow::saveGeometry() {
|
|
// If we are a top-level widget, store the current geometry
|
|
// otherwise, store the last backup
|
|
if (parent() == nullptr)
|
|
return ((QGLWidget*)this)->saveGeometry();
|
|
else
|
|
return geometry;
|
|
}
|
|
|
|
qreal GRenderWindow::windowPixelRatio() const {
|
|
// windowHandle() might not be accessible until the window is displayed to screen.
|
|
return windowHandle() ? windowHandle()->screen()->devicePixelRatio() : 1.0f;
|
|
}
|
|
|
|
std::pair<unsigned, unsigned> GRenderWindow::ScaleTouch(const QPointF pos) const {
|
|
const qreal pixel_ratio = windowPixelRatio();
|
|
return {static_cast<unsigned>(std::max(std::round(pos.x() * pixel_ratio), qreal{0.0})),
|
|
static_cast<unsigned>(std::max(std::round(pos.y() * pixel_ratio), qreal{0.0}))};
|
|
}
|
|
|
|
void GRenderWindow::closeEvent(QCloseEvent* event) {
|
|
emit Closed();
|
|
QWidget::closeEvent(event);
|
|
}
|
|
|
|
void GRenderWindow::keyPressEvent(QKeyEvent* event) {
|
|
InputCommon::GetKeyboard()->PressKey(event->key());
|
|
}
|
|
|
|
void GRenderWindow::keyReleaseEvent(QKeyEvent* event) {
|
|
InputCommon::GetKeyboard()->ReleaseKey(event->key());
|
|
}
|
|
|
|
void GRenderWindow::mousePressEvent(QMouseEvent* event) {
|
|
if (event->source() == Qt::MouseEventSynthesizedBySystem)
|
|
return; // touch input is handled in TouchBeginEvent
|
|
|
|
auto pos = event->pos();
|
|
if (event->button() == Qt::LeftButton) {
|
|
const auto [x, y] = ScaleTouch(pos);
|
|
this->TouchPressed(x, y);
|
|
} else if (event->button() == Qt::RightButton) {
|
|
InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y());
|
|
}
|
|
}
|
|
|
|
void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
|
if (event->source() == Qt::MouseEventSynthesizedBySystem)
|
|
return; // touch input is handled in TouchUpdateEvent
|
|
|
|
auto pos = event->pos();
|
|
const auto [x, y] = ScaleTouch(pos);
|
|
this->TouchMoved(x, y);
|
|
InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y());
|
|
}
|
|
|
|
void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
|
|
if (event->source() == Qt::MouseEventSynthesizedBySystem)
|
|
return; // touch input is handled in TouchEndEvent
|
|
|
|
if (event->button() == Qt::LeftButton)
|
|
this->TouchReleased();
|
|
else if (event->button() == Qt::RightButton)
|
|
InputCommon::GetMotionEmu()->EndTilt();
|
|
}
|
|
|
|
void GRenderWindow::TouchBeginEvent(const QTouchEvent* event) {
|
|
// TouchBegin always has exactly one touch point, so take the .first()
|
|
const auto [x, y] = ScaleTouch(event->touchPoints().first().pos());
|
|
this->TouchPressed(x, y);
|
|
}
|
|
|
|
void GRenderWindow::TouchUpdateEvent(const QTouchEvent* event) {
|
|
QPointF pos;
|
|
int active_points = 0;
|
|
|
|
// average all active touch points
|
|
for (const auto tp : event->touchPoints()) {
|
|
if (tp.state() & (Qt::TouchPointPressed | Qt::TouchPointMoved | Qt::TouchPointStationary)) {
|
|
active_points++;
|
|
pos += tp.pos();
|
|
}
|
|
}
|
|
|
|
pos /= active_points;
|
|
|
|
const auto [x, y] = ScaleTouch(pos);
|
|
this->TouchMoved(x, y);
|
|
}
|
|
|
|
void GRenderWindow::TouchEndEvent() {
|
|
this->TouchReleased();
|
|
}
|
|
|
|
bool GRenderWindow::event(QEvent* event) {
|
|
if (event->type() == QEvent::TouchBegin) {
|
|
TouchBeginEvent(static_cast<QTouchEvent*>(event));
|
|
return true;
|
|
} else if (event->type() == QEvent::TouchUpdate) {
|
|
TouchUpdateEvent(static_cast<QTouchEvent*>(event));
|
|
return true;
|
|
} else if (event->type() == QEvent::TouchEnd || event->type() == QEvent::TouchCancel) {
|
|
TouchEndEvent();
|
|
return true;
|
|
}
|
|
|
|
return QWidget::event(event);
|
|
}
|
|
|
|
void GRenderWindow::focusOutEvent(QFocusEvent* event) {
|
|
QWidget::focusOutEvent(event);
|
|
InputCommon::GetKeyboard()->ReleaseAllKeys();
|
|
}
|
|
|
|
void GRenderWindow::OnClientAreaResized(unsigned width, unsigned height) {
|
|
NotifyClientAreaSizeChanged(std::make_pair(width, height));
|
|
}
|
|
|
|
void GRenderWindow::InitRenderTarget() {
|
|
if (child) {
|
|
delete child;
|
|
}
|
|
|
|
if (layout()) {
|
|
delete layout();
|
|
}
|
|
|
|
first_frame = false;
|
|
|
|
// TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground,
|
|
// WA_DontShowOnScreen, WA_DeleteOnClose
|
|
QGLFormat fmt;
|
|
fmt.setVersion(4, 3);
|
|
fmt.setProfile(QGLFormat::CoreProfile);
|
|
fmt.setSwapInterval(false);
|
|
|
|
// Requests a forward-compatible context, which is required to get a 3.2+ context on OS X
|
|
fmt.setOption(QGL::NoDeprecatedFunctions);
|
|
|
|
child = new GGLWidgetInternal(fmt, this);
|
|
QBoxLayout* layout = new QHBoxLayout(this);
|
|
|
|
resize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height);
|
|
layout->addWidget(child);
|
|
layout->setMargin(0);
|
|
setLayout(layout);
|
|
|
|
OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size);
|
|
|
|
OnFramebufferSizeChanged();
|
|
NotifyClientAreaSizeChanged(std::pair<unsigned, unsigned>(child->width(), child->height()));
|
|
|
|
BackupGeometry();
|
|
}
|
|
|
|
void GRenderWindow::CaptureScreenshot(u16 res_scale, const QString& screenshot_path) {
|
|
auto& renderer = Core::System::GetInstance().Renderer();
|
|
|
|
if (!res_scale)
|
|
res_scale = VideoCore::GetResolutionScaleFactor(renderer);
|
|
|
|
const Layout::FramebufferLayout layout{Layout::FrameLayoutFromResolutionScale(res_scale)};
|
|
screenshot_image = QImage(QSize(layout.width, layout.height), QImage::Format_RGB32);
|
|
renderer.RequestScreenshot(screenshot_image.bits(),
|
|
[=] {
|
|
screenshot_image.mirrored(false, true).save(screenshot_path);
|
|
LOG_INFO(Frontend, "The screenshot is saved.");
|
|
},
|
|
layout);
|
|
}
|
|
|
|
void GRenderWindow::OnMinimalClientAreaChangeRequest(
|
|
const std::pair<unsigned, unsigned>& minimal_size) {
|
|
setMinimumSize(minimal_size.first, minimal_size.second);
|
|
}
|
|
|
|
void GRenderWindow::OnEmulationStarting(EmuThread* emu_thread) {
|
|
this->emu_thread = emu_thread;
|
|
child->DisablePainting();
|
|
}
|
|
|
|
void GRenderWindow::OnEmulationStopping() {
|
|
emu_thread = nullptr;
|
|
child->EnablePainting();
|
|
}
|
|
|
|
void GRenderWindow::showEvent(QShowEvent* event) {
|
|
QWidget::showEvent(event);
|
|
|
|
// windowHandle() is not initialized until the Window is shown, so we connect it here.
|
|
connect(windowHandle(), &QWindow::screenChanged, this, &GRenderWindow::OnFramebufferSizeChanged,
|
|
Qt::UniqueConnection);
|
|
}
|