Adds SwapChain as an explicit DeviceObject

It allows us more flexibility on how we control swapchain. That includes
toggling V-Sync in real time.
This commit is contained in:
Vladislav Belov 2026-06-08 18:26:58 +02:00
parent dab6e1a37a
commit cf4a4d8fd5
No known key found for this signature in database
GPG key ID: 353545E45DB9CCB3
22 changed files with 701 additions and 360 deletions

View file

@ -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<IDeviceCommandContext> 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<ISwapChain> CreateSwapChain(
const char* name, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight, const bool vsync,
std::unique_ptr<ISwapChain> 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<IShaderProgram> 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;

View file

@ -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,

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_RENDERER_BACKEND_ISWAPCHAIN
#define INCLUDED_RENDERER_BACKEND_ISWAPCHAIN
#include "renderer/backend/IDeviceObject.h"
#include "renderer/backend/IFramebuffer.h"
#include <cstdint>
namespace Renderer
{
namespace Backend
{
class ISwapChain : public IDeviceObject<ISwapChain>
{
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

View file

@ -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<IDeviceCommandContext> CDevice::CreateCommandContext()
return CDeviceCommandContext::Create(this);
}
std::unique_ptr<ISwapChain> CDevice::CreateSwapChain(
const char*, SDL_Window*, int, int,
const bool, std::unique_ptr<ISwapChain> oldSwapChain)
{
oldSwapChain.reset();
return CSwapChain::Create(this);
}
void CDevice::WaitUntilIdle()
{
}
std::unique_ptr<IGraphicsPipelineState> CDevice::CreateGraphicsPipelineState(
const SGraphicsPipelineStateDesc& pipelineStateDesc)
{
@ -129,28 +140,6 @@ std::unique_ptr<IShaderProgram> 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;

View file

@ -59,6 +59,13 @@ public:
std::unique_ptr<IDeviceCommandContext> CreateCommandContext() override;
std::unique_ptr<ISwapChain> CreateSwapChain(
const char* name, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> oldSwapChain) override;
void WaitUntilIdle() override;
std::unique_ptr<IGraphicsPipelineState> CreateGraphicsPipelineState(
const SGraphicsPipelineStateDesc& pipelineStateDesc) override;
@ -88,16 +95,6 @@ public:
std::unique_ptr<IShaderProgram> 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<std::string> m_Extensions;
std::unique_ptr<IFramebuffer> m_Backbuffer;
Capabilities m_Capabilities{};
};

View file

@ -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*)
{
}

View file

@ -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,

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<ISwapChain> CSwapChain::Create(CDevice* device)
{
std::unique_ptr<CSwapChain> 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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_RENDERER_BACKEND_DUMMY_SWAPCHAIN
#define INCLUDED_RENDERER_BACKEND_DUMMY_SWAPCHAIN
#include "renderer/backend/ISwapChain.h"
#include <cstdint>
#include <memory>
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<ISwapChain> Create(CDevice* device);
CSwapChain();
CDevice* m_Device{nullptr};
std::unique_ptr<IFramebuffer> m_Backbuffer;
};
} // namespace Dummy
} // namespace Backend
} // namespace Renderer
#endif // INCLUDED_RENDERER_BACKEND_DUMMY_SWAPCHAIN

View file

@ -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<IDevice> 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<IDeviceCommandContext> CDevice::CreateCommandContext()
return commandContet;
}
std::unique_ptr<ISwapChain> CDevice::CreateSwapChain(
[[maybe_unused]] const char* name, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> 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<IGraphicsPipelineState> CDevice::CreateGraphicsPipelineState(
const SGraphicsPipelineStateDesc& pipelineStateDesc)
{
@ -857,74 +877,6 @@ std::unique_ptr<IShaderProgram> 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<uint32_t>(m_Backbuffers.size()));
statistics.emplace_back("Query count", "", static_cast<uint32_t>(m_Queries.size()));
}
std::unique_ptr<IDevice> CreateDevice(SDL_Window* window)

View file

@ -73,6 +73,13 @@ public:
std::unique_ptr<IDeviceCommandContext> CreateCommandContext() override;
std::unique_ptr<ISwapChain> CreateSwapChain(
const char* name, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> oldSwapChain) override;
void WaitUntilIdle() override;
std::unique_ptr<IGraphicsPipelineState> CreateGraphicsPipelineState(
const SGraphicsPipelineStateDesc& pipelineStateDesc) override;
@ -104,18 +111,6 @@ public:
std::unique_ptr<IShaderProgram> 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<CFramebuffer>, BackbufferKeyHash> m_Backbuffers;
bool m_BackbufferAcquired = false;
bool m_UseFramebufferInvalidating = false;
struct Query

View file

@ -227,11 +227,6 @@ void InvalidateFramebuffer(
std::unique_ptr<CDeviceCommandContext> CDeviceCommandContext::Create(CDevice* device)
{
std::unique_ptr<CDeviceCommandContext> 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<CFramebuffer>();
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<CFramebuffer>();
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<CFramebuffer>();
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();
}

View file

@ -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,

View file

@ -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<CFramebuffer> Create(
CDevice* device, const char* name, SColorAttachment* colorAttachment,

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <SDL_video.h>
namespace Renderer
{
namespace Backend
{
namespace GL
{
namespace
{
} // anonymous namespace
// static
std::unique_ptr<CSwapChain> CSwapChain::Create(
CDevice* device, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync)
{
std::unique_ptr<CSwapChain> 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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <cstdint>
#include <memory>
#include <unordered_map>
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<CSwapChain> 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<CFramebuffer>, BackbufferKeyHash> m_Backbuffers;
bool m_BackbufferAcquired{false};
};
} // namespace GL
} // namespace Backend
} // namespace Renderer
#endif // INCLUDED_RENDERER_BACKEND_GL_SWAPCHAIN

View file

@ -635,12 +635,6 @@ std::unique_ptr<CDevice> CDevice::Create(SDL_Window* window)
device->m_DescriptorManager =
std::make_unique<CDescriptorManager>(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<ISwapChain> CDevice::CreateSwapChain(
const char* name, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> 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<IGraphicsPipelineState> CDevice::CreateGraphicsPipelineState(
const SGraphicsPipelineStateDesc& pipelineStateDesc)
{
@ -813,55 +825,14 @@ std::unique_ptr<IDeviceCommandContext> 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<CRingCommandContext> 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<DeviceObjectUID>::max());

View file

@ -84,6 +84,13 @@ public:
std::unique_ptr<IDeviceCommandContext> CreateCommandContext() override;
std::unique_ptr<ISwapChain> CreateSwapChain(
const char* name, SDL_Window* window,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> oldSwapChain) override;
void WaitUntilIdle() override;
std::unique_ptr<IGraphicsPipelineState> CreateGraphicsPipelineState(
const SGraphicsPipelineStateDesc& pipelineStateDesc) override;
@ -116,18 +123,6 @@ public:
std::unique_ptr<IShaderProgram> 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<uint64_t>(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<Query> m_Queries;
std::unique_ptr<CSwapChain> m_SwapChain;
std::unique_ptr<CTexture> m_BackbufferReadbackTexture;
uint32_t m_FrameID = 0;
struct ObjectToDestroy

View file

@ -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<CSwapChain>()};
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()

View file

@ -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<QueuedReadback, 2> m_QueuedReadbacks;
CSwapChain* m_QueuedReadbackSwapChain{nullptr};
bool m_DebugBarrierAfterFramebufferPass = false;
};

View file

@ -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 <cstdio>
#include <iterator>
#include <limits>
#include <SDL_video.h>
#include <utility>
namespace Renderer
@ -51,9 +53,18 @@ namespace Vulkan
// static
std::unique_ptr<CSwapChain> CSwapChain::Create(
CDevice* device, VkSurfaceKHR surface, int surfaceDrawableWidth, int surfaceDrawableHeight,
std::unique_ptr<CSwapChain> oldSwapChain)
CDevice* device, CSubmitScheduler* submitScheduler,
const char* name, VkSurfaceKHR surface,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> 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> 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> CSwapChain::Create(
swapChainCreateInfo.presentMode = presentMode;
swapChainCreateInfo.clipped = VK_TRUE;
if (oldSwapChain)
swapChainCreateInfo.oldSwapchain = oldSwapChain->GetVkSwapchain();
swapChainCreateInfo.oldSwapchain = oldSwapChain->As<CSwapChain>()->GetVkSwapchain();
std::unique_ptr<CSwapChain> 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> 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<uint32_t>::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<uint32_t>::max());
VkSwapchainKHR swapChains[] = {m_SwapChain};
@ -358,12 +393,13 @@ void CSwapChain::Present(VkSemaphore submitDone, VkQueue queue)
m_CurrentImageIndex = std::numeric_limits<uint32_t>::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<uint32_t>::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

View file

@ -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 <cstddef>
#include <cstdint>
@ -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<CSwapChain> Create(
CDevice* device, VkSurfaceKHR surface, int surfaceDrawableWidth, int surfaceDrawableHeight,
std::unique_ptr<CSwapChain> oldSwapChain);
CDevice* device, CSubmitScheduler* submitScheduler,
const char* name, VkSurfaceKHR surface,
int surfaceDrawableWidth, int surfaceDrawableHeight,
const bool vsync, std::unique_ptr<ISwapChain> 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<SwapChainBackbuffer> m_Backbuffers;
std::unique_ptr<CTexture> m_BackbufferReadbackTexture;
};
} // namespace Vulkan