From cf4a4d8fd585eb0b818f7c21b3342381f966af61 Mon Sep 17 00:00:00 2001 From: Vladislav Belov Date: Mon, 8 Jun 2026 18:26:58 +0200 Subject: [PATCH] Adds SwapChain as an explicit DeviceObject It allows us more flexibility on how we control swapchain. That includes toggling V-Sync in real time. --- source/renderer/backend/IDevice.h | 67 ++++----- .../renderer/backend/IDeviceCommandContext.h | 6 +- source/renderer/backend/ISwapChain.h | 78 ++++++++++ source/renderer/backend/dummy/Device.cpp | 39 ++--- source/renderer/backend/dummy/Device.h | 19 +-- .../backend/dummy/DeviceCommandContext.cpp | 4 +- .../backend/dummy/DeviceCommandContext.h | 5 +- source/renderer/backend/dummy/SwapChain.cpp | 75 ++++++++++ source/renderer/backend/dummy/SwapChain.h | 74 ++++++++++ source/renderer/backend/gl/Device.cpp | 94 +++--------- source/renderer/backend/gl/Device.h | 33 +---- .../backend/gl/DeviceCommandContext.cpp | 28 +--- .../backend/gl/DeviceCommandContext.h | 5 +- source/renderer/backend/gl/Framebuffer.h | 3 +- source/renderer/backend/gl/SwapChain.cpp | 136 ++++++++++++++++++ source/renderer/backend/gl/SwapChain.h | 97 +++++++++++++ source/renderer/backend/vulkan/Device.cpp | 132 ++++------------- source/renderer/backend/vulkan/Device.h | 30 ++-- .../backend/vulkan/DeviceCommandContext.cpp | 19 ++- .../backend/vulkan/DeviceCommandContext.h | 7 +- source/renderer/backend/vulkan/SwapChain.cpp | 69 +++++++-- source/renderer/backend/vulkan/SwapChain.h | 41 ++++-- 22 files changed, 701 insertions(+), 360 deletions(-) create mode 100644 source/renderer/backend/ISwapChain.h create mode 100644 source/renderer/backend/dummy/SwapChain.cpp create mode 100644 source/renderer/backend/dummy/SwapChain.h create mode 100644 source/renderer/backend/gl/SwapChain.cpp create mode 100644 source/renderer/backend/gl/SwapChain.h diff --git a/source/renderer/backend/IDevice.h b/source/renderer/backend/IDevice.h index 4c791339e7..8d8949ce22 100644 --- a/source/renderer/backend/IDevice.h +++ b/source/renderer/backend/IDevice.h @@ -39,6 +39,7 @@ namespace Renderer::Backend { class IDeviceCommandContext; } namespace Renderer::Backend { class IFramebuffer; } namespace Renderer::Backend { class IGraphicsPipelineState; } namespace Renderer::Backend { class IShaderProgram; } +namespace Renderer::Backend { class ISwapChain; } namespace Renderer::Backend { class IVertexInputLayout; } namespace Renderer::Backend { enum class AttachmentLoadOp; } namespace Renderer::Backend { enum class AttachmentStoreOp; } @@ -51,6 +52,8 @@ namespace Renderer::Backend { struct SGraphicsPipelineStateDesc; } namespace Renderer::Backend { struct SVertexAttributeFormat; } namespace Renderer::Backend::Sampler { struct Desc; } +typedef struct SDL_Window SDL_Window; + namespace Renderer { @@ -77,6 +80,10 @@ public: double timestampMultiplier; }; + /** + * It's a responsibility of a device owner to make sure (via WaitUntilIdle) + * that the device is available to be destroyed. + */ virtual ~IDevice() {} virtual Backend GetBackend() const = 0; @@ -90,6 +97,27 @@ public: virtual std::unique_ptr CreateCommandContext() = 0; + /** + * To be able to present something on a window it needs to create swapchain + * which provides framebuffer to output rendering result. Generally it's + * not allowed to have multiple swapchains for the same window. + * + * We use the provided surface size when the window is nullptr. It's used + * in Atlas when we don't have an SDL_Window. + * + * @return A valid swapchain if it was created successfully else nullptr. + */ + virtual std::unique_ptr CreateSwapChain( + const char* name, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, const bool vsync, + std::unique_ptr oldSwapChain) = 0; + + /** + * Waits until all submitted work (via DeviceCommandContext::Flush) to + * backend is completed and it's safe to release a resource like SwapChain. + */ + virtual void WaitUntilIdle() = 0; + /** * Creates a graphics pipeline state. It's a caller responsibility to * guarantee a lifespan of IShaderProgram stored in the description. @@ -141,45 +169,6 @@ public: virtual std::unique_ptr CreateShaderProgram( const CStr& name, const CShaderDefines& defines) = 0; - /** - * Acquires a backbuffer for rendering a frame. - * - * @return True if it was successfully acquired and we can render to it. - */ - virtual bool AcquireNextBackbuffer() = 0; - - /** - * Returns a framebuffer for the current backbuffer with the required - * attachment operations. It should not be called if the last - * AcquireNextBackbuffer call returned false. - * - * It's guaranteed that for the same acquired backbuffer this function returns - * a framebuffer with the same attachments and properties except load and - * store operations. - * - * @return The last successfully acquired framebuffer that wasn't - * presented. - */ - virtual IFramebuffer* GetCurrentBackbuffer( - const AttachmentLoadOp colorAttachmentLoadOp, - const AttachmentStoreOp colorAttachmentStoreOp, - const AttachmentLoadOp depthStencilAttachmentLoadOp, - const AttachmentStoreOp depthStencilAttachmentStoreOp) = 0; - - /** - * Presents the backbuffer to the swapchain queue to be flipped on a - * screen. Should be called only if the last AcquireNextBackbuffer call - * returned true. - */ - virtual void Present() = 0; - - /** - * Should be called on window surface resize. It's the device owner - * responsibility to call that function. Shouldn't be called during - * rendering to an acquired backbuffer. - */ - virtual void OnWindowResize(const uint32_t width, const uint32_t height) = 0; - virtual bool IsTextureFormatSupported(const Format format) const = 0; virtual bool IsFramebufferFormatSupported(const Format format) const = 0; diff --git a/source/renderer/backend/IDeviceCommandContext.h b/source/renderer/backend/IDeviceCommandContext.h index bfd6136890..33ab31f1eb 100644 --- a/source/renderer/backend/IDeviceCommandContext.h +++ b/source/renderer/backend/IDeviceCommandContext.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -29,6 +29,7 @@ namespace Renderer::Backend { class IComputePipelineState; } namespace Renderer::Backend { class IGraphicsPipelineState; } +namespace Renderer::Backend { class ISwapChain; } namespace Renderer::Backend { class IVertexInputLayout; } namespace Renderer::Backend { enum class Format; } namespace Renderer::Backend::Sampler { enum class Filter; } @@ -112,7 +113,8 @@ public: * but a client doesn't support that yet. */ virtual void ReadbackFramebufferSync( - const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height, + ISwapChain& swapChain, const uint32_t x, const uint32_t y, + const uint32_t width, const uint32_t height, void* data) = 0; virtual void UploadTexture(ITexture* texture, const Format dataFormat, diff --git a/source/renderer/backend/ISwapChain.h b/source/renderer/backend/ISwapChain.h new file mode 100644 index 0000000000..abb1b59f21 --- /dev/null +++ b/source/renderer/backend/ISwapChain.h @@ -0,0 +1,78 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_RENDERER_BACKEND_ISWAPCHAIN +#define INCLUDED_RENDERER_BACKEND_ISWAPCHAIN + +#include "renderer/backend/IDeviceObject.h" +#include "renderer/backend/IFramebuffer.h" + +#include + +namespace Renderer +{ + +namespace Backend +{ + +class ISwapChain : public IDeviceObject +{ +public: + /** + * @return True if we still can use the swapchain to present. A swapchain + * can become invalid on window resize or some other system event. + */ + virtual bool IsValid() const = 0; + + /** + * Acquires a backbuffer for rendering a frame. + * + * @return True if it was successfully acquired and we can render to it. + */ + virtual bool AcquireNextBackbuffer() = 0; + + /** + * Returns a framebuffer for the current backbuffer with the required + * attachment operations. It should not be called if the last + * AcquireNextBackbuffer call returned false. + * + * It's guaranteed that for the same acquired backbuffer this function returns + * a framebuffer with the same attachments and properties except load and + * store operations. + * + * @return The last successfully acquired framebuffer that wasn't + * presented. + */ + virtual IFramebuffer* GetCurrentBackbuffer( + const AttachmentLoadOp colorAttachmentLoadOp, + const AttachmentStoreOp colorAttachmentStoreOp, + const AttachmentLoadOp depthStencilAttachmentLoadOp, + const AttachmentStoreOp depthStencilAttachmentStoreOp) = 0; + + /** + * Presents the backbuffer to the swapchain queue to be flipped on a + * screen. Should be called only if the last AcquireNextBackbuffer call + * returned true. + */ + virtual void Present() = 0; +}; + +} // namespace Backend + +} // namespace Renderer + +#endif // INCLUDED_RENDERER_BACKEND_ISWAPCHAIN diff --git a/source/renderer/backend/dummy/Device.cpp b/source/renderer/backend/dummy/Device.cpp index 4c27c785d4..df840ff822 100644 --- a/source/renderer/backend/dummy/Device.cpp +++ b/source/renderer/backend/dummy/Device.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -26,6 +26,7 @@ #include "renderer/backend/dummy/Framebuffer.h" #include "renderer/backend/dummy/PipelineState.h" #include "renderer/backend/dummy/ShaderProgram.h" +#include "renderer/backend/dummy/SwapChain.h" #include "renderer/backend/dummy/Texture.h" #include "scriptinterface/Object.h" @@ -49,8 +50,6 @@ CDevice::CDevice() m_DriverInformation = "Unknown"; m_Extensions = {}; - m_Backbuffer = CFramebuffer::Create(this); - m_Capabilities.S3TC = true; m_Capabilities.computeShaders = true; m_Capabilities.debugLabels = true; @@ -75,6 +74,18 @@ std::unique_ptr CDevice::CreateCommandContext() return CDeviceCommandContext::Create(this); } +std::unique_ptr CDevice::CreateSwapChain( + const char*, SDL_Window*, int, int, + const bool, std::unique_ptr oldSwapChain) +{ + oldSwapChain.reset(); + return CSwapChain::Create(this); +} + +void CDevice::WaitUntilIdle() +{ +} + std::unique_ptr CDevice::CreateGraphicsPipelineState( const SGraphicsPipelineStateDesc& pipelineStateDesc) { @@ -129,28 +140,6 @@ std::unique_ptr CDevice::CreateShaderProgram( return CShaderProgram::Create(this); } -bool CDevice::AcquireNextBackbuffer() -{ - // We have nothing to acquire. - return true; -} - -IFramebuffer* CDevice::GetCurrentBackbuffer( - const AttachmentLoadOp, const AttachmentStoreOp, - const AttachmentLoadOp, const AttachmentStoreOp) -{ - return m_Backbuffer.get(); -} - -void CDevice::Present() -{ - // We have nothing to present. -} - -void CDevice::OnWindowResize(const uint32_t /*width*/, const uint32_t /*height*/) -{ -} - bool CDevice::IsTextureFormatSupported(const Format) const { return true; diff --git a/source/renderer/backend/dummy/Device.h b/source/renderer/backend/dummy/Device.h index 8a0286c07d..ee86123c98 100644 --- a/source/renderer/backend/dummy/Device.h +++ b/source/renderer/backend/dummy/Device.h @@ -59,6 +59,13 @@ public: std::unique_ptr CreateCommandContext() override; + std::unique_ptr CreateSwapChain( + const char* name, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain) override; + + void WaitUntilIdle() override; + std::unique_ptr CreateGraphicsPipelineState( const SGraphicsPipelineStateDesc& pipelineStateDesc) override; @@ -88,16 +95,6 @@ public: std::unique_ptr CreateShaderProgram( const CStr& name, const CShaderDefines& defines) override; - bool AcquireNextBackbuffer() override; - - IFramebuffer* GetCurrentBackbuffer( - const AttachmentLoadOp, const AttachmentStoreOp, - const AttachmentLoadOp, const AttachmentStoreOp) override; - - void Present() override; - - void OnWindowResize(const uint32_t width, const uint32_t height) override; - bool IsTextureFormatSupported(const Format format) const override; bool IsFramebufferFormatSupported(const Format format) const override; @@ -124,8 +121,6 @@ protected: std::string m_DriverInformation; std::vector m_Extensions; - std::unique_ptr m_Backbuffer; - Capabilities m_Capabilities{}; }; diff --git a/source/renderer/backend/dummy/DeviceCommandContext.cpp b/source/renderer/backend/dummy/DeviceCommandContext.cpp index 475021341e..45198e7dd3 100644 --- a/source/renderer/backend/dummy/DeviceCommandContext.cpp +++ b/source/renderer/backend/dummy/DeviceCommandContext.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -126,7 +126,7 @@ void CDeviceCommandContext::EndFramebufferPass() } void CDeviceCommandContext::ReadbackFramebufferSync( - const uint32_t, const uint32_t, const uint32_t, const uint32_t, void*) + ISwapChain&, const uint32_t, const uint32_t, const uint32_t, const uint32_t, void*) { } diff --git a/source/renderer/backend/dummy/DeviceCommandContext.h b/source/renderer/backend/dummy/DeviceCommandContext.h index 0db72043d0..7d60ade954 100644 --- a/source/renderer/backend/dummy/DeviceCommandContext.h +++ b/source/renderer/backend/dummy/DeviceCommandContext.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -59,7 +59,8 @@ public: void BeginFramebufferPass(IFramebuffer* framebuffer) override; void EndFramebufferPass() override; void ReadbackFramebufferSync( - const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height, + ISwapChain& swapChain, const uint32_t x, const uint32_t y, + const uint32_t width, const uint32_t height, void* data) override; void UploadTexture(ITexture* texture, const Format dataFormat, diff --git a/source/renderer/backend/dummy/SwapChain.cpp b/source/renderer/backend/dummy/SwapChain.cpp new file mode 100644 index 0000000000..d5b9301153 --- /dev/null +++ b/source/renderer/backend/dummy/SwapChain.cpp @@ -0,0 +1,75 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "SwapChain.h" + +#include "renderer/backend/dummy/Device.h" +#include "renderer/backend/dummy/Framebuffer.h" + +namespace Renderer +{ + +namespace Backend +{ + +namespace Dummy +{ + +// static +std::unique_ptr CSwapChain::Create(CDevice* device) +{ + std::unique_ptr swapChain(new CSwapChain()); + swapChain->m_Device = device; + swapChain->m_Backbuffer = + device->CreateFramebuffer("Backbuffer", nullptr, nullptr); + return swapChain; +} + +CSwapChain::CSwapChain() = default; + +CSwapChain::~CSwapChain() = default; + +IDevice* CSwapChain::GetDevice() +{ + return m_Device; +} + +bool CSwapChain::AcquireNextBackbuffer() +{ + // We have nothing to acquire. + return true; +} + +IFramebuffer* CSwapChain::GetCurrentBackbuffer( + const AttachmentLoadOp, const AttachmentStoreOp, + const AttachmentLoadOp, const AttachmentStoreOp) +{ + return m_Backbuffer.get(); +} + +void CSwapChain::Present() +{ + // We have nothing to present. +} + +} // namespace Dummy + +} // namespace Backend + +} // namespace Renderer diff --git a/source/renderer/backend/dummy/SwapChain.h b/source/renderer/backend/dummy/SwapChain.h new file mode 100644 index 0000000000..aee169bf82 --- /dev/null +++ b/source/renderer/backend/dummy/SwapChain.h @@ -0,0 +1,74 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_RENDERER_BACKEND_DUMMY_SWAPCHAIN +#define INCLUDED_RENDERER_BACKEND_DUMMY_SWAPCHAIN + +#include "renderer/backend/ISwapChain.h" + +#include +#include + +namespace Renderer +{ + +namespace Backend +{ + +namespace Dummy +{ + +class CDevice; + +class CSwapChain : public ISwapChain +{ +public: + ~CSwapChain() override; + + IDevice* GetDevice() override; + + bool IsValid() const override { return true; } + + bool AcquireNextBackbuffer() override; + + IFramebuffer* GetCurrentBackbuffer( + const AttachmentLoadOp colorAttachmentLoadOp, + const AttachmentStoreOp colorAttachmentStoreOp, + const AttachmentLoadOp depthStencilAttachmentLoadOp, + const AttachmentStoreOp depthStencilAttachmentStoreOp) override; + + void Present() override; + +private: + friend class CDevice; + + static std::unique_ptr Create(CDevice* device); + + CSwapChain(); + + CDevice* m_Device{nullptr}; + + std::unique_ptr m_Backbuffer; +}; + +} // namespace Dummy + +} // namespace Backend + +} // namespace Renderer + +#endif // INCLUDED_RENDERER_BACKEND_DUMMY_SWAPCHAIN diff --git a/source/renderer/backend/gl/Device.cpp b/source/renderer/backend/gl/Device.cpp index c484cc858a..3a7c63c3d1 100644 --- a/source/renderer/backend/gl/Device.cpp +++ b/source/renderer/backend/gl/Device.cpp @@ -24,7 +24,6 @@ #include "lib/config2.h" #include "lib/debug.h" #include "lib/external_libraries/libsdl.h" -#include "lib/hash.h" #include "lib/ogl.h" #include "lib/secure_crt.h" #include "lib/sysdep/os.h" @@ -38,6 +37,7 @@ #include "renderer/backend/gl/Framebuffer.h" #include "renderer/backend/gl/PipelineState.h" #include "renderer/backend/gl/ShaderProgram.h" +#include "renderer/backend/gl/SwapChain.h" #include "renderer/backend/gl/Texture.h" #include "scriptinterface/Object.h" @@ -241,7 +241,6 @@ std::unique_ptr CDevice::Create(SDL_Window* window) LOGERROR("SDL_GL_CreateContext failed: '%s'", SDL_GetError()); return nullptr; } - SDL_GL_GetDrawableSize(window, &device->m_SurfaceDrawableWidth, &device->m_SurfaceDrawableHeight); #if OS_WIN ogl_Init(SDL_GL_GetProcAddress, wutil_GetAppHDC()); @@ -801,6 +800,27 @@ std::unique_ptr CDevice::CreateCommandContext() return commandContet; } +std::unique_ptr CDevice::CreateSwapChain( + [[maybe_unused]] const char* name, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain) +{ + oldSwapChain.reset(); + ENSURE(window == m_Window); + if (window) + SDL_GL_GetDrawableSize(window, &surfaceDrawableWidth, &surfaceDrawableHeight); + return CSwapChain::Create( + this, window, surfaceDrawableWidth, surfaceDrawableHeight, vsync); +} + +void CDevice::WaitUntilIdle() +{ + // Technically we don't need to call glFinish since a driver ensures safety + // itself. Though it might give a hint to a driver and it might reduce a + // memory spike during recreating resources. + glFinish(); +} + std::unique_ptr CDevice::CreateGraphicsPipelineState( const SGraphicsPipelineStateDesc& pipelineStateDesc) { @@ -857,74 +877,6 @@ std::unique_ptr CDevice::CreateShaderProgram( return CShaderProgram::Create(this, name, defines); } -bool CDevice::AcquireNextBackbuffer() -{ - ENSURE(!m_BackbufferAcquired); - m_BackbufferAcquired = true; - return true; -} - -size_t CDevice::BackbufferKeyHash::operator()(const BackbufferKey& key) const -{ - size_t seed = 0; - hash_combine(seed, std::get<0>(key)); - hash_combine(seed, std::get<1>(key)); - hash_combine(seed, std::get<2>(key)); - hash_combine(seed, std::get<3>(key)); - return seed; -} - -IFramebuffer* CDevice::GetCurrentBackbuffer( - const AttachmentLoadOp colorAttachmentLoadOp, - const AttachmentStoreOp colorAttachmentStoreOp, - const AttachmentLoadOp depthStencilAttachmentLoadOp, - const AttachmentStoreOp depthStencilAttachmentStoreOp) -{ - const BackbufferKey key{ - colorAttachmentLoadOp, colorAttachmentStoreOp, - depthStencilAttachmentLoadOp, depthStencilAttachmentStoreOp}; - auto it = m_Backbuffers.find(key); - if (it == m_Backbuffers.end()) - { - it = m_Backbuffers.emplace(key, CFramebuffer::CreateBackbuffer( - this, m_SurfaceDrawableWidth, m_SurfaceDrawableHeight, - colorAttachmentLoadOp, colorAttachmentStoreOp, - depthStencilAttachmentLoadOp, depthStencilAttachmentStoreOp)).first; - } - return it->second.get(); -} - -void CDevice::Present() -{ - ENSURE(m_BackbufferAcquired); - m_BackbufferAcquired = false; - - if (m_Window) - { - PROFILE3("swap buffers"); - SDL_GL_SwapWindow(m_Window); - ogl_WarnIfError(); - } - -#if defined(NDEBUG) - if (!g_ConfigDB.Get("gl.checkerrorafterswap", false)) - return; -#endif - PROFILE3("error check"); - // We have to check GL errors after SwapBuffer to avoid possible - // synchronizations during rendering. - if (GLenum err = glGetError()) - ONCE(LOGERROR("GL error %s (0x%04x) occurred", ogl_GetErrorName(err), err)); -} - -void CDevice::OnWindowResize(const uint32_t width, const uint32_t height) -{ - ENSURE(!m_BackbufferAcquired); - m_Backbuffers.clear(); - m_SurfaceDrawableWidth = width; - m_SurfaceDrawableHeight = height; -} - bool CDevice::IsTextureFormatSupported(const Format format) const { bool supported = false; @@ -1081,7 +1033,7 @@ void CDevice::InsertTimestampQuery(const uint32_t handle) void CDevice::CollectStatistics(StatisticsVector& statistics) const { - statistics.emplace_back("Backbuffer count", "", static_cast(m_Backbuffers.size())); + statistics.emplace_back("Query count", "", static_cast(m_Queries.size())); } std::unique_ptr CreateDevice(SDL_Window* window) diff --git a/source/renderer/backend/gl/Device.h b/source/renderer/backend/gl/Device.h index 7f249a0be2..dcba647b52 100644 --- a/source/renderer/backend/gl/Device.h +++ b/source/renderer/backend/gl/Device.h @@ -73,6 +73,13 @@ public: std::unique_ptr CreateCommandContext() override; + std::unique_ptr CreateSwapChain( + const char* name, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain) override; + + void WaitUntilIdle() override; + std::unique_ptr CreateGraphicsPipelineState( const SGraphicsPipelineStateDesc& pipelineStateDesc) override; @@ -104,18 +111,6 @@ public: std::unique_ptr CreateShaderProgram( const CStr& name, const CShaderDefines& defines) override; - bool AcquireNextBackbuffer() override; - - IFramebuffer* GetCurrentBackbuffer( - const AttachmentLoadOp colorAttachmentLoadOp, - const AttachmentStoreOp colorAttachmentStoreOp, - const AttachmentLoadOp depthStencilAttachmentLoadOp, - const AttachmentStoreOp depthStencilAttachmentStoreOp) override; - - void Present() override; - - void OnWindowResize(const uint32_t width, const uint32_t height) override; - bool UseFramebufferInvalidating() const { return m_UseFramebufferInvalidating; } bool IsTextureFormatSupported(const Format format) const override; @@ -144,7 +139,6 @@ private: SDL_Window* m_Window = nullptr; SDL_GLContext m_Context = nullptr; - int m_SurfaceDrawableWidth = 0, m_SurfaceDrawableHeight = 0; std::string m_Name; std::string m_Version; @@ -156,19 +150,6 @@ private: // it's used only as a helper for transition. CDeviceCommandContext* m_ActiveCommandContext = nullptr; - using BackbufferKey = std::tuple< - AttachmentLoadOp, AttachmentStoreOp, - AttachmentLoadOp, AttachmentStoreOp>; - struct BackbufferKeyHash - { - size_t operator()(const BackbufferKey& key) const; - }; - // We use std::unordered_map to avoid storing sizes of Attachment*Op - // enumerations. If it becomes a performance issue we'll replace it - // by an array. - std::unordered_map< - BackbufferKey, std::unique_ptr, BackbufferKeyHash> m_Backbuffers; - bool m_BackbufferAcquired = false; bool m_UseFramebufferInvalidating = false; struct Query diff --git a/source/renderer/backend/gl/DeviceCommandContext.cpp b/source/renderer/backend/gl/DeviceCommandContext.cpp index 28425ccd6e..3e9cd2d9c1 100644 --- a/source/renderer/backend/gl/DeviceCommandContext.cpp +++ b/source/renderer/backend/gl/DeviceCommandContext.cpp @@ -227,11 +227,6 @@ void InvalidateFramebuffer( std::unique_ptr CDeviceCommandContext::Create(CDevice* device) { std::unique_ptr deviceCommandContext(new CDeviceCommandContext(device)); - deviceCommandContext->m_Framebuffer = device->GetCurrentBackbuffer( - Renderer::Backend::AttachmentLoadOp::DONT_CARE, - Renderer::Backend::AttachmentStoreOp::DONT_CARE, - Renderer::Backend::AttachmentLoadOp::DONT_CARE, - Renderer::Backend::AttachmentStoreOp::DONT_CARE)->As(); deviceCommandContext->ResetStates(); return deviceCommandContext; } @@ -610,12 +605,8 @@ void CDeviceCommandContext::ResetStates() { SetGraphicsPipelineStateImpl(MakeDefaultGraphicsPipelineStateDesc(), true); SetScissors(0, nullptr); - m_Framebuffer = m_Device->GetCurrentBackbuffer( - Renderer::Backend::AttachmentLoadOp::DONT_CARE, - Renderer::Backend::AttachmentStoreOp::DONT_CARE, - Renderer::Backend::AttachmentLoadOp::DONT_CARE, - Renderer::Backend::AttachmentStoreOp::DONT_CARE)->As(); - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_Framebuffer->GetHandle()); + m_Framebuffer = nullptr; + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); ogl_WarnIfError(); } @@ -994,26 +985,21 @@ void CDeviceCommandContext::EndFramebufferPass() } ENSURE(m_InsideFramebufferPass); m_InsideFramebufferPass = false; - CFramebuffer* framebuffer = m_Device->GetCurrentBackbuffer( - Renderer::Backend::AttachmentLoadOp::DONT_CARE, - Renderer::Backend::AttachmentStoreOp::DONT_CARE, - Renderer::Backend::AttachmentLoadOp::DONT_CARE, - Renderer::Backend::AttachmentStoreOp::DONT_CARE)->As(); - if (framebuffer->GetHandle() != m_Framebuffer->GetHandle()) + if (m_Framebuffer->GetHandle() != 0) { - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer->GetHandle()); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); ogl_WarnIfError(); } - m_Framebuffer = framebuffer; + m_Framebuffer = nullptr; SetGraphicsPipelineStateImpl(MakeDefaultGraphicsPipelineStateDesc(), false); } void CDeviceCommandContext::ReadbackFramebufferSync( - const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height, + ISwapChain&, const uint32_t x, const uint32_t y, + const uint32_t width, const uint32_t height, void* data) { - ENSURE(m_Framebuffer); glReadPixels(x, y, width, height, GL_RGB, GL_UNSIGNED_BYTE, data); ogl_WarnIfError(); } diff --git a/source/renderer/backend/gl/DeviceCommandContext.h b/source/renderer/backend/gl/DeviceCommandContext.h index 9bb9ebdb35..c96cd79aea 100644 --- a/source/renderer/backend/gl/DeviceCommandContext.h +++ b/source/renderer/backend/gl/DeviceCommandContext.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -74,7 +74,8 @@ public: void EndFramebufferPass() override; void ClearFramebuffer(const bool color, const bool depth, const bool stencil) override; void ReadbackFramebufferSync( - const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height, + ISwapChain& swapChain, const uint32_t x, const uint32_t y, + const uint32_t width, const uint32_t height, void* data) override; void UploadTexture(ITexture* texture, const Format dataFormat, diff --git a/source/renderer/backend/gl/Framebuffer.h b/source/renderer/backend/gl/Framebuffer.h index 874ec27287..b15be87ce7 100644 --- a/source/renderer/backend/gl/Framebuffer.h +++ b/source/renderer/backend/gl/Framebuffer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -58,6 +58,7 @@ public: private: friend class CDevice; + friend class CSwapChain; static std::unique_ptr Create( CDevice* device, const char* name, SColorAttachment* colorAttachment, diff --git a/source/renderer/backend/gl/SwapChain.cpp b/source/renderer/backend/gl/SwapChain.cpp new file mode 100644 index 0000000000..d50f85330f --- /dev/null +++ b/source/renderer/backend/gl/SwapChain.cpp @@ -0,0 +1,136 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "SwapChain.h" + +#include "lib/config2.h" +#include "lib/debug.h" +#include "lib/hash.h" +#include "ps/ConfigDB.h" +#include "ps/CLogger.h" +#include "ps/Profile.h" +#include "renderer/backend/gl/Device.h" + +#include + +namespace Renderer +{ + +namespace Backend +{ + +namespace GL +{ + +namespace +{ + +} // anonymous namespace + +// static +std::unique_ptr CSwapChain::Create( + CDevice* device, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync) +{ + std::unique_ptr swapChain(new CSwapChain()); + swapChain->m_Device = device; + swapChain->m_Window = window; + swapChain->m_SurfaceDrawableWidth = surfaceDrawableWidth; + swapChain->m_SurfaceDrawableHeight = surfaceDrawableHeight; + SDL_GL_SetSwapInterval(vsync ? 1 : 0); + return swapChain; +} + +CSwapChain::CSwapChain() = default; + +CSwapChain::~CSwapChain() +{ +} + +IDevice* CSwapChain::GetDevice() +{ + return m_Device; +} + +bool CSwapChain::AcquireNextBackbuffer() +{ + ENSURE(!m_BackbufferAcquired); + m_BackbufferAcquired = true; + return true; +} + +size_t CSwapChain::BackbufferKeyHash::operator()(const BackbufferKey& key) const +{ + size_t seed = 0; + hash_combine(seed, std::get<0>(key)); + hash_combine(seed, std::get<1>(key)); + hash_combine(seed, std::get<2>(key)); + hash_combine(seed, std::get<3>(key)); + return seed; +} + +IFramebuffer* CSwapChain::GetCurrentBackbuffer( + const AttachmentLoadOp colorAttachmentLoadOp, + const AttachmentStoreOp colorAttachmentStoreOp, + const AttachmentLoadOp depthStencilAttachmentLoadOp, + const AttachmentStoreOp depthStencilAttachmentStoreOp) +{ + const BackbufferKey key{ + colorAttachmentLoadOp, colorAttachmentStoreOp, + depthStencilAttachmentLoadOp, depthStencilAttachmentStoreOp}; + auto it = m_Backbuffers.find(key); + if (it == m_Backbuffers.end()) + { + it = m_Backbuffers.emplace(key, CFramebuffer::CreateBackbuffer( + m_Device, m_SurfaceDrawableWidth, m_SurfaceDrawableHeight, + colorAttachmentLoadOp, colorAttachmentStoreOp, + depthStencilAttachmentLoadOp, depthStencilAttachmentStoreOp)).first; + } + return it->second.get(); +} + +void CSwapChain::Present() +{ + ENSURE(m_BackbufferAcquired); + m_BackbufferAcquired = false; + + if (m_Window) + { + PROFILE3("swap buffers"); + SDL_GL_SwapWindow(m_Window); + ogl_WarnIfError(); + } + +#if defined(NDEBUG) + if (!g_ConfigDB.Get("gl.checkerrorafterswap", false)) + return; +#endif + PROFILE3("error check"); + // We have to check GL errors after SwapBuffer to avoid possible + // synchronizations during rendering. + if (GLenum err = glGetError()) + ONCE(LOGERROR("GL error %s (0x%04x) occurred", ogl_GetErrorName(err), err)); +} + +} // namespace GL + +} // namespace Backend + +} // namespace Renderer diff --git a/source/renderer/backend/gl/SwapChain.h b/source/renderer/backend/gl/SwapChain.h new file mode 100644 index 0000000000..b1f9c85a8f --- /dev/null +++ b/source/renderer/backend/gl/SwapChain.h @@ -0,0 +1,97 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_RENDERER_BACKEND_GL_SWAPCHAIN +#define INCLUDED_RENDERER_BACKEND_GL_SWAPCHAIN + +#include "lib/ogl.h" +#include "renderer/backend/gl/Framebuffer.h" +#include "renderer/backend/ISwapChain.h" + +#include +#include +#include + +typedef struct SDL_Window SDL_Window; + +namespace Renderer +{ + +namespace Backend +{ + +namespace GL +{ + +class CDevice; + +class CSwapChain final : public ISwapChain +{ +public: + ~CSwapChain() override; + + IDevice* GetDevice() override; + + bool IsValid() const override { return true; } + + bool AcquireNextBackbuffer() override; + + IFramebuffer* GetCurrentBackbuffer( + const AttachmentLoadOp colorAttachmentLoadOp, + const AttachmentStoreOp colorAttachmentStoreOp, + const AttachmentLoadOp depthStencilAttachmentLoadOp, + const AttachmentStoreOp depthStencilAttachmentStoreOp) override; + + void Present() override; + +private: + friend class CDevice; + + static std::unique_ptr Create( + CDevice* device, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync); + + CSwapChain(); + + CDevice* m_Device{nullptr}; + SDL_Window* m_Window{nullptr}; + int m_SurfaceDrawableWidth{0}; + int m_SurfaceDrawableHeight{0}; + + using BackbufferKey = std::tuple< + AttachmentLoadOp, AttachmentStoreOp, + AttachmentLoadOp, AttachmentStoreOp>; + struct BackbufferKeyHash + { + size_t operator()(const BackbufferKey& key) const; + }; + // We use std::unordered_map to avoid storing sizes of Attachment*Op + // enumerations. If it becomes a performance issue we'll replace it + // by an array. + std::unordered_map< + BackbufferKey, std::unique_ptr, BackbufferKeyHash> m_Backbuffers; + bool m_BackbufferAcquired{false}; +}; + +} // namespace GL + +} // namespace Backend + +} // namespace Renderer + +#endif // INCLUDED_RENDERER_BACKEND_GL_SWAPCHAIN diff --git a/source/renderer/backend/vulkan/Device.cpp b/source/renderer/backend/vulkan/Device.cpp index ec26b7309b..200dea1446 100644 --- a/source/renderer/backend/vulkan/Device.cpp +++ b/source/renderer/backend/vulkan/Device.cpp @@ -635,12 +635,6 @@ std::unique_ptr CDevice::Create(SDL_Window* window) device->m_DescriptorManager = std::make_unique(device.get(), useDescriptorIndexing); - device->RecreateSwapChain(); - // Currently we assume that we should have a valid swapchain on the device - // creation. - if (!device->m_SwapChain) - return nullptr; - device->m_Name = choosenDevice.properties.deviceName; device->m_Version = std::to_string(VK_API_VERSION_VARIANT(choosenDevice.properties.apiVersion)) + @@ -677,14 +671,9 @@ CDevice::CDevice() = default; CDevice::~CDevice() { - if (m_Device) - vkDeviceWaitIdle(m_Device); - // The order of destroying does matter to avoid use-after-free and validation // layers complaints. - m_BackbufferReadbackTexture.reset(); - m_SubmitScheduler.reset(); if (m_QueryPool) @@ -695,7 +684,6 @@ CDevice::~CDevice() m_RenderPassManager.reset(); m_SamplerManager.reset(); m_DescriptorManager.reset(); - m_SwapChain.reset(); ProcessObjectToDestroyQueue(true); @@ -744,6 +732,30 @@ void CDevice::Report(const ScriptRequest& rq, JS::HandleValue settings) Script::SetProperty(rq, settings, "validation_layers", m_ValidationLayers); } +std::unique_ptr CDevice::CreateSwapChain( + const char* name, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain) +{ + ENSURE(window == m_Window); + ENSURE(m_Window); + if (window) + SDL_Vulkan_GetDrawableSize(window, &surfaceDrawableWidth, &surfaceDrawableHeight); + return CSwapChain::Create( + this, m_SubmitScheduler.get(), name, m_Surface, surfaceDrawableWidth, surfaceDrawableHeight, + vsync, std::move(oldSwapChain)); +} + +void CDevice::WaitUntilIdle() +{ + vkDeviceWaitIdle(m_Device); + + // Since we know there is no GPU work in progress we can free resources + // queued for deletion. + ProcessDeviceObjectToDestroyQueue(true); + ProcessObjectToDestroyQueue(true); +} + std::unique_ptr CDevice::CreateGraphicsPipelineState( const SGraphicsPipelineStateDesc& pipelineStateDesc) { @@ -813,55 +825,14 @@ std::unique_ptr CDevice::CreateCommandContext() return CDeviceCommandContext::Create(this); } -bool CDevice::AcquireNextBackbuffer() +void CDevice::OnPresent() { - if (!IsSwapChainValid()) - { - RecreateSwapChain(); - if (!IsSwapChainValid()) - return false; - } - - PROFILE3("AcquireNextBackbuffer"); - return m_SubmitScheduler->AcquireNextImage(*m_SwapChain); -} - -IFramebuffer* CDevice::GetCurrentBackbuffer( - const AttachmentLoadOp colorAttachmentLoadOp, - const AttachmentStoreOp colorAttachmentStoreOp, - const AttachmentLoadOp depthStencilAttachmentLoadOp, - const AttachmentStoreOp depthStencilAttachmentStoreOp) -{ - return IsSwapChainValid() ? m_SwapChain->GetCurrentBackbuffer( - colorAttachmentLoadOp, colorAttachmentStoreOp, - depthStencilAttachmentLoadOp, depthStencilAttachmentStoreOp) : nullptr; -} - -void CDevice::Present() -{ - if (!IsSwapChainValid()) - return; - - PROFILE3("Present"); - - m_SubmitScheduler->Present(*m_SwapChain); - ProcessObjectToDestroyQueue(); ProcessDeviceObjectToDestroyQueue(); ++m_FrameID; } -void CDevice::OnWindowResize(const uint32_t width, const uint32_t height) -{ - if (!IsSwapChainValid() || - width != m_SwapChain->GetDepthTexture()->GetWidth() || - height != m_SwapChain->GetDepthTexture()->GetHeight()) - { - RecreateSwapChain(); - } -} - bool CDevice::IsTextureFormatSupported(const Format format) const { switch (format) @@ -1069,38 +1040,6 @@ std::unique_ptr CDevice::CreateRingCommandContext(const siz this, size, m_GraphicsQueueFamilyIndex, *m_SubmitScheduler); } -void CDevice::RecreateSwapChain() -{ - if (m_SwapChain) - { - vkDeviceWaitIdle(m_Device); - - // It seems some drivers might not reuse the same swapchain memory. So - // to avoid higher memory peaks destroy the old swapchain before. - const bool destroyOldSwapchainBefore{ - g_ConfigDB.Get("renderer.backend.vulkan.destroyoldswapchainbefore", false)}; - if (destroyOldSwapchainBefore) - m_SwapChain.reset(); - - m_BackbufferReadbackTexture.reset(); - - // Since we know there is no GPU work in progress we can free resources - // queued for deletion. - ProcessDeviceObjectToDestroyQueue(true); - ProcessObjectToDestroyQueue(true); - } - - int surfaceDrawableWidth = 0, surfaceDrawableHeight = 0; - SDL_Vulkan_GetDrawableSize(m_Window, &surfaceDrawableWidth, &surfaceDrawableHeight); - m_SwapChain = CSwapChain::Create( - this, m_Surface, surfaceDrawableWidth, surfaceDrawableHeight, std::move(m_SwapChain)); -} - -bool CDevice::IsSwapChainValid() -{ - return m_SwapChain && m_SwapChain->IsValid(); -} - void CDevice::ProcessObjectToDestroyQueue(const bool ignoreFrameID) { while (!m_ObjectToDestroyQueue.empty() && @@ -1171,27 +1110,6 @@ void CDevice::ProcessDeviceObjectToDestroyQueue(const bool ignoreFrameID) } } -CTexture* CDevice::GetCurrentBackbufferTexture() -{ - return IsSwapChainValid() ? m_SwapChain->GetCurrentBackbufferTexture() : nullptr; -} - -CTexture* CDevice::GetOrCreateBackbufferReadbackTexture() -{ - if (!IsSwapChainValid()) - return nullptr; - if (!m_BackbufferReadbackTexture) - { - CTexture* currentBackbufferTexture = m_SwapChain->GetCurrentBackbufferTexture(); - m_BackbufferReadbackTexture = CTexture::CreateReadback( - this, "BackbufferReadback", - currentBackbufferTexture->GetFormat(), - currentBackbufferTexture->GetWidth(), - currentBackbufferTexture->GetHeight()); - } - return m_BackbufferReadbackTexture.get(); -} - DeviceObjectUID CDevice::GenerateNextDeviceObjectUID() { ENSURE(m_LastAvailableUID < std::numeric_limits::max()); diff --git a/source/renderer/backend/vulkan/Device.h b/source/renderer/backend/vulkan/Device.h index 404f0b027b..91b4e4a366 100644 --- a/source/renderer/backend/vulkan/Device.h +++ b/source/renderer/backend/vulkan/Device.h @@ -84,6 +84,13 @@ public: std::unique_ptr CreateCommandContext() override; + std::unique_ptr CreateSwapChain( + const char* name, SDL_Window* window, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain) override; + + void WaitUntilIdle() override; + std::unique_ptr CreateGraphicsPipelineState( const SGraphicsPipelineStateDesc& pipelineStateDesc) override; @@ -116,18 +123,6 @@ public: std::unique_ptr CreateShaderProgram( const CStr& name, const CShaderDefines& defines) override; - bool AcquireNextBackbuffer() override; - - IFramebuffer* GetCurrentBackbuffer( - const AttachmentLoadOp colorAttachmentLoadOp, - const AttachmentStoreOp colorAttachmentStoreOp, - const AttachmentLoadOp depthStencilAttachmentLoadOp, - const AttachmentStoreOp depthStencilAttachmentStoreOp) override; - - void Present() override; - - void OnWindowResize(const uint32_t width, const uint32_t height) override; - bool IsTextureFormatSupported(const Format format) const override; bool IsFramebufferFormatSupported(const Format format) const override; @@ -167,6 +162,8 @@ public: void ScheduleBufferToDestroy(const DeviceObjectUID uid); + void OnPresent(); + void SetObjectName(VkObjectType type, const void* handle, const char* name) { SetObjectName(type, reinterpret_cast(handle), name); @@ -184,10 +181,6 @@ public: CDescriptorManager& GetDescriptorManager() { return *m_DescriptorManager; } - CTexture* GetCurrentBackbufferTexture(); - - CTexture* GetOrCreateBackbufferReadbackTexture(); - DeviceObjectUID GenerateNextDeviceObjectUID(); uint32_t GetFrameID() const { return m_FrameID; } @@ -195,8 +188,6 @@ public: private: CDevice(); - void RecreateSwapChain(); - bool IsSwapChainValid(); void ProcessObjectToDestroyQueue(const bool ignoreFrameID = false); void ProcessDeviceObjectToDestroyQueue(const bool ignoreFrameID = false); @@ -235,9 +226,6 @@ private: }; std::vector m_Queries; - std::unique_ptr m_SwapChain; - std::unique_ptr m_BackbufferReadbackTexture; - uint32_t m_FrameID = 0; struct ObjectToDestroy diff --git a/source/renderer/backend/vulkan/DeviceCommandContext.cpp b/source/renderer/backend/vulkan/DeviceCommandContext.cpp index 35f4ee20e1..017dbc5ed7 100644 --- a/source/renderer/backend/vulkan/DeviceCommandContext.cpp +++ b/source/renderer/backend/vulkan/DeviceCommandContext.cpp @@ -41,6 +41,7 @@ #include "renderer/backend/vulkan/PipelineState.h" #include "renderer/backend/vulkan/RingCommandContext.h" #include "renderer/backend/vulkan/ShaderProgram.h" +#include "renderer/backend/vulkan/SwapChain.h" #include "renderer/backend/vulkan/Texture.h" #include "renderer/backend/vulkan/Utilities.h" @@ -677,10 +678,13 @@ void CDeviceCommandContext::EndFramebufferPass() } void CDeviceCommandContext::ReadbackFramebufferSync( - const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height, + ISwapChain& swapChain, const uint32_t x, const uint32_t y, + const uint32_t width, const uint32_t height, void* data) { - CTexture* texture = m_Device->GetCurrentBackbufferTexture(); + CSwapChain* queuedSwapChain{(&swapChain)->As()}; + ENSURE(queuedSwapChain->IsValid()); + CTexture* texture{queuedSwapChain->GetCurrentBackbufferTexture()}; if (!texture) { LOGERROR("Vulkan: backbuffer is unavailable."); @@ -692,6 +696,8 @@ void CDeviceCommandContext::ReadbackFramebufferSync( return; } m_QueuedReadbacks.emplace_back(x, y, width, height, data); + ENSURE(!m_QueuedReadbackSwapChain || m_QueuedReadbackSwapChain == queuedSwapChain); + m_QueuedReadbackSwapChain = queuedSwapChain; } void CDeviceCommandContext::UploadTexture(ITexture* texture, const Format dataFormat, @@ -1056,12 +1062,14 @@ void CDeviceCommandContext::Flush() m_IsPipelineStateDirty = true; - CTexture* backbufferReadbackTexture = m_QueuedReadbacks.empty() - ? nullptr : m_Device->GetOrCreateBackbufferReadbackTexture(); + CTexture* backbufferReadbackTexture{ + !m_QueuedReadbacks.empty() && m_QueuedReadbackSwapChain && m_QueuedReadbackSwapChain->IsValid() + ? m_QueuedReadbackSwapChain->GetOrCreateBackbufferReadbackTexture() + : nullptr}; const bool needsReadback = backbufferReadbackTexture; if (needsReadback) { - CTexture* backbufferTexture = m_Device->GetCurrentBackbufferTexture(); + CTexture* backbufferTexture{m_QueuedReadbackSwapChain->GetCurrentBackbufferTexture()}; { // We assume that the readback texture is in linear tiling. @@ -1139,6 +1147,7 @@ void CDeviceCommandContext::Flush() } m_QueuedReadbacks.clear(); + m_QueuedReadbackSwapChain = nullptr; } void CDeviceCommandContext::PreDraw() diff --git a/source/renderer/backend/vulkan/DeviceCommandContext.h b/source/renderer/backend/vulkan/DeviceCommandContext.h index 745530a4c0..34645fd50a 100644 --- a/source/renderer/backend/vulkan/DeviceCommandContext.h +++ b/source/renderer/backend/vulkan/DeviceCommandContext.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -44,6 +44,7 @@ class CFramebuffer; class CGraphicsPipelineState; class CRingCommandContext; class CShaderProgram; +class CSwapChain; class CVertexInputLayout; class CDeviceCommandContext final : public IDeviceCommandContext @@ -67,7 +68,8 @@ public: void BeginFramebufferPass(IFramebuffer* framebuffer) override; void EndFramebufferPass() override; void ReadbackFramebufferSync( - const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height, + ISwapChain& swapChain, const uint32_t x, const uint32_t y, + const uint32_t width, const uint32_t height, void* data) override; void UploadTexture(ITexture* texture, const Format dataFormat, @@ -218,6 +220,7 @@ private: void* data = nullptr; }; PS::StaticVector m_QueuedReadbacks; + CSwapChain* m_QueuedReadbackSwapChain{nullptr}; bool m_DebugBarrierAfterFramebufferPass = false; }; diff --git a/source/renderer/backend/vulkan/SwapChain.cpp b/source/renderer/backend/vulkan/SwapChain.cpp index ad4374e0f8..555616880d 100644 --- a/source/renderer/backend/vulkan/SwapChain.cpp +++ b/source/renderer/backend/vulkan/SwapChain.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -25,6 +25,7 @@ #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" +#include "ps/Profile.h" #include "renderer/backend/ITexture.h" #include "renderer/backend/Sampler.h" #include "renderer/backend/vulkan/Device.h" @@ -38,6 +39,7 @@ #include #include #include +#include #include namespace Renderer @@ -51,9 +53,18 @@ namespace Vulkan // static std::unique_ptr CSwapChain::Create( - CDevice* device, VkSurfaceKHR surface, int surfaceDrawableWidth, int surfaceDrawableHeight, - std::unique_ptr oldSwapChain) + CDevice* device, CSubmitScheduler* submitScheduler, + const char* name, VkSurfaceKHR surface, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain) { + // It seems some drivers might not reuse the same swapchain memory. So + // to avoid higher memory peaks destroy the old swapchain before. + const bool destroyOldSwapchainBefore{ + g_ConfigDB.Get("renderer.backend.vulkan.destroyoldswapchainbefore", false)}; + if (destroyOldSwapchainBefore) + oldSwapChain.reset(); + VkPhysicalDevice physicalDevice = device->GetChoosenPhysicalDevice().device; VkSurfaceCapabilitiesKHR surfaceCapabilities{}; @@ -103,7 +114,7 @@ std::unique_ptr CSwapChain::Create( { return std::find(presentModes.begin(), presentModes.end(), presentMode) != presentModes.end(); }; - if (g_ConfigDB.Get("vsync", true)) + if (vsync) { // TODO: use the adaptive one when possible. // https://gitlab.freedesktop.org/mesa/mesa/-/issues/5516 @@ -184,17 +195,16 @@ std::unique_ptr CSwapChain::Create( swapChainCreateInfo.presentMode = presentMode; swapChainCreateInfo.clipped = VK_TRUE; if (oldSwapChain) - swapChainCreateInfo.oldSwapchain = oldSwapChain->GetVkSwapchain(); + swapChainCreateInfo.oldSwapchain = oldSwapChain->As()->GetVkSwapchain(); std::unique_ptr swapChain(new CSwapChain()); swapChain->m_Device = device; + swapChain->m_SubmitScheduler = submitScheduler; RETURN_NULLPTR_IF_NOT_VK_SUCCESS(vkCreateSwapchainKHR( device->GetVkDevice(), &swapChainCreateInfo, nullptr, &swapChain->m_SwapChain)); - char nameBuffer[64]; - snprintf(nameBuffer, std::size(nameBuffer), "SwapChain: %dx%d", surfaceDrawableWidth, surfaceDrawableHeight); - device->SetObjectName(VK_OBJECT_TYPE_SWAPCHAIN_KHR, swapChain->m_SwapChain, nameBuffer); + device->SetObjectName(VK_OBJECT_TYPE_SWAPCHAIN_KHR, swapChain->m_SwapChain, name); uint32_t imageCount = 0; VkResult getSwapchainImagesResult = VK_INCOMPLETE; @@ -227,6 +237,7 @@ std::unique_ptr CSwapChain::Create( swapChain->m_Textures.resize(imageCount); swapChain->m_Backbuffers.resize(imageCount); + char nameBuffer[64]; for (size_t index = 0; index < imageCount; ++index) { snprintf(nameBuffer, std::size(nameBuffer), "SwapChainImage #%zu", index); @@ -255,6 +266,28 @@ CSwapChain::~CSwapChain() vkDestroySwapchainKHR(m_Device->GetVkDevice(), m_SwapChain, nullptr); } +IDevice* CSwapChain::GetDevice() +{ + return m_Device; +} + +bool CSwapChain::AcquireNextBackbuffer() +{ + ENSURE(m_IsValid); + + PROFILE3("AcquireNextBackbuffer"); + return m_SubmitScheduler->AcquireNextImage(*this); +} + +void CSwapChain::Present() +{ + ENSURE(m_IsValid); + + PROFILE3("Present"); + m_SubmitScheduler->Present(*this); + m_Device->OnPresent(); +} + size_t CSwapChain::SwapChainBackbuffer::BackbufferKeyHash::operator()(const BackbufferKey& key) const { size_t seed = 0; @@ -273,6 +306,7 @@ CSwapChain::SwapChainBackbuffer& CSwapChain::SwapChainBackbuffer::operator=(Swap bool CSwapChain::AcquireNextImage(VkSemaphore acquireImageSemaphore) { + ENSURE(m_IsValid); ENSURE(m_CurrentImageIndex == std::numeric_limits::max()); const VkResult acquireResult = vkAcquireNextImageKHR( @@ -331,6 +365,7 @@ void CSwapChain::SubmitCommandsBeforePresent( void CSwapChain::Present(VkSemaphore submitDone, VkQueue queue) { + ENSURE(m_IsValid); ENSURE(m_CurrentImageIndex != std::numeric_limits::max()); VkSwapchainKHR swapChains[] = {m_SwapChain}; @@ -358,12 +393,13 @@ void CSwapChain::Present(VkSemaphore submitDone, VkQueue queue) m_CurrentImageIndex = std::numeric_limits::max(); } -CFramebuffer* CSwapChain::GetCurrentBackbuffer( +IFramebuffer* CSwapChain::GetCurrentBackbuffer( const AttachmentLoadOp colorAttachmentLoadOp, const AttachmentStoreOp colorAttachmentStoreOp, const AttachmentLoadOp depthStencilAttachmentLoadOp, const AttachmentStoreOp depthStencilAttachmentStoreOp) { + ENSURE(m_IsValid); ENSURE(m_CurrentImageIndex != std::numeric_limits::max()); SwapChainBackbuffer& swapChainBackbuffer = m_Backbuffers[m_CurrentImageIndex]; @@ -398,6 +434,21 @@ CTexture* CSwapChain::GetCurrentBackbufferTexture() return m_Textures[m_CurrentImageIndex].get(); } +CTexture* CSwapChain::GetOrCreateBackbufferReadbackTexture() +{ + ENSURE(m_IsValid); + if (!m_BackbufferReadbackTexture) + { + CTexture* currentBackbufferTexture{GetCurrentBackbufferTexture()}; + m_BackbufferReadbackTexture = CTexture::CreateReadback( + m_Device, "BackbufferReadback", + currentBackbufferTexture->GetFormat(), + currentBackbufferTexture->GetWidth(), + currentBackbufferTexture->GetHeight()); + } + return m_BackbufferReadbackTexture.get(); +} + } // namespace Vulkan } // namespace Backend diff --git a/source/renderer/backend/vulkan/SwapChain.h b/source/renderer/backend/vulkan/SwapChain.h index e5ab5a2ab0..70b4c5695d 100644 --- a/source/renderer/backend/vulkan/SwapChain.h +++ b/source/renderer/backend/vulkan/SwapChain.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -19,6 +19,7 @@ #define INCLUDED_RENDERER_BACKEND_VULKAN_SWAPCHAIN #include "renderer/backend/IFramebuffer.h" +#include "renderer/backend/ISwapChain.h" #include #include @@ -32,6 +33,7 @@ namespace Renderer::Backend::Vulkan { class CDevice; } namespace Renderer::Backend::Vulkan { class CFramebuffer; } namespace Renderer::Backend::Vulkan { class CRingCommandContext; } +namespace Renderer::Backend::Vulkan { class CSubmitScheduler; } namespace Renderer::Backend::Vulkan { class CTexture; } namespace Renderer @@ -43,15 +45,27 @@ namespace Backend namespace Vulkan { -class CSwapChain final +class CSwapChain final : public ISwapChain { public: - ~CSwapChain(); + ~CSwapChain() override; + + IDevice* GetDevice() override; + + bool IsValid() const override { return m_IsValid; } + + bool AcquireNextBackbuffer() override; + + IFramebuffer* GetCurrentBackbuffer( + const AttachmentLoadOp colorAttachmentLoadOp, + const AttachmentStoreOp colorAttachmentStoreOp, + const AttachmentLoadOp depthStencilAttachmentLoadOp, + const AttachmentStoreOp depthStencilAttachmentStoreOp) override; + + void Present() override; VkSwapchainKHR GetVkSwapchain() { return m_SwapChain; } - bool IsValid() const { return m_IsValid; } - bool AcquireNextImage(VkSemaphore acquireImageSemaphore); void SubmitCommandsAfterAcquireNextImage( CRingCommandContext& commandContext); @@ -59,26 +73,25 @@ public: CRingCommandContext& commandContext); void Present(VkSemaphore submitDone, VkQueue queue); - CFramebuffer* GetCurrentBackbuffer( - const AttachmentLoadOp colorAttachmentLoadOp, - const AttachmentStoreOp colorAttachmentStoreOp, - const AttachmentLoadOp depthStencilAttachmentLoadOp, - const AttachmentStoreOp depthStencilAttachmentStoreOp); - CTexture* GetCurrentBackbufferTexture(); + CTexture* GetOrCreateBackbufferReadbackTexture(); + CTexture* GetDepthTexture() { return m_DepthTexture.get(); } private: friend class CDevice; static std::unique_ptr Create( - CDevice* device, VkSurfaceKHR surface, int surfaceDrawableWidth, int surfaceDrawableHeight, - std::unique_ptr oldSwapChain); + CDevice* device, CSubmitScheduler* submitScheduler, + const char* name, VkSurfaceKHR surface, + int surfaceDrawableWidth, int surfaceDrawableHeight, + const bool vsync, std::unique_ptr oldSwapChain); CSwapChain(); CDevice* m_Device = nullptr; + CSubmitScheduler* m_SubmitScheduler{nullptr}; bool m_IsValid = false; VkSwapchainKHR m_SwapChain = VK_NULL_HANDLE; @@ -111,6 +124,8 @@ private: SwapChainBackbuffer& operator=(SwapChainBackbuffer&& other); }; std::vector m_Backbuffers; + + std::unique_ptr m_BackbufferReadbackTexture; }; } // namespace Vulkan