From 53a7e21b3003d638eeb33e488c68ee6c9af14187 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 9 Nov 2026 14:53:21 +0300 Subject: [PATCH 1/3] 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/3] 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/3] 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