diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 8f07aedc96..f8662d1931 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -6,6 +6,7 @@ #include "common/assert.h" #include "core/frontend/framebuffer_layout.h" +#include "core/settings.h" namespace Layout { @@ -42,4 +43,18 @@ FramebufferLayout DefaultFrameLayout(unsigned width, unsigned height) { return res; } +FramebufferLayout FrameLayoutFromResolutionScale(u16 res_scale) { + int width, height; + + if (Settings::values.use_docked_mode) { + width = ScreenDocked::WidthDocked * res_scale; + height = ScreenDocked::HeightDocked * res_scale; + } else { + width = ScreenUndocked::Width * res_scale; + height = ScreenUndocked::Height * res_scale; + } + + return DefaultFrameLayout(width, height); +} + } // namespace Layout diff --git a/src/core/frontend/framebuffer_layout.h b/src/core/frontend/framebuffer_layout.h index f3fddfa947..e06647794f 100644 --- a/src/core/frontend/framebuffer_layout.h +++ b/src/core/frontend/framebuffer_layout.h @@ -9,6 +9,7 @@ namespace Layout { enum ScreenUndocked : unsigned { Width = 1280, Height = 720 }; +enum ScreenDocked : unsigned { WidthDocked = 1920, HeightDocked = 1080 }; /// Describes the layout of the window framebuffer struct FramebufferLayout { @@ -34,4 +35,10 @@ struct FramebufferLayout { */ FramebufferLayout DefaultFrameLayout(unsigned width, unsigned height); +/** + * Convenience method to get frame layout by resolution scale + * @param res_scale resolution scale factor + */ +FramebufferLayout FrameLayoutFromResolutionScale(u16 res_scale); + } // namespace Layout diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp index 1482cdb409..94223f45f9 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -27,4 +27,16 @@ void RendererBase::UpdateCurrentFramebufferLayout() { render_window.UpdateCurrentFramebufferLayout(layout.width, layout.height); } +void RendererBase::RequestScreenshot(void* data, std::function callback, + const Layout::FramebufferLayout& layout) { + if (renderer_settings.screenshot_requested) { + LOG_ERROR(Render, "A screenshot is already requested or in progress, ignoring the request"); + return; + } + renderer_settings.screenshot_bits = data; + renderer_settings.screenshot_complete_callback = std::move(callback); + renderer_settings.screenshot_framebuffer_layout = layout; + renderer_settings.screenshot_requested = true; +} + } // namespace VideoCore diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index 669e26e15c..1d54c3723f 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h @@ -9,6 +9,7 @@ #include #include "common/common_types.h" +#include "core/frontend/emu_window.h" #include "video_core/gpu.h" #include "video_core/rasterizer_interface.h" @@ -21,6 +22,12 @@ namespace VideoCore { struct RendererSettings { std::atomic_bool use_framelimiter{false}; std::atomic_bool set_background_color{false}; + + // Screenshot + std::atomic screenshot_requested{false}; + void* screenshot_bits; + std::function screenshot_complete_callback; + Layout::FramebufferLayout screenshot_framebuffer_layout; }; class RendererBase : NonCopyable { @@ -57,9 +64,29 @@ public: return *rasterizer; } + Core::Frontend::EmuWindow& GetRenderWindow() { + return render_window; + } + + const Core::Frontend::EmuWindow& GetRenderWindow() const { + return render_window; + } + + RendererSettings& Settings() { + return renderer_settings; + } + + const RendererSettings& Settings() const { + return renderer_settings; + } + /// Refreshes the settings common to all renderers void RefreshBaseSettings(); + /// Request a screenshot of the next frame + void RequestScreenshot(void* data, std::function callback, + const Layout::FramebufferLayout& layout); + protected: Core::Frontend::EmuWindow& render_window; ///< Reference to the render window handle. std::unique_ptr rasterizer; diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 4fd0d66c55..49a1989e49 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -138,7 +138,12 @@ void RendererOpenGL::SwapBuffers( // Load the framebuffer from memory, draw it to the screen, and swap buffers LoadFBToScreenInfo(*framebuffer); - DrawScreen(); + + if (renderer_settings.screenshot_requested) + CaptureScreenshot(); + + DrawScreen(render_window.GetFramebufferLayout()); + render_window.SwapBuffers(); } @@ -383,14 +388,13 @@ void RendererOpenGL::DrawScreenTriangles(const ScreenInfo& screen_info, float x, /** * Draws the emulated screens to the emulator window. */ -void RendererOpenGL::DrawScreen() { +void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) { if (renderer_settings.set_background_color) { // Update background color before drawing glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue, 0.0f); } - const auto& layout = render_window.GetFramebufferLayout(); const auto& screen = layout.screen; glViewport(0, 0, layout.width, layout.height); @@ -414,6 +418,37 @@ void RendererOpenGL::DrawScreen() { /// Updates the framerate void RendererOpenGL::UpdateFramerate() {} +void RendererOpenGL::CaptureScreenshot() { + // Draw the current 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{renderer_settings.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); + + DrawScreen(layout); + + glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + renderer_settings.screenshot_bits); + + screenshot_framebuffer.Release(); + state.draw.read_framebuffer = old_read_fb; + state.draw.draw_framebuffer = old_draw_fb; + state.Apply(); + glDeleteRenderbuffers(1, &renderbuffer); + + renderer_settings.screenshot_complete_callback(); + renderer_settings.screenshot_requested = false; +} + static const char* GetSource(GLenum source) { #define RET(s) \ case GL_DEBUG_SOURCE_##s: \ diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index c0868c0e47..067fad81b7 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -16,6 +16,10 @@ namespace Core::Frontend { class EmuWindow; } +namespace Layout { +class FramebufferLayout; +} + namespace OpenGL { /// Structure used for storing information about the textures for the Switch screen @@ -66,10 +70,12 @@ private: void ConfigureFramebufferTexture(TextureInfo& texture, const Tegra::FramebufferConfig& framebuffer); - void DrawScreen(); + void DrawScreen(const Layout::FramebufferLayout& layout); void DrawScreenTriangles(const ScreenInfo& screen_info, float x, float y, float w, float h); void UpdateFramerate(); + void CaptureScreenshot(); + // Loads framebuffer from emulated memory into the display information structure void LoadFBToScreenInfo(const Tegra::FramebufferConfig& framebuffer); // Fills active OpenGL texture with the given RGBA color. @@ -82,6 +88,7 @@ private: OGLVertexArray vertex_array; OGLBuffer vertex_buffer; OGLProgram shader; + OGLFramebuffer screenshot_framebuffer; /// Display information for Switch screen ScreenInfo screen_info; diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index 07e3a7d246..f7de3471bd 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -3,6 +3,8 @@ // Refer to the license.txt file included. #include +#include "core/core.h" +#include "core/settings.h" #include "video_core/renderer_base.h" #include "video_core/renderer_opengl/renderer_opengl.h" #include "video_core/video_core.h" @@ -13,4 +15,10 @@ std::unique_ptr CreateRenderer(Core::Frontend::EmuWindow& emu_wind return std::make_unique(emu_window); } +u16 GetResolutionScaleFactor(const RendererBase& renderer) { + return !Settings::values.resolution_factor + ? renderer.GetRenderWindow().GetFramebufferLayout().GetScalingRatio() + : Settings::values.resolution_factor; +} + } // namespace VideoCore diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h index f79f85dfeb..5b373bcb1f 100644 --- a/src/video_core/video_core.h +++ b/src/video_core/video_core.h @@ -22,4 +22,6 @@ class RendererBase; */ std::unique_ptr CreateRenderer(Core::Frontend::EmuWindow& emu_window); +u16 GetResolutionScaleFactor(const RendererBase& renderer); + } // namespace VideoCore diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 384e17921e..40db7a5e92 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -14,6 +14,8 @@ #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" EmuThread::EmuThread(GRenderWindow* render_window) : render_window(render_window) {} @@ -333,6 +335,22 @@ void GRenderWindow::InitRenderTarget() { 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& minimal_size) { setMinimumSize(minimal_size.first, minimal_size.second); diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index 873985564f..4e30282155 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "common/thread.h" #include "core/core.h" @@ -139,6 +140,8 @@ public: void InitRenderTarget(); + void CaptureScreenshot(u16 res_scale, const QString& screenshot_path); + public slots: void moveContext(); // overridden @@ -165,6 +168,9 @@ private: EmuThread* emu_thread; + /// Temporary storage of the screenshot taken + QImage screenshot_image; + protected: void showEvent(QShowEvent* event) override; }; diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index c261611693..02e09fa184 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -445,6 +445,8 @@ void Config::ReadValues() { UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString(); UISettings::values.enable_discord_presence = qt_config->value("enable_discord_presence", true).toBool(); + UISettings::values.screenshot_resolution_factor = + static_cast(qt_config->value("screenshot_resolution_factor", 0).toUInt()); qt_config->beginGroup("UIGameList"); UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool(); @@ -648,6 +650,8 @@ void Config::SaveValues() { qt_config->beginGroup("UI"); qt_config->setValue("theme", UISettings::values.theme); qt_config->setValue("enable_discord_presence", UISettings::values.enable_discord_presence); + qt_config->setValue("screenshot_resolution_factor", + UISettings::values.screenshot_resolution_factor); qt_config->beginGroup("UIGameList"); qt_config->setValue("show_unknown", UISettings::values.show_unknown); @@ -669,6 +673,7 @@ void Config::SaveValues() { qt_config->beginGroup("Paths"); qt_config->setValue("romsPath", UISettings::values.roms_path); qt_config->setValue("symbolsPath", UISettings::values.symbols_path); + qt_config->setValue("screenshotPath", UISettings::values.screenshot_path); qt_config->setValue("gameListRootDir", UISettings::values.gamedir); qt_config->setValue("gameListDeepScan", UISettings::values.gamedir_deepscan); qt_config->setValue("recentFiles", UISettings::values.recent_files); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 22c207a3af..808f14fb3d 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -334,6 +334,9 @@ void GMainWindow::InitializeHotkeys() { Qt::ApplicationShortcut); hotkey_registry.RegisterHotkey("Main Window", "Load Amiibo", QKeySequence(Qt::Key_F2), Qt::ApplicationShortcut); + hotkey_registry.RegisterHotkey("Main Window", "Capture Screenshot", + QKeySequence(QKeySequence::Print)); + hotkey_registry.LoadHotkeys(); connect(hotkey_registry.GetHotkey("Main Window", "Load File", this), &QShortcut::activated, @@ -393,6 +396,12 @@ void GMainWindow::InitializeHotkeys() { OnLoadAmiibo(); } }); + connect(hotkey_registry.GetHotkey("Main Window", "Capture Screenshot", this), + &QShortcut::activated, this, [&] { + if (emu_thread->IsRunning()) { + OnCaptureScreenshot(); + } + }); } void GMainWindow::SetDefaultUIGeometry() { @@ -488,6 +497,10 @@ void GMainWindow::ConnectMenuEvents() { hotkey_registry.GetHotkey("Main Window", "Fullscreen", this)->key()); connect(ui.action_Fullscreen, &QAction::triggered, this, &GMainWindow::ToggleFullscreen); + // Movie + connect(ui.action_Capture_Screenshot, &QAction::triggered, this, + &GMainWindow::OnCaptureScreenshot); + // Help connect(ui.action_Open_yuzu_Folder, &QAction::triggered, this, &GMainWindow::OnOpenYuzuFolder); connect(ui.action_Rederive, &QAction::triggered, this, @@ -724,6 +737,7 @@ void GMainWindow::ShutdownGame() { ui.action_Restart->setEnabled(false); ui.action_Report_Compatibility->setEnabled(false); ui.action_Load_Amiibo->setEnabled(false); + ui.action_Capture_Screenshot->setEnabled(false); render_window->hide(); game_list->show(); game_list->setFilterFocus(); @@ -1261,6 +1275,7 @@ void GMainWindow::OnStartGame() { discord_rpc->Update(); ui.action_Load_Amiibo->setEnabled(true); + ui.action_Capture_Screenshot->setEnabled(true); } void GMainWindow::OnPauseGame() { @@ -1269,6 +1284,7 @@ void GMainWindow::OnPauseGame() { ui.action_Start->setEnabled(true); ui.action_Pause->setEnabled(false); ui.action_Stop->setEnabled(true); + ui.action_Capture_Screenshot->setEnabled(false); } void GMainWindow::OnStopGame() { @@ -1431,6 +1447,18 @@ void GMainWindow::OnToggleFilterBar() { } } +void GMainWindow::OnCaptureScreenshot() { + OnPauseGame(); + const QString path = + QFileDialog::getSaveFileName(this, tr("Capture Screenshot"), + UISettings::values.screenshot_path, tr("PNG Image (*.png)")); + if (!path.isEmpty()) { + UISettings::values.screenshot_path = QFileInfo(path).path(); + render_window->CaptureScreenshot(UISettings::values.screenshot_resolution_factor, path); + } + OnStartGame(); +} + void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { status_bar_update_timer.stop(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 674e734125..9a1df51682 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -186,6 +186,7 @@ private slots: void ShowFullscreen(); void HideFullscreen(); void ToggleWindowMode(); + void OnCaptureScreenshot(); void OnCoreError(Core::System::ResultStatus, std::string); void OnReinitializeKeys(ReinitializeKeyBehavior behavior); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 75e96387f6..ffcabb4954 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -101,11 +101,13 @@ - + Tools - + + + @@ -118,7 +120,7 @@ - + @@ -173,11 +175,11 @@ &Stop - - - Reinitialize keys... - - + + + Reinitialize keys... + + About yuzu @@ -252,39 +254,47 @@ Fullscreen - - - false - - - Restart - - - - - false - - - Load Amiibo... - + + + false + + + Restart + - - - false - - - Report Compatibility - - - false - - - - - Open yuzu Folder - - - + + + false + + + Load Amiibo... + + + + + false + + + Report Compatibility + + + false + + + + + Open yuzu Folder + + + + + false + + + Capture Screenshot + + + diff --git a/src/yuzu/ui_settings.h b/src/yuzu/ui_settings.h index e80aebc0a2..b03bc2de60 100644 --- a/src/yuzu/ui_settings.h +++ b/src/yuzu/ui_settings.h @@ -10,6 +10,7 @@ #include #include #include +#include "common/common_types.h" namespace UISettings { @@ -42,8 +43,11 @@ struct Values { // Discord RPC bool enable_discord_presence; + u16 screenshot_resolution_factor; + QString roms_path; QString symbols_path; + QString screenshot_path; QString gamedir; bool gamedir_deepscan; QStringList recent_files;