123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- // 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/AbstractGfx.h"
- #include "VideoCommon/AbstractStagingTexture.h"
- #include "VideoCommon/AbstractTexture.h"
- #include "VideoCommon/OnScreenDisplay.h"
- #include "VideoCommon/Present.h"
- #include "VideoCommon/VideoConfig.h"
- // The video encoder needs the image to be a multiple of x samples.
- static constexpr int VIDEO_ENCODER_LCM = 4;
- 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()
- {
- m_frame_end_handle =
- AfterFrameEvent::Register([this](Core::System&) { FlushFrameDump(); }, "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_gfx->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_gfx->CreateTexture(
- TextureConfig(target_width, target_height, 1, 1, 1, AbstractTextureFormat::RGBA8,
- AbstractTextureFlag_RenderTarget, AbstractTextureType::Texture_2DArray),
- "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_gfx->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_gfx->CreateStagingTexture(StagingTextureType::Readback,
- TextureConfig(target_width, target_height, 1, 1, 1,
- AbstractTextureFormat::RGBA8, 0,
- AbstractTextureType::Texture_2DArray));
- 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 FrameDumper::StartFrameDumpToFFMPEG(const FrameData&)
- {
- return false;
- }
- void FrameDumper::DumpFrameToFFMPEG(const FrameData&)
- {
- }
- void FrameDumper::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;
- }
- int FrameDumper::GetRequiredResolutionLeastCommonMultiple() const
- {
- if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES))
- return VIDEO_ENCODER_LCM;
- return 1;
- }
- void FrameDumper::DoState(PointerWrap& p)
- {
- #ifdef HAVE_FFMPEG
- m_ffmpeg_dump.DoState(p);
- #endif
- }
- std::unique_ptr<FrameDumper> g_frame_dumper;
|