FrameDumper.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. // Copyright 2023 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "VideoCommon/FrameDumper.h"
  4. #include "Common/Assert.h"
  5. #include "Common/FileUtil.h"
  6. #include "Common/Image.h"
  7. #include "Core/Config/GraphicsSettings.h"
  8. #include "Core/Config/MainSettings.h"
  9. #include "VideoCommon/AbstractFramebuffer.h"
  10. #include "VideoCommon/AbstractGfx.h"
  11. #include "VideoCommon/AbstractStagingTexture.h"
  12. #include "VideoCommon/AbstractTexture.h"
  13. #include "VideoCommon/OnScreenDisplay.h"
  14. #include "VideoCommon/Present.h"
  15. #include "VideoCommon/VideoConfig.h"
  16. // The video encoder needs the image to be a multiple of x samples.
  17. static constexpr int VIDEO_ENCODER_LCM = 4;
  18. static bool DumpFrameToPNG(const FrameData& frame, const std::string& file_name)
  19. {
  20. return Common::ConvertRGBAToRGBAndSavePNG(file_name, frame.data, frame.width, frame.height,
  21. frame.stride,
  22. Config::Get(Config::GFX_PNG_COMPRESSION_LEVEL));
  23. }
  24. FrameDumper::FrameDumper()
  25. {
  26. m_frame_end_handle =
  27. AfterFrameEvent::Register([this](Core::System&) { FlushFrameDump(); }, "FrameDumper");
  28. }
  29. FrameDumper::~FrameDumper()
  30. {
  31. ShutdownFrameDumping();
  32. }
  33. void FrameDumper::DumpCurrentFrame(const AbstractTexture* src_texture,
  34. const MathUtil::Rectangle<int>& src_rect,
  35. const MathUtil::Rectangle<int>& target_rect, u64 ticks,
  36. int frame_number)
  37. {
  38. int source_width = src_rect.GetWidth();
  39. int source_height = src_rect.GetHeight();
  40. int target_width = target_rect.GetWidth();
  41. int target_height = target_rect.GetHeight();
  42. // We only need to render a copy if we need to stretch/scale the XFB copy.
  43. MathUtil::Rectangle<int> copy_rect = src_rect;
  44. if (source_width != target_width || source_height != target_height)
  45. {
  46. if (!CheckFrameDumpRenderTexture(target_width, target_height))
  47. return;
  48. g_gfx->ScaleTexture(m_frame_dump_render_framebuffer.get(),
  49. m_frame_dump_render_framebuffer->GetRect(), src_texture, src_rect);
  50. src_texture = m_frame_dump_render_texture.get();
  51. copy_rect = src_texture->GetRect();
  52. }
  53. if (!CheckFrameDumpReadbackTexture(target_width, target_height))
  54. return;
  55. m_frame_dump_readback_texture->CopyFromTexture(src_texture, copy_rect, 0, 0,
  56. m_frame_dump_readback_texture->GetRect());
  57. m_last_frame_state = m_ffmpeg_dump.FetchState(ticks, frame_number);
  58. m_frame_dump_needs_flush = true;
  59. }
  60. bool FrameDumper::CheckFrameDumpRenderTexture(u32 target_width, u32 target_height)
  61. {
  62. // Ensure framebuffer exists (we lazily allocate it in case frame dumping isn't used).
  63. // Or, resize texture if it isn't large enough to accommodate the current frame.
  64. if (m_frame_dump_render_texture && m_frame_dump_render_texture->GetWidth() == target_width &&
  65. m_frame_dump_render_texture->GetHeight() == target_height)
  66. {
  67. return true;
  68. }
  69. // Recreate texture, but release before creating so we don't temporarily use twice the RAM.
  70. m_frame_dump_render_framebuffer.reset();
  71. m_frame_dump_render_texture.reset();
  72. m_frame_dump_render_texture = g_gfx->CreateTexture(
  73. TextureConfig(target_width, target_height, 1, 1, 1, AbstractTextureFormat::RGBA8,
  74. AbstractTextureFlag_RenderTarget, AbstractTextureType::Texture_2DArray),
  75. "Frame dump render texture");
  76. if (!m_frame_dump_render_texture)
  77. {
  78. PanicAlertFmt("Failed to allocate frame dump render texture");
  79. return false;
  80. }
  81. m_frame_dump_render_framebuffer =
  82. g_gfx->CreateFramebuffer(m_frame_dump_render_texture.get(), nullptr);
  83. ASSERT(m_frame_dump_render_framebuffer);
  84. return true;
  85. }
  86. bool FrameDumper::CheckFrameDumpReadbackTexture(u32 target_width, u32 target_height)
  87. {
  88. std::unique_ptr<AbstractStagingTexture>& rbtex = m_frame_dump_readback_texture;
  89. if (rbtex && rbtex->GetWidth() == target_width && rbtex->GetHeight() == target_height)
  90. return true;
  91. rbtex.reset();
  92. rbtex = g_gfx->CreateStagingTexture(StagingTextureType::Readback,
  93. TextureConfig(target_width, target_height, 1, 1, 1,
  94. AbstractTextureFormat::RGBA8, 0,
  95. AbstractTextureType::Texture_2DArray));
  96. if (!rbtex)
  97. return false;
  98. return true;
  99. }
  100. void FrameDumper::FlushFrameDump()
  101. {
  102. if (!m_frame_dump_needs_flush)
  103. return;
  104. // Ensure dumping thread is done with output texture before swapping.
  105. FinishFrameData();
  106. std::swap(m_frame_dump_output_texture, m_frame_dump_readback_texture);
  107. // Queue encoding of the last frame dumped.
  108. auto& output = m_frame_dump_output_texture;
  109. output->Flush();
  110. if (output->Map())
  111. {
  112. DumpFrameData(reinterpret_cast<u8*>(output->GetMappedPointer()), output->GetConfig().width,
  113. output->GetConfig().height, static_cast<int>(output->GetMappedStride()));
  114. }
  115. else
  116. {
  117. ERROR_LOG_FMT(VIDEO, "Failed to map texture for dumping.");
  118. }
  119. m_frame_dump_needs_flush = false;
  120. // Shutdown frame dumping if it is no longer active.
  121. if (!IsFrameDumping())
  122. ShutdownFrameDumping();
  123. }
  124. void FrameDumper::ShutdownFrameDumping()
  125. {
  126. // Ensure the last queued readback has been sent to the encoder.
  127. FlushFrameDump();
  128. if (!m_frame_dump_thread_running.IsSet())
  129. return;
  130. // Ensure previous frame has been encoded.
  131. FinishFrameData();
  132. // Wake thread up, and wait for it to exit.
  133. m_frame_dump_thread_running.Clear();
  134. m_frame_dump_start.Set();
  135. if (m_frame_dump_thread.joinable())
  136. m_frame_dump_thread.join();
  137. m_frame_dump_render_framebuffer.reset();
  138. m_frame_dump_render_texture.reset();
  139. m_frame_dump_readback_texture.reset();
  140. m_frame_dump_output_texture.reset();
  141. }
  142. void FrameDumper::DumpFrameData(const u8* data, int w, int h, int stride)
  143. {
  144. m_frame_dump_data = FrameData{data, w, h, stride, m_last_frame_state};
  145. if (!m_frame_dump_thread_running.IsSet())
  146. {
  147. if (m_frame_dump_thread.joinable())
  148. m_frame_dump_thread.join();
  149. m_frame_dump_thread_running.Set();
  150. m_frame_dump_thread = std::thread(&FrameDumper::FrameDumpThreadFunc, this);
  151. }
  152. // Wake worker thread up.
  153. m_frame_dump_start.Set();
  154. m_frame_dump_frame_running = true;
  155. }
  156. void FrameDumper::FinishFrameData()
  157. {
  158. if (!m_frame_dump_frame_running)
  159. return;
  160. m_frame_dump_done.Wait();
  161. m_frame_dump_frame_running = false;
  162. m_frame_dump_output_texture->Unmap();
  163. }
  164. void FrameDumper::FrameDumpThreadFunc()
  165. {
  166. Common::SetCurrentThreadName("FrameDumping");
  167. bool dump_to_ffmpeg = !g_ActiveConfig.bDumpFramesAsImages;
  168. bool frame_dump_started = false;
  169. // If Dolphin was compiled without ffmpeg, we only support dumping to images.
  170. #if !defined(HAVE_FFMPEG)
  171. if (dump_to_ffmpeg)
  172. {
  173. WARN_LOG_FMT(VIDEO, "FrameDump: Dolphin was not compiled with FFmpeg, using fallback option. "
  174. "Frames will be saved as PNG images instead.");
  175. dump_to_ffmpeg = false;
  176. }
  177. #endif
  178. while (true)
  179. {
  180. m_frame_dump_start.Wait();
  181. if (!m_frame_dump_thread_running.IsSet())
  182. break;
  183. auto frame = m_frame_dump_data;
  184. // Save screenshot
  185. if (m_screenshot_request.TestAndClear())
  186. {
  187. std::lock_guard<std::mutex> lk(m_screenshot_lock);
  188. if (DumpFrameToPNG(frame, m_screenshot_name))
  189. OSD::AddMessage("Screenshot saved to " + m_screenshot_name);
  190. // Reset settings
  191. m_screenshot_name.clear();
  192. m_screenshot_completed.Set();
  193. }
  194. if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES))
  195. {
  196. if (!frame_dump_started)
  197. {
  198. if (dump_to_ffmpeg)
  199. frame_dump_started = StartFrameDumpToFFMPEG(frame);
  200. else
  201. frame_dump_started = StartFrameDumpToImage(frame);
  202. // Stop frame dumping if we fail to start.
  203. if (!frame_dump_started)
  204. Config::SetCurrent(Config::MAIN_MOVIE_DUMP_FRAMES, false);
  205. }
  206. // If we failed to start frame dumping, don't write a frame.
  207. if (frame_dump_started)
  208. {
  209. if (dump_to_ffmpeg)
  210. DumpFrameToFFMPEG(frame);
  211. else
  212. DumpFrameToImage(frame);
  213. }
  214. }
  215. m_frame_dump_done.Set();
  216. }
  217. if (frame_dump_started)
  218. {
  219. // No additional cleanup is needed when dumping to images.
  220. if (dump_to_ffmpeg)
  221. StopFrameDumpToFFMPEG();
  222. }
  223. }
  224. #if defined(HAVE_FFMPEG)
  225. bool FrameDumper::StartFrameDumpToFFMPEG(const FrameData& frame)
  226. {
  227. // If dumping started at boot, the start time must be set to the boot time to maintain audio sync.
  228. // TODO: Perhaps we should care about this when starting dumping in the middle of emulation too,
  229. // but it's less important there since the first frame to dump usually gets delivered quickly.
  230. const u64 start_ticks = frame.state.frame_number == 0 ? 0 : frame.state.ticks;
  231. return m_ffmpeg_dump.Start(frame.width, frame.height, start_ticks);
  232. }
  233. void FrameDumper::DumpFrameToFFMPEG(const FrameData& frame)
  234. {
  235. m_ffmpeg_dump.AddFrame(frame);
  236. }
  237. void FrameDumper::StopFrameDumpToFFMPEG()
  238. {
  239. m_ffmpeg_dump.Stop();
  240. }
  241. #else
  242. bool FrameDumper::StartFrameDumpToFFMPEG(const FrameData&)
  243. {
  244. return false;
  245. }
  246. void FrameDumper::DumpFrameToFFMPEG(const FrameData&)
  247. {
  248. }
  249. void FrameDumper::StopFrameDumpToFFMPEG()
  250. {
  251. }
  252. #endif // defined(HAVE_FFMPEG)
  253. std::string FrameDumper::GetFrameDumpNextImageFileName() const
  254. {
  255. return fmt::format("{}framedump_{}.png", File::GetUserPath(D_DUMPFRAMES_IDX),
  256. m_frame_dump_image_counter);
  257. }
  258. bool FrameDumper::StartFrameDumpToImage(const FrameData&)
  259. {
  260. m_frame_dump_image_counter = 1;
  261. if (!Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES_SILENT))
  262. {
  263. // Only check for the presence of the first image to confirm overwriting.
  264. // A previous run will always have at least one image, and it's safe to assume that if the user
  265. // has allowed the first image to be overwritten, this will apply any remaining images as well.
  266. std::string filename = GetFrameDumpNextImageFileName();
  267. if (File::Exists(filename))
  268. {
  269. if (!AskYesNoFmtT("Frame dump image(s) '{0}' already exists. Overwrite?", filename))
  270. return false;
  271. }
  272. }
  273. return true;
  274. }
  275. void FrameDumper::DumpFrameToImage(const FrameData& frame)
  276. {
  277. DumpFrameToPNG(frame, GetFrameDumpNextImageFileName());
  278. m_frame_dump_image_counter++;
  279. }
  280. void FrameDumper::SaveScreenshot(std::string filename)
  281. {
  282. std::lock_guard<std::mutex> lk(m_screenshot_lock);
  283. m_screenshot_name = std::move(filename);
  284. m_screenshot_request.Set();
  285. }
  286. bool FrameDumper::IsFrameDumping() const
  287. {
  288. if (m_screenshot_request.IsSet())
  289. return true;
  290. if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES))
  291. return true;
  292. return false;
  293. }
  294. int FrameDumper::GetRequiredResolutionLeastCommonMultiple() const
  295. {
  296. if (Config::Get(Config::MAIN_MOVIE_DUMP_FRAMES))
  297. return VIDEO_ENCODER_LCM;
  298. return 1;
  299. }
  300. void FrameDumper::DoState(PointerWrap& p)
  301. {
  302. #ifdef HAVE_FFMPEG
  303. m_ffmpeg_dump.DoState(p);
  304. #endif
  305. }
  306. std::unique_ptr<FrameDumper> g_frame_dumper;