diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.cpp b/Source/Core/VideoBackends/Vulkan/Renderer.cpp index af19ad4043..c9e762eac6 100644 --- a/Source/Core/VideoBackends/Vulkan/Renderer.cpp +++ b/Source/Core/VideoBackends/Vulkan/Renderer.cpp @@ -611,7 +611,6 @@ void Renderer::DrawScreen(VKTexture* xfb_texture, const EFBRectangle& xfb_region VK_SUBPASS_CONTENTS_INLINE); // Draw - TargetRectangle source_rc = xfb_texture->GetConfig().GetRect(); BlitScreen(m_swap_chain->GetRenderPass(), GetTargetRectangle(), xfb_region, xfb_texture->GetRawTexIdentifier()); diff --git a/Source/Core/VideoCommon/MainBase.cpp b/Source/Core/VideoCommon/MainBase.cpp index 280fed683d..4dfb97f84a 100644 --- a/Source/Core/VideoCommon/MainBase.cpp +++ b/Source/Core/VideoCommon/MainBase.cpp @@ -49,7 +49,7 @@ void VideoBackendBase::Video_CleanupShared() { // First stop any framedumping, which might need to dump the last xfb frame. This process // can require additional graphics sub-systems so it needs to be done first - g_renderer->ExitFramedumping(); + g_renderer->ShutdownFrameDumping(); Video_Cleanup(); } diff --git a/Source/Core/VideoCommon/RenderBase.cpp b/Source/Core/VideoCommon/RenderBase.cpp index 00e34cff71..d20e23ac46 100644 --- a/Source/Core/VideoCommon/RenderBase.cpp +++ b/Source/Core/VideoCommon/RenderBase.cpp @@ -43,6 +43,7 @@ #include "Core/Movie.h" #include "VideoCommon/AVIDump.h" +#include "VideoCommon/AbstractStagingTexture.h" #include "VideoCommon/AbstractTexture.h" #include "VideoCommon/BPMemory.h" #include "VideoCommon/CPMemory.h" @@ -99,15 +100,6 @@ Renderer::Renderer(int backbuffer_width, int backbuffer_height) Renderer::~Renderer() = default; -void Renderer::ExitFramedumping() -{ - ShutdownFrameDumping(); - if (m_frame_dump_thread.joinable()) - m_frame_dump_thread.join(); - - m_dump_texture.reset(); -} - void Renderer::RenderToXFB(u32 xfbAddr, const EFBRectangle& sourceRc, u32 fbStride, u32 fbHeight, float Gamma) { @@ -629,14 +621,10 @@ void Renderer::Swap(u32 xfbAddr, u32 fbWidth, u32 fbStride, u32 fbHeight, const m_aspect_wide = flush_count_anamorphic > 0.75 * flush_total; } - if (IsFrameDumping() && m_last_xfb_texture) - { - FinishFrameData(); - } - else - { - ShutdownFrameDumping(); - } + // Ensure the last frame was written to the dump. + // This is required even if frame dumping has stopped, since the frame dump is one frame + // behind the renderer. + FlushFrameDump(); bool update_frame_count = false; if (xfbAddr && fbWidth && fbStride && fbHeight) @@ -662,10 +650,9 @@ void Renderer::Swap(u32 xfbAddr, u32 fbWidth, u32 fbStride, u32 fbHeight, const m_fps_counter.Update(); update_frame_count = true; + if (IsFrameDumping()) - { - DoDumpFrame(); - } + DumpCurrentFrame(); } // Update our last xfb values @@ -695,20 +682,16 @@ bool Renderer::IsFrameDumping() return false; } -void Renderer::DoDumpFrame() +void Renderer::DumpCurrentFrame() { - UpdateFrameDumpTexture(); + // Scale/render to frame dump texture. + RenderFrameDump(); - auto result = m_dump_texture->Map(); - if (result.has_value()) - { - auto raw_data = result.value(); - DumpFrameData(raw_data.data, raw_data.width, raw_data.height, raw_data.stride, - AVIDump::FetchState(m_last_xfb_ticks)); - } + // Queue a readback for the next frame. + QueueFrameDumpReadback(); } -void Renderer::UpdateFrameDumpTexture() +void Renderer::RenderFrameDump() { int target_width, target_height; if (!g_ActiveConfig.bInternalResolutionFrameDumps) @@ -723,33 +706,99 @@ void Renderer::UpdateFrameDumpTexture() m_last_xfb_texture->GetConfig().width, m_last_xfb_texture->GetConfig().height); } - if (m_dump_texture == nullptr || - m_dump_texture->GetConfig().width != static_cast(target_width) || - m_dump_texture->GetConfig().height != static_cast(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->GetConfig().width != static_cast(target_width) || + m_frame_dump_render_texture->GetConfig().height == static_cast(target_height)) { - TextureConfig config; - config.width = target_width; - config.height = target_height; - config.rendertarget = true; - m_dump_texture = CreateTexture(config); + // Recreate texture objects. Release before creating so we don't temporarily use twice the RAM. + TextureConfig config(target_width, target_height, 1, 1, AbstractTextureFormat::RGBA8, true); + m_frame_dump_render_texture.reset(); + m_frame_dump_render_texture = CreateTexture(config); + _assert_(m_frame_dump_render_texture); } - m_dump_texture->CopyRectangleFromTexture(m_last_xfb_texture, m_last_xfb_region, 0, 0, - EFBRectangle{0, 0, target_width, target_height}, 0, 0); + + // Scaling is likely to occur here, but if possible, do a bit-for-bit copy. + if (m_last_xfb_region.GetWidth() != target_width || + m_last_xfb_region.GetHeight() != target_height) + { + m_frame_dump_render_texture->ScaleRectangleFromTexture( + m_last_xfb_texture, m_last_xfb_region, EFBRectangle{0, 0, target_width, target_height}); + } + else + { + m_frame_dump_render_texture->CopyRectangleFromTexture( + m_last_xfb_texture, m_last_xfb_region, 0, 0, + EFBRectangle{0, 0, target_width, target_height}, 0, 0); + } +} + +void Renderer::QueueFrameDumpReadback() +{ + // Index 0 was just sent to AVI dump. Swap with the second texture. + if (m_frame_dump_readback_textures[0]) + std::swap(m_frame_dump_readback_textures[0], m_frame_dump_readback_textures[1]); + + std::unique_ptr& rbtex = m_frame_dump_readback_textures[0]; + if (!rbtex || rbtex->GetConfig() != m_frame_dump_render_texture->GetConfig()) + { + rbtex = CreateStagingTexture(StagingTextureType::Readback, + m_frame_dump_render_texture->GetConfig()); + } + + m_last_frame_state = AVIDump::FetchState(m_last_xfb_ticks); + m_last_frame_exported = true; + rbtex->CopyFromTexture(m_frame_dump_render_texture.get(), 0, 0); +} + +void Renderer::FlushFrameDump() +{ + if (!m_last_frame_exported) + return; + + // Ensure the previously-queued frame was encoded. + FinishFrameData(); + + // Queue encoding of the last frame dumped. + std::unique_ptr& rbtex = m_frame_dump_readback_textures[0]; + rbtex->Flush(); + if (rbtex->Map()) + { + DumpFrameData(reinterpret_cast(rbtex->GetMappedPointer()), rbtex->GetConfig().width, + rbtex->GetConfig().height, static_cast(rbtex->GetMappedStride()), + m_last_frame_state); + rbtex->Unmap(); + } + + m_last_frame_exported = false; + + // Shutdown frame dumping if it is no longer active. + if (!IsFrameDumping()) + ShutdownFrameDumping(); } void Renderer::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(); } void Renderer::DumpFrameData(const u8* data, int w, int h, int stride, const AVIDump::Frame& state) { - m_frame_dump_config = FrameDumpConfig{m_last_xfb_texture, data, w, h, stride, state}; + m_frame_dump_config = FrameDumpConfig{data, w, h, stride, state}; if (!m_frame_dump_thread_running.IsSet()) { @@ -759,6 +808,7 @@ void Renderer::DumpFrameData(const u8* data, int w, int h, int stride, const AVI m_frame_dump_thread = std::thread(&Renderer::RunFrameDumps, this); } + // Wake worker thread up. m_frame_dump_start.Set(); m_frame_dump_frame_running = true; } @@ -770,7 +820,6 @@ void Renderer::FinishFrameData() m_frame_dump_done.Wait(); m_frame_dump_frame_running = false; - m_frame_dump_config.texture->Unmap(); } void Renderer::RunFrameDumps() diff --git a/Source/Core/VideoCommon/RenderBase.h b/Source/Core/VideoCommon/RenderBase.h index b614c6a375..551fc36871 100644 --- a/Source/Core/VideoCommon/RenderBase.h +++ b/Source/Core/VideoCommon/RenderBase.h @@ -151,7 +151,7 @@ public: virtual void ChangeSurface(void* new_surface_handle) {} bool UseVertexDepthRange() const; - void ExitFramedumping(); + void ShutdownFrameDumping(); protected: std::tuple CalculateTargetScale(int x, int y) const; @@ -190,11 +190,8 @@ protected: u32 m_last_host_config_bits = 0; private: - void DoDumpFrame(); void RunFrameDumps(); - void ShutdownFrameDumping(); std::tuple CalculateOutputDimensions(int width, int height); - void UpdateFrameDumpTexture(); PEControl::PixelFormat m_prev_efb_format = PEControl::INVALID_FMT; unsigned int m_efb_scale = 1; @@ -212,7 +209,6 @@ private: bool m_frame_dump_frame_running = false; struct FrameDumpConfig { - AbstractTexture* texture; const u8* data; int width; int height; @@ -220,13 +216,18 @@ private: AVIDump::Frame state; } m_frame_dump_config; + // Texture used for screenshot/frame dumping + std::unique_ptr m_frame_dump_render_texture; + std::array, 2> m_frame_dump_readback_textures; + AVIDump::Frame m_last_frame_state; + bool m_last_frame_exported = false; + + // Tracking of XFB textures so we don't render duplicate frames. AbstractTexture* m_last_xfb_texture = nullptr; u64 m_last_xfb_id = std::numeric_limits::max(); u64 m_last_xfb_ticks = 0; EFBRectangle m_last_xfb_region; - std::unique_ptr m_dump_texture; - // Note: Only used for auto-ir u32 m_last_xfb_width = MAX_XFB_WIDTH; u32 m_last_xfb_height = MAX_XFB_HEIGHT; @@ -240,7 +241,23 @@ private: void DumpFrameToImage(const FrameDumpConfig& config); bool IsFrameDumping(); + + // Asynchronously encodes the current staging texture to the frame dump. + void DumpCurrentFrame(); + + // Fills the frame dump render texture with the current XFB texture. + void RenderFrameDump(); + + // Queues the current frame for readback, which will be written to AVI next frame. + void QueueFrameDumpReadback(); + + // Asynchronously encodes the specified pointer of frame data to the frame dump. void DumpFrameData(const u8* data, int w, int h, int stride, const AVIDump::Frame& state); + + // Ensures all rendered frames are queued for encoding. + void FlushFrameDump(); + + // Ensures all encoded frames have been written to the output file. void FinishFrameData(); };