From 2fcb2fd64cb6af0a883f276407a4e5998363e6e6 Mon Sep 17 00:00:00 2001 From: Romain BOULLARD Date: Sun, 13 Sep 2026 17:55:54 +0200 Subject: [PATCH] SwapChain --- .../Graphics/GraphicsCore/GraphicsContext.cpp | 14 ++ .../Include/GraphicsCore/GraphicsContext.hpp | 20 ++ .../GraphicsInterface/GraphicsSwapchain.hpp | 89 +++++++ .../GraphicsVulkan/VulkanSwapchain.hpp | 88 +++++++ .../GraphicsVulkan/VulkanSwapchain.cpp | 235 ++++++++++++++++++ .../Graphics/GraphicsCore/GraphicsContext.cpp | 9 +- .../GraphicsVulkan/VulkanSwapchain.cpp | 76 ++++++ 7 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 Bigfoot/Libs/Graphics/GraphicsInterface/Include/GraphicsInterface/GraphicsSwapchain.hpp create mode 100644 Bigfoot/Libs/Graphics/GraphicsVulkan/Include/GraphicsVulkan/VulkanSwapchain.hpp create mode 100644 Bigfoot/Libs/Graphics/GraphicsVulkan/VulkanSwapchain.cpp create mode 100644 Bigfoot/Tests/Graphics/GraphicsVulkan/VulkanSwapchain.cpp diff --git a/Bigfoot/Libs/Graphics/GraphicsCore/GraphicsContext.cpp b/Bigfoot/Libs/Graphics/GraphicsCore/GraphicsContext.cpp index ed634af..9a73ff8 100644 --- a/Bigfoot/Libs/Graphics/GraphicsCore/GraphicsContext.cpp +++ b/Bigfoot/Libs/Graphics/GraphicsCore/GraphicsContext.cpp @@ -32,6 +32,13 @@ GraphicsContext::GraphicsContext(const GraphicsContextCreateInfo& p_createInfo) m_device != nullptr, "No graphics device registered for GraphicsAPI '{}'", EnumNameGraphicsAPI(p_createInfo.m_api)); + + const SwapchainCreateInfo swapchainCreateInfo {m_device.get(), m_surface.get(), p_createInfo.m_extent}; + m_swapchain = g_graphicsSwapchainFactory->Create(p_createInfo.m_api, swapchainCreateInfo); + CRITICAL_ASSERT(GraphicsCoreAssertHandler, + m_swapchain != nullptr, + "No graphics swapchain registered for GraphicsAPI '{}'", + EnumNameGraphicsAPI(p_createInfo.m_api)); } /****************************************************************************************/ @@ -54,4 +61,11 @@ GraphicsDevice& GraphicsContext::GetDevice() const { return *m_device; } + +/****************************************************************************************/ + +GraphicsSwapchain& GraphicsContext::GetSwapchain() const +{ + return *m_swapchain; +} } // namespace Bigfoot diff --git a/Bigfoot/Libs/Graphics/GraphicsCore/Include/GraphicsCore/GraphicsContext.hpp b/Bigfoot/Libs/Graphics/GraphicsCore/Include/GraphicsCore/GraphicsContext.hpp index 861f33d..d8b39e2 100644 --- a/Bigfoot/Libs/Graphics/GraphicsCore/Include/GraphicsCore/GraphicsContext.hpp +++ b/Bigfoot/Libs/Graphics/GraphicsCore/Include/GraphicsCore/GraphicsContext.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -16,6 +17,7 @@ #include #include +#include namespace Bigfoot { @@ -45,6 +47,11 @@ struct GraphicsContextCreateInfo * Pick a specific physical device by name. */ eastl::string_view m_deviceNameHint; + + /** + * The desired size of the swapchain images. + */ + glm::uvec2 m_extent = {}; }; class GraphicsContext @@ -87,6 +94,14 @@ class GraphicsContext [[nodiscard]] GraphicsDevice& GetDevice() const; + /** + * Get the graphics swapchain + * + * \return The graphics swapchain + */ + [[nodiscard]] + GraphicsSwapchain& GetSwapchain() const; + private: /** * The graphics instance. @@ -102,6 +117,11 @@ class GraphicsContext * The graphics device. */ eastl::unique_ptr m_device; + + /** + * The graphics swapchain. + */ + eastl::unique_ptr m_swapchain; }; } // namespace Bigfoot diff --git a/Bigfoot/Libs/Graphics/GraphicsInterface/Include/GraphicsInterface/GraphicsSwapchain.hpp b/Bigfoot/Libs/Graphics/GraphicsInterface/Include/GraphicsInterface/GraphicsSwapchain.hpp new file mode 100644 index 0000000..32ce2f3 --- /dev/null +++ b/Bigfoot/Libs/Graphics/GraphicsInterface/Include/GraphicsInterface/GraphicsSwapchain.hpp @@ -0,0 +1,89 @@ +/********************************************************************* + * \file GraphicsSwapchain.hpp + * + * \author Romain BOULLARD + * \date September 2026 + *********************************************************************/ +#ifndef BIGFOOT_GRAPHICSINTERFACE_GRAPHICSSWAPCHAIN_HPP +#define BIGFOOT_GRAPHICSINTERFACE_GRAPHICSSWAPCHAIN_HPP +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include + +namespace Bigfoot +{ +struct SwapchainCreateInfo +{ + /** + * The device to create the swapchain from. Must be able to present to m_surface. + */ + const GraphicsDevice* m_device = nullptr; + + /** + * The surface the swapchain will present to. + */ + const GraphicsSurface* m_surface = nullptr; + + /** + * The desired size of the swapchain images. + */ + glm::uvec2 m_extent = {}; +}; + +class GraphicsSwapchain +{ + public: + virtual ~GraphicsSwapchain() = default; + + /** + * Get the number of images owned by the swapchain + * + * \return The image count + */ + [[nodiscard]] + virtual std::uint32_t GetImageCount() const = 0; + + /** + * Get one of the images owned by the swapchain + * + * \param p_index The index of the image to get. + * \return The image + */ + [[nodiscard]] + virtual const GraphicsImage& GetImage(std::uint32_t p_index) const = 0; + + GraphicsSwapchain(const GraphicsSwapchain& p_swapchain) = delete; + GraphicsSwapchain(GraphicsSwapchain&& p_swapchain) = delete; + + GraphicsSwapchain& operator=(const GraphicsSwapchain& p_swapchain) = delete; + GraphicsSwapchain& operator=(GraphicsSwapchain&& p_swapchain) = delete; + + protected: + GraphicsSwapchain() = default; +}; + +/****************************************************************************************/ + +using GraphicsSwapchainFactory = + Factory(const SwapchainCreateInfo&)>>; + +/****************************************************************************************/ + +inline LazyGlobal g_graphicsSwapchainFactory; +} // namespace Bigfoot + +#endif diff --git a/Bigfoot/Libs/Graphics/GraphicsVulkan/Include/GraphicsVulkan/VulkanSwapchain.hpp b/Bigfoot/Libs/Graphics/GraphicsVulkan/Include/GraphicsVulkan/VulkanSwapchain.hpp new file mode 100644 index 0000000..b270092 --- /dev/null +++ b/Bigfoot/Libs/Graphics/GraphicsVulkan/Include/GraphicsVulkan/VulkanSwapchain.hpp @@ -0,0 +1,88 @@ +/********************************************************************* + * \file VulkanSwapchain.hpp + * + * \author Romain BOULLARD + * \date September 2026 + *********************************************************************/ +#ifndef BIGFOOT_GRAPHICSVULKAN_VULKANSWAPCHAIN_HPP +#define BIGFOOT_GRAPHICSVULKAN_VULKANSWAPCHAIN_HPP +#include +#include +#include +#include + +#include + +#include +#include + +#include + +namespace Bigfoot::Vulkan +{ +class VulkanSwapchain final: public GraphicsSwapchain +{ + public: + /** + * Constructor. + * + * \param p_device The device to create the swapchain from. + * \param p_surface The surface the swapchain will present to. + * \param p_extent The desired size of the swapchain images. + */ + VulkanSwapchain(const VulkanDevice& p_device, const VulkanSurface& p_surface, glm::uvec2 p_extent); + + ~VulkanSwapchain() override; + + /** + * Get the number of images owned by the swapchain + * + * \return The image count + */ + [[nodiscard]] + std::uint32_t GetImageCount() const override; + + /** + * Get one of the images owned by the swapchain + * + * \param p_index The index of the image to get. Must be less than GetImageCount() + * \return The image + */ + [[nodiscard]] + const GraphicsImage& GetImage(std::uint32_t p_index) const override; + + /** + * Get the underlying Vulkan swapchain handle + * + * \return The vk::SwapchainKHR handle + */ + [[nodiscard]] + vk::SwapchainKHR GetHandle() const; + + /** + * Get the swapchain images + * + * \return The images owned by the swapchain + */ + [[nodiscard]] + eastl::span GetImages() const; + + private: + /** + * The device the swapchain was created from + */ + const VulkanDevice& m_device; + + /** + * The swapchain + */ + vk::SwapchainKHR m_swapchain; + + /** + * The images owned by the swapchain + */ + eastl::vector m_images; +}; +} // namespace Bigfoot::Vulkan + +#endif diff --git a/Bigfoot/Libs/Graphics/GraphicsVulkan/VulkanSwapchain.cpp b/Bigfoot/Libs/Graphics/GraphicsVulkan/VulkanSwapchain.cpp new file mode 100644 index 0000000..6bc567b --- /dev/null +++ b/Bigfoot/Libs/Graphics/GraphicsVulkan/VulkanSwapchain.cpp @@ -0,0 +1,235 @@ +/********************************************************************* + * \file VulkanSwapchain.cpp + * + * \author Romain BOULLARD + * \date September 2026 + *********************************************************************/ +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace Bigfoot::Vulkan +{ +[[nodiscard]] +vk::SurfaceFormatKHR SelectSurfaceFormat(const std::span p_formats) +{ + const auto it = eastl::find_if(p_formats.begin(), + p_formats.end(), + [](const vk::SurfaceFormatKHR& p_format) + { + return p_format.format == vk::Format::eB8G8R8A8Srgb && + p_format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; + }); + + return it != p_formats.end() ? *it : p_formats.front(); +} + +/****************************************************************************************/ + +[[nodiscard]] +vk::PresentModeKHR SelectPresentMode(const std::span p_presentModes) +{ + const auto it = eastl::find(p_presentModes.begin(), p_presentModes.end(), vk::PresentModeKHR::eMailbox); + + // eFifo is guaranteed by the spec to always be supported + return it != p_presentModes.end() ? *it : vk::PresentModeKHR::eFifo; +} + +/****************************************************************************************/ + +[[nodiscard]] +vk::Extent2D SelectExtent(const vk::SurfaceCapabilitiesKHR& p_capabilities, const glm::uvec2 p_extent) +{ + constexpr std::uint32_t undefinedExtent = std::numeric_limits::max(); + if (p_capabilities.currentExtent.width != undefinedExtent) + { + return p_capabilities.currentExtent; + } + + return {eastl::clamp(p_extent.x, p_capabilities.minImageExtent.width, p_capabilities.maxImageExtent.width), + eastl::clamp(p_extent.y, p_capabilities.minImageExtent.height, p_capabilities.maxImageExtent.height)}; +} + +/****************************************************************************************/ + +[[nodiscard]] +vk::CompositeAlphaFlagBitsKHR SelectCompositeAlpha(const vk::CompositeAlphaFlagsKHR p_supportedCompositeAlpha) +{ + for (const vk::CompositeAlphaFlagBitsKHR candidate: {vk::CompositeAlphaFlagBitsKHR::eOpaque, + vk::CompositeAlphaFlagBitsKHR::ePreMultiplied, + vk::CompositeAlphaFlagBitsKHR::ePostMultiplied, + vk::CompositeAlphaFlagBitsKHR::eInherit}) + { + if (p_supportedCompositeAlpha & candidate) + { + return candidate; + } + } + + return vk::CompositeAlphaFlagBitsKHR::eOpaque; +} + +/****************************************************************************************/ + +VulkanSwapchain::VulkanSwapchain(const VulkanDevice& p_device, + const VulkanSurface& p_surface, + const glm::uvec2 p_extent): + m_device(p_device) +{ + const vk::PhysicalDevice physicalDevice = p_device.GetPhysicalDeviceHandle(); + const vk::SurfaceKHR surface = p_surface.GetHandle(); + + const vk::ResultValue capabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + capabilities.result == vk::Result::eSuccess, + "({}) Failed to get Vulkan surface capabilities", + capabilities.result); + + const vk::ResultValue> formats = physicalDevice.getSurfaceFormatsKHR(surface); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + formats.result == vk::Result::eSuccess && !formats.value.empty(), + "({}) Failed to get Vulkan surface formats", + formats.result); + + const vk::ResultValue> presentModes = physicalDevice.getSurfacePresentModesKHR( + surface); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + presentModes.result == vk::Result::eSuccess && !presentModes.value.empty(), + "({}) Failed to get Vulkan surface present modes", + presentModes.result); + + const vk::SurfaceFormatKHR surfaceFormat = SelectSurfaceFormat(formats.value); + const vk::PresentModeKHR presentMode = SelectPresentMode(presentModes.value); + const vk::Extent2D extent = SelectExtent(capabilities.value, p_extent); + + std::uint32_t imageCount = capabilities.value.minImageCount + 1; + if (capabilities.value.maxImageCount > 0) + { + imageCount = eastl::min(imageCount, capabilities.value.maxImageCount); + } + + const auto* const graphicsQueue = static_cast(p_device.GetQueue(GraphicsQueueType::Graphics)); + const auto* const presentQueue = static_cast(p_device.GetQueue(GraphicsQueueType::Present)); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + graphicsQueue != nullptr && presentQueue != nullptr, + "Cannot create a swapchain on a device with no graphics or present queue"); + + const bool sameQueueFamily = graphicsQueue->GetFamilyIndex() == presentQueue->GetFamilyIndex(); + + eastl::vector queueFamilyIndices; + if (!sameQueueFamily) + { + queueFamilyIndices = {graphicsQueue->GetFamilyIndex(), presentQueue->GetFamilyIndex()}; + } + + const vk::SwapchainCreateInfoKHR swapchainCreateInfo { + {}, + surface, + imageCount, + surfaceFormat.format, + surfaceFormat.colorSpace, + extent, + 1, + vk::ImageUsageFlagBits::eColorAttachment, + sameQueueFamily ? vk::SharingMode::eExclusive : vk::SharingMode::eConcurrent, + queueFamilyIndices, + capabilities.value.currentTransform, + SelectCompositeAlpha(capabilities.value.supportedCompositeAlpha), + presentMode, + VK_TRUE}; + + const vk::ResultValue swapchain = p_device.GetHandle().createSwapchainKHR(swapchainCreateInfo); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + swapchain.result == vk::Result::eSuccess, + "({}) Failed to create Vulkan swapchain", + swapchain.result); + m_swapchain = swapchain.value; + + const vk::ResultValue> images = p_device.GetHandle().getSwapchainImagesKHR(m_swapchain); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + images.result == vk::Result::eSuccess, + "({}) Failed to get Vulkan swapchain images", + images.result); + const PixelFormat imageFormat = FromVulkanFormat(surfaceFormat.format); + m_images.reserve(images.value.size()); + for (const vk::Image image: images.value) + { + m_images.emplace_back(image, imageFormat, extent); + } +} + +/****************************************************************************************/ + +VulkanSwapchain::~VulkanSwapchain() +{ + m_device.GetHandle().destroySwapchainKHR(m_swapchain); +} + +/****************************************************************************************/ + +std::uint32_t VulkanSwapchain::GetImageCount() const +{ + return static_cast(m_images.size()); +} + +/****************************************************************************************/ + +const GraphicsImage& VulkanSwapchain::GetImage(const std::uint32_t p_index) const +{ + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, + p_index < m_images.size(), + "Swapchain image index {} out of range (swapchain has {} images)", + p_index, + m_images.size()); + return m_images[p_index]; +} + +/****************************************************************************************/ + +vk::SwapchainKHR VulkanSwapchain::GetHandle() const +{ + return m_swapchain; +} + +/****************************************************************************************/ + +eastl::span VulkanSwapchain::GetImages() const +{ + return {m_images.data(), m_images.size()}; +} + +/****************************************************************************************/ + +[[nodiscard]] +eastl::unique_ptr CreateVulkanSwapchain(const SwapchainCreateInfo& p_createInfo) +{ + return eastl::make_unique(static_cast(*p_createInfo.m_device), + static_cast(*p_createInfo.m_surface), + p_createInfo.m_extent); +} + +/****************************************************************************************/ + +[[nodiscard]] +bool RegisterVulkanSwapchainFactory() +{ + const bool inserted = g_graphicsSwapchainFactory->Register(GraphicsAPI::Vulkan, &CreateVulkanSwapchain); + CRITICAL_ASSERT(GraphicsVulkanAssertHandler, inserted, "Failed to register the Vulkan graphics swapchain factory"); + return inserted; +} + +/****************************************************************************************/ + +const bool g_vulkanSwapchainFactoryRegistered = RegisterVulkanSwapchainFactory(); +} // namespace Bigfoot::Vulkan diff --git a/Bigfoot/Tests/Graphics/GraphicsCore/GraphicsContext.cpp b/Bigfoot/Tests/Graphics/GraphicsCore/GraphicsContext.cpp index f6b7172..899c958 100644 --- a/Bigfoot/Tests/Graphics/GraphicsCore/GraphicsContext.cpp +++ b/Bigfoot/Tests/Graphics/GraphicsCore/GraphicsContext.cpp @@ -20,12 +20,16 @@ class GraphicsContextFixture: public ::testing::Test { SDL_Init(SDL_INIT_VIDEO); - m_window = SDL_CreateWindow("GraphicsContextTests", 64, 64, SDL_WINDOW_HIDDEN | SDL_WINDOW_VULKAN); + { + BIGFOOT_LSAN_ONLY(const __lsan::ScopedDisabler lsanDisabler;) + m_window = SDL_CreateWindow("GraphicsContextTests", 64, 64, SDL_WINDOW_HIDDEN | SDL_WINDOW_VULKAN); + } const SDL_PropertiesID windowProperties = SDL_GetWindowProperties(m_window); m_createInfo.m_applicationName = "GraphicsContextTests"; m_createInfo.m_applicationVersion = Version {1, 0, 0}; + m_createInfo.m_extent = {64, 64}; #ifdef BIGFOOT_WINDOWS m_createInfo.m_surfaceCreateInfo.m_windowHandle = static_cast( SDL_GetPointerProperty(windowProperties, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr)); @@ -50,7 +54,7 @@ class GraphicsContextFixture: public ::testing::Test /****************************************************************************************/ -TEST_F(GraphicsContextFixture, Constructor_ShouldCreateAValidInstanceSurfaceAndDevice) +TEST_F(GraphicsContextFixture, Constructor_ShouldCreateAValidContext) { const GraphicsContext context {m_createInfo}; @@ -58,5 +62,6 @@ TEST_F(GraphicsContextFixture, Constructor_ShouldCreateAValidInstanceSurfaceAndD EXPECT_NE(context.GetDevice().GetQueue(GraphicsQueueType::Compute), nullptr); EXPECT_NE(context.GetDevice().GetQueue(GraphicsQueueType::Transfer), nullptr); EXPECT_NE(context.GetDevice().GetQueue(GraphicsQueueType::Present), nullptr); + EXPECT_GT(context.GetSwapchain().GetImageCount(), 0u); } } // namespace Bigfoot diff --git a/Bigfoot/Tests/Graphics/GraphicsVulkan/VulkanSwapchain.cpp b/Bigfoot/Tests/Graphics/GraphicsVulkan/VulkanSwapchain.cpp new file mode 100644 index 0000000..76b2b3e --- /dev/null +++ b/Bigfoot/Tests/Graphics/GraphicsVulkan/VulkanSwapchain.cpp @@ -0,0 +1,76 @@ +/********************************************************************* + * \file VulkanSwapchain.cpp + * + * \author Romain BOULLARD + * \date September 2026 + *********************************************************************/ +#include + +#include +#include +#include +#include + +#include + +#include +#include + +namespace Bigfoot::Vulkan +{ +class VulkanSwapchainFixture: public ::testing::Test +{ + protected: + void SetUp() override + { + SDL_Init(SDL_INIT_VIDEO); + + { + BIGFOOT_LSAN_ONLY(const __lsan::ScopedDisabler lsanDisabler;) + m_window = SDL_CreateWindow("VulkanSwapchainTests", 64, 64, SDL_WINDOW_HIDDEN | SDL_WINDOW_VULKAN); + } + + const SDL_PropertiesID windowProperties = SDL_GetWindowProperties(m_window); + +#ifdef BIGFOOT_WINDOWS + m_surfaceCreateInfo.m_windowHandle = static_cast( + SDL_GetPointerProperty(windowProperties, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr)); + m_surfaceCreateInfo.m_windowInstance = GetModuleHandle(nullptr); +#elif defined BIGFOOT_LINUX + m_surfaceCreateInfo.m_display = static_cast( + SDL_GetPointerProperty(windowProperties, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr)); + m_surfaceCreateInfo.m_surface = static_cast( + SDL_GetPointerProperty(windowProperties, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr)); +#endif + } + + void TearDown() override + { + SDL_DestroyWindow(m_window); + SDL_Quit(); + } + + VulkanInstance m_instance {"VulkanSwapchainTests", Version {1, 0, 0}}; + SDL_Window* m_window = nullptr; + SurfaceCreateInfo m_surfaceCreateInfo; +}; + +/****************************************************************************************/ + +TEST_F(VulkanSwapchainFixture, Constructor_ShouldCreateAValidSwapchainWithAtLeastOneImage) +{ + const VulkanSurface surface {m_instance, m_surfaceCreateInfo}; + const VulkanDevice device {m_instance, &surface, {}}; + + const VulkanSwapchain swapchain {device, surface, glm::uvec2 {64, 64}}; + + EXPECT_TRUE(swapchain.GetHandle()); + ASSERT_FALSE(swapchain.GetImages().empty()); + + const VulkanImage& image = swapchain.GetImages().front(); + EXPECT_TRUE(image.GetHandle()); + EXPECT_NE(image.GetFormat(), PixelFormat::Undefined); + EXPECT_GT(image.GetExtent().x, 0u); + EXPECT_GT(image.GetExtent().y, 0u); +} +} // namespace Bigfoot::Vulkan