| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
|
|
||
| class GLContext; | ||
|
|
||
| namespace OGL | ||
| { | ||
| enum GlslVersion | ||
| { | ||
| Glsl130, | ||
| Glsl140, | ||
| Glsl150, | ||
| Glsl330, | ||
| Glsl400, // and above | ||
| Glsl430, | ||
| GlslEs300, // GLES 3.0 | ||
| GlslEs310, // GLES 3.1 | ||
| GlslEs320, // GLES 3.2 | ||
| }; | ||
| enum class EsTexbufType | ||
| { | ||
| TexbufNone, | ||
| TexbufCore, | ||
| TexbufOes, | ||
| TexbufExt | ||
| }; | ||
|
|
||
| enum class EsFbFetchType | ||
| { | ||
| FbFetchNone, | ||
| FbFetchExt, | ||
| FbFetchArm, | ||
| }; | ||
|
|
||
| // ogl-only config, so not in VideoConfig.h | ||
| struct VideoConfig | ||
| { | ||
| bool bIsES; | ||
| bool bSupportsGLPinnedMemory; | ||
| bool bSupportsGLSync; | ||
| bool bSupportsGLBaseVertex; | ||
| bool bSupportsGLBufferStorage; | ||
| bool bSupportsMSAA; | ||
| GlslVersion eSupportedGLSLVersion; | ||
| bool bSupportViewportFloat; | ||
| bool bSupportsAEP; | ||
| bool bSupportsDebug; | ||
| bool bSupportsCopySubImage; | ||
| u8 SupportedESPointSize; | ||
| EsTexbufType SupportedESTextureBuffer; | ||
| bool bSupportsTextureStorage; | ||
| bool bSupports2DTextureStorageMultisample; | ||
| bool bSupports3DTextureStorageMultisample; | ||
| bool bSupportsConservativeDepth; | ||
| bool bSupportsImageLoadStore; | ||
| bool bSupportsAniso; | ||
| bool bSupportsBitfield; | ||
| bool bSupportsTextureSubImage; | ||
| EsFbFetchType SupportedFramebufferFetch; | ||
| bool bSupportsShaderThreadShuffleNV; | ||
|
|
||
| const char* gl_vendor; | ||
| const char* gl_renderer; | ||
| const char* gl_version; | ||
|
|
||
| s32 max_samples; | ||
| }; | ||
|
|
||
| void InitDriverInfo(); | ||
| bool PopulateConfig(GLContext* main_gl_context); | ||
|
|
||
| extern VideoConfig g_ogl_config; | ||
|
|
||
| } // namespace OGL |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -12,7 +12,7 @@ | ||
|
|
||
| namespace OGL | ||
| { | ||
| std::unique_ptr<PerfQueryBase> GetPerfQuery(bool is_gles); | ||
|
|
||
| class PerfQuery : public PerfQueryBase | ||
| { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -9,7 +9,7 @@ | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
| #include "Common/GL/GLUtil.h" | ||
| #include "VideoCommon/RenderState.h" | ||
|
|
||
| namespace OGL | ||
| { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include "VideoBackends/Software/SWGfx.h" | ||
|
|
||
| #include "Common/GL/GLContext.h" | ||
|
|
||
| #include "VideoBackends/Software/EfbCopy.h" | ||
| #include "VideoBackends/Software/Rasterizer.h" | ||
| #include "VideoBackends/Software/SWOGLWindow.h" | ||
| #include "VideoBackends/Software/SWTexture.h" | ||
|
|
||
| #include "VideoCommon/AbstractPipeline.h" | ||
| #include "VideoCommon/AbstractShader.h" | ||
| #include "VideoCommon/AbstractTexture.h" | ||
| #include "VideoCommon/NativeVertexFormat.h" | ||
| #include "VideoCommon/Present.h" | ||
|
|
||
| namespace SW | ||
| { | ||
| SWGfx::SWGfx(std::unique_ptr<SWOGLWindow> window) : m_window(std::move(window)) | ||
| { | ||
| } | ||
|
|
||
| bool SWGfx::IsHeadless() const | ||
| { | ||
| return m_window->IsHeadless(); | ||
| } | ||
|
|
||
| bool SWGfx::SupportsUtilityDrawing() const | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| std::unique_ptr<AbstractTexture> SWGfx::CreateTexture(const TextureConfig& config, | ||
| [[maybe_unused]] std::string_view name) | ||
| { | ||
| return std::make_unique<SWTexture>(config); | ||
| } | ||
|
|
||
| std::unique_ptr<AbstractStagingTexture> SWGfx::CreateStagingTexture(StagingTextureType type, | ||
| const TextureConfig& config) | ||
| { | ||
| return std::make_unique<SWStagingTexture>(type, config); | ||
| } | ||
|
|
||
| std::unique_ptr<AbstractFramebuffer> SWGfx::CreateFramebuffer(AbstractTexture* color_attachment, | ||
| AbstractTexture* depth_attachment) | ||
| { | ||
| return SWFramebuffer::Create(static_cast<SWTexture*>(color_attachment), | ||
| static_cast<SWTexture*>(depth_attachment)); | ||
| } | ||
|
|
||
| void SWGfx::BindBackbuffer(const ClearColor& clear_color) | ||
| { | ||
| // Look for framebuffer resizes | ||
| if (!g_presenter->SurfaceResizedTestAndClear()) | ||
| return; | ||
|
|
||
| GLContext* context = m_window->GetContext(); | ||
| context->Update(); | ||
| g_presenter->SetBackbuffer(context->GetBackBufferWidth(), context->GetBackBufferHeight()); | ||
| } | ||
|
|
||
| class SWShader final : public AbstractShader | ||
| { | ||
| public: | ||
| explicit SWShader(ShaderStage stage) : AbstractShader(stage) {} | ||
| ~SWShader() = default; | ||
|
|
||
| BinaryData GetBinary() const override { return {}; } | ||
| }; | ||
|
|
||
| std::unique_ptr<AbstractShader> | ||
| SWGfx::CreateShaderFromSource(ShaderStage stage, [[maybe_unused]] std::string_view source, | ||
| [[maybe_unused]] std::string_view name) | ||
| { | ||
| return std::make_unique<SWShader>(stage); | ||
| } | ||
|
|
||
| std::unique_ptr<AbstractShader> | ||
| SWGfx::CreateShaderFromBinary(ShaderStage stage, const void* data, size_t length, | ||
| [[maybe_unused]] std::string_view name) | ||
| { | ||
| return std::make_unique<SWShader>(stage); | ||
| } | ||
|
|
||
| class SWPipeline final : public AbstractPipeline | ||
| { | ||
| public: | ||
| SWPipeline() = default; | ||
| ~SWPipeline() override = default; | ||
| }; | ||
|
|
||
| std::unique_ptr<AbstractPipeline> SWGfx::CreatePipeline(const AbstractPipelineConfig& config, | ||
| const void* cache_data, | ||
| size_t cache_data_length) | ||
| { | ||
| return std::make_unique<SWPipeline>(); | ||
| } | ||
|
|
||
| // Called on the GPU thread | ||
| void SWGfx::ShowImage(const AbstractTexture* source_texture, | ||
| const MathUtil::Rectangle<int>& source_rc) | ||
| { | ||
| if (!IsHeadless()) | ||
| m_window->ShowImage(source_texture, source_rc); | ||
| } | ||
|
|
||
| void SWGfx::ClearRegion(const MathUtil::Rectangle<int>& target_rc, bool colorEnable, | ||
| bool alphaEnable, bool zEnable, u32 color, u32 z) | ||
| { | ||
| EfbCopy::ClearEfb(); | ||
| } | ||
|
|
||
| std::unique_ptr<NativeVertexFormat> | ||
| SWGfx::CreateNativeVertexFormat(const PortableVertexDeclaration& vtx_decl) | ||
| { | ||
| return std::make_unique<NativeVertexFormat>(vtx_decl); | ||
| } | ||
|
|
||
| void SWGfx::SetScissorRect(const MathUtil::Rectangle<int>& rc) | ||
| { | ||
| // BPFunctions calls SetScissorRect with the "best" scissor rect whenever the viewport or scissor | ||
| // changes. However, the software renderer is actually able to use multiple scissor rects (which | ||
| // is necessary in a few renderering edge cases, such as with Major Minor's Majestic March). | ||
| // Thus, we use this as a signal to update the list of scissor rects, but ignore the parameter. | ||
| Rasterizer::ScissorChanged(); | ||
| } | ||
|
|
||
| } // namespace SW |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "VideoCommon/AbstractGfx.h" | ||
|
|
||
| class SWOGLWindow; | ||
|
|
||
| namespace SW | ||
| { | ||
| class SWGfx final : public AbstractGfx | ||
| { | ||
| public: | ||
| SWGfx(std::unique_ptr<SWOGLWindow> window); | ||
|
|
||
| bool IsHeadless() const override; | ||
| virtual bool SupportsUtilityDrawing() const override; | ||
|
|
||
| std::unique_ptr<AbstractTexture> CreateTexture(const TextureConfig& config, | ||
| std::string_view name) override; | ||
| std::unique_ptr<AbstractStagingTexture> | ||
| CreateStagingTexture(StagingTextureType type, const TextureConfig& config) override; | ||
| std::unique_ptr<AbstractFramebuffer> | ||
| CreateFramebuffer(AbstractTexture* color_attachment, AbstractTexture* depth_attachment) override; | ||
|
|
||
| void BindBackbuffer(const ClearColor& clear_color = {}) override; | ||
|
|
||
| std::unique_ptr<AbstractShader> CreateShaderFromSource(ShaderStage stage, std::string_view source, | ||
| std::string_view name) override; | ||
| std::unique_ptr<AbstractShader> CreateShaderFromBinary(ShaderStage stage, const void* data, | ||
| size_t length, | ||
| std::string_view name) override; | ||
| std::unique_ptr<NativeVertexFormat> | ||
| CreateNativeVertexFormat(const PortableVertexDeclaration& vtx_decl) override; | ||
| std::unique_ptr<AbstractPipeline> CreatePipeline(const AbstractPipelineConfig& config, | ||
| const void* cache_data = nullptr, | ||
| size_t cache_data_length = 0) override; | ||
|
|
||
| void ShowImage(const AbstractTexture* source_texture, | ||
| const MathUtil::Rectangle<int>& source_rc) override; | ||
|
|
||
| void ScaleTexture(AbstractFramebuffer* dst_framebuffer, const MathUtil::Rectangle<int>& dst_rect, | ||
| const AbstractTexture* src_texture, | ||
| const MathUtil::Rectangle<int>& src_rect) override; | ||
|
|
||
| void SetScissorRect(const MathUtil::Rectangle<int>& rc) override; | ||
|
|
||
| void ClearRegion(const MathUtil::Rectangle<int>& target_rc, bool colorEnable, bool alphaEnable, | ||
| bool zEnable, u32 color, u32 z) override; | ||
|
|
||
| private: | ||
| std::unique_ptr<SWOGLWindow> m_window; | ||
| }; | ||
|
|
||
| } // namespace SW |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -9,7 +9,6 @@ | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
| #include "VideoBackends/Vulkan/Constants.h" | ||
|
|
||
| namespace Vulkan | ||
| { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include "VideoCommon/AbstractGfx.h" | ||
|
|
||
| #include "Common/Assert.h" | ||
|
|
||
| #include "VideoCommon/AbstractFramebuffer.h" | ||
| #include "VideoCommon/AbstractTexture.h" | ||
| #include "VideoCommon/BPFunctions.h" | ||
| #include "VideoCommon/FramebufferManager.h" | ||
| #include "VideoCommon/RenderBase.h" | ||
| #include "VideoCommon/ShaderCache.h" | ||
| #include "VideoCommon/VertexManagerBase.h" | ||
| #include "VideoCommon/VideoConfig.h" | ||
|
|
||
| std::unique_ptr<AbstractGfx> g_gfx; | ||
|
|
||
| AbstractGfx::AbstractGfx() | ||
| { | ||
| ConfigChangedEvent::Register([this](u32 bits) { OnConfigChanged(bits); }, "AbstractGfx"); | ||
| } | ||
|
|
||
| bool AbstractGfx::IsHeadless() const | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| void AbstractGfx::BeginUtilityDrawing() | ||
| { | ||
| g_vertex_manager->Flush(); | ||
| } | ||
|
|
||
| void AbstractGfx::EndUtilityDrawing() | ||
| { | ||
| // Reset framebuffer/scissor/viewport. Pipeline will be reset at next draw. | ||
| g_framebuffer_manager->BindEFBFramebuffer(); | ||
| BPFunctions::SetScissorAndViewport(); | ||
| } | ||
|
|
||
| void AbstractGfx::SetFramebuffer(AbstractFramebuffer* framebuffer) | ||
| { | ||
| m_current_framebuffer = framebuffer; | ||
| } | ||
|
|
||
| void AbstractGfx::SetAndDiscardFramebuffer(AbstractFramebuffer* framebuffer) | ||
| { | ||
| m_current_framebuffer = framebuffer; | ||
| } | ||
|
|
||
| void AbstractGfx::SetAndClearFramebuffer(AbstractFramebuffer* framebuffer, | ||
| const ClearColor& color_value, float depth_value) | ||
| { | ||
| m_current_framebuffer = framebuffer; | ||
| } | ||
|
|
||
| void AbstractGfx::ClearRegion(const MathUtil::Rectangle<int>& target_rc, bool colorEnable, | ||
| bool alphaEnable, bool zEnable, u32 color, u32 z) | ||
| { | ||
| // This is a generic fallback for any ClearRegion operations that backends don't support. | ||
| // It simply draws a Quad. | ||
|
|
||
| BeginUtilityDrawing(); | ||
|
|
||
| // Set up uniforms. | ||
| struct Uniforms | ||
| { | ||
| float clear_color[4]; | ||
| float clear_depth; | ||
| float padding1, padding2, padding3; | ||
| }; | ||
| static_assert(std::is_standard_layout<Uniforms>::value); | ||
| Uniforms uniforms = {{static_cast<float>((color >> 16) & 0xFF) / 255.0f, | ||
| static_cast<float>((color >> 8) & 0xFF) / 255.0f, | ||
| static_cast<float>((color >> 0) & 0xFF) / 255.0f, | ||
| static_cast<float>((color >> 24) & 0xFF) / 255.0f}, | ||
| static_cast<float>(z & 0xFFFFFF) / 16777216.0f}; | ||
| if (!g_ActiveConfig.backend_info.bSupportsReversedDepthRange) | ||
| uniforms.clear_depth = 1.0f - uniforms.clear_depth; | ||
| g_vertex_manager->UploadUtilityUniforms(&uniforms, sizeof(uniforms)); | ||
|
|
||
| g_gfx->SetPipeline(g_framebuffer_manager->GetClearPipeline(colorEnable, alphaEnable, zEnable)); | ||
| g_gfx->SetViewportAndScissor(target_rc); | ||
| g_gfx->Draw(0, 3); | ||
| EndUtilityDrawing(); | ||
| } | ||
|
|
||
| void AbstractGfx::SetViewportAndScissor(const MathUtil::Rectangle<int>& rect, float min_depth, | ||
| float max_depth) | ||
| { | ||
| SetViewport(static_cast<float>(rect.left), static_cast<float>(rect.top), | ||
| static_cast<float>(rect.GetWidth()), static_cast<float>(rect.GetHeight()), min_depth, | ||
| max_depth); | ||
| SetScissorRect(rect); | ||
| } | ||
|
|
||
| void AbstractGfx::ScaleTexture(AbstractFramebuffer* dst_framebuffer, | ||
| const MathUtil::Rectangle<int>& dst_rect, | ||
| const AbstractTexture* src_texture, | ||
| const MathUtil::Rectangle<int>& src_rect) | ||
| { | ||
| ASSERT(dst_framebuffer->GetColorFormat() == AbstractTextureFormat::RGBA8); | ||
|
|
||
| BeginUtilityDrawing(); | ||
|
|
||
| // The shader needs to know the source rectangle. | ||
| const auto converted_src_rect = | ||
| ConvertFramebufferRectangle(src_rect, src_texture->GetWidth(), src_texture->GetHeight()); | ||
| const float rcp_src_width = 1.0f / src_texture->GetWidth(); | ||
| const float rcp_src_height = 1.0f / src_texture->GetHeight(); | ||
| const std::array<float, 4> uniforms = {{converted_src_rect.left * rcp_src_width, | ||
| converted_src_rect.top * rcp_src_height, | ||
| converted_src_rect.GetWidth() * rcp_src_width, | ||
| converted_src_rect.GetHeight() * rcp_src_height}}; | ||
| g_vertex_manager->UploadUtilityUniforms(&uniforms, sizeof(uniforms)); | ||
|
|
||
| // Discard if we're overwriting the whole thing. | ||
| if (static_cast<u32>(dst_rect.GetWidth()) == dst_framebuffer->GetWidth() && | ||
| static_cast<u32>(dst_rect.GetHeight()) == dst_framebuffer->GetHeight()) | ||
| { | ||
| SetAndDiscardFramebuffer(dst_framebuffer); | ||
| } | ||
| else | ||
| { | ||
| SetFramebuffer(dst_framebuffer); | ||
| } | ||
|
|
||
| SetViewportAndScissor(ConvertFramebufferRectangle(dst_rect, dst_framebuffer)); | ||
| SetPipeline(dst_framebuffer->GetLayers() > 1 ? g_shader_cache->GetRGBA8StereoCopyPipeline() : | ||
| g_shader_cache->GetRGBA8CopyPipeline()); | ||
| SetTexture(0, src_texture); | ||
| SetSamplerState(0, RenderState::GetLinearSamplerState()); | ||
| Draw(0, 3); | ||
| EndUtilityDrawing(); | ||
| if (dst_framebuffer->GetColorAttachment()) | ||
| dst_framebuffer->GetColorAttachment()->FinishedRendering(); | ||
| } | ||
|
|
||
| MathUtil::Rectangle<int> | ||
| AbstractGfx::ConvertFramebufferRectangle(const MathUtil::Rectangle<int>& rect, | ||
| const AbstractFramebuffer* framebuffer) const | ||
| { | ||
| return ConvertFramebufferRectangle(rect, framebuffer->GetWidth(), framebuffer->GetHeight()); | ||
| } | ||
|
|
||
| MathUtil::Rectangle<int> | ||
| AbstractGfx::ConvertFramebufferRectangle(const MathUtil::Rectangle<int>& rect, u32 fb_width, | ||
| u32 fb_height) const | ||
| { | ||
| MathUtil::Rectangle<int> ret = rect; | ||
| if (g_ActiveConfig.backend_info.bUsesLowerLeftOrigin) | ||
| { | ||
| ret.top = fb_height - rect.bottom; | ||
| ret.bottom = fb_height - rect.top; | ||
| } | ||
| return ret; | ||
| } | ||
|
|
||
| std::unique_ptr<VideoCommon::AsyncShaderCompiler> AbstractGfx::CreateAsyncShaderCompiler() | ||
| { | ||
| return std::make_unique<VideoCommon::AsyncShaderCompiler>(); | ||
| } | ||
|
|
||
| void AbstractGfx::OnConfigChanged(u32 changed_bits) | ||
| { | ||
| // If there's any shader changes, wait for the GPU to finish before destroying anything. | ||
| if (changed_bits & (CONFIG_CHANGE_BIT_HOST_CONFIG | CONFIG_CHANGE_BIT_MULTISAMPLES)) | ||
| { | ||
| WaitForGPUIdle(); | ||
| SetPipeline(nullptr); | ||
| } | ||
| } | ||
|
|
||
| bool AbstractGfx::UseGeometryShaderForUI() const | ||
| { | ||
| // OpenGL doesn't render to a 2-layer backbuffer like D3D/Vulkan for quad-buffered stereo, | ||
| // instead drawing twice and the eye selected by glDrawBuffer() (see Presenter::RenderXFBToScreen) | ||
| return g_ActiveConfig.stereo_mode == StereoMode::QuadBuffer && | ||
| g_ActiveConfig.backend_info.api_type != APIType::OpenGL; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "Common/MathUtil.h" | ||
|
|
||
| #include "VideoCommon/RenderState.h" | ||
|
|
||
| #include <array> | ||
| #include <memory> | ||
|
|
||
| class AbstractFramebuffer; | ||
| class AbstractPipeline; | ||
| class AbstractShader; | ||
| class AbstractTexture; | ||
| class AbstractStagingTexture; | ||
| class NativeVertexFormat; | ||
| struct ComputePipelineConfig; | ||
| struct AbstractPipelineConfig; | ||
| struct PortableVertexDeclaration; | ||
| struct TextureConfig; | ||
| enum class AbstractTextureFormat : u32; | ||
| enum class ShaderStage; | ||
| enum class StagingTextureType; | ||
|
|
||
| struct SurfaceInfo | ||
| { | ||
| u32 width = 0; | ||
| u32 height = 0; | ||
| float scale = 0.0f; | ||
| AbstractTextureFormat format = {}; | ||
| }; | ||
|
|
||
| namespace VideoCommon | ||
| { | ||
| class AsyncShaderCompiler; | ||
| } | ||
|
|
||
| using ClearColor = std::array<float, 4>; | ||
|
|
||
| // AbstractGfx is the root of Dolphin's Graphics API abstraction layer. | ||
| // | ||
| // Abstract knows nothing about the internals of the GameCube/Wii, that is all handled elsewhere in | ||
| // VideoCommon. | ||
|
|
||
| class AbstractGfx | ||
| { | ||
| public: | ||
| AbstractGfx(); | ||
| virtual ~AbstractGfx() = default; | ||
|
|
||
| virtual bool IsHeadless() const = 0; | ||
|
|
||
| // Does the backend support drawing a UI or doing post-processing | ||
| virtual bool SupportsUtilityDrawing() const { return true; } | ||
|
|
||
| virtual void SetPipeline(const AbstractPipeline* pipeline) {} | ||
| virtual void SetScissorRect(const MathUtil::Rectangle<int>& rc) {} | ||
| virtual void SetTexture(u32 index, const AbstractTexture* texture) {} | ||
| virtual void SetSamplerState(u32 index, const SamplerState& state) {} | ||
| virtual void SetComputeImageTexture(AbstractTexture* texture, bool read, bool write) {} | ||
| virtual void UnbindTexture(const AbstractTexture* texture) {} | ||
| virtual void SetViewport(float x, float y, float width, float height, float near_depth, | ||
| float far_depth) | ||
| { | ||
| } | ||
| virtual void SetFullscreen(bool enable_fullscreen) {} | ||
| virtual bool IsFullscreen() const { return false; } | ||
| virtual void BeginUtilityDrawing(); | ||
| virtual void EndUtilityDrawing(); | ||
| virtual std::unique_ptr<AbstractTexture> CreateTexture(const TextureConfig& config, | ||
| std::string_view name = "") = 0; | ||
| virtual std::unique_ptr<AbstractStagingTexture> | ||
| CreateStagingTexture(StagingTextureType type, const TextureConfig& config) = 0; | ||
| virtual std::unique_ptr<AbstractFramebuffer> | ||
| CreateFramebuffer(AbstractTexture* color_attachment, AbstractTexture* depth_attachment) = 0; | ||
|
|
||
| // Framebuffer operations. | ||
| virtual void SetFramebuffer(AbstractFramebuffer* framebuffer); | ||
| virtual void SetAndDiscardFramebuffer(AbstractFramebuffer* framebuffer); | ||
| virtual void SetAndClearFramebuffer(AbstractFramebuffer* framebuffer, | ||
| const ClearColor& color_value = {}, float depth_value = 0.0f); | ||
|
|
||
| virtual void ClearRegion(const MathUtil::Rectangle<int>& target_rc, bool colorEnable, | ||
| bool alphaEnable, bool zEnable, u32 color, u32 z); | ||
|
|
||
| // Drawing with currently-bound pipeline state. | ||
| virtual void Draw(u32 base_vertex, u32 num_vertices) {} | ||
| virtual void DrawIndexed(u32 base_index, u32 num_indices, u32 base_vertex) {} | ||
|
|
||
| // Dispatching compute shaders with currently-bound state. | ||
| virtual void DispatchComputeShader(const AbstractShader* shader, u32 groupsize_x, u32 groupsize_y, | ||
| u32 groupsize_z, u32 groups_x, u32 groups_y, u32 groups_z) | ||
| { | ||
| } | ||
|
|
||
| // Binds the backbuffer for rendering. The buffer will be cleared immediately after binding. | ||
| // This is where any window size changes are detected, therefore m_backbuffer_width and/or | ||
| // m_backbuffer_height may change after this function returns. | ||
| virtual void BindBackbuffer(const ClearColor& clear_color = {}) {} | ||
|
|
||
| // Presents the backbuffer to the window system, or "swaps buffers". | ||
| virtual void PresentBackbuffer() {} | ||
|
|
||
| // Shader modules/objects. | ||
| virtual std::unique_ptr<AbstractShader> CreateShaderFromSource(ShaderStage stage, | ||
| std::string_view source, | ||
| std::string_view name = "") = 0; | ||
| virtual std::unique_ptr<AbstractShader> CreateShaderFromBinary(ShaderStage stage, | ||
| const void* data, size_t length, | ||
| std::string_view name = "") = 0; | ||
| virtual std::unique_ptr<NativeVertexFormat> | ||
| CreateNativeVertexFormat(const PortableVertexDeclaration& vtx_decl) = 0; | ||
| virtual std::unique_ptr<AbstractPipeline> CreatePipeline(const AbstractPipelineConfig& config, | ||
| const void* cache_data = nullptr, | ||
| size_t cache_data_length = 0) = 0; | ||
|
|
||
| AbstractFramebuffer* GetCurrentFramebuffer() const { return m_current_framebuffer; } | ||
|
|
||
| // Sets viewport and scissor to the specified rectangle. rect is assumed to be in framebuffer | ||
| // coordinates, i.e. lower-left origin in OpenGL. | ||
| void SetViewportAndScissor(const MathUtil::Rectangle<int>& rect, float min_depth = 0.0f, | ||
| float max_depth = 1.0f); | ||
|
|
||
| // Scales a GPU texture using a copy shader. | ||
| virtual void ScaleTexture(AbstractFramebuffer* dst_framebuffer, | ||
| const MathUtil::Rectangle<int>& dst_rect, | ||
| const AbstractTexture* src_texture, | ||
| const MathUtil::Rectangle<int>& src_rect); | ||
|
|
||
| // Converts an upper-left to lower-left if required by the backend, optionally | ||
| // clamping to the framebuffer size. | ||
| MathUtil::Rectangle<int> ConvertFramebufferRectangle(const MathUtil::Rectangle<int>& rect, | ||
| u32 fb_width, u32 fb_height) const; | ||
| MathUtil::Rectangle<int> | ||
| ConvertFramebufferRectangle(const MathUtil::Rectangle<int>& rect, | ||
| const AbstractFramebuffer* framebuffer) const; | ||
|
|
||
| virtual void Flush() {} | ||
| virtual void WaitForGPUIdle() {} | ||
|
|
||
| // For opengl's glDrawBuffer | ||
| virtual void SelectLeftBuffer() {} | ||
| virtual void SelectRightBuffer() {} | ||
| virtual void SelectMainBuffer() {} | ||
|
|
||
| // A simple presentation fallback, only used by video software | ||
| virtual void ShowImage(const AbstractTexture* source_texture, | ||
| const MathUtil::Rectangle<int>& source_rc) | ||
| { | ||
| } | ||
|
|
||
| virtual std::unique_ptr<VideoCommon::AsyncShaderCompiler> CreateAsyncShaderCompiler(); | ||
|
|
||
| // Called when the configuration changes, and backend structures need to be updated. | ||
| virtual void OnConfigChanged(u32 changed_bits); | ||
|
|
||
| // Returns true if a layer-expanding geometry shader should be used when rendering the user | ||
| // interface and final XFB. | ||
| bool UseGeometryShaderForUI() const; | ||
|
|
||
| // Returns info about the main surface (aka backbuffer) | ||
| virtual SurfaceInfo GetSurfaceInfo() const { return {}; } | ||
|
|
||
| protected: | ||
| AbstractFramebuffer* m_current_framebuffer = nullptr; | ||
| const AbstractPipeline* m_current_pipeline = nullptr; | ||
| }; | ||
|
|
||
| extern std::unique_ptr<AbstractGfx> g_gfx; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,361 @@ | ||
| // 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" | ||
|
|
||
| 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] { 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), | ||
| "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)); | ||
| 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; | ||
| } | ||
|
|
||
| void FrameDumper::DoState(PointerWrap& p) | ||
| { | ||
| #ifdef HAVE_FFMPEG | ||
| m_ffmpeg_dump.DoState(p); | ||
| #endif | ||
| } | ||
| std::unique_ptr<FrameDumper> g_frame_dumper; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
| #include "Common/Event.h" | ||
| #include "Common/Flag.h" | ||
| #include "Common/MathUtil.h" | ||
| #include "Common/Thread.h" | ||
|
|
||
| #include "VideoCommon/FrameDumpFFMpeg.h" | ||
| #include "VideoCommon/VideoEvents.h" | ||
|
|
||
| class AbstractStagingTexture; | ||
| class AbstractTexture; | ||
| class AbstractFramebuffer; | ||
|
|
||
| class FrameDumper | ||
| { | ||
| public: | ||
| FrameDumper(); | ||
| ~FrameDumper(); | ||
|
|
||
| // Ensures all rendered frames are queued for encoding. | ||
| void FlushFrameDump(); | ||
|
|
||
| // Fills the frame dump staging texture with the current XFB texture. | ||
| void DumpCurrentFrame(const AbstractTexture* src_texture, | ||
| const MathUtil::Rectangle<int>& src_rect, | ||
| const MathUtil::Rectangle<int>& target_rect, u64 ticks, int frame_number); | ||
|
|
||
| void SaveScreenshot(std::string filename); | ||
|
|
||
| bool IsFrameDumping() const; | ||
|
|
||
| void DoState(PointerWrap& p); | ||
|
|
||
| private: | ||
| // NOTE: The methods below are called on the framedumping thread. | ||
| void FrameDumpThreadFunc(); | ||
| bool StartFrameDumpToFFMPEG(const FrameData&); | ||
| void DumpFrameToFFMPEG(const FrameData&); | ||
| void StopFrameDumpToFFMPEG(); | ||
| std::string GetFrameDumpNextImageFileName() const; | ||
| bool StartFrameDumpToImage(const FrameData&); | ||
| void DumpFrameToImage(const FrameData&); | ||
|
|
||
| void ShutdownFrameDumping(); | ||
|
|
||
| // Checks that the frame dump render texture exists and is the correct size. | ||
| bool CheckFrameDumpRenderTexture(u32 target_width, u32 target_height); | ||
|
|
||
| // Checks that the frame dump readback texture exists and is the correct size. | ||
| bool CheckFrameDumpReadbackTexture(u32 target_width, u32 target_height); | ||
|
|
||
| // Asynchronously encodes the specified pointer of frame data to the frame dump. | ||
| void DumpFrameData(const u8* data, int w, int h, int stride); | ||
|
|
||
| // Ensures all encoded frames have been written to the output file. | ||
| void FinishFrameData(); | ||
|
|
||
| std::thread m_frame_dump_thread; | ||
| Common::Flag m_frame_dump_thread_running; | ||
|
|
||
| // Used to kick frame dump thread. | ||
| Common::Event m_frame_dump_start; | ||
|
|
||
| // Set by frame dump thread on frame completion. | ||
| Common::Event m_frame_dump_done; | ||
|
|
||
| // Holds emulation state during the last swap when dumping. | ||
| FrameState m_last_frame_state; | ||
|
|
||
| // Communication of frame between video and dump threads. | ||
| FrameData m_frame_dump_data; | ||
|
|
||
| // Texture used for screenshot/frame dumping | ||
| std::unique_ptr<AbstractTexture> m_frame_dump_render_texture; | ||
| std::unique_ptr<AbstractFramebuffer> m_frame_dump_render_framebuffer; | ||
|
|
||
| // Double buffer: | ||
| std::unique_ptr<AbstractStagingTexture> m_frame_dump_readback_texture; | ||
| std::unique_ptr<AbstractStagingTexture> m_frame_dump_output_texture; | ||
| // Set when readback texture holds a frame that needs to be dumped. | ||
| bool m_frame_dump_needs_flush = false; | ||
| // Set when thread is processing output texture. | ||
| bool m_frame_dump_frame_running = false; | ||
|
|
||
| // Used to generate screenshot names. | ||
| u32 m_frame_dump_image_counter = 0; | ||
|
|
||
| FFMpegFrameDump m_ffmpeg_dump; | ||
|
|
||
| // Screenshots | ||
| Common::Flag m_screenshot_request; | ||
| Common::Event m_screenshot_completed; | ||
| std::mutex m_screenshot_lock; | ||
| std::string m_screenshot_name; | ||
|
|
||
| Common::EventHook m_frame_end_handle; | ||
| }; | ||
|
|
||
| extern std::unique_ptr<FrameDumper> g_frame_dumper; |