summaryrefslogtreecommitdiff
path: root/Source/Core/VideoCommon/FrameDumper.cpp
diff options
context:
space:
mode:
authorScott Mansell <phiren@gmail.com>2023-01-27 17:03:15 +1300
committerScott Mansell <phiren@gmail.com>2023-01-31 18:45:21 +1300
commit0d4537d60f695e50379c1542d6c4b1e643e8a5a0 (patch)
tree7c0dd9c2963b7eead104df5077444e3179030b4f /Source/Core/VideoCommon/FrameDumper.cpp
parentc38c76abad039c3c0ccb3f7ebb891dc62560f8a3 (diff)
Move Presenting, Dumping and ImGui out of Renderer
Diffstat (limited to 'Source/Core/VideoCommon/FrameDumper.cpp')
-rw-r--r--Source/Core/VideoCommon/FrameDumper.cpp354
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;