diff options
| author | Scott Mansell <phiren@gmail.com> | 2023-01-27 17:03:15 +1300 |
|---|---|---|
| committer | Scott Mansell <phiren@gmail.com> | 2023-01-31 18:45:21 +1300 |
| commit | 0d4537d60f695e50379c1542d6c4b1e643e8a5a0 (patch) | |
| tree | 7c0dd9c2963b7eead104df5077444e3179030b4f /Source/Core/VideoCommon/FrameDumper.cpp | |
| parent | c38c76abad039c3c0ccb3f7ebb891dc62560f8a3 (diff) | |
Move Presenting, Dumping and ImGui out of Renderer
Diffstat (limited to 'Source/Core/VideoCommon/FrameDumper.cpp')
| -rw-r--r-- | Source/Core/VideoCommon/FrameDumper.cpp | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/Source/Core/VideoCommon/FrameDumper.cpp b/Source/Core/VideoCommon/FrameDumper.cpp new file mode 100644 index 0000000000..db0d099376 --- /dev/null +++ b/Source/Core/VideoCommon/FrameDumper.cpp @@ -0,0 +1,354 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "VideoCommon/FrameDumper.h" + +#include "Common/Assert.h" +#include "Common/FileUtil.h" +#include "Common/Image.h" + +#include "Core/Config/GraphicsSettings.h" +#include "Core/Config/MainSettings.h" + +#include "VideoCommon/AbstractFramebuffer.h" +#include "VideoCommon/AbstractStagingTexture.h" +#include "VideoCommon/AbstractTexture.h" +#include "VideoCommon/OnScreenDisplay.h" +#include "VideoCommon/Present.h" +#include "VideoCommon/RenderBase.h" +#include "VideoCommon/VideoConfig.h" + +static bool DumpFrameToPNG(const FrameData& frame, const std::string& file_name) +{ + return Common::ConvertRGBAToRGBAndSavePNG(file_name, frame.data, frame.width, frame.height, + frame.stride, + Config::Get(Config::GFX_PNG_COMPRESSION_LEVEL)); +} + +FrameDumper::FrameDumper() +{ +} + +FrameDumper::~FrameDumper() +{ + ShutdownFrameDumping(); +} + +void FrameDumper::DumpCurrentFrame(const AbstractTexture* src_texture, + const MathUtil::Rectangle<int>& src_rect, + const MathUtil::Rectangle<int>& target_rect, u64 ticks, + int frame_number) +{ + int source_width = src_rect.GetWidth(); + int source_height = src_rect.GetHeight(); + int target_width = target_rect.GetWidth(); + int target_height = target_rect.GetHeight(); + + // We only need to render a copy if we need to stretch/scale the XFB copy. + MathUtil::Rectangle<int> copy_rect = src_rect; + if (source_width != target_width || source_height != target_height) + { + if (!CheckFrameDumpRenderTexture(target_width, target_height)) + return; + + g_renderer->ScaleTexture(m_frame_dump_render_framebuffer.get(), + m_frame_dump_render_framebuffer->GetRect(), src_texture, src_rect); + src_texture = m_frame_dump_render_texture.get(); + copy_rect = src_texture->GetRect(); + } + + if (!CheckFrameDumpReadbackTexture(target_width, target_height)) + return; + + m_frame_dump_readback_texture->CopyFromTexture(src_texture, copy_rect, 0, 0, + m_frame_dump_readback_texture->GetRect()); + m_last_frame_state = m_ffmpeg_dump.FetchState(ticks, frame_number); + m_frame_dump_needs_flush = true; +} + +bool FrameDumper::CheckFrameDumpRenderTexture(u32 target_width, u32 target_height) +{ + // Ensure framebuffer exists (we lazily allocate it in case frame dumping isn't used). + // Or, resize texture if it isn't large enough to accommodate the current frame. + if (m_frame_dump_render_texture && m_frame_dump_render_texture->GetWidth() == target_width && + m_frame_dump_render_texture->GetHeight() == target_height) + { + return true; + } + + // Recreate texture, but release before creating so we don't temporarily use twice the RAM. + m_frame_dump_render_framebuffer.reset(); + m_frame_dump_render_texture.reset(); + m_frame_dump_render_texture = g_renderer->CreateTexture( + TextureConfig(target_width, target_height, 1, 1, 1, AbstractTextureFormat::RGBA8, + AbstractTextureFlag_RenderTarget), + "Frame dump render texture"); + if (!m_frame_dump_render_texture) + { + PanicAlertFmt("Failed to allocate frame dump render texture"); + return false; + } + m_frame_dump_render_framebuffer = + g_renderer->CreateFramebuffer(m_frame_dump_render_texture.get(), nullptr); + ASSERT(m_frame_dump_render_framebuffer); + return true; +} + +bool FrameDumper::CheckFrameDumpReadbackTexture(u32 target_width, u32 target_height) +{ + std::unique_ptr<AbstractStagingTexture>& rbtex = m_frame_dump_readback_texture; + if (rbtex && rbtex->GetWidth() == target_width && rbtex->GetHeight() == target_height) + return true; + + rbtex.reset(); + rbtex = g_renderer->CreateStagingTexture( + StagingTextureType::Readback, + TextureConfig(target_width, target_height, 1, 1, 1, AbstractTextureFormat::RGBA8, 0)); + if (!rbtex) + return false; + + return true; +} + +void FrameDumper::FlushFrameDump() +{ + if (!m_frame_dump_needs_flush) + return; + + // Ensure dumping thread is done with output texture before swapping. + FinishFrameData(); + + std::swap(m_frame_dump_output_texture, m_frame_dump_readback_texture); + + // Queue encoding of the last frame dumped. + auto& output = m_frame_dump_output_texture; + output->Flush(); + if (output->Map()) + { + DumpFrameData(reinterpret_cast<u8*>(output->GetMappedPointer()), output->GetConfig().width, + output->GetConfig().height, static_cast<int>(output->GetMappedStride())); + } + else + { + ERROR_LOG_FMT(VIDEO, "Failed to map texture for dumping."); + } + + m_frame_dump_needs_flush = false; + + // Shutdown frame dumping if it is no longer active. + if (!IsFrameDumping()) + ShutdownFrameDumping(); +} + +void FrameDumper::ShutdownFrameDumping() +{ + // Ensure the last queued readback has been sent to the encoder. + FlushFrameDump(); + + if (!m_frame_dump_thread_running.IsSet()) + return; + + // Ensure previous frame has been encoded. + FinishFrameData(); + + // Wake thread up, and wait for it to exit. + m_frame_dump_thread_running.Clear(); + m_frame_dump_start.Set(); + if (m_frame_dump_thread.joinable()) + m_frame_dump_thread.join(); + m_frame_dump_render_framebuffer.reset(); + m_frame_dump_render_texture.reset(); + + m_frame_dump_readback_texture.reset(); + m_frame_dump_output_texture.reset(); +} + +void FrameDumper::DumpFrameData(const u8* data, int w, int h, int stride) +{ + m_frame_dump_data = FrameData{data, w, h, stride, m_last_frame_state}; + + if (!m_frame_dump_thread_running.IsSet()) + { + if (m_frame_dump_thread.joinable()) + m_frame_dump_thread.join(); + m_frame_dump_thread_running.Set(); + m_frame_dump_thread = std::thread(&FrameDumper::FrameDumpThreadFunc, this); + } + + // Wake worker thread up. + m_frame_dump_start.Set(); + m_frame_dump_frame_running = true; +} + +void FrameDumper::FinishFrameData() +{ + if (!m_frame_dump_frame_running) + return; + + m_frame_dump_done.Wait(); + m_frame_dump_frame_running = false; + + m_frame_dump_output_texture->Unmap(); +} + +void FrameDumper::FrameDumpThreadFunc() +{ + Common::SetCurrentThreadName("FrameDumping"); + + bool dump_to_ffmpeg = !g_ActiveConfig.bDumpFramesAsImages; + bool frame_dump_started = false; + +// If Dolphin was compiled without ffmpeg, we only support dumping to images. +#if !defined(HAVE_FFMPEG) + if (dump_to_ffmpeg) + { + WARN_LOG_FMT(VIDEO, "FrameDump: Dolphin was not compiled with FFmpeg, using fallback option. " + "Frames will be saved as PNG images instead."); + dump_to_ffmpeg = false; + } +#endif + + while (true) + { + m_frame_dump_start.Wait(); + if (!m_frame_dump_thread_running.IsSet()) + break; + + auto frame = m_frame_dump_data; + + // Save screenshot + if (m_screenshot_request.TestAndClear()) + { + std::lock_guard<std::mutex> lk(m_screenshot_lock); + + if (DumpFrameToPNG(frame, m_screenshot_name)) + OSD::AddMessage("Screenshot saved to " + m_screenshot_name); + + // Reset settings + m_screenshot_name.clear(); + m_screenshot_completed.Set(); + } + + if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES)) + { + if (!frame_dump_started) + { + if (dump_to_ffmpeg) + frame_dump_started = StartFrameDumpToFFMPEG(frame); + else + frame_dump_started = StartFrameDumpToImage(frame); + + // Stop frame dumping if we fail to start. + if (!frame_dump_started) + Config::SetCurrent(Config::MAIN_MOVIE_DUMP_FRAMES, false); + } + + // If we failed to start frame dumping, don't write a frame. + if (frame_dump_started) + { + if (dump_to_ffmpeg) + DumpFrameToFFMPEG(frame); + else + DumpFrameToImage(frame); + } + } + + m_frame_dump_done.Set(); + } + + if (frame_dump_started) + { + // No additional cleanup is needed when dumping to images. + if (dump_to_ffmpeg) + StopFrameDumpToFFMPEG(); + } +} + +#if defined(HAVE_FFMPEG) + +bool FrameDumper::StartFrameDumpToFFMPEG(const FrameData& frame) +{ + // If dumping started at boot, the start time must be set to the boot time to maintain audio sync. + // TODO: Perhaps we should care about this when starting dumping in the middle of emulation too, + // but it's less important there since the first frame to dump usually gets delivered quickly. + const u64 start_ticks = frame.state.frame_number == 0 ? 0 : frame.state.ticks; + return m_ffmpeg_dump.Start(frame.width, frame.height, start_ticks); +} + +void FrameDumper::DumpFrameToFFMPEG(const FrameData& frame) +{ + m_ffmpeg_dump.AddFrame(frame); +} + +void FrameDumper::StopFrameDumpToFFMPEG() +{ + m_ffmpeg_dump.Stop(); +} + +#else + +bool FrameDump::StartFrameDumpToFFMPEG(const FrameData&) +{ + return false; +} + +void FrameDump::DumpFrameToFFMPEG(const FrameData&) +{ +} + +void FrameDump::StopFrameDumpToFFMPEG() +{ +} + +#endif // defined(HAVE_FFMPEG) + +std::string FrameDumper::GetFrameDumpNextImageFileName() const +{ + return fmt::format("{}framedump_{}.png", File::GetUserPath(D_DUMPFRAMES_IDX), + m_frame_dump_image_counter); +} + +bool FrameDumper::StartFrameDumpToImage(const FrameData&) +{ + m_frame_dump_image_counter = 1; + if (!Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES_SILENT)) + { + // Only check for the presence of the first image to confirm overwriting. + // A previous run will always have at least one image, and it's safe to assume that if the user + // has allowed the first image to be overwritten, this will apply any remaining images as well. + std::string filename = GetFrameDumpNextImageFileName(); + if (File::Exists(filename)) + { + if (!AskYesNoFmtT("Frame dump image(s) '{0}' already exists. Overwrite?", filename)) + return false; + } + } + + return true; +} + +void FrameDumper::DumpFrameToImage(const FrameData& frame) +{ + DumpFrameToPNG(frame, GetFrameDumpNextImageFileName()); + m_frame_dump_image_counter++; +} + +void FrameDumper::SaveScreenshot(std::string filename) +{ + std::lock_guard<std::mutex> lk(m_screenshot_lock); + m_screenshot_name = std::move(filename); + m_screenshot_request.Set(); +} + +bool FrameDumper::IsFrameDumping() const +{ + if (m_screenshot_request.IsSet()) + return true; + + if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES)) + return true; + + return false; +} + +std::unique_ptr<FrameDumper> g_frame_dumper; |
