From 53a7e21b3003d638eeb33e488c68ee6c9af14187 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 9 Nov 2026 14:53:21 +0300 Subject: [PATCH 1/5] Vulkan: Isolate oversized memory pages from shared allocations --- Graphics/GraphicsEngine/interface/GraphicsTypes.h | 5 +++++ .../src/VulkanUtilities/MemoryManager.cpp | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Graphics/GraphicsEngine/interface/GraphicsTypes.h b/Graphics/GraphicsEngine/interface/GraphicsTypes.h index c222c1ffce..e0dff24fe2 100644 --- a/Graphics/GraphicsEngine/interface/GraphicsTypes.h +++ b/Graphics/GraphicsEngine/interface/GraphicsTypes.h @@ -4109,12 +4109,17 @@ struct EngineVkCreateInfo DILIGENT_DERIVE(EngineCreateInfo) /// Device-local memory is used for USAGE_DEFAULT and USAGE_IMMUTABLE /// GPU resources, such as buffers and textures. /// + /// Allocations larger than this size use separate pages that are not shared + /// with other resources and are released independently of the memory reserve. + /// /// If there is no available GPU memory, the resource will fail to be created. Uint32 DeviceLocalMemoryPageSize DEFAULT_INITIALIZER(16 << 20); /// Allocation granularity for host-visible memory. /// Host-visible memory is primarily used to upload data to GPU resources. + /// Allocations larger than this size use separate pages that are not shared + /// with other resources and are released independently of the memory reserve. Uint32 HostVisibleMemoryPageSize DEFAULT_INITIALIZER(16 << 20); /// Amount of device-local memory reserved by the engine. diff --git a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp index edd4ed9bee..1dd1c9ff4c 100644 --- a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp +++ b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp @@ -173,9 +173,15 @@ MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignme MemoryPageIndex PageIdx{MemoryTypeIndex, HostVisible, AllocateFlags}; std::lock_guard Lock{m_PagesMtx}; + const VkDeviceSize BasePageSize = HostVisible ? m_HostVisiblePageSize : m_DeviceLocalPageSize; + auto range = m_Pages.equal_range(PageIdx); for (auto page_it = range.first; page_it != range.second; ++page_it) { + // Oversized pages belong to a single allocation and must never be shared. + if (page_it->second.GetPageSize() > BasePageSize) + continue; + Allocation = page_it->second.Allocate(Size, Alignment); if (Allocation.Page != nullptr) break; @@ -184,9 +190,7 @@ MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignme size_t stat_ind = HostVisible ? 1 : 0; if (Allocation.Page == nullptr) { - VkDeviceSize PageSize = HostVisible ? m_HostVisiblePageSize : m_DeviceLocalPageSize; - while (PageSize < Size) - PageSize *= 2; + const VkDeviceSize PageSize = std::max(BasePageSize, Size); m_CurrAllocatedSize[stat_ind] += PageSize; m_PeakAllocatedSize[stat_ind] = std::max(m_PeakAllocatedSize[stat_ind], m_CurrAllocatedSize[stat_ind]); @@ -214,8 +218,6 @@ MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignme void MemoryManager::ShrinkMemory() { std::lock_guard Lock{m_PagesMtx}; - if (m_CurrAllocatedSize[0] <= m_DeviceLocalReserveSize && m_CurrAllocatedSize[1] <= m_HostVisibleReserveSize) - return; auto it = m_Pages.begin(); while (it != m_Pages.end()) @@ -225,7 +227,8 @@ void MemoryManager::ShrinkMemory() MemoryPage& Page = curr_it->second; bool IsHostVisible = Page.GetCPUMemory() != nullptr; VkDeviceSize ReserveSize = IsHostVisible ? m_HostVisibleReserveSize : m_DeviceLocalReserveSize; - if (Page.IsEmpty() && m_CurrAllocatedSize[IsHostVisible ? 1 : 0] > ReserveSize) + VkDeviceSize BasePageSize = IsHostVisible ? m_HostVisiblePageSize : m_DeviceLocalPageSize; + if (Page.IsEmpty() && (Page.GetPageSize() > BasePageSize || m_CurrAllocatedSize[IsHostVisible ? 1 : 0] > ReserveSize)) { VkDeviceSize PageSize = Page.GetPageSize(); m_CurrAllocatedSize[IsHostVisible ? 1 : 0] -= PageSize; From 8609d766ce68e3e7bfba333cba8ca470b703bd6f Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 9 Nov 2026 15:00:47 +0300 Subject: [PATCH 2/5] Tests: Cover oversized Vulkan page isolation and reclamation --- Tests/DiligentCoreAPITest/CMakeLists.txt | 3 + .../src/Vulkan/MemoryManagerTestVk.cpp | 114 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp diff --git a/Tests/DiligentCoreAPITest/CMakeLists.txt b/Tests/DiligentCoreAPITest/CMakeLists.txt index afb78105b3..1bb38d8842 100644 --- a/Tests/DiligentCoreAPITest/CMakeLists.txt +++ b/Tests/DiligentCoreAPITest/CMakeLists.txt @@ -135,6 +135,9 @@ if(TARGET Diligent-HLSL2GLSLConverterLib) endif() if(VULKAN_SUPPORTED) + target_include_directories(DiligentCoreAPITest PRIVATE ../../Graphics/GraphicsEngineVulkan/include) + target_link_libraries(DiligentCoreAPITest PRIVATE Diligent-GraphicsEngineNextGenBase Diligent-GraphicsEngineVk-static) + target_compile_definitions(DiligentCoreAPITest PRIVATE DILIGENT_USE_VOLK=1) if(PLATFORM_MACOS) if(VULKAN_LIB_PATH) # Configure rpath so that the executable can find vulkan library diff --git a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp new file mode 100644 index 0000000000..35b2c8af3b --- /dev/null +++ b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp @@ -0,0 +1,114 @@ +/* + * Copyright 2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#include "volk.h" + +#include "GPUTestingEnvironment.hpp" +#include "RenderDeviceVkImpl.hpp" +#include "DefaultRawMemoryAllocator.hpp" +#include "gtest/gtest.h" + +namespace Diligent +{ +namespace Testing +{ +namespace +{ + +class TrackingMemoryManager : public VulkanUtilities::MemoryManager +{ +public: + using MemoryManager::MemoryManager; + size_t LivePages = 0; + +private: + void OnNewPageCreated(VulkanUtilities::MemoryPage&) override { ++LivePages; } + void OnPageDestroy(VulkanUtilities::MemoryPage&) override { --LivePages; } +}; + +TEST(VulkanMemoryManagerTest, OversizedPages) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible ? "host-visible" : "device-local"); + TrackingMemoryManager Mgr{"Oversized page test", pDevice->GetLogicalDevice(), + pDevice->GetPhysicalDevice(), DefaultRawMemoryAllocator::GetAllocator(), + PageSize, PageSize, 16 * PageSize, 16 * PageSize}; + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + + { + auto Large = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + ASSERT_TRUE(Large); + EXPECT_EQ(Large.Page->GetPageSize(), 3 * PageSize / 2); + auto* LargePage = Large.Page; + { + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + EXPECT_NE(Small.Page, LargePage); + { + auto OtherSmall = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(OtherSmall); + EXPECT_EQ(OtherSmall.Page, Small.Page); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 2u); // Live allocations must survive collection. + { + auto Released = std::move(Large); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); // Oversized pages bypass the reserve. + { + auto Larger = Mgr.Allocate(2 * PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(Larger); + EXPECT_EQ(Larger.Page->GetPageSize(), 2 * PageSize); + EXPECT_NE(Larger.Page, Small.Page); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); + } + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); // Empty shared pages still obey the reserve. + { + auto Boundary = Mgr.Allocate(PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(Boundary); + EXPECT_EQ(Mgr.LivePages, 1u); + EXPECT_EQ(Boundary.Page->GetPageSize(), PageSize); + } + } +} + +} // namespace +} // namespace Testing +} // namespace Diligent From 47b098ff4dd38dab59374686226b30aeddc4307b Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 9 Nov 2026 18:28:47 +0300 Subject: [PATCH 3/5] Vulkan: Account for suballocation alignment when sizing new pages --- .../src/VulkanUtilities/MemoryManager.cpp | 3 +- .../src/Vulkan/MemoryManagerTestVk.cpp | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp index 1dd1c9ff4c..46aba4758d 100644 --- a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp +++ b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp @@ -190,7 +190,8 @@ MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignme size_t stat_ind = HostVisible ? 1 : 0; if (Allocation.Page == nullptr) { - const VkDeviceSize PageSize = std::max(BasePageSize, Size); + // The suballocator rounds the requested size up to Alignment, even at offset zero. + const VkDeviceSize PageSize = std::max(BasePageSize, Diligent::AlignUp(Size, Alignment)); m_CurrAllocatedSize[stat_ind] += PageSize; m_PeakAllocatedSize[stat_ind] = std::max(m_PeakAllocatedSize[stat_ind], m_CurrAllocatedSize[stat_ind]); diff --git a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp index 35b2c8af3b..5a878d52df 100644 --- a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp +++ b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp @@ -109,6 +109,42 @@ TEST(VulkanMemoryManagerTest, OversizedPages) } } +TEST(VulkanMemoryManagerTest, AllocationAlignment) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible ? "host-visible" : "device-local"); + TrackingMemoryManager Mgr{"Alignment test", pDevice->GetLogicalDevice(), + pDevice->GetPhysicalDevice(), DefaultRawMemoryAllocator::GetAllocator(), + PageSize, PageSize, 0, 0}; + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + + const VkDeviceSize Sizes[] = {PageSize + 1, 1}; + const VkDeviceSize Alignments[] = {256, 2 * PageSize}; + for (size_t i = 0; i < 2; ++i) + { + SCOPED_TRACE(i); + { + auto Allocation = Mgr.Allocate(Sizes[i], Alignments[i], Type, HostVisible, 0); + ASSERT_TRUE(Allocation); + EXPECT_EQ(Allocation.UnalignedOffset, 0u); + EXPECT_GE(Allocation.Size, AlignUp(Sizes[i], Alignments[i])); + EXPECT_GE(Allocation.Page->GetPageSize(), Allocation.Size); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 0u); + } + } +} + } // namespace } // namespace Testing } // namespace Diligent From fd6df772b88cf9e6bde64f208dd372ff45250e38 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Fri, 13 Nov 2026 18:02:19 +0300 Subject: [PATCH 4/5] Vulkan: Reuse empty oversized pages and preserve shared reserves --- .../include/VulkanUtilities/MemoryManager.hpp | 2 +- .../src/VulkanUtilities/MemoryManager.cpp | 26 +++-- .../src/Vulkan/MemoryManagerTestVk.cpp | 108 +++++++++++++++++- 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp b/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp index 432d81c436..2bd4196a45 100644 --- a/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp +++ b/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp @@ -132,7 +132,7 @@ class MemoryPage // clang-format on - MemoryAllocation Allocate(VkDeviceSize size, VkDeviceSize alignment); + MemoryAllocation Allocate(VkDeviceSize size, VkDeviceSize alignment, bool RequireEmpty = false); VkDeviceMemory GetVkMemory() const { return m_VkMemory; } void* GetCPUMemory() const { return m_CPUMemory; } diff --git a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp index 46aba4758d..f6a40e265c 100644 --- a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp +++ b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp @@ -96,9 +96,11 @@ MemoryPage::~MemoryPage() VERIFY(IsEmpty(), "Destroying a page with not all allocations released"); } -MemoryAllocation MemoryPage::Allocate(VkDeviceSize size, VkDeviceSize alignment) +MemoryAllocation MemoryPage::Allocate(VkDeviceSize size, VkDeviceSize alignment, bool RequireEmpty) { std::lock_guard Lock{m_Mutex}; + if (RequireEmpty && !m_AllocationMgr.IsEmpty()) + return MemoryAllocation{}; VERIFY(size <= std::numeric_limits::max(), "Allocation size (", size, ") exceeds maximum allowed value ", std::numeric_limits::max()); @@ -178,11 +180,9 @@ MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignme auto range = m_Pages.equal_range(PageIdx); for (auto page_it = range.first; page_it != range.second; ++page_it) { - // Oversized pages belong to a single allocation and must never be shared. - if (page_it->second.GetPageSize() > BasePageSize) - continue; - - Allocation = page_it->second.Allocate(Size, Alignment); + // Reuse empty oversized pages, but never share a live oversized allocation. + // Check emptiness under the page lock, since releases may run concurrently. + Allocation = page_it->second.Allocate(Size, Alignment, page_it->second.GetPageSize() > BasePageSize); if (Allocation.Page != nullptr) break; } @@ -220,6 +220,16 @@ void MemoryManager::ShrinkMemory() { std::lock_guard Lock{m_PagesMtx}; + // Isolated pages must not consume the reserve for shared pages. + std::array SharedAllocatedSize = {}; + for (const auto& Entry : m_Pages) + { + const bool HostVisible = Entry.first.IsHostVisible; + const VkDeviceSize BasePageSize = HostVisible ? m_HostVisiblePageSize : m_DeviceLocalPageSize; + if (Entry.second.GetPageSize() <= BasePageSize) + SharedAllocatedSize[HostVisible ? 1 : 0] += Entry.second.GetPageSize(); + } + auto it = m_Pages.begin(); while (it != m_Pages.end()) { @@ -229,9 +239,11 @@ void MemoryManager::ShrinkMemory() bool IsHostVisible = Page.GetCPUMemory() != nullptr; VkDeviceSize ReserveSize = IsHostVisible ? m_HostVisibleReserveSize : m_DeviceLocalReserveSize; VkDeviceSize BasePageSize = IsHostVisible ? m_HostVisiblePageSize : m_DeviceLocalPageSize; - if (Page.IsEmpty() && (Page.GetPageSize() > BasePageSize || m_CurrAllocatedSize[IsHostVisible ? 1 : 0] > ReserveSize)) + if (Page.IsEmpty() && (Page.GetPageSize() > BasePageSize || SharedAllocatedSize[IsHostVisible ? 1 : 0] > ReserveSize)) { VkDeviceSize PageSize = Page.GetPageSize(); + if (PageSize <= BasePageSize) + SharedAllocatedSize[IsHostVisible ? 1 : 0] -= PageSize; m_CurrAllocatedSize[IsHostVisible ? 1 : 0] -= PageSize; LOG_INFO_MESSAGE("MemoryManager '", m_MgrName, "': destroying ", (IsHostVisible ? "host-visible" : "device-local"), " page (", Diligent::FormatMemorySize(PageSize, 2), diff --git a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp index 5a878d52df..dd47ac8a71 100644 --- a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp +++ b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp @@ -42,10 +42,15 @@ class TrackingMemoryManager : public VulkanUtilities::MemoryManager { public: using MemoryManager::MemoryManager; - size_t LivePages = 0; + size_t LivePages = 0; + size_t CreatedPages = 0; private: - void OnNewPageCreated(VulkanUtilities::MemoryPage&) override { ++LivePages; } + void OnNewPageCreated(VulkanUtilities::MemoryPage&) override + { + ++LivePages; + ++CreatedPages; + } void OnPageDestroy(VulkanUtilities::MemoryPage&) override { --LivePages; } }; @@ -109,6 +114,105 @@ TEST(VulkanMemoryManagerTest, OversizedPages) } } +TEST(VulkanMemoryManagerTest, ReuseEmptyOversizedPage) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible ? "host-visible" : "device-local"); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + TrackingMemoryManager Mgr{"Oversized reuse test", pDevice->GetLogicalDevice(), + pDevice->GetPhysicalDevice(), DefaultRawMemoryAllocator::GetAllocator(), + PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + VulkanUtilities::MemoryPage* OriginalPage = nullptr; + { + auto Large = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + ASSERT_TRUE(Large); + OriginalPage = Large.Page; + // Match collection before completed deferred releases are purged. + Mgr.ShrinkMemory(); + } + { + auto Replacement = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + ASSERT_TRUE(Replacement); + EXPECT_EQ(Replacement.Page, OriginalPage); + EXPECT_EQ(Mgr.CreatedPages, 1u); + EXPECT_EQ(Mgr.LivePages, 1u); + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + EXPECT_NE(Small.Page, Replacement.Page); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); + } +} + +TEST(VulkanMemoryManagerTest, OversizedPagesPreserveSharedReserve) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible ? "host-visible" : "device-local"); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + for (bool LargeFirst : {false, true}) + { + SCOPED_TRACE(LargeFirst); + TrackingMemoryManager Mgr{"Shared reserve test", pDevice->GetLogicalDevice(), + pDevice->GetPhysicalDevice(), DefaultRawMemoryAllocator::GetAllocator(), + PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + VulkanUtilities::MemoryAllocation Large; + if (LargeFirst) + Large = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + { + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + if (!LargeFirst) + Large = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + ASSERT_TRUE(Large); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 2u); + { + auto Released = std::move(Large); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); + const auto CreatedPages = Mgr.CreatedPages; + { + auto Small = Mgr.Allocate(PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + EXPECT_EQ(Mgr.CreatedPages, CreatedPages); + } + // The reserve remains a limit: excess empty shared pages are collected. + { + auto First = Mgr.Allocate(PageSize, 256, Type, HostVisible, 0); + auto Second = Mgr.Allocate(PageSize, 256, Type, HostVisible, 0); + auto Third = Mgr.Allocate(PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(First); + ASSERT_TRUE(Second); + ASSERT_TRUE(Third); + EXPECT_EQ(Mgr.LivePages, 3u); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 2u); + } + } +} + TEST(VulkanMemoryManagerTest, AllocationAlignment) { auto* pEnv = GPUTestingEnvironment::GetInstance(); From dd6aa4c6257e680404bcac5c187980da1c63515d Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Fri, 13 Nov 2026 18:12:17 +0300 Subject: [PATCH 5/5] Vulkan: Harden oversized page reuse, collection and failure accounting --- .../include/VulkanUtilities/MemoryManager.hpp | 8 +- .../src/VulkanUtilities/MemoryManager.cpp | 66 ++++- .../src/Vulkan/MemoryManagerTestVk.cpp | 262 +++++++++++++++++- 3 files changed, 321 insertions(+), 15 deletions(-) diff --git a/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp b/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp index 2bd4196a45..1ec5dddd66 100644 --- a/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp +++ b/Graphics/GraphicsEngineVulkan/include/VulkanUtilities/MemoryManager.hpp @@ -125,10 +125,10 @@ class MemoryPage MemoryPage& operator= (MemoryPage&) = delete; MemoryPage& operator= (MemoryPage&& rhs) = delete; - bool IsEmpty() const { return m_AllocationMgr.IsEmpty(); } - bool IsFull() const { return m_AllocationMgr.IsFull(); } + bool IsEmpty() const; + bool IsFull() const; VkDeviceSize GetPageSize() const { return m_AllocationMgr.GetMaxSize(); } - VkDeviceSize GetUsedSize() const { return m_AllocationMgr.GetUsedSize(); } + VkDeviceSize GetUsedSize() const; // clang-format on @@ -146,7 +146,7 @@ class MemoryPage void Free(MemoryAllocation&& Allocation); MemoryManager& m_ParentMemoryMgr; - std::mutex m_Mutex; + mutable std::mutex m_Mutex; Diligent::VariableSizeAllocationsManager m_AllocationMgr; VulkanUtilities::DeviceMemoryWrapper m_VkMemory; void* m_CPUMemory = nullptr; diff --git a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp index f6a40e265c..cef436b35a 100644 --- a/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp +++ b/Graphics/GraphicsEngineVulkan/src/VulkanUtilities/MemoryManager.cpp @@ -96,6 +96,24 @@ MemoryPage::~MemoryPage() VERIFY(IsEmpty(), "Destroying a page with not all allocations released"); } +bool MemoryPage::IsEmpty() const +{ + std::lock_guard Lock{m_Mutex}; + return m_AllocationMgr.IsEmpty(); +} + +bool MemoryPage::IsFull() const +{ + std::lock_guard Lock{m_Mutex}; + return m_AllocationMgr.IsFull(); +} + +VkDeviceSize MemoryPage::GetUsedSize() const +{ + std::lock_guard Lock{m_Mutex}; + return m_AllocationMgr.GetUsedSize(); +} + MemoryAllocation MemoryPage::Allocate(VkDeviceSize size, VkDeviceSize alignment, bool RequireEmpty) { std::lock_guard Lock{m_Mutex}; @@ -163,7 +181,15 @@ MemoryAllocation MemoryManager::Allocate(const VkMemoryRequirements& MemReqs, Vk MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignment, uint32_t MemoryTypeIndex, bool HostVisible, VkMemoryAllocateFlags AllocateFlags) { - MemoryAllocation Allocation; + // The suballocator doubles its alignment while initializing a page. Keep sizes below + // half its offset range to avoid overflow there as well as in alignment arithmetic. + constexpr VkDeviceSize MaxSize = std::numeric_limits::max() / 2; + if (Size == 0 || Alignment == 0 || (Alignment & (Alignment - 1)) != 0 || + Alignment > MaxSize || Size > MaxSize - (Alignment - 1)) + LOG_ERROR_AND_THROW("Invalid Vulkan memory allocation size or alignment"); + + const VkDeviceSize AlignedSize = Diligent::AlignUp(Size, Alignment); + MemoryAllocation Allocation; // On integrated GPUs, there is no difference between host-visible and GPU-only // memory, so MemoryTypeIndex is the same. As GPU-only pages do not have CPU address, @@ -177,26 +203,52 @@ MemoryAllocation MemoryManager::Allocate(VkDeviceSize Size, VkDeviceSize Alignme const VkDeviceSize BasePageSize = HostVisible ? m_HostVisiblePageSize : m_DeviceLocalPageSize; - auto range = m_Pages.equal_range(PageIdx); + auto range = m_Pages.equal_range(PageIdx); + auto BestOversizedPage = range.second; for (auto page_it = range.first; page_it != range.second; ++page_it) { - // Reuse empty oversized pages, but never share a live oversized allocation. - // Check emptiness under the page lock, since releases may run concurrently. - Allocation = page_it->second.Allocate(Size, Alignment, page_it->second.GetPageSize() > BasePageSize); + MemoryPage& Page = page_it->second; + if (Page.GetPageSize() > BasePageSize) + { + // Small requests must not pin large pages or bypass available shared space. + if (AlignedSize > BasePageSize && Page.GetPageSize() >= AlignedSize && Page.IsEmpty() && + (BestOversizedPage == range.second || Page.GetPageSize() < BestOversizedPage->second.GetPageSize())) + BestOversizedPage = page_it; + continue; + } + Allocation = Page.Allocate(Size, Alignment); if (Allocation.Page != nullptr) break; } + if (Allocation.Page == nullptr && BestOversizedPage != range.second) + Allocation = BestOversizedPage->second.Allocate(Size, Alignment, true); size_t stat_ind = HostVisible ? 1 : 0; if (Allocation.Page == nullptr) { + // Collection may precede deferred releases. Reclaim unusable isolated pages + // before asking Vulkan for more memory, even if the next collection has not run. + for (auto page_it = range.first; page_it != range.second;) + { + MemoryPage& Page = page_it->second; + if (Page.GetPageSize() > BasePageSize && Page.IsEmpty()) + { + m_CurrAllocatedSize[stat_ind] -= Page.GetPageSize(); + OnPageDestroy(Page); + page_it = m_Pages.erase(page_it); + } + else + ++page_it; + } + // The suballocator rounds the requested size up to Alignment, even at offset zero. - const VkDeviceSize PageSize = std::max(BasePageSize, Diligent::AlignUp(Size, Alignment)); + const VkDeviceSize PageSize = std::max(BasePageSize, AlignedSize); + auto it = m_Pages.emplace(PageIdx, MemoryPage{*this, PageSize, MemoryTypeIndex, HostVisible, AllocateFlags}); + // Page construction, mapping and map insertion may throw. Count only resident pages. m_CurrAllocatedSize[stat_ind] += PageSize; m_PeakAllocatedSize[stat_ind] = std::max(m_PeakAllocatedSize[stat_ind], m_CurrAllocatedSize[stat_ind]); - auto it = m_Pages.emplace(PageIdx, MemoryPage{*this, PageSize, MemoryTypeIndex, HostVisible, AllocateFlags}); LOG_INFO_MESSAGE("MemoryManager '", m_MgrName, "': created new ", (HostVisible ? "host-visible" : "device-local"), " page. (", Diligent::FormatMemorySize(PageSize, 2), ", type idx: ", MemoryTypeIndex, "). Current allocated size: ", Diligent::FormatMemorySize(m_CurrAllocatedSize[stat_ind], 2)); diff --git a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp index dd47ac8a71..286c6a20bf 100644 --- a/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp +++ b/Tests/DiligentCoreAPITest/src/Vulkan/MemoryManagerTestVk.cpp @@ -24,6 +24,13 @@ * of the possibility of such damages. */ +#include +#include +#include +#include +#include +#include + #include "volk.h" #include "GPUTestingEnvironment.hpp" @@ -42,18 +49,40 @@ class TrackingMemoryManager : public VulkanUtilities::MemoryManager { public: using MemoryManager::MemoryManager; - size_t LivePages = 0; - size_t CreatedPages = 0; + size_t LivePages = 0; + size_t CreatedPages = 0; + size_t PeakLivePages = 0; + VkDeviceSize AllocatedSize(bool HostVisible) const { return m_CurrAllocatedSize[HostVisible ? 1 : 0]; } + VkDeviceSize PeakAllocatedSize(bool HostVisible) const { return m_PeakAllocatedSize[HostVisible ? 1 : 0]; } private: void OnNewPageCreated(VulkanUtilities::MemoryPage&) override { ++LivePages; ++CreatedPages; + PeakLivePages = std::max(PeakLivePages, LivePages); } void OnPageDestroy(VulkanUtilities::MemoryPage&) override { --LivePages; } }; +class FailingMemoryAllocator : public DefaultRawMemoryAllocator +{ +public: + bool Fail = false; + void* Allocate(size_t Size, const Char* Description, const char* File, Int32 Line) override + { + if (Fail) + throw std::bad_alloc{}; + return DefaultRawMemoryAllocator::Allocate(Size, Description, File, Line); + } + void* AllocateAligned(size_t Size, size_t Alignment, const Char* Description, const char* File, Int32 Line) override + { + if (Fail) + throw std::bad_alloc{}; + return DefaultRawMemoryAllocator::AllocateAligned(Size, Alignment, Description, File, Line); + } +}; + TEST(VulkanMemoryManagerTest, OversizedPages) { auto* pEnv = GPUTestingEnvironment::GetInstance(); @@ -128,7 +157,7 @@ TEST(VulkanMemoryManagerTest, ReuseEmptyOversizedPage) const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); - TrackingMemoryManager Mgr{"Oversized reuse test", pDevice->GetLogicalDevice(), + TrackingMemoryManager Mgr{"Oversized reuse test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; VulkanUtilities::MemoryPage* OriginalPage = nullptr; @@ -171,7 +200,7 @@ TEST(VulkanMemoryManagerTest, OversizedPagesPreserveSharedReserve) for (bool LargeFirst : {false, true}) { SCOPED_TRACE(LargeFirst); - TrackingMemoryManager Mgr{"Shared reserve test", pDevice->GetLogicalDevice(), + TrackingMemoryManager Mgr{"Shared reserve test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; VulkanUtilities::MemoryAllocation Large; @@ -213,6 +242,231 @@ TEST(VulkanMemoryManagerTest, OversizedPagesPreserveSharedReserve) } } +TEST(VulkanMemoryManagerTest, PreferSharedPages) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + for (bool LargeFirst : {false, true}) + { + SCOPED_TRACE(LargeFirst); + TrackingMemoryManager Mgr{"Safety test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), + DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + VulkanUtilities::MemoryAllocation Large; + if (LargeFirst) + Large = Mgr.Allocate(4 * PageSize, 256, Type, HostVisible, 0); + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + auto* SharedPage = Small.Page; + if (!LargeFirst) + Large = Mgr.Allocate(4 * PageSize, 256, Type, HostVisible, 0); + { + auto Released = std::move(Large); + } + auto OtherSmall = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(OtherSmall); + EXPECT_EQ(OtherSmall.Page, SharedPage); + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); + } + } +} + +TEST(VulkanMemoryManagerTest, ReclaimUnusableOversizedPagesBeforeGrowth) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + TrackingMemoryManager Mgr{"Safety test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), + DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + { + auto Large = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + ASSERT_TRUE(Large); + } + { + auto Larger = Mgr.Allocate(2 * PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(Larger); + EXPECT_EQ(Mgr.LivePages, 1u); + EXPECT_EQ(Mgr.PeakLivePages, 1u); + EXPECT_EQ(Mgr.AllocatedSize(HostVisible), 2 * PageSize); + } + { + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + EXPECT_EQ(Small.Page->GetPageSize(), PageSize); + EXPECT_EQ(Mgr.LivePages, 1u); + EXPECT_EQ(Mgr.PeakLivePages, 1u); + } + } +} + +TEST(VulkanMemoryManagerTest, FailedPageCreationPreservesAccounting) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + FailingMemoryAllocator Allocator; + TrackingMemoryManager Mgr{"Safety test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), + Allocator, PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + Allocator.Fail = true; + EXPECT_THROW(Mgr.Allocate(2 * PageSize, 256, Type, HostVisible, 0), std::bad_alloc); + Allocator.Fail = false; + EXPECT_EQ(Mgr.AllocatedSize(HostVisible), 0u); + EXPECT_EQ(Mgr.PeakAllocatedSize(HostVisible), 0u); + EXPECT_EQ(Mgr.LivePages, 0u); + { + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + ASSERT_TRUE(Small); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.AllocatedSize(HostVisible), PageSize); + EXPECT_EQ(Mgr.LivePages, 1u); + } +} + +TEST(VulkanMemoryManagerTest, ConcurrentReleaseAndCollection) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + TrackingMemoryManager Mgr{"Safety test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), + DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + std::atomic Done{false}; + std::thread Collector{[&] { + while (!Done.load()) + { + Mgr.ShrinkMemory(); + std::this_thread::yield(); + } + }}; + bool Succeeded = true; + try + { + for (size_t i = 0; i < 500; ++i) + { + auto Large = Mgr.Allocate(3 * PageSize / 2, 256, Type, HostVisible, 0); + auto Small = Mgr.Allocate(256, 256, Type, HostVisible, 0); + Succeeded = Succeeded && Large && Small; + if ((i % 8) == 0) + std::this_thread::yield(); + } + } + catch (...) + { + Succeeded = false; + } + Done.store(true); + Collector.join(); + EXPECT_TRUE(Succeeded); + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 1u); + EXPECT_EQ(Mgr.AllocatedSize(HostVisible), PageSize); + } +} + +TEST(VulkanMemoryManagerTest, BestFitOversizedReuse) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + TrackingMemoryManager Mgr{"Safety test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), + DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + for (bool LargerFirst : {false, true}) + { + VulkanUtilities::MemoryAllocation First = Mgr.Allocate((LargerFirst ? 4 : 2) * PageSize, 256, Type, HostVisible, 0); + auto Second = Mgr.Allocate((LargerFirst ? 2 : 4) * PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(First); + ASSERT_TRUE(Second); + { + auto ReleasedFirst = std::move(First); + auto ReleasedSecond = std::move(Second); + } + const auto CreatedPages = Mgr.CreatedPages; + { + auto Replacement = Mgr.Allocate(2 * PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(Replacement); + EXPECT_EQ(Replacement.Page->GetPageSize(), 2 * PageSize); + EXPECT_EQ(Mgr.CreatedPages, CreatedPages); + } + Mgr.ShrinkMemory(); + EXPECT_EQ(Mgr.LivePages, 0u); + } + } +} + +TEST(VulkanMemoryManagerTest, RejectInvalidAllocationRequests) +{ + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_VULKAN) + GTEST_SKIP(); + auto* pDevice = static_cast(pEnv->GetDevice()); + constexpr VkDeviceSize PageSize = 64 << 10; + for (bool HostVisible : {false, true}) + { + SCOPED_TRACE(HostVisible); + const VkMemoryPropertyFlags Props = HostVisible ? VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const uint32_t Type = pDevice->GetPhysicalDevice().GetMemoryTypeIndex(~0u, Props); + ASSERT_NE(Type, VulkanUtilities::PhysicalDevice::InvalidMemoryTypeIndex); + TrackingMemoryManager Mgr{"Safety test", pDevice->GetLogicalDevice(), pDevice->GetPhysicalDevice(), + DefaultRawMemoryAllocator::GetAllocator(), PageSize, PageSize, 2 * PageSize, 2 * PageSize}; + const VkDeviceSize MaxSize = std::numeric_limits::max(); + const VkDeviceSize Sizes[] = {0, 1, 1, MaxSize, MaxSize / 2 + 1}; + const VkDeviceSize Alignments[] = {256, 0, 3, 256, 1}; + for (size_t i = 0; i < 5; ++i) + { + SCOPED_TRACE(i); + TestingEnvironment::ErrorScope ExpectedError{"Invalid Vulkan memory allocation size or alignment"}; + EXPECT_THROW(Mgr.Allocate(Sizes[i], Alignments[i], Type, HostVisible, 0), std::runtime_error); + } + EXPECT_EQ(Mgr.CreatedPages, 0u); + EXPECT_EQ(Mgr.AllocatedSize(HostVisible), 0u); + { + auto Valid = Mgr.Allocate(PageSize, 256, Type, HostVisible, 0); + ASSERT_TRUE(Valid); + } + } +} + TEST(VulkanMemoryManagerTest, AllocationAlignment) { auto* pEnv = GPUTestingEnvironment::GetInstance();