diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index 294ba7b88..debc54b1c 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -35,6 +35,7 @@ #include "core/file_sys/cia_container.h" #include "core/frontend/applets/default_applets.h" #include "core/frontend/framebuffer_layout.h" +#include "core/frontend/scope_acquire_context.h" #include "core/gdbstub/gdbstub.h" #include "core/hle/service/am/am.h" #include "core/hle/service/cfg/cfg.h" @@ -347,7 +348,7 @@ int main(int argc, char** argv) { Core::System::GetInstance().RegisterImageInterface(std::make_shared()); std::unique_ptr emu_window{std::make_unique(fullscreen)}; - + Frontend::ScopeAcquireContext scope(*emu_window); Core::System& system{Core::System::GetInstance()}; const Core::System::ResultStatus load_result{system.Load(*emu_window, filepath)}; @@ -411,9 +412,11 @@ int main(int argc, char** argv) { system.VideoDumper().StartDumping(dump_video, "webm", layout); } + std::thread render_thread([&emu_window] { emu_window->Present(); }); while (emu_window->IsOpen()) { system.RunLoop(); } + render_thread.join(); Core::Movie::GetInstance().Shutdown(); if (system.VideoDumper().IsDumping()) { diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 9b0b1a8c1..86d592b8c 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -121,10 +121,11 @@ void Config::ReadValues() { Settings::values.use_shader_jit = sdl2_config->GetBoolean("Renderer", "use_shader_jit", true); Settings::values.resolution_factor = static_cast(sdl2_config->GetInteger("Renderer", "resolution_factor", 1)); - Settings::values.vsync_enabled = sdl2_config->GetBoolean("Renderer", "vsync_enabled", false); Settings::values.use_frame_limit = sdl2_config->GetBoolean("Renderer", "use_frame_limit", true); Settings::values.frame_limit = static_cast(sdl2_config->GetInteger("Renderer", "frame_limit", 100)); + Settings::values.use_vsync_new = + static_cast(sdl2_config->GetInteger("Renderer", "use_vsync_new", 1)); Settings::values.render_3d = static_cast( sdl2_config->GetInteger("Renderer", "render_3d", 0)); diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 2b6aa2147..d646823e2 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -112,15 +112,16 @@ shaders_accurate_mul = # 0: Interpreter (slow), 1 (default): JIT (fast) use_shader_jit = +# Forces VSync on the display thread. Usually doesn't impact performance, but on some drivers it can +# so only turn this off if you notice a speed difference. +# 0: Off, 1 (default): On +use_vsync_new = + # Resolution scale factor # 0: Auto (scales resolution to window size), 1: Native 3DS screen resolution, Otherwise a scale # factor for the 3DS resolution resolution_factor = -# Whether to enable V-Sync (caps the framerate at 60FPS) or not. -# 0 (default): Off, 1: On -vsync_enabled = - # Turns on the frame limiter, which will limit frames output to the target game speed # 0: Off, 1: On (default) use_frame_limit = diff --git a/src/citra/emu_window/emu_window_sdl2.cpp b/src/citra/emu_window/emu_window_sdl2.cpp index 34d3d2fdb..f8fc2b3a1 100644 --- a/src/citra/emu_window/emu_window_sdl2.cpp +++ b/src/citra/emu_window/emu_window_sdl2.cpp @@ -20,6 +20,28 @@ #include "input_common/motion_emu.h" #include "input_common/sdl/sdl.h" #include "network/network.h" +#include "video_core/renderer_base.h" +#include "video_core/video_core.h" + +SharedContext_SDL2::SharedContext_SDL2() { + window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, + SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL); + context = SDL_GL_CreateContext(window); +} + +SharedContext_SDL2::~SharedContext_SDL2() { + DoneCurrent(); + SDL_GL_DeleteContext(context); + SDL_DestroyWindow(window); +} + +void SharedContext_SDL2::MakeCurrent() { + SDL_GL_MakeCurrent(window, context); +} + +void SharedContext_SDL2::DoneCurrent() { + SDL_GL_MakeCurrent(window, nullptr); +} void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) { TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); @@ -135,6 +157,10 @@ EmuWindow_SDL2::EmuWindow_SDL2(bool fullscreen) { SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0); + // Enable context sharing for the shared context + SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1); + // Enable vsync + SDL_GL_SetSwapInterval(1); std::string window_title = fmt::format("Citra {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); @@ -150,16 +176,24 @@ EmuWindow_SDL2::EmuWindow_SDL2(bool fullscreen) { exit(1); } + dummy_window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, + SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL); + if (fullscreen) { Fullscreen(); } - gl_context = SDL_GL_CreateContext(render_window); + window_context = SDL_GL_CreateContext(render_window); + core_context = CreateSharedContext(); - if (gl_context == nullptr) { + if (window_context == nullptr) { LOG_CRITICAL(Frontend, "Failed to create SDL2 GL context: {}", SDL_GetError()); exit(1); } + if (core_context == nullptr) { + LOG_CRITICAL(Frontend, "Failed to create shared SDL2 GL context: {}", SDL_GetError()); + exit(1); + } auto gl_load_func = Settings::values.use_gles ? gladLoadGLES2Loader : gladLoadGLLoader; @@ -171,23 +205,31 @@ EmuWindow_SDL2::EmuWindow_SDL2(bool fullscreen) { OnResize(); OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); SDL_PumpEvents(); - SDL_GL_SetSwapInterval(Settings::values.vsync_enabled); LOG_INFO(Frontend, "Citra Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); Settings::LogSettings(); - - DoneCurrent(); } EmuWindow_SDL2::~EmuWindow_SDL2() { + core_context.reset(); Network::Shutdown(); InputCommon::Shutdown(); - SDL_GL_DeleteContext(gl_context); + SDL_GL_DeleteContext(window_context); SDL_Quit(); } -void EmuWindow_SDL2::SwapBuffers() { - SDL_GL_SwapWindow(render_window); +std::unique_ptr EmuWindow_SDL2::CreateSharedContext() const { + return std::make_unique(); +} + +void EmuWindow_SDL2::Present() { + SDL_GL_MakeCurrent(render_window, window_context); + SDL_GL_SetSwapInterval(1); + while (IsOpen()) { + VideoCore::g_renderer->TryPresent(100); + SDL_GL_SwapWindow(render_window); + } + SDL_GL_MakeCurrent(render_window, nullptr); } void EmuWindow_SDL2::PollEvents() { @@ -256,11 +298,11 @@ void EmuWindow_SDL2::PollEvents() { } void EmuWindow_SDL2::MakeCurrent() { - SDL_GL_MakeCurrent(render_window, gl_context); + core_context->MakeCurrent(); } void EmuWindow_SDL2::DoneCurrent() { - SDL_GL_MakeCurrent(render_window, nullptr); + core_context->DoneCurrent(); } void EmuWindow_SDL2::OnMinimalClientAreaChangeRequest(std::pair minimal_size) { diff --git a/src/citra/emu_window/emu_window_sdl2.h b/src/citra/emu_window/emu_window_sdl2.h index 3462c5bd9..b0834fd06 100644 --- a/src/citra/emu_window/emu_window_sdl2.h +++ b/src/citra/emu_window/emu_window_sdl2.h @@ -10,13 +10,29 @@ struct SDL_Window; +class SharedContext_SDL2 : public Frontend::GraphicsContext { +public: + using SDL_GLContext = void*; + + SharedContext_SDL2(); + + ~SharedContext_SDL2() override; + + void MakeCurrent() override; + + void DoneCurrent() override; + +private: + SDL_GLContext context; + SDL_Window* window; +}; + class EmuWindow_SDL2 : public Frontend::EmuWindow { public: explicit EmuWindow_SDL2(bool fullscreen); ~EmuWindow_SDL2(); - /// Swap buffers to display the next frame - void SwapBuffers() override; + void Present(); /// Polls window events void PollEvents() override; @@ -30,6 +46,9 @@ public: /// Whether the window is still open, and a close request hasn't yet been sent bool IsOpen() const; + /// Creates a new context that is shared with the current context + std::unique_ptr CreateSharedContext() const override; + private: /// Called by PollEvents when a key is pressed or released. void OnKeyEvent(int key, u8 state); @@ -67,9 +86,16 @@ private: /// Internal SDL2 render window SDL_Window* render_window; + /// Fake hidden window for the core context + SDL_Window* dummy_window; + using SDL_GLContext = void*; + /// The OpenGL context associated with the window - SDL_GLContext gl_context; + SDL_GLContext window_context; + + /// The OpenGL context associated with the core + std::unique_ptr core_context; /// Keeps track of how often to update the title bar during gameplay u32 last_time = 0; diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 400a0104c..a9cdddc18 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -1,31 +1,50 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + #include +#include #include #include +#include +#include +#include +#include +#include #include #include #include - #include "citra_qt/bootmanager.h" +#include "citra_qt/main.h" #include "common/microprofile.h" #include "common/scm_rev.h" #include "core/3ds.h" #include "core/core.h" +#include "core/frontend/scope_acquire_context.h" #include "core/settings.h" #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/motion_emu.h" #include "network/network.h" +#include "video_core/renderer_base.h" #include "video_core/video_core.h" -EmuThread::EmuThread(GRenderWindow* render_window) : render_window(render_window) {} +EmuThread::EmuThread(Frontend::GraphicsContext& core_context) : core_context(core_context) {} EmuThread::~EmuThread() = default; +static GMainWindow* GetMainWindow() { + for (QWidget* w : qApp->topLevelWidgets()) { + if (GMainWindow* main = qobject_cast(w)) { + return main; + } + } + return nullptr; +} + void EmuThread::run() { - render_window->MakeCurrent(); - MicroProfileOnThreadCreate("EmuThread"); - + Frontend::ScopeAcquireContext scope(core_context); // Holds whether the cpu was running during the last iteration, // so that the DebugModeLeft signal can be emitted before the // next execution step. @@ -72,48 +91,104 @@ void EmuThread::run() { #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) {} +OpenGLWindow::OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context) + : QWindow(parent), event_handler(event_handler), + context(new QOpenGLContext(shared_context->parent())) { - void paintEvent(QPaintEvent* ev) override { - if (do_painting) { - QPainter painter(this); - } - } + // disable vsync for any shared contexts + auto format = shared_context->format(); + format.setSwapInterval(Settings::values.use_vsync_new ? 1 : 0); + this->setFormat(format); - void resizeEvent(QResizeEvent* ev) override { - parent->OnClientAreaResized(ev->size().width(), ev->size().height()); - parent->OnFramebufferSizeChanged(); - } + context->setShareContext(shared_context); + context->setScreen(this->screen()); + context->setFormat(format); + context->create(); - void DisablePainting() { - do_painting = false; - } - void EnablePainting() { - do_painting = true; - } + LOG_WARNING(Frontend, "OpenGLWindow context format Interval {}", + context->format().swapInterval()); -private: - GRenderWindow* parent; - bool do_painting; -}; + LOG_WARNING(Frontend, "OpenGLWindow surface format interval {}", this->format().swapInterval()); + + setSurfaceType(QWindow::OpenGLSurface); + + // TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground, + // WA_DontShowOnScreen, WA_DeleteOnClose +} + +OpenGLWindow::~OpenGLWindow() { + context->doneCurrent(); +} + +void OpenGLWindow::Present() { + if (!isExposed()) + return; + context->makeCurrent(this); + VideoCore::g_renderer->TryPresent(100); + context->swapBuffers(this); + auto f = context->versionFunctions(); + f->glFinish(); + QWindow::requestUpdate(); +} + +bool OpenGLWindow::event(QEvent* event) { + switch (event->type()) { + case QEvent::UpdateRequest: + Present(); + return true; + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseButtonDblClick: + case QEvent::MouseMove: + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::FocusIn: + case QEvent::FocusOut: + case QEvent::FocusAboutToChange: + case QEvent::Enter: + case QEvent::Leave: + case QEvent::Wheel: + case QEvent::TabletMove: + case QEvent::TabletPress: + case QEvent::TabletRelease: + case QEvent::TabletEnterProximity: + case QEvent::TabletLeaveProximity: + case QEvent::TouchBegin: + case QEvent::TouchUpdate: + case QEvent::TouchEnd: + case QEvent::InputMethodQuery: + case QEvent::TouchCancel: + return QCoreApplication::sendEvent(event_handler, event); + case QEvent::Drop: + GetMainWindow()->DropAction(static_cast(event)); + return true; + case QEvent::DragResponse: + case QEvent::DragEnter: + case QEvent::DragLeave: + case QEvent::DragMove: + GetMainWindow()->AcceptDropEvent(static_cast(event)); + return true; + default: + return QWindow::event(event); + } +} + +void OpenGLWindow::exposeEvent(QExposeEvent* event) { + QWindow::requestUpdate(); + QWindow::exposeEvent(event); +} GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread) - : QWidget(parent), child(nullptr), emu_thread(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)); setAttribute(Qt::WA_AcceptTouchEvents); - + auto layout = new QHBoxLayout(this); + layout->setMargin(0); + setLayout(layout); InputCommon::Init(); } @@ -121,35 +196,12 @@ 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(); -} - void GRenderWindow::MakeCurrent() { - child->makeCurrent(); + core_context->MakeCurrent(); } void GRenderWindow::DoneCurrent() { - child->doneCurrent(); + core_context->DoneCurrent(); } void GRenderWindow::PollEvents() {} @@ -163,8 +215,8 @@ void GRenderWindow::OnFramebufferSizeChanged() { // Screen changes potentially incur a change in screen DPI, hence we should update the // framebuffer size const qreal pixel_ratio = windowPixelRatio(); - const u32 width = child->QPaintDevice::width() * pixel_ratio; - const u32 height = child->QPaintDevice::height() * pixel_ratio; + const u32 width = this->width() * pixel_ratio; + const u32 height = this->height() * pixel_ratio; UpdateCurrentFramebufferLayout(width, height); } @@ -194,8 +246,7 @@ QByteArray GRenderWindow::saveGeometry() { } qreal GRenderWindow::windowPixelRatio() const { - // windowHandle() might not be accessible until the window is displayed to screen. - return windowHandle() ? windowHandle()->screen()->devicePixelRatio() : 1.0f; + return devicePixelRatio(); } std::pair GRenderWindow::ScaleTouch(const QPointF pos) const { @@ -279,15 +330,19 @@ void GRenderWindow::TouchEndEvent() { } bool GRenderWindow::event(QEvent* event) { - if (event->type() == QEvent::TouchBegin) { + switch (event->type()) { + case QEvent::TouchBegin: TouchBeginEvent(static_cast(event)); return true; - } else if (event->type() == QEvent::TouchUpdate) { + case QEvent::TouchUpdate: TouchUpdateEvent(static_cast(event)); return true; - } else if (event->type() == QEvent::TouchEnd || event->type() == QEvent::TouchCancel) { + case QEvent::TouchEnd: + case QEvent::TouchCancel: TouchEndEvent(); return true; + default: + break; } return QWidget::event(event); @@ -298,45 +353,36 @@ void GRenderWindow::focusOutEvent(QFocusEvent* event) { InputCommon::GetKeyboard()->ReleaseAllKeys(); } -void GRenderWindow::OnClientAreaResized(u32 width, u32 height) { - NotifyClientAreaSizeChanged(std::make_pair(width, height)); +void GRenderWindow::resizeEvent(QResizeEvent* event) { + QWidget::resizeEvent(event); + OnFramebufferSizeChanged(); } void GRenderWindow::InitRenderTarget() { - if (child) { - delete child; - } + ReleaseRenderTarget(); - if (layout()) { - delete layout(); - } - - // TODO: One of these flags might be interesting: WA_OpaquePaintEvent, WA_NoBackground, - // WA_DontShowOnScreen, WA_DeleteOnClose - QGLFormat fmt; - fmt.setVersion(3, 3); - fmt.setProfile(QGLFormat::CoreProfile); - fmt.setSwapInterval(Settings::values.vsync_enabled); - - // 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); + 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(); resize(Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight); - layout->addWidget(child); - layout->setMargin(0); - setLayout(layout); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - - OnFramebufferSizeChanged(); - NotifyClientAreaSizeChanged(std::pair(child->width(), child->height())); - BackupGeometry(); } +void GRenderWindow::ReleaseRenderTarget() { + if (child_widget) { + layout()->removeWidget(child_widget); + delete child_widget; + child_widget = nullptr; + } +} + void GRenderWindow::CaptureScreenshot(u32 res_scale, const QString& screenshot_path) { if (res_scale == 0) res_scale = VideoCore::GetResolutionScaleFactor(); @@ -361,18 +407,40 @@ void GRenderWindow::OnMinimalClientAreaChangeRequest(std::pair minimal 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); +} + +std::unique_ptr GRenderWindow::CreateSharedContext() const { + return std::make_unique(QOpenGLContext::globalShareContext()); +} + +GLContext::GLContext(QOpenGLContext* shared_context) + : context(new QOpenGLContext(shared_context->parent())), + surface(new QOffscreenSurface(nullptr)) { + + // disable vsync for any shared contexts + auto format = shared_context->format(); + format.setSwapInterval(0); + + context->setShareContext(shared_context); + context->setFormat(format); + context->create(); + surface->setParent(shared_context->parent()); + surface->setFormat(format); + surface->create(); +} + +void GLContext::MakeCurrent() { + context->makeCurrent(surface); +} + +void GLContext::DoneCurrent() { + context->doneCurrent(); } diff --git a/src/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h index b36d45868..922e093f9 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/bootmanager.h @@ -7,9 +7,9 @@ #include #include #include -#include -#include #include +#include +#include #include "common/thread.h" #include "core/core.h" #include "core/frontend/emu_window.h" @@ -17,16 +17,30 @@ class QKeyEvent; class QScreen; class QTouchEvent; +class QOffscreenSurface; +class QOpenGLContext; -class GGLWidgetInternal; class GMainWindow; class GRenderWindow; +class GLContext : public Frontend::GraphicsContext { +public: + explicit GLContext(QOpenGLContext* shared_context); + + void MakeCurrent() override; + + void DoneCurrent() override; + +private: + QOpenGLContext* context; + QOffscreenSurface* surface; +}; + class EmuThread final : public QThread { Q_OBJECT public: - explicit EmuThread(GRenderWindow* render_window); + explicit EmuThread(Frontend::GraphicsContext& context); ~EmuThread() override; /** @@ -80,7 +94,7 @@ private: std::mutex running_mutex; std::condition_variable running_cv; - GRenderWindow* render_window; + Frontend::GraphicsContext& core_context; signals: /** @@ -104,6 +118,24 @@ signals: void ErrorThrown(Core::System::ResultStatus, std::string); }; +class OpenGLWindow : public QWindow { + Q_OBJECT +public: + explicit OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context); + + ~OpenGLWindow(); + + void Present(); + +protected: + bool event(QEvent* event) override; + void exposeEvent(QExposeEvent* event) override; + +private: + QOpenGLContext* context; + QWidget* event_handler; +}; + class GRenderWindow : public QWidget, public Frontend::EmuWindow { Q_OBJECT @@ -111,11 +143,11 @@ public: GRenderWindow(QWidget* parent, EmuThread* emu_thread); ~GRenderWindow() override; - // EmuWindow implementation - void SwapBuffers() override; + // EmuWindow implementation. void MakeCurrent() override; void DoneCurrent() override; void PollEvents() override; + std::unique_ptr CreateSharedContext() const override; void BackupGeometry(); void RestoreGeometry(); @@ -126,6 +158,8 @@ public: void closeEvent(QCloseEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; @@ -137,14 +171,14 @@ public: void focusOutEvent(QFocusEvent* event) override; - void OnClientAreaResized(u32 width, u32 height); - void InitRenderTarget(); + /// Destroy the previous run's child_widget which should also destroy the child_window + void ReleaseRenderTarget(); + void CaptureScreenshot(u32 res_scale, const QString& screenshot_path); public slots: - void moveContext(); // overridden void OnEmulationStarting(EmuThread* emu_thread); void OnEmulationStopping(); @@ -162,10 +196,18 @@ private: void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; - GGLWidgetInternal* child; + std::unique_ptr core_context; QByteArray geometry; + /// Native window handle that backs this presentation widget + QWindow* child_window = nullptr; + + /// In order to embed the window into GRenderWindow, you need to use createWindowContainer to + /// put the child_window into a widget then add it to the layout. This child_widget can be + /// parented to GRenderWindow and use Qt's lifetime system + QWidget* child_widget = nullptr; + EmuThread* emu_thread; /// Temporary storage of the screenshot taken diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 2114a9c7e..6bf3b8171 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -430,9 +430,9 @@ void Config::ReadRendererValues() { Settings::values.shaders_accurate_mul = ReadSetting(QStringLiteral("shaders_accurate_mul"), false).toBool(); Settings::values.use_shader_jit = ReadSetting(QStringLiteral("use_shader_jit"), true).toBool(); + Settings::values.use_vsync_new = ReadSetting(QStringLiteral("use_vsync_new"), true).toBool(); Settings::values.resolution_factor = static_cast(ReadSetting(QStringLiteral("resolution_factor"), 1).toInt()); - Settings::values.vsync_enabled = ReadSetting(QStringLiteral("vsync_enabled"), false).toBool(); Settings::values.use_frame_limit = ReadSetting(QStringLiteral("use_frame_limit"), true).toBool(); Settings::values.frame_limit = ReadSetting(QStringLiteral("frame_limit"), 100).toInt(); @@ -859,8 +859,8 @@ void Config::SaveRendererValues() { WriteSetting(QStringLiteral("shaders_accurate_mul"), Settings::values.shaders_accurate_mul, false); WriteSetting(QStringLiteral("use_shader_jit"), Settings::values.use_shader_jit, true); + WriteSetting(QStringLiteral("use_vsync_new"), Settings::values.use_vsync_new, true); WriteSetting(QStringLiteral("resolution_factor"), Settings::values.resolution_factor, 1); - WriteSetting(QStringLiteral("vsync_enabled"), Settings::values.vsync_enabled, false); WriteSetting(QStringLiteral("use_frame_limit"), Settings::values.use_frame_limit, true); WriteSetting(QStringLiteral("frame_limit"), Settings::values.frame_limit, 100); diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp index d04c0d6fe..b9dd4b468 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/citra_qt/configuration/configure_graphics.cpp @@ -18,6 +18,8 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent) SetConfiguration(); ui->hw_renderer_group->setEnabled(ui->toggle_hw_renderer->isChecked()); + ui->toggle_vsync_new->setEnabled(!Core::System::GetInstance().IsPoweredOn()); + connect(ui->toggle_hw_renderer, &QCheckBox::toggled, this, [this] { auto checked = ui->toggle_hw_renderer->isChecked(); ui->hw_renderer_group->setEnabled(checked); @@ -46,6 +48,7 @@ void ConfigureGraphics::SetConfiguration() { ui->toggle_hw_shader->setChecked(Settings::values.use_hw_shader); ui->toggle_accurate_mul->setChecked(Settings::values.shaders_accurate_mul); ui->toggle_shader_jit->setChecked(Settings::values.use_shader_jit); + ui->toggle_vsync_new->setChecked(Settings::values.use_vsync_new); } void ConfigureGraphics::ApplyConfiguration() { @@ -53,6 +56,7 @@ void ConfigureGraphics::ApplyConfiguration() { Settings::values.use_hw_shader = ui->toggle_hw_shader->isChecked(); Settings::values.shaders_accurate_mul = ui->toggle_accurate_mul->isChecked(); Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); + Settings::values.use_vsync_new = ui->toggle_vsync_new->isChecked(); } void ConfigureGraphics::RetranslateUI() { diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui index 88b9a0caa..43f538558 100644 --- a/src/citra_qt/configuration/configure_graphics.ui +++ b/src/citra_qt/configuration/configure_graphics.ui @@ -105,6 +105,25 @@ + + + + Advanced + + + + + + VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. + + + Enable VSync + + + + + + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 11da2be20..dd56f4b13 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -5,12 +5,11 @@ #include #include #include -#include -#define QT_NO_OPENGL #include #include #include #include +#include #include #include #include @@ -72,6 +71,7 @@ #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/frontend/applets/default_applets.h" +#include "core/frontend/scope_acquire_context.h" #include "core/gdbstub/gdbstub.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/nfc/nfc.h" @@ -768,13 +768,14 @@ bool GMainWindow::LoadROM(const QString& filename) { ShutdownGame(); render_window->InitRenderTarget(); - render_window->MakeCurrent(); + + Frontend::ScopeAcquireContext scope(*render_window); const QString below_gl33_title = tr("OpenGL 3.3 Unsupported"); const QString below_gl33_message = tr("Your GPU may not support OpenGL 3.3, or you do not " "have the latest graphics driver."); - if (!gladLoadGL()) { + if (!QOpenGLContext::globalShareContext()->versionFunctions()) { QMessageBox::critical(this, below_gl33_title, below_gl33_message); return false; } @@ -893,9 +894,8 @@ void GMainWindow::BootGame(const QString& filename) { return; // Create and start the emulation thread - emu_thread = std::make_unique(render_window); + emu_thread = std::make_unique(*render_window); emit EmulationStarting(emu_thread.get()); - render_window->moveContext(); emu_thread->start(); connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); @@ -1019,6 +1019,9 @@ void GMainWindow::ShutdownGame() { UpdateWindowTitle(); game_path.clear(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); } void GMainWindow::StoreRecentFile(const QString& filename) { @@ -1869,14 +1872,33 @@ void GMainWindow::closeEvent(QCloseEvent* event) { QWidget::closeEvent(event); } -static bool IsSingleFileDropEvent(QDropEvent* event) { - const QMimeData* mimeData = event->mimeData(); - return mimeData->hasUrls() && mimeData->urls().length() == 1; +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; } -void GMainWindow::dropEvent(QDropEvent* event) { - if (!IsSingleFileDropEvent(event)) { - return; +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; } const QMimeData* mime_data = event->mimeData(); @@ -1891,16 +1913,19 @@ void GMainWindow::dropEvent(QDropEvent* event) { BootGame(filename); } } + return true; +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); } void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { - if (IsSingleFileDropEvent(event)) { - event->acceptProposedAction(); - } + AcceptDropEvent(event); } void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { - event->acceptProposedAction(); + AcceptDropEvent(event); } bool GMainWindow::ConfirmChangeGame() { @@ -2050,11 +2075,20 @@ int main(int argc, char* argv[]) { QCoreApplication::setOrganizationName("Citra team"); QCoreApplication::setApplicationName("Citra"); + QSurfaceFormat format; + format.setVersion(3, 3); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setSwapInterval(0); + // TODO: expose a setting for buffer value (ie default/single/double/triple) + format.setSwapBehavior(QSurfaceFormat::DefaultSwapBehavior); + QSurfaceFormat::setDefaultFormat(format); + #ifdef __APPLE__ std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + ".."; chdir(bin_path.c_str()); #endif - + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); QApplication app(argc, argv); // Qt changes the locale and causes issues in float conversion using std::to_string() when diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 9c7888056..0fa04f5b8 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -41,6 +41,7 @@ class QProgressBar; class RegistersWidget; class Updater; class WaitTreeWidget; + namespace DiscordRPC { class DiscordInterface; } @@ -69,8 +70,12 @@ public: GameList* game_list; std::unique_ptr discord_rpc; + bool DropAction(QDropEvent* event); + void AcceptDropEvent(QDropEvent* event); + public slots: void OnAppFocusStateChanged(Qt::ApplicationState state); + signals: /** @@ -78,8 +83,8 @@ signals: * about to start. At this time, the core system emulation has been initialized, and all * emulation handles and memory should be valid. * - * @param emu_thread Pointer to the newly created EmuThread (to be used by widgets that need to - * access/change emulation state). + * @param emu_thread Pointer to the newly created EmuThread (to be used by widgets that need + * to access/change emulation state). */ void EmulationStarting(EmuThread* emu_thread); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3a27d8f81..67167fe5d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -106,6 +106,8 @@ add_library(core STATIC frontend/input.h frontend/mic.h frontend/mic.cpp + frontend/scope_acquire_context.cpp + frontend/scope_acquire_context.h gdbstub/gdbstub.cpp gdbstub/gdbstub.h hle/applets/applet.cpp diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp index 4fc285fee..f393e4c76 100644 --- a/src/core/frontend/emu_window.cpp +++ b/src/core/frontend/emu_window.cpp @@ -10,6 +10,8 @@ namespace Frontend { +GraphicsContext::~GraphicsContext() = default; + class EmuWindow::TouchState : public Input::Factory, public std::enable_shared_from_this { public: diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index f2878b202..df6203842 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -12,6 +12,61 @@ namespace Frontend { +struct Frame; +/** + * For smooth Vsync rendering, we want to always present the latest frame that the core generates, + * but also make sure that rendering happens at the pace that the frontend dictates. This is a + * helper class that the renderer can define to sync frames between the render thread and the + * presentation thread + */ +class TextureMailbox { +public: + virtual ~TextureMailbox() = default; + + /** + * Recreate the render objects attached to this frame with the new specified width/height + */ + virtual void ReloadRenderFrame(Frontend::Frame* frame, u32 width, u32 height) = 0; + + /** + * Recreate the presentation objects attached to this frame with the new specified width/height + */ + virtual void ReloadPresentFrame(Frontend::Frame* frame, u32 width, u32 height) = 0; + + /** + * Render thread calls this to get an available frame to present + */ + virtual Frontend::Frame* GetRenderFrame() = 0; + + /** + * Render thread calls this after draw commands are done to add to the presentation mailbox + */ + virtual void ReleaseRenderFrame(Frame* frame) = 0; + + /** + * Presentation thread calls this to get the latest frame available to present. If there is no + * frame available after timeout, returns the previous frame. If there is no previous frame it + * returns nullptr + */ + virtual Frontend::Frame* TryGetPresentFrame(int timeout_ms) = 0; +}; + +/** + * Represents a graphics context that can be used for background computation or drawing. If the + * graphics backend doesn't require the context, then the implementation of these methods can be + * stubs + */ +class GraphicsContext { +public: + virtual ~GraphicsContext(); + + /// Makes the graphics context current for the caller thread + virtual void MakeCurrent() = 0; + + /// Releases (dunno if this is the "right" word) the context from the caller thread + virtual void DoneCurrent() = 0; +}; + /** * Abstraction class used to provide an interface between emulation code and the frontend * (e.g. SDL, QGLWidget, GLFW, etc...). @@ -30,7 +85,7 @@ namespace Frontend { * - DO NOT TREAT THIS CLASS AS A GUI TOOLKIT ABSTRACTION LAYER. That's not what it is. Please * re-read the upper points again and think about it if you don't see this. */ -class EmuWindow { +class EmuWindow : public GraphicsContext { public: /// Data structure to store emuwindow configuration struct WindowConfig { @@ -40,17 +95,21 @@ public: std::pair min_client_area_size; }; - /// Swap buffers to display the next frame - virtual void SwapBuffers() = 0; - /// Polls window events virtual void PollEvents() = 0; - /// Makes the graphics context current for the caller thread - virtual void MakeCurrent() = 0; - - /// Releases (dunno if this is the "right" word) the GLFW context from the caller thread - virtual void DoneCurrent() = 0; + /** + * Returns a GraphicsContext that the frontend provides that is shared with the emu window. This + * context can be used from other threads for background graphics computation. If the frontend + * is using a graphics backend that doesn't need anything specific to run on a different thread, + * then it can use a stubbed implemenation for GraphicsContext. + * + * If the return value is null, then the core should assume that the frontend cannot provide a + * Shared Context + */ + virtual std::unique_ptr CreateSharedContext() const { + return nullptr; + } /** * Signal that a touch pressed event has occurred (e.g. mouse click pressed) @@ -102,6 +161,8 @@ public: */ void UpdateCurrentFramebufferLayout(unsigned width, unsigned height); + std::unique_ptr mailbox = nullptr; + protected: EmuWindow(); virtual ~EmuWindow(); @@ -131,15 +192,6 @@ protected: framebuffer_layout = layout; } - /** - * Update internal client area size with the given parameter. - * @note EmuWindow implementations will usually use this in window resize event handlers. - */ - void NotifyClientAreaSizeChanged(const std::pair& size) { - client_area_width = size.first; - client_area_height = size.second; - } - private: /** * Handler called when the minimal client area was requested to be changed via SetConfig. @@ -152,9 +204,6 @@ private: Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout - unsigned client_area_width; ///< Current client width, should be set by window impl. - unsigned client_area_height; ///< Current client height, should be set by window impl. - WindowConfig config; ///< Internal configuration (changes pending for being applied in /// ProcessConfigurationChanges) WindowConfig active_config; ///< Internal active configuration diff --git a/src/core/frontend/scope_acquire_context.cpp b/src/core/frontend/scope_acquire_context.cpp new file mode 100644 index 000000000..3689483af --- /dev/null +++ b/src/core/frontend/scope_acquire_context.cpp @@ -0,0 +1,17 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/frontend/emu_window.h" +#include "core/frontend/scope_acquire_context.h" + +namespace Frontend { + +ScopeAcquireContext::ScopeAcquireContext(Frontend::GraphicsContext& context) : context{context} { + context.MakeCurrent(); +} +ScopeAcquireContext::~ScopeAcquireContext() { + context.DoneCurrent(); +} + +} // namespace Frontend \ No newline at end of file diff --git a/src/core/frontend/scope_acquire_context.h b/src/core/frontend/scope_acquire_context.h new file mode 100644 index 000000000..12ab61ec6 --- /dev/null +++ b/src/core/frontend/scope_acquire_context.h @@ -0,0 +1,23 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/common_types.h" + +namespace Frontend { + +class GraphicsContext; + +/// Helper class to acquire/release window context within a given scope +class ScopeAcquireContext : NonCopyable { +public: + explicit ScopeAcquireContext(Frontend::GraphicsContext& context); + ~ScopeAcquireContext(); + +private: + Frontend::GraphicsContext& context; +}; + +} // namespace Frontend \ No newline at end of file diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 92409364f..81a6d7db6 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -78,7 +78,6 @@ void LogSettings() { LogSetting("Renderer_ShadersAccurateMul", Settings::values.shaders_accurate_mul); LogSetting("Renderer_UseShaderJit", Settings::values.use_shader_jit); LogSetting("Renderer_UseResolutionFactor", Settings::values.resolution_factor); - LogSetting("Renderer_VsyncEnabled", Settings::values.vsync_enabled); LogSetting("Renderer_UseFrameLimit", Settings::values.use_frame_limit); LogSetting("Renderer_FrameLimit", Settings::values.frame_limit); LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name); diff --git a/src/core/settings.h b/src/core/settings.h index 213cc3ec3..d7d351a4c 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -144,7 +144,6 @@ struct Values { bool shaders_accurate_mul; bool use_shader_jit; u16 resolution_factor; - bool vsync_enabled; bool use_frame_limit; u16 frame_limit; @@ -174,6 +173,8 @@ struct Values { bool custom_textures; bool preload_textures; + bool use_vsync_new; + // Audio bool enable_dsp_lle; bool enable_dsp_lle_multithread; diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp index 274257eab..e9ec39b76 100644 --- a/src/core/telemetry_session.cpp +++ b/src/core/telemetry_session.cpp @@ -192,7 +192,6 @@ void TelemetrySession::AddInitialInfo(Loader::AppLoader& app_loader) { Settings::values.shaders_accurate_mul); AddField(Telemetry::FieldType::UserConfig, "Renderer_UseShaderJit", Settings::values.use_shader_jit); - AddField(Telemetry::FieldType::UserConfig, "Renderer_UseVsync", Settings::values.vsync_enabled); AddField(Telemetry::FieldType::UserConfig, "Renderer_FilterMode", Settings::values.filter_mode); AddField(Telemetry::FieldType::UserConfig, "Renderer_Render3d", static_cast(Settings::values.render_3d)); diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index 963107f1e..b1ee6a973 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h @@ -19,21 +19,22 @@ class Backend; class RendererBase : NonCopyable { public: - /// Used to reference a framebuffer - enum kFramebuffer { kFramebuffer_VirtualXFB = 0, kFramebuffer_EFB, kFramebuffer_Texture }; - explicit RendererBase(Frontend::EmuWindow& window); virtual ~RendererBase(); - /// Swap buffers (render frame) - virtual void SwapBuffers() = 0; - /// Initialize the renderer virtual Core::System::ResultStatus Init() = 0; /// Shutdown the renderer virtual void ShutDown() = 0; + /// Finalize rendering the guest frame and draw into the presentation texture + virtual void SwapBuffers() = 0; + + /// Draws the latest frame to the window waiting timeout_ms for a frame to arrive (Renderer + /// specific implementation) + virtual void TryPresent(int timeout_ms) = 0; + /// Prepares for video dumping (e.g. create necessary buffers, etc) virtual void PrepareVideoDumping() = 0; diff --git a/src/video_core/renderer_opengl/gl_resource_manager.cpp b/src/video_core/renderer_opengl/gl_resource_manager.cpp index 1cffc8ea7..836cbb90c 100644 --- a/src/video_core/renderer_opengl/gl_resource_manager.cpp +++ b/src/video_core/renderer_opengl/gl_resource_manager.cpp @@ -15,6 +15,24 @@ MICROPROFILE_DEFINE(OpenGL_ResourceDeletion, "OpenGL", "Resource Deletion", MP_R namespace OpenGL { +void OGLRenderbuffer::Create() { + if (handle != 0) + return; + + MICROPROFILE_SCOPE(OpenGL_ResourceCreation); + glGenRenderbuffers(1, &handle); +} + +void OGLRenderbuffer::Release() { + if (handle == 0) + return; + + MICROPROFILE_SCOPE(OpenGL_ResourceDeletion); + glDeleteRenderbuffers(1, &handle); + OpenGLState::GetCurState().ResetRenderbuffer(handle).Apply(); + handle = 0; +} + void OGLTexture::Create() { if (handle != 0) return; diff --git a/src/video_core/renderer_opengl/gl_resource_manager.h b/src/video_core/renderer_opengl/gl_resource_manager.h index ce7f7fbfb..7f94c8a39 100644 --- a/src/video_core/renderer_opengl/gl_resource_manager.h +++ b/src/video_core/renderer_opengl/gl_resource_manager.h @@ -12,6 +12,31 @@ namespace OpenGL { +class OGLRenderbuffer : private NonCopyable { +public: + OGLRenderbuffer() = default; + + OGLRenderbuffer(OGLRenderbuffer&& o) noexcept : handle(std::exchange(o.handle, 0)) {} + + ~OGLRenderbuffer() { + Release(); + } + + OGLRenderbuffer& operator=(OGLRenderbuffer&& o) noexcept { + Release(); + handle = std::exchange(o.handle, 0); + return *this; + } + + /// Creates a new internal OpenGL resource and stores the handle + void Create(); + + /// Deletes the internal OpenGL resource + void Release(); + + GLuint handle = 0; +}; + class OGLTexture : private NonCopyable { public: OGLTexture() = default; diff --git a/src/video_core/renderer_opengl/gl_state.cpp b/src/video_core/renderer_opengl/gl_state.cpp index 17bf2b2f7..3e15d64b9 100644 --- a/src/video_core/renderer_opengl/gl_state.cpp +++ b/src/video_core/renderer_opengl/gl_state.cpp @@ -89,6 +89,8 @@ OpenGLState::OpenGLState() { viewport.height = 0; clip_distance = {}; + + renderbuffer = 0; } void OpenGLState::Apply() const { @@ -337,6 +339,10 @@ void OpenGLState::Apply() const { } } + if (renderbuffer != cur_state.renderbuffer) { + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + } + cur_state = *this; } @@ -422,4 +428,11 @@ OpenGLState& OpenGLState::ResetFramebuffer(GLuint handle) { return *this; } +OpenGLState& OpenGLState::ResetRenderbuffer(GLuint handle) { + if (renderbuffer == handle) { + renderbuffer = 0; + } + return *this; +} + } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h index b1842a820..cc7a864a2 100644 --- a/src/video_core/renderer_opengl/gl_state.h +++ b/src/video_core/renderer_opengl/gl_state.h @@ -144,6 +144,8 @@ public: std::array clip_distance; // GL_CLIP_DISTANCE + GLuint renderbuffer; // GL_RENDERBUFFER_BINDING + OpenGLState(); /// Get the currently active OpenGL state @@ -162,6 +164,7 @@ public: OpenGLState& ResetBuffer(GLuint handle); OpenGLState& ResetVertexArray(GLuint handle); OpenGLState& ResetFramebuffer(GLuint handle); + OpenGLState& ResetRenderbuffer(GLuint handle); private: static OpenGLState cur_state; diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 8de06a7d2..4526330d1 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -3,13 +3,19 @@ // Refer to the license.txt file included. #include +#include +#include #include #include +#include #include +#include #include +#include #include "common/assert.h" #include "common/bit_field.h" #include "common/logging/log.h" +#include "common/microprofile.h" #include "core/core.h" #include "core/core_timing.h" #include "core/dumping/backend.h" @@ -28,8 +34,151 @@ #include "video_core/renderer_opengl/renderer_opengl.h" #include "video_core/video_core.h" +namespace Frontend { + +struct Frame { + u32 width{}; /// Width of the frame (to detect resize) + u32 height{}; /// Height of the frame + bool color_reloaded = false; /// Texture attachment was recreated (ie: resized) + OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO + OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread + OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread + GLsync render_fence{}; /// Fence created on the render thread + GLsync present_fence{}; /// Fence created on the presentation thread +}; +} // namespace Frontend + namespace OpenGL { +// If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have +// to wait on available presentation frames. There doesn't seem to be much of a downside to a larger +// number but 9 swap textures at 60FPS presentation allows for 800% speed so thats probably fine +constexpr std::size_t SWAP_CHAIN_SIZE = 9; + +class OGLTextureMailbox : public Frontend::TextureMailbox { +public: + std::mutex swap_chain_lock; + std::condition_variable free_cv; + std::condition_variable present_cv; + std::array swap_chain{}; + std::queue free_queue{}; + std::deque present_queue{}; + Frontend::Frame* previous_frame = nullptr; + + OGLTextureMailbox() { + for (auto& frame : swap_chain) { + free_queue.push(&frame); + } + } + + ~OGLTextureMailbox() override { + // lock the mutex and clear out the present and free_queues and notify any people who are + // blocked to prevent deadlock on shutdown + std::scoped_lock lock(swap_chain_lock); + std::queue().swap(free_queue); + present_queue.clear(); + free_cv.notify_all(); + present_cv.notify_all(); + } + + void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override { + frame->present.Release(); + frame->present.Create(); + GLint previous_draw_fbo{}; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + frame->color.handle); + if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!"); + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo); + frame->color_reloaded = false; + } + + void ReloadRenderFrame(Frontend::Frame* frame, u32 width, u32 height) override { + OpenGLState prev_state = OpenGLState::GetCurState(); + OpenGLState state = OpenGLState::GetCurState(); + + // Recreate the color texture attachment + frame->color.Release(); + frame->color.Create(); + state.renderbuffer = frame->color.handle; + state.Apply(); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, width, height); + + // Recreate the FBO for the render target + frame->render.Release(); + frame->render.Create(); + state.draw.read_framebuffer = frame->render.handle; + state.draw.draw_framebuffer = frame->render.handle; + state.Apply(); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + frame->color.handle); + if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!"); + } + prev_state.Apply(); + frame->width = width; + frame->height = height; + frame->color_reloaded = true; + } + + Frontend::Frame* GetRenderFrame() override { + std::unique_lock lock(swap_chain_lock); + // wait for new entries in the free_queue + // we want to break at some point to prevent a softlock on close if the presentation thread + // stops consuming buffers + free_cv.wait_for(lock, std::chrono::milliseconds(100), [&] { return !free_queue.empty(); }); + + // If theres no free frames, we will reuse the oldest render frame + if (free_queue.empty()) { + auto frame = present_queue.back(); + present_queue.pop_back(); + return frame; + } + + Frontend::Frame* frame = free_queue.front(); + free_queue.pop(); + return frame; + } + + void ReleaseRenderFrame(Frontend::Frame* frame) override { + std::unique_lock lock(swap_chain_lock); + present_queue.push_front(frame); + present_cv.notify_one(); + } + + Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { + std::unique_lock lock(swap_chain_lock); + // wait for new entries in the present_queue + present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), + [&] { return !present_queue.empty(); }); + if (present_queue.empty()) { + // timed out waiting for a frame to draw so return the previous frame + return previous_frame; + } + + // free the previous frame and add it back to the free queue + if (previous_frame) { + free_queue.push(previous_frame); + free_cv.notify_one(); + } + + // the newest entries are pushed to the front of the queue + Frontend::Frame* frame = present_queue.front(); + present_queue.pop_front(); + // remove all old entries from the present queue and move them back to the free_queue + for (auto f : present_queue) { + free_queue.push(f); + } + free_cv.notify_one(); + present_queue.clear(); + previous_frame = frame; + return frame; + } +}; + static const char vertex_shader[] = R"( in vec2 vert_position; in vec2 vert_tex_coord; @@ -53,7 +202,7 @@ void main() { static const char fragment_shader[] = R"( in vec2 frag_tex_coord; -out vec4 color; +layout(location = 0) out vec4 color; uniform vec4 i_resolution; uniform vec4 o_resolution; @@ -130,15 +279,127 @@ static std::array MakeOrthographicMatrix(const float width, cons return matrix; } -RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} {} +RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} { + window.mailbox = std::make_unique(); +} + RendererOpenGL::~RendererOpenGL() = default; +MICROPROFILE_DEFINE(OpenGL_RenderFrame, "OpenGL", "Render Frame", MP_RGB(128, 128, 64)); +MICROPROFILE_DEFINE(OpenGL_WaitPresent, "OpenGL", "Wait For Present", MP_RGB(128, 128, 128)); + /// Swap buffers (render frame) void RendererOpenGL::SwapBuffers() { // Maintain the rasterizer's state as a priority OpenGLState prev_state = OpenGLState::GetCurState(); state.Apply(); + PrepareRendertarget(); + + RenderScreenshot(); + + RenderVideoDumping(); + + const auto& layout = render_window.GetFramebufferLayout(); + + Frontend::Frame* frame; + { + MICROPROFILE_SCOPE(OpenGL_WaitPresent); + + frame = render_window.mailbox->GetRenderFrame(); + + // Clean up sync objects before drawing + + // INTEL driver workaround. We can't delete the previous render sync object until we are + // sure that the presentation is done + if (frame->present_fence) { + glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); + } + + // delete the draw fence if the frame wasn't presented + if (frame->render_fence) { + glDeleteSync(frame->render_fence); + frame->render_fence = 0; + } + + // wait for the presentation to be done + if (frame->present_fence) { + glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(frame->present_fence); + frame->present_fence = 0; + } + } + + { + MICROPROFILE_SCOPE(OpenGL_RenderFrame); + // Recreate the frame if the size of the window has changed + if (layout.width != frame->width || layout.height != frame->height) { + LOG_DEBUG(Render_OpenGL, "Reloading render frame"); + render_window.mailbox->ReloadRenderFrame(frame, layout.width, layout.height); + } + + GLuint render_texture = frame->color.handle; + state.draw.draw_framebuffer = frame->render.handle; + state.Apply(); + DrawScreens(layout); + // Create a fence for the frontend to wait on and swap this frame to OffTex + frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + render_window.mailbox->ReleaseRenderFrame(frame); + m_current_frame++; + } + + Core::System::GetInstance().perf_stats->EndSystemFrame(); + + render_window.PollEvents(); + + Core::System::GetInstance().frame_limiter.DoFrameLimiting( + Core::System::GetInstance().CoreTiming().GetGlobalTimeUs()); + Core::System::GetInstance().perf_stats->BeginSystemFrame(); + + prev_state.Apply(); + RefreshRasterizerSetting(); + + if (Pica::g_debug_context && Pica::g_debug_context->recorder) { + Pica::g_debug_context->recorder->FrameFinished(); + } +} + +void RendererOpenGL::RenderScreenshot() { + if (VideoCore::g_renderer_screenshot_requested) { + // Draw this frame to the screenshot framebuffer + screenshot_framebuffer.Create(); + GLuint old_read_fb = state.draw.read_framebuffer; + GLuint old_draw_fb = state.draw.draw_framebuffer; + state.draw.read_framebuffer = state.draw.draw_framebuffer = screenshot_framebuffer.handle; + state.Apply(); + + Layout::FramebufferLayout layout{VideoCore::g_screenshot_framebuffer_layout}; + + GLuint renderbuffer; + glGenRenderbuffers(1, &renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + renderbuffer); + + DrawScreens(layout); + + glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + VideoCore::g_screenshot_bits); + + screenshot_framebuffer.Release(); + state.draw.read_framebuffer = old_read_fb; + state.draw.draw_framebuffer = old_draw_fb; + state.Apply(); + glDeleteRenderbuffers(1, &renderbuffer); + + VideoCore::g_screenshot_complete_callback(); + VideoCore::g_renderer_screenshot_requested = false; + } +} + +void RendererOpenGL::PrepareRendertarget() { for (int i : {0, 1, 2}) { int fb_id = i == 2 ? 1 : 0; const auto& framebuffer = GPU::g_regs.framebuffer_config[fb_id]; @@ -173,39 +434,9 @@ void RendererOpenGL::SwapBuffers() { screen_infos[i].texture.height = framebuffer.height; } } +} - if (VideoCore::g_renderer_screenshot_requested) { - // Draw this frame to the screenshot framebuffer - screenshot_framebuffer.Create(); - GLuint old_read_fb = state.draw.read_framebuffer; - GLuint old_draw_fb = state.draw.draw_framebuffer; - state.draw.read_framebuffer = state.draw.draw_framebuffer = screenshot_framebuffer.handle; - state.Apply(); - - Layout::FramebufferLayout layout{VideoCore::g_screenshot_framebuffer_layout}; - - GLuint renderbuffer; - glGenRenderbuffers(1, &renderbuffer); - glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - renderbuffer); - - DrawScreens(layout); - - glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, - VideoCore::g_screenshot_bits); - - screenshot_framebuffer.Release(); - state.draw.read_framebuffer = old_read_fb; - state.draw.draw_framebuffer = old_draw_fb; - state.Apply(); - glDeleteRenderbuffers(1, &renderbuffer); - - VideoCore::g_screenshot_complete_callback(); - VideoCore::g_renderer_screenshot_requested = false; - } - +void RendererOpenGL::RenderVideoDumping() { if (cleanup_video_dumping.exchange(false)) { ReleaseVideoDumpingGLObjects(); } @@ -230,31 +461,9 @@ void RendererOpenGL::SwapBuffers() { glUnmapBuffer(GL_PIXEL_PACK_BUFFER); glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); current_pbo = (current_pbo + 1) % 2; next_pbo = (current_pbo + 1) % 2; } - - DrawScreens(render_window.GetFramebufferLayout()); - m_current_frame++; - - Core::System::GetInstance().perf_stats->EndSystemFrame(); - - // Swap buffers - render_window.PollEvents(); - render_window.SwapBuffers(); - - Core::System::GetInstance().frame_limiter.DoFrameLimiting( - Core::System::GetInstance().CoreTiming().GetGlobalTimeUs()); - Core::System::GetInstance().perf_stats->BeginSystemFrame(); - - prev_state.Apply(); - RefreshRasterizerSetting(); - - if (Pica::g_debug_context && Pica::g_debug_context->recorder) { - Pica::g_debug_context->recorder->FrameFinished(); - } } /** @@ -669,6 +878,41 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) { } } +void RendererOpenGL::TryPresent(int timeout_ms) { + const auto& layout = render_window.GetFramebufferLayout(); + auto frame = render_window.mailbox->TryGetPresentFrame(timeout_ms); + if (!frame) { + LOG_DEBUG(Render_OpenGL, "TryGetPresentFrame returned no frame to present"); + return; + } + + // Clearing before a full overwrite of a fbo can signal to drivers that they can avoid a + // readback since we won't be doing any blending + glClear(GL_COLOR_BUFFER_BIT); + + // Recreate the presentation FBO if the color attachment was changed + if (frame->color_reloaded) { + LOG_DEBUG(Render_OpenGL, "Reloading present frame"); + render_window.mailbox->ReloadPresentFrame(frame, layout.width, layout.height); + } + glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED); + // INTEL workaround. + // Normally we could just delete the draw fence here, but due to driver bugs, we can just delete + // it on the emulation thread without too much penalty + // glDeleteSync(frame.render_sync); + // frame.render_sync = 0; + + glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle); + glBlitFramebuffer(0, 0, frame->width, frame->height, 0, 0, layout.width, layout.height, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + + /* insert fence for the main thread to block on */ + frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); +} + /// Updates the framerate void RendererOpenGL::UpdateFramerate() {} @@ -766,7 +1010,9 @@ static void APIENTRY DebugHandler(GLenum source, GLenum type, GLuint id, GLenum /// Initialize the renderer Core::System::ResultStatus RendererOpenGL::Init() { - render_window.MakeCurrent(); + if (!gladLoadGL()) { + return Core::System::ResultStatus::ErrorVideoCore_ErrorBelowGL33; + } if (GLAD_GL_KHR_debug) { glEnable(GL_DEBUG_OUTPUT); diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 19f31ab52..40c850b78 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -36,20 +36,30 @@ struct ScreenInfo { TextureInfo texture; }; +struct PresentationTexture { + u32 width = 0; + u32 height = 0; + OGLTexture texture; +}; + class RendererOpenGL : public RendererBase { public: explicit RendererOpenGL(Frontend::EmuWindow& window); ~RendererOpenGL() override; - /// Swap buffers (render frame) - void SwapBuffers() override; - /// Initialize the renderer Core::System::ResultStatus Init() override; /// Shutdown the renderer void ShutDown() override; + /// Finalizes rendering the guest frame + void SwapBuffers() override; + + /// Draws the latest frame from texture mailbox to the currently bound draw framebuffer in this + /// context + void TryPresent(int timeout_ms) override; + /// Prepares for video dumping (e.g. create necessary buffers, etc) void PrepareVideoDumping() override; @@ -60,6 +70,9 @@ private: void InitOpenGLObjects(); void ReloadSampler(); void ReloadShader(); + void PrepareRendertarget(); + void RenderScreenshot(); + void RenderVideoDumping(); void ConfigureFramebufferTexture(TextureInfo& texture, const GPU::Regs::FramebufferConfig& framebuffer); void DrawScreens(const Layout::FramebufferLayout& layout);