From 4d83aa28e5156ad9ad64163ff4850402a008e0f1 Mon Sep 17 00:00:00 2001 From: Vladislav Belov Date: Mon, 8 Jun 2026 18:27:06 +0200 Subject: [PATCH] Uses SwapChain instead of direct Device calls --- source/ps/VideoMode.cpp | 72 +++++++++++++++++-- source/ps/VideoMode.h | 18 ++++- source/renderer/Renderer.cpp | 64 ++++++++++------- source/renderer/Renderer.h | 7 +- .../tools/atlas/GameInterface/ActorViewer.cpp | 8 ++- .../Handlers/GraphicsSetupHandlers.cpp | 2 - source/tools/atlas/GameInterface/View.cpp | 8 ++- 7 files changed, 139 insertions(+), 40 deletions(-) diff --git a/source/ps/VideoMode.cpp b/source/ps/VideoMode.cpp index 75fd973861..d676e4adb0 100644 --- a/source/ps/VideoMode.cpp +++ b/source/ps/VideoMode.cpp @@ -42,6 +42,7 @@ #include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include "renderer/backend/IDevice.h" +#include "renderer/backend/ISwapChain.h" #include "renderer/backend/dummy/DeviceForward.h" #include "renderer/backend/gl/DeviceForward.h" #include "renderer/backend/vulkan/DeviceForward.h" @@ -474,9 +475,6 @@ bool CVideoMode::SetVideoMode(int w, int h, int bpp, bool fullscreen) m_Window = nullptr; return SetVideoMode(w, h, bpp, fullscreen); } - - if (isGLBackend) - SDL_GL_SetSwapInterval(m_ConfigVSync ? 1 : 0); } else { @@ -623,6 +621,14 @@ void CVideoMode::Shutdown() m_IsFullscreen = false; m_IsInitialised = false; + + if (m_BackendDevice) + { + // We need to wait before we can destroy the device. + m_BackendDevice->WaitUntilIdle(); + } + + m_SwapChain.reset(); m_BackendDevice.reset(); if (m_Window) { @@ -672,6 +678,14 @@ bool CVideoMode::TryCreateBackendDevice(SDL_Window* window) } break; } + // We don't need to wait because we don't have any swapchain in use. + RecreateSwapChain(); + if (m_BackendDevice && (!m_SwapChain || !m_SwapChain->IsValid())) + { + // Currently we assume that we should have a valid swapchain on the device + // creation. + m_BackendDevice.reset(); + } return static_cast(m_BackendDevice); } @@ -683,6 +697,45 @@ void CVideoMode::DowngradeBackendSettingAfterCreationFailure() m_Backend = fallback; } +void CVideoMode::RecreateSwapChain() +{ + if (m_BackendDevice) + { + char nameBuffer[64]; + snprintf(nameBuffer, std::size(nameBuffer), "SwapChain: %dx%d", g_xres, g_yres); + m_SwapChain = m_BackendDevice->CreateSwapChain( + nameBuffer, m_Window, g_xres, g_yres, m_ConfigVSync, std::move(m_SwapChain)); + } +} + +Renderer::Backend::ISwapChain* CVideoMode::GetOrCreateSwapChain() +{ +#if !OS_MACOSX + const bool vsync{g_ConfigDB.Get("vsync", false)}; + const bool vsyncChanged{m_ConfigVSync != vsync}; + if (vsyncChanged) + m_ConfigVSync = vsync; +#else + // Currently there is a bug in MoltenVK which doesn't allow to recreate + // a swapchain with the same size (it makes it 1x1): + // https://github.com/KhronosGroup/MoltenVK/pull/2722 + const bool vsyncChanged{false}; +#endif + const bool shouldRecreate{ + !m_SwapChain || !m_SwapChain->IsValid() || vsyncChanged}; + if (shouldRecreate) + { + if (m_SwapChain) + { + // We need to wait until we can destroy the swapchain because it + // might be in use. + m_BackendDevice->WaitUntilIdle(); + } + RecreateSwapChain(); + } + return m_SwapChain.get(); +} + bool CVideoMode::ResizeWindow(int w, int h) { ENSURE(m_IsInitialised); @@ -810,8 +863,17 @@ void CVideoMode::UpdateRenderer(int w, int h) SViewPort vp = { 0, 0, w, h }; - if (g_VideoMode.GetBackendDevice()) - g_VideoMode.GetBackendDevice()->OnWindowResize(w, h); + if (g_VideoMode.m_BackendDevice) + { + // TODO: implement freeing window size dependent resources before + // waiting to reduce a memory spike. + + // We need to wait until we can destroy the swapchain because it + // might be in use. + g_VideoMode.m_BackendDevice->WaitUntilIdle(); + + g_VideoMode.RecreateSwapChain(); + } if (CRenderer::IsInitialised()) g_Renderer.Resize(w, h); diff --git a/source/ps/VideoMode.h b/source/ps/VideoMode.h index d33407a038..6a1efeae4a 100644 --- a/source/ps/VideoMode.h +++ b/source/ps/VideoMode.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 @@ -24,6 +24,7 @@ class CStrW; namespace Renderer::Backend { class IDevice; } +namespace Renderer::Backend { class ISwapChain; } typedef struct SDL_Window SDL_Window; @@ -114,6 +115,13 @@ public: Renderer::Backend::IDevice* GetBackendDevice() { return m_BackendDevice.get(); } + /** + * @return A swapchain (may be invalid) if available. It's not allowed to + * call the function during a frame rendering when an old swapchain is + * already in use. + */ + Renderer::Backend::ISwapChain* GetOrCreateSwapChain(); + private: void ReadConfig(); int GetBestBPP(); @@ -122,6 +130,12 @@ private: bool TryCreateBackendDevice(SDL_Window* window); void DowngradeBackendSettingAfterCreationFailure(); + /** + * Immediately recreates a swapchain. It's a caller's responsibility to + * wait till the swapchain isn't in use anymore. + */ + void RecreateSwapChain(); + /** * Remember whether Init has been called. (This isn't used for anything * important, just for verifying that the callers call our methods in @@ -172,6 +186,8 @@ private: Renderer::Backend::Backend m_Backend = Renderer::Backend::Backend::GL; std::unique_ptr m_BackendDevice; + // SwapChain for the corresponding device. + std::unique_ptr m_SwapChain; }; extern CVideoMode g_VideoMode; diff --git a/source/renderer/Renderer.cpp b/source/renderer/Renderer.cpp index 6d66ee364a..a7545c9dae 100644 --- a/source/renderer/Renderer.cpp +++ b/source/renderer/Renderer.cpp @@ -72,6 +72,7 @@ #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/backend/IFramebuffer.h" #include "renderer/backend/IShaderProgram.h" +#include "renderer/backend/ISwapChain.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" @@ -517,29 +518,32 @@ void CRenderer::RenderFrame(const bool needsPresent) } else { - if (needsPresent) - { - // In case of no acquired backbuffer we have nothing render to. - if (!m->device->AcquireNextBackbuffer()) - return; - } + Renderer::Backend::ISwapChain* swapChain{g_VideoMode.GetOrCreateSwapChain()}; + if (!swapChain || !swapChain->IsValid()) + return; + + // In case of no acquired backbuffer we have nothing render to. + if (needsPresent && !swapChain->AcquireNextBackbuffer()) + return; if (m_ShouldPreloadResourcesBeforeNextFrame) { m_ShouldPreloadResourcesBeforeNextFrame = false; // We don't need to render logger for the preload. - RenderFrameImpl(true, false); + RenderFrameImpl(*swapChain, true, false); } - RenderFrameImpl(true, true); + RenderFrameImpl(*swapChain, true, true); m->deviceCommandContext->Flush(); if (needsPresent) - m->device->Present(); + swapChain->Present(); } } -void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger) +void CRenderer::RenderFrameImpl( + Renderer::Backend::ISwapChain& swapChain, + const bool renderGUI, const bool renderLogger) { PROFILE3("render"); @@ -585,7 +589,7 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger) // We don't need to clear the color attachment of the framebuffer as the sky // is going to be rendered anyway. framebuffer = - m->deviceCommandContext->GetDevice()->GetCurrentBackbuffer( + swapChain.GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::DONT_CARE, Renderer::Backend::AttachmentStoreOp::STORE, Renderer::Backend::AttachmentLoadOp::CLEAR, @@ -610,7 +614,7 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger) postprocManager.ApplyPostproc(m->deviceCommandContext.get()); Renderer::Backend::IFramebuffer* backbuffer = - m->deviceCommandContext->GetDevice()->GetCurrentBackbuffer( + swapChain.GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::LOAD, Renderer::Backend::AttachmentStoreOp::STORE, Renderer::Backend::AttachmentLoadOp::LOAD, @@ -642,7 +646,7 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger) ? Renderer::Backend::AttachmentLoadOp::CLEAR : Renderer::Backend::AttachmentLoadOp::DONT_CARE; Renderer::Backend::IFramebuffer* backbuffer = - m->deviceCommandContext->GetDevice()->GetCurrentBackbuffer( + swapChain.GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::DONT_CARE, Renderer::Backend::AttachmentStoreOp::STORE, depthStencilLoadOp, @@ -734,12 +738,6 @@ void CRenderer::RenderScreenShot(const bool needsPresent) const size_t width = static_cast(g_xres), height = static_cast(g_yres); const size_t bpp = 24; - if (needsPresent && !m->device->AcquireNextBackbuffer()) - return; - - // Hide log messages and re-render - RenderFrameImpl(true, false); - const size_t img_size = width * height * bpp / 8; const size_t hdr_size = tex_hdr_size(filename); std::shared_ptr buf; @@ -749,10 +747,20 @@ void CRenderer::RenderScreenShot(const bool needsPresent) if (t.wrap(width, height, bpp, TEX_BOTTOM_UP, buf, hdr_size) < 0) return; - m->deviceCommandContext->ReadbackFramebufferSync(0, 0, width, height, img); + Renderer::Backend::ISwapChain* swapChain{g_VideoMode.GetOrCreateSwapChain()}; + if (!swapChain || !swapChain->IsValid()) + return; + + if (needsPresent && !swapChain->AcquireNextBackbuffer()) + return; + + // Hide log messages and re-render + RenderFrameImpl(*swapChain, false, false); + + m->deviceCommandContext->ReadbackFramebufferSync(*swapChain, 0, 0, width, height, img); m->deviceCommandContext->Flush(); if (needsPresent) - m->device->Present(); + swapChain->Present(); if (tex_write(&t, filename) == INFO::OK) { @@ -850,15 +858,19 @@ void CRenderer::RenderBigScreenShot(const bool needsPresent) } g_Game->GetView()->GetCamera()->SetProjection(projection); - if (!needsPresent || m->device->AcquireNextBackbuffer()) - { - RenderFrameImpl(false, false); + Renderer::Backend::ISwapChain* swapChain{g_VideoMode.GetOrCreateSwapChain()}; + if (!swapChain || !swapChain->IsValid()) + continue; - m->deviceCommandContext->ReadbackFramebufferSync(0, 0, tileWidth, tileHeight, tileData); + if (!needsPresent || swapChain->AcquireNextBackbuffer()) + { + RenderFrameImpl(*swapChain, false, false); + + m->deviceCommandContext->ReadbackFramebufferSync(*swapChain, 0, 0, tileWidth, tileHeight, tileData); m->deviceCommandContext->Flush(); if (needsPresent) - m->device->Present(); + swapChain->Present(); } // Copy the tile pixels into the main image diff --git a/source/renderer/Renderer.h b/source/renderer/Renderer.h index f218c0f8e7..e4a70b6ba4 100644 --- a/source/renderer/Renderer.h +++ b/source/renderer/Renderer.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 @@ -36,6 +36,7 @@ class CVertexBufferManager; namespace PS::Memory { class LinearAllocator; } namespace Renderer::Backend { class IDevice; } namespace Renderer::Backend { class IDeviceCommandContext; } +namespace Renderer::Backend { class ISwapChain; } namespace Renderer::Backend { class IVertexInputLayout; } namespace Renderer::Backend { struct SVertexAttributeFormat; } @@ -161,7 +162,9 @@ protected: bool ShouldRender() const; - void RenderFrameImpl(const bool renderGUI, const bool renderLogger); + void RenderFrameImpl( + Renderer::Backend::ISwapChain& swapChain, + const bool renderGUI, const bool renderLogger); void RenderFrame2D(const bool renderGUI, const bool renderLogger); void RenderScreenShot(const bool needsPresent); void RenderBigScreenShot(const bool needsPresent); diff --git a/source/tools/atlas/GameInterface/ActorViewer.cpp b/source/tools/atlas/GameInterface/ActorViewer.cpp index 51edd83e4a..eced3819b9 100644 --- a/source/tools/atlas/GameInterface/ActorViewer.cpp +++ b/source/tools/atlas/GameInterface/ActorViewer.cpp @@ -60,6 +60,7 @@ #include "renderer/backend/IDevice.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/backend/IFramebuffer.h" +#include "renderer/backend/ISwapChain.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpAttack.h" @@ -528,6 +529,11 @@ void ActorViewer::Render() { // TODO: ActorViewer should reuse CRenderer code and not duplicate it. + Renderer::Backend::ISwapChain* swapChain{ + g_VideoMode.GetOrCreateSwapChain()}; + if (!swapChain || !swapChain->IsValid()) + return; + CSceneRenderer& sceneRenderer = g_Renderer.GetSceneRenderer(); // Set simulation context for rendering purposes @@ -557,7 +563,7 @@ void ActorViewer::Render() sceneRenderer.PrepareScene(deviceCommandContext, m); Renderer::Backend::IFramebuffer* backbuffer = - deviceCommandContext->GetDevice()->GetCurrentBackbuffer( + swapChain->GetCurrentBackbuffer( Renderer::Backend::AttachmentLoadOp::DONT_CARE, Renderer::Backend::AttachmentStoreOp::STORE, Renderer::Backend::AttachmentLoadOp::CLEAR, diff --git a/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp index 2184e34a71..fb2c9b67c2 100644 --- a/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp @@ -130,8 +130,6 @@ MESSAGEHANDLER(InitGraphics) { g_VideoMode.CreateBackendDevice(false); - g_VideoMode.GetBackendDevice()->OnWindowResize(g_xres, g_yres); - g_ScriptInterface.emplace("Engine", "GUIManager", *g_ScriptContext); InitGraphics(g_AtlasGameLoop->args, g_InitFlags, {}, *g_ScriptContext, *g_ScriptInterface); } diff --git a/source/tools/atlas/GameInterface/View.cpp b/source/tools/atlas/GameInterface/View.cpp index dca1ae1781..3b4b4ecb75 100644 --- a/source/tools/atlas/GameInterface/View.cpp +++ b/source/tools/atlas/GameInterface/View.cpp @@ -37,6 +37,7 @@ #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/backend/IDevice.h" +#include "renderer/backend/ISwapChain.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpParticleManager.h" #include "simulation2/components/ICmpPathfinder.h" @@ -228,7 +229,8 @@ void AtlasViewGame::Update(float realFrameLength) void AtlasViewGame::Render() { - if (!g_VideoMode.GetBackendDevice()->AcquireNextBackbuffer()) + Renderer::Backend::ISwapChain* swapChain{g_VideoMode.GetOrCreateSwapChain()}; + if (!swapChain || !swapChain->IsValid() || !swapChain->AcquireNextBackbuffer()) return; SViewPort vp = { 0, 0, g_xres, g_yres }; @@ -239,9 +241,9 @@ void AtlasViewGame::Render() g_Renderer.RenderFrame(false); Atlas_GLSwapBuffers((void*)g_AtlasGameLoop->glCanvas); - // In case of atlas the device's present will do only internal stuff + // In case of atlas the swapchain's present will do only internal stuff // without calling a real backbuffer swap. - g_VideoMode.GetBackendDevice()->Present(); + swapChain->Present(); } void AtlasViewGame::DrawCinemaPathTool(Renderer::Backend::IDeviceCommandContext& deviceCommandContext)