diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 39115f31e..ef47dcf9c 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -15,6 +15,7 @@ #include "input_common/main.h" #include "input_common/motion_emu.h" #include "network/network.h" +#include "video_core/video_core.h" EmuThread::EmuThread(GRenderWindow* render_window) : render_window(render_window) {} @@ -335,6 +336,19 @@ void GRenderWindow::InitRenderTarget() { BackupGeometry(); } +void GRenderWindow::CaptureScreenshot(u16 res_scale, const QString& screenshot_path) { + if (!res_scale) + res_scale = VideoCore::GetResolutionScaleFactor(); + const Layout::FramebufferLayout layout{Layout::FrameLayoutFromResolutionScale(res_scale)}; + screenshot_image = QImage(QSize(layout.width, layout.height), QImage::Format_RGB32); + VideoCore::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/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h index 56b00ea70..567fe19f1 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/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/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 3004fa936..d696ed98d 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -206,6 +206,8 @@ void Config::ReadValues() { UISettings::values.theme = ReadSetting("theme", UISettings::themes[0].second).toString(); UISettings::values.enable_discord_presence = ReadSetting("enable_discord_presence", true).toBool(); + UISettings::values.screenshot_resolution_factor = + static_cast(ReadSetting("screenshot_resolution_factor", 0).toUInt()); qt_config->beginGroup("Updater"); UISettings::values.check_for_update_on_start = @@ -251,6 +253,7 @@ void Config::ReadValues() { UISettings::values.symbols_path = ReadSetting("symbolsPath").toString(); UISettings::values.movie_record_path = ReadSetting("movieRecordPath").toString(); UISettings::values.movie_playback_path = ReadSetting("moviePlaybackPath").toString(); + UISettings::values.screenshot_path = ReadSetting("screenshotPath").toString(); UISettings::values.game_dir_deprecated = ReadSetting("gameListRootDir", ".").toString(); UISettings::values.game_dir_deprecated_deepscan = ReadSetting("gameListDeepScan", false).toBool(); @@ -460,6 +463,8 @@ void Config::SaveValues() { qt_config->beginGroup("UI"); WriteSetting("theme", UISettings::values.theme, UISettings::themes[0].second); WriteSetting("enable_discord_presence", UISettings::values.enable_discord_presence, true); + WriteSetting("screenshot_resolution_factor", UISettings::values.screenshot_resolution_factor, + 0); qt_config->beginGroup("Updater"); WriteSetting("check_for_update_on_start", UISettings::values.check_for_update_on_start, true); @@ -487,6 +492,7 @@ void Config::SaveValues() { WriteSetting("symbolsPath", UISettings::values.symbols_path); WriteSetting("movieRecordPath", UISettings::values.movie_record_path); WriteSetting("moviePlaybackPath", UISettings::values.movie_playback_path); + WriteSetting("screenshotPath", UISettings::values.screenshot_path); qt_config->beginWriteArray("gamedirs"); for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { qt_config->setArrayIndex(i); diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index db6ba6202..341c11dc3 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -365,6 +365,7 @@ void GMainWindow::InitializeHotkeys() { Qt::ApplicationShortcut); hotkey_registry.RegisterHotkey("Main Window", "Remove Amiibo", QKeySequence(Qt::Key_F3), Qt::ApplicationShortcut); + hotkey_registry.RegisterHotkey("Main Window", "Capture Screenshot", QKeySequence(tr("CTRL+P"))); hotkey_registry.LoadHotkeys(); @@ -439,6 +440,12 @@ void GMainWindow::InitializeHotkeys() { OnRemoveAmiibo(); } }); + connect(hotkey_registry.GetHotkey("Main Window", "Capture Screenshot", this), + &QShortcut::activated, this, [&] { + if (emu_thread->IsRunning()) { + OnCaptureScreenshot(); + } + }); } void GMainWindow::ShowUpdaterWidgets() { @@ -588,6 +595,8 @@ void GMainWindow::ConnectMenuEvents() { Core::System::GetInstance().frame_limiter.AdvanceFrame(); } }); + connect(ui.action_Capture_Screenshot, &QAction::triggered, this, + &GMainWindow::OnCaptureScreenshot); // Help connect(ui.action_Open_Citra_Folder, &QAction::triggered, this, @@ -882,6 +891,7 @@ void GMainWindow::ShutdownGame() { ui.action_Enable_Frame_Advancing->setEnabled(false); ui.action_Enable_Frame_Advancing->setChecked(false); ui.action_Advance_Frame->setEnabled(false); + ui.action_Capture_Screenshot->setEnabled(false); render_window->hide(); if (game_list->isEmpty()) game_list_placeholder->show(); @@ -1169,6 +1179,7 @@ void GMainWindow::OnStartGame() { ui.action_Load_Amiibo->setEnabled(true); ui.action_Report_Compatibility->setEnabled(true); ui.action_Enable_Frame_Advancing->setEnabled(true); + ui.action_Capture_Screenshot->setEnabled(true); discord_rpc->Update(); } @@ -1179,6 +1190,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() { @@ -1539,6 +1551,18 @@ void GMainWindow::OnStopRecordingPlayback() { ui.action_Stop_Recording_Playback->setEnabled(false); } +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/citra_qt/main.h b/src/citra_qt/main.h index 84ee2cbf7..0d561508a 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -183,6 +183,7 @@ private slots: void OnRecordMovie(); void OnPlayMovie(); void OnStopRecordingPlayback(); + void OnCaptureScreenshot(); void OnCoreError(Core::System::ResultStatus, std::string); /// Called whenever a user selects Help->About Citra void OnMenuAboutCitra(); diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index a3d66ba11..8bcc9cce3 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -131,6 +131,8 @@ + + @@ -315,6 +317,14 @@ Advance Frame + + + false + + + Capture Screenshot + + true diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index caa6a353f..1e09dd7e8 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -10,6 +10,7 @@ #include #include #include +#include "common/common_types.h" namespace UISettings { @@ -78,10 +79,13 @@ struct Values { GameListText game_list_row_2; bool game_list_hide_no_icon; + u16 screenshot_resolution_factor; + QString roms_path; QString symbols_path; QString movie_record_path; QString movie_playback_path; + QString screenshot_path; QString game_dir_deprecated; bool game_dir_deprecated_deepscan; QList game_dirs; diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index b49d7f509..8456e1300 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -191,4 +191,50 @@ FramebufferLayout CustomFrameLayout(unsigned width, unsigned height) { res.bottom_screen = bot_screen; return res; } + +FramebufferLayout FrameLayoutFromResolutionScale(u16 res_scale) { + FramebufferLayout layout; + if (Settings::values.custom_layout == true) { + layout = CustomFrameLayout( + std::max(Settings::values.custom_top_right, Settings::values.custom_bottom_right), + std::max(Settings::values.custom_top_bottom, Settings::values.custom_bottom_bottom)); + } else { + int width, height; + switch (Settings::values.layout_option) { + case Settings::LayoutOption::SingleScreen: + if (Settings::values.swap_screen) { + width = Core::kScreenBottomWidth * res_scale; + height = Core::kScreenBottomHeight * res_scale; + } else { + width = Core::kScreenTopWidth * res_scale; + height = Core::kScreenTopHeight * res_scale; + } + layout = SingleFrameLayout(width, height, Settings::values.swap_screen); + break; + case Settings::LayoutOption::LargeScreen: + if (Settings::values.swap_screen) { + width = (Core::kScreenBottomWidth + Core::kScreenTopWidth / 4) * res_scale; + height = Core::kScreenBottomHeight * res_scale; + } else { + width = (Core::kScreenTopWidth + Core::kScreenBottomWidth / 4) * res_scale; + height = Core::kScreenTopHeight * res_scale; + } + layout = LargeFrameLayout(width, height, Settings::values.swap_screen); + break; + case Settings::LayoutOption::SideScreen: + width = (Core::kScreenTopWidth + Core::kScreenBottomWidth) * res_scale; + height = Core::kScreenTopHeight * res_scale; + layout = SideFrameLayout(width, height, Settings::values.swap_screen); + break; + case Settings::LayoutOption::Default: + default: + width = Core::kScreenTopWidth * res_scale; + height = (Core::kScreenTopHeight + Core::kScreenBottomHeight) * res_scale; + layout = DefaultFrameLayout(width, height, Settings::values.swap_screen); + break; + } + } + return layout; +} + } // namespace Layout diff --git a/src/core/frontend/framebuffer_layout.h b/src/core/frontend/framebuffer_layout.h index b40bf94b6..3eb9d468c 100644 --- a/src/core/frontend/framebuffer_layout.h +++ b/src/core/frontend/framebuffer_layout.h @@ -72,4 +72,11 @@ FramebufferLayout SideFrameLayout(unsigned width, unsigned height, bool is_swapp */ FramebufferLayout CustomFrameLayout(unsigned width, unsigned height); +/** + * Convenience method to get frame layout by resolution scale + * Read from the current settings to determine which layout to use. + * @param res_scale resolution scale factor + */ +FramebufferLayout FrameLayoutFromResolutionScale(u16 res_scale); + } // namespace Layout diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index 2a377e543..638096e34 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -24,7 +24,6 @@ #include "common/vector_math.h" #include "core/frontend/emu_window.h" #include "core/memory.h" -#include "core/settings.h" #include "video_core/pica_state.h" #include "video_core/renderer_base.h" #include "video_core/renderer_opengl/gl_rasterizer_cache.h" @@ -78,12 +77,6 @@ constexpr auto RangeFromInterval(Map& map, const Interval& interval) { return boost::make_iterator_range(map.equal_range(interval)); } -static u16 GetResolutionScaleFactor() { - return !Settings::values.resolution_factor - ? VideoCore::g_renderer->GetRenderWindow().GetFramebufferLayout().GetScalingRatio() - : Settings::values.resolution_factor; -} - template static void MortonCopyTile(u32 stride, u8* tile_buffer, u8* gl_buffer) { constexpr u32 bytes_per_pixel = SurfaceParams::GetFormatBpp(format) / 8; @@ -1360,9 +1353,9 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( const auto& config = regs.framebuffer.framebuffer; // update resolution_scale_factor and reset cache if changed - static u16 resolution_scale_factor = GetResolutionScaleFactor(); - if (resolution_scale_factor != GetResolutionScaleFactor()) { - resolution_scale_factor = GetResolutionScaleFactor(); + static u16 resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); + if (resolution_scale_factor != VideoCore::GetResolutionScaleFactor()) { + resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); FlushAll(); while (!surface_cache.empty()) UnregisterSurface(*surface_cache.begin()->second.begin()); diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index ee6e4045c..e5c4e290a 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -140,7 +140,39 @@ void RendererOpenGL::SwapBuffers() { } } - DrawScreens(); + 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; + } + + DrawScreens(render_window.GetFramebufferLayout()); Core::System::GetInstance().perf_stats.EndSystemFrame(); @@ -386,14 +418,13 @@ void RendererOpenGL::DrawSingleScreenRotated(const ScreenInfo& screen_info, floa /** * Draws the emulated screens to the emulator window. */ -void RendererOpenGL::DrawScreens() { +void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) { if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) { // Update background color before drawing glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue, 0.0f); } - auto layout = render_window.GetFramebufferLayout(); const auto& top_screen = layout.top_screen; const auto& bottom_screen = layout.bottom_screen; diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 87b45529b..4dfc40968 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -13,6 +13,10 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" +namespace Layout { +class FramebufferLayout; +} + namespace OpenGL { /// Structure used for storing information about the textures for each 3DS screen @@ -50,7 +54,7 @@ private: void InitOpenGLObjects(); void ConfigureFramebufferTexture(TextureInfo& texture, const GPU::Regs::FramebufferConfig& framebuffer); - void DrawScreens(); + void DrawScreens(const Layout::FramebufferLayout& layout); void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h); void UpdateFramerate(); @@ -66,6 +70,7 @@ private: OGLVertexArray vertex_array; OGLBuffer vertex_buffer; OGLProgram shader; + OGLFramebuffer screenshot_framebuffer; /// Display information for top and bottom screens respectively std::array screen_infos; diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index f8b636de4..e9bc65f6e 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -4,6 +4,7 @@ #include #include "common/logging/log.h" +#include "core/settings.h" #include "video_core/pica.h" #include "video_core/renderer_base.h" #include "video_core/renderer_opengl/renderer_opengl.h" @@ -22,6 +23,11 @@ std::atomic g_hw_shader_enabled; std::atomic g_hw_shader_accurate_gs; std::atomic g_hw_shader_accurate_mul; std::atomic g_renderer_bg_color_update_requested; +// Screenshot +std::atomic g_renderer_screenshot_requested; +void* g_screenshot_bits; +std::function g_screenshot_complete_callback; +Layout::FramebufferLayout g_screenshot_framebuffer_layout; /// Initialize the video core Core::System::ResultStatus Init(EmuWindow& emu_window) { @@ -48,4 +54,27 @@ void Shutdown() { LOG_DEBUG(Render, "shutdown OK"); } +void RequestScreenshot(void* data, std::function callback, + const Layout::FramebufferLayout& layout) { + if (g_renderer_screenshot_requested) { + LOG_ERROR(Render, "A screenshot is already requested or in progress, ignoring the request"); + return; + } + g_screenshot_bits = data; + g_screenshot_complete_callback = std::move(callback); + g_screenshot_framebuffer_layout = layout; + g_renderer_screenshot_requested = true; +} + +u16 GetResolutionScaleFactor() { + if (g_hw_renderer_enabled) { + return !Settings::values.resolution_factor + ? g_renderer->GetRenderWindow().GetFramebufferLayout().GetScalingRatio() + : Settings::values.resolution_factor; + } else { + // Software renderer always render at native resolution + return 1; + } +} + } // namespace VideoCore diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h index deee7a7a7..0c582f1fc 100644 --- a/src/video_core/video_core.h +++ b/src/video_core/video_core.h @@ -7,6 +7,7 @@ #include #include #include "core/core.h" +#include "core/frontend/emu_window.h" class EmuWindow; class RendererBase; @@ -26,6 +27,11 @@ extern std::atomic g_hw_shader_enabled; extern std::atomic g_hw_shader_accurate_gs; extern std::atomic g_hw_shader_accurate_mul; extern std::atomic g_renderer_bg_color_update_requested; +// Screenshot +extern std::atomic g_renderer_screenshot_requested; +extern void* g_screenshot_bits; +extern std::function g_screenshot_complete_callback; +extern Layout::FramebufferLayout g_screenshot_framebuffer_layout; /// Initialize the video core Core::System::ResultStatus Init(EmuWindow& emu_window); @@ -33,4 +39,10 @@ Core::System::ResultStatus Init(EmuWindow& emu_window); /// Shutdown the video core void Shutdown(); +/// Request a screenshot of the next frame +void RequestScreenshot(void* data, std::function callback, + const Layout::FramebufferLayout& layout); + +u16 GetResolutionScaleFactor(); + } // namespace VideoCore