/* * Copyright (c) 2021 The Khronos Group Inc. * Copyright (c) 2021 Valve Corporation * Copyright (c) 2021 LunarG, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and/or associated documentation files (the "Materials"), to * deal in the Materials without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Materials, and to permit persons to whom the Materials are * furnished to do so, subject to the following conditions: * * The above copyright notice(s) and this permission notice shall be included in * all copies or substantial portions of the Materials. * * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE * USE OR OTHER DEALINGS IN THE MATERIALS. * * Author: Charles Giessen */ #include "test_environment.h" #include struct MemoryTrackerSettings { MemoryTrackerSettings(bool should_fail_on_allocation, size_t fail_after_allocations) : should_fail_on_allocation(should_fail_on_allocation), fail_after_allocations(fail_after_allocations) {} bool should_fail_on_allocation = false; size_t fail_after_allocations = 0; }; class MemoryTracker { std::mutex main_mutex; MemoryTrackerSettings settings; VkAllocationCallbacks callbacks{}; // Implementation internals struct AllocationDetails { size_t requested_size_bytes; size_t actual_size_bytes; VkSystemAllocationScope alloc_scope; }; const static size_t UNKNOWN_ALLOCATION = std::numeric_limits::max(); size_t allocation_count = 0; std::vector> allocations; std::vector allocations_aligned; std::vector allocation_details; void add_element(std::unique_ptr&& alloc, void* aligned_alloc, AllocationDetails detail) { allocations.push_back(std::move(alloc)); allocations_aligned.push_back(aligned_alloc); allocation_details.push_back(detail); } void erase_index(size_t index) { allocations.erase(std::next(allocations.begin(), index)); allocations_aligned.erase(std::next(allocations_aligned.begin(), index)); allocation_details.erase(std::next(allocation_details.begin(), index)); } size_t find_element(void* ptr) { auto it = std::find(allocations_aligned.begin(), allocations_aligned.end(), ptr); if (it == allocations_aligned.end()) return UNKNOWN_ALLOCATION; return it - allocations_aligned.begin(); } void* allocate(size_t size, size_t alignment, VkSystemAllocationScope alloc_scope) { AllocationDetails detail{size, size + (alignment - 1), alloc_scope}; auto alloc = std::unique_ptr(new char[detail.actual_size_bytes]); if (!alloc) return nullptr; uint64_t addr = (uint64_t)alloc.get(); addr += (alignment - 1); addr &= ~(alignment - 1); void* aligned_alloc = (void*)addr; add_element(std::move(alloc), aligned_alloc, detail); allocation_count++; return allocations_aligned.back(); } void* reallocate(void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope alloc_scope) { if (pOriginal == nullptr) { return allocate(size, alignment, alloc_scope); } size_t index = find_element(pOriginal); if (index == UNKNOWN_ALLOCATION) return nullptr; size_t original_size = allocation_details[index].requested_size_bytes; if (size == 0) { erase_index(index); allocation_count--; return nullptr; } else if (size < original_size) { return pOriginal; } else { void* new_alloc = allocate(size, alignment, alloc_scope); if (new_alloc == nullptr) return nullptr; memcpy(new_alloc, pOriginal, original_size); erase_index(index); return new_alloc; } } void free(void* pMemory) { if (pMemory == nullptr) return; size_t index = find_element(pMemory); if (index == UNKNOWN_ALLOCATION) return; erase_index(index); assert(allocation_count != 0 && "Cant free when there are no valid allocations"); allocation_count--; } // Implementation of public functions void* impl_allocation(size_t size, size_t alignment, VkSystemAllocationScope allocationScope) noexcept { std::lock_guard lg(main_mutex); if (settings.should_fail_on_allocation && allocation_count == settings.fail_after_allocations) return nullptr; void* addr = allocate(size, alignment, allocationScope); return addr; } void* impl_reallocation(void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) noexcept { std::lock_guard lg(main_mutex); void* addr = reallocate(pOriginal, size, alignment, allocationScope); return addr; } void impl_free(void* pMemory) noexcept { std::lock_guard lg(main_mutex); free(pMemory); } void impl_internal_allocation_notification(size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope) noexcept { std::lock_guard lg(main_mutex); // TODO? } void impl_internal_free(size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope) noexcept { std::lock_guard lg(main_mutex); // TODO? } public: MemoryTracker(MemoryTrackerSettings settings) noexcept : settings(settings) { allocations.reserve(512); allocations_aligned.reserve(512); allocation_details.reserve(512); callbacks.pUserData = this; callbacks.pfnAllocation = public_allocation; callbacks.pfnReallocation = public_reallocation; callbacks.pfnFree = public_free; callbacks.pfnInternalAllocation = public_internal_allocation_notification; callbacks.pfnInternalFree = public_internal_free; } MemoryTracker() noexcept : MemoryTracker(MemoryTrackerSettings{false, 0}) {} VkAllocationCallbacks* get() noexcept { return &callbacks; } bool empty() noexcept { return allocation_count == 0; } // Static callbacks static VKAPI_ATTR void* VKAPI_CALL public_allocation(void* pUserData, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) noexcept { return reinterpret_cast(pUserData)->impl_allocation(size, alignment, allocationScope); } static VKAPI_ATTR void* VKAPI_CALL public_reallocation(void* pUserData, void* pOriginal, size_t size, size_t alignment, VkSystemAllocationScope allocationScope) noexcept { return reinterpret_cast(pUserData)->impl_reallocation(pOriginal, size, alignment, allocationScope); } static VKAPI_ATTR void VKAPI_CALL public_free(void* pUserData, void* pMemory) noexcept { reinterpret_cast(pUserData)->impl_free(pMemory); } static VKAPI_ATTR void VKAPI_CALL public_internal_allocation_notification(void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope) noexcept { reinterpret_cast(pUserData)->impl_internal_allocation_notification(size, allocationType, allocationScope); } static VKAPI_ATTR void VKAPI_CALL public_internal_free(void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope) noexcept { reinterpret_cast(pUserData)->impl_internal_free(size, allocationType, allocationScope); } }; class Allocation : public ::testing::Test { protected: virtual void SetUp() { env = std::unique_ptr(new SingleICDShim(TestICDDetails(TEST_ICD_PATH_VERSION_2, VK_MAKE_VERSION(1, 0, 0)))); } virtual void TearDown() { env.reset(); } std::unique_ptr env; }; // Test making sure the allocation functions are called to allocate and cleanup everything during // a CreateInstance/DestroyInstance call pair. TEST_F(Allocation, Instance) { MemoryTracker tracker; { InstWrapper inst{env->vulkan_functions, tracker.get()}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); } ASSERT_TRUE(tracker.empty()); } // Test making sure the allocation functions are called to allocate and cleanup everything during // a CreateInstance/DestroyInstance call pair with a call to GetInstanceProcAddr. TEST_F(Allocation, GetInstanceProcAddr) { MemoryTracker tracker; { InstWrapper inst{env->vulkan_functions, tracker.get()}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); auto* pfnCreateDevice = inst->vkGetInstanceProcAddr(inst, "vkCreateDevice"); auto* pfnDestroyDevice = inst->vkGetInstanceProcAddr(inst, "vkDestroyDevice"); ASSERT_TRUE(pfnCreateDevice != nullptr && pfnDestroyDevice != nullptr); } ASSERT_TRUE(tracker.empty()); } // Test making sure the allocation functions are called to allocate and cleanup everything during // a vkEnumeratePhysicalDevices call pair. TEST_F(Allocation, EnumeratePhysicalDevices) { MemoryTracker tracker; auto& driver = env->get_test_icd(); driver.physical_devices.emplace_back("physical_device_0"); { InstWrapper inst{env->vulkan_functions, tracker.get()}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); uint32_t physical_count = 1; uint32_t returned_physical_count = 0; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, nullptr)); ASSERT_EQ(physical_count, returned_physical_count); VkPhysicalDevice physical_device; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, &physical_device)); ASSERT_EQ(physical_count, returned_physical_count); } ASSERT_TRUE(tracker.empty()); } // Test making sure the allocation functions are called to allocate and cleanup everything from // vkCreateInstance, to vkCreateDevicce, and then through their destructors. With special // allocators used on both the instance and device. TEST_F(Allocation, InstanceAndDevice) { MemoryTracker tracker; auto& driver = env->get_test_icd(); driver.physical_devices.emplace_back("physical_device_0"); driver.physical_devices[0].add_queue_family_properties({VK_QUEUE_GRAPHICS_BIT, 1, 0, {1, 1, 1}}); { InstWrapper inst{env->vulkan_functions, tracker.get()}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); uint32_t physical_count = 1; uint32_t returned_physical_count = 0; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, nullptr)); ASSERT_EQ(physical_count, returned_physical_count); VkPhysicalDevice physical_device; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, &physical_device)); ASSERT_EQ(physical_count, returned_physical_count); uint32_t family_count = 1; uint32_t returned_family_count = 0; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, nullptr); ASSERT_EQ(returned_family_count, family_count); VkQueueFamilyProperties family; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, &family); ASSERT_EQ(returned_family_count, family_count); ASSERT_EQ(family.queueFlags, VK_QUEUE_GRAPHICS_BIT); ASSERT_EQ(family.queueCount, family_count); ASSERT_EQ(family.timestampValidBits, 0); DeviceCreateInfo dev_create_info; DeviceQueueCreateInfo queue_info; queue_info.add_priority(0.0f); dev_create_info.add_device_queue(queue_info); VkDevice device; ASSERT_EQ(inst->vkCreateDevice(physical_device, dev_create_info.get(), tracker.get(), &device), VK_SUCCESS); inst->vkDestroyDevice(device, tracker.get()); } ASSERT_TRUE(tracker.empty()); } // Test making sure the allocation functions are called to allocate and cleanup everything from // vkCreateInstance, to vkCreateDevicce, and then through their destructors. With special // allocators used on only the instance and not the device. TEST_F(Allocation, InstanceButNotDevice) { MemoryTracker tracker; { auto& driver = env->get_test_icd(); driver.physical_devices.emplace_back("physical_device_0"); driver.physical_devices[0].add_queue_family_properties({VK_QUEUE_GRAPHICS_BIT, 1, 0, {1, 1, 1}}); InstWrapper inst{env->vulkan_functions, tracker.get()}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); uint32_t physical_count = 1; uint32_t returned_physical_count = 0; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, nullptr)); ASSERT_EQ(physical_count, returned_physical_count); VkPhysicalDevice physical_device; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, &physical_device)); ASSERT_EQ(physical_count, returned_physical_count); uint32_t family_count = 1; uint32_t returned_family_count = 0; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, nullptr); ASSERT_EQ(returned_family_count, family_count); VkQueueFamilyProperties family; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, &family); ASSERT_EQ(returned_family_count, family_count); ASSERT_EQ(family.queueFlags, VK_QUEUE_GRAPHICS_BIT); ASSERT_EQ(family.queueCount, family_count); ASSERT_EQ(family.timestampValidBits, 0); DeviceCreateInfo dev_create_info; DeviceQueueCreateInfo queue_info; queue_info.add_priority(0.0f); dev_create_info.add_device_queue(queue_info); VkDevice device; ASSERT_EQ(inst->vkCreateDevice(physical_device, dev_create_info.get(), nullptr, &device), VK_SUCCESS); inst->vkDestroyDevice(device, nullptr); } ASSERT_TRUE(tracker.empty()); } // Test making sure the allocation functions are called to allocate and cleanup everything from // vkCreateInstance, to vkCreateDevicce, and then through their destructors. With special // allocators used on only the device and not the instance. TEST_F(Allocation, DeviceButNotInstance) { MemoryTracker tracker; { auto& driver = env->get_test_icd(); driver.physical_devices.emplace_back("physical_device_0"); driver.physical_devices[0].add_queue_family_properties({VK_QUEUE_GRAPHICS_BIT, 1, 0, {1, 1, 1}}); InstWrapper inst{env->vulkan_functions}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); uint32_t physical_count = 1; uint32_t returned_physical_count = 0; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, nullptr)); ASSERT_EQ(physical_count, returned_physical_count); VkPhysicalDevice physical_device; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, &physical_device)); ASSERT_EQ(physical_count, returned_physical_count); uint32_t family_count = 1; uint32_t returned_family_count = 0; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, nullptr); ASSERT_EQ(returned_family_count, family_count); VkQueueFamilyProperties family; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, &family); ASSERT_EQ(returned_family_count, family_count); ASSERT_EQ(family.queueFlags, VK_QUEUE_GRAPHICS_BIT); ASSERT_EQ(family.queueCount, family_count); ASSERT_EQ(family.timestampValidBits, 0); DeviceCreateInfo dev_create_info; DeviceQueueCreateInfo queue_info; queue_info.add_priority(0.0f); dev_create_info.add_device_queue(queue_info); VkDevice device; ASSERT_EQ(inst->vkCreateDevice(physical_device, dev_create_info.get(), tracker.get(), &device), VK_SUCCESS); inst->vkDestroyDevice(device, tracker.get()); } ASSERT_TRUE(tracker.empty()); } // Test failure during vkCreateInstance to make sure we don't leak memory if // one of the out-of-memory conditions trigger. TEST_F(Allocation, CreateInstanceIntentionalAllocFail) { size_t fail_index = 0; VkResult result = VK_ERROR_OUT_OF_HOST_MEMORY; while (result == VK_ERROR_OUT_OF_HOST_MEMORY && fail_index <= 10000) { MemoryTracker tracker(MemoryTrackerSettings{true, fail_index}); InstanceCreateInfo inst_create_info; VkInstance instance; result = env->vulkan_functions.vkCreateInstance(inst_create_info.get(), tracker.get(), &instance); if (result == VK_SUCCESS) { env->vulkan_functions.vkDestroyInstance(instance, tracker.get()); } ASSERT_TRUE(tracker.empty()); fail_index++; } } // Test failure during vkCreateDevice to make sure we don't leak memory if // one of the out-of-memory conditions trigger. TEST_F(Allocation, CreateDeviceIntentionalAllocFail) { auto& driver = env->get_test_icd(); driver.physical_devices.emplace_back("physical_device_0"); driver.physical_devices[0].add_queue_family_properties({VK_QUEUE_GRAPHICS_BIT, 1, 0, {1, 1, 1}}); InstWrapper inst{env->vulkan_functions}; InstanceCreateInfo inst_create_info; ASSERT_EQ(CreateInst(inst, inst_create_info), VK_SUCCESS); uint32_t physical_count = 1; uint32_t returned_physical_count = 0; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, nullptr)); ASSERT_EQ(physical_count, returned_physical_count); VkPhysicalDevice physical_device; ASSERT_EQ(VK_SUCCESS, inst->vkEnumeratePhysicalDevices(inst.inst, &returned_physical_count, &physical_device)); ASSERT_EQ(physical_count, returned_physical_count); uint32_t family_count = 1; uint32_t returned_family_count = 0; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, nullptr); ASSERT_EQ(returned_family_count, family_count); VkQueueFamilyProperties family; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, &family); ASSERT_EQ(returned_family_count, family_count); ASSERT_EQ(family.queueFlags, VK_QUEUE_GRAPHICS_BIT); ASSERT_EQ(family.queueCount, family_count); ASSERT_EQ(family.timestampValidBits, 0); size_t fail_index = 0; VkResult result = VK_ERROR_OUT_OF_HOST_MEMORY; while (result == VK_ERROR_OUT_OF_HOST_MEMORY) { MemoryTracker tracker(MemoryTrackerSettings{true, fail_index}); DeviceCreateInfo dev_create_info; DeviceQueueCreateInfo queue_info; queue_info.add_priority(0.0f); dev_create_info.add_device_queue(queue_info); VkDevice device; result = inst->vkCreateDevice(physical_device, dev_create_info.get(), tracker.get(), &device); if (result == VK_SUCCESS || fail_index > 10000) { inst->vkDestroyDevice(device, tracker.get()); break; } ASSERT_TRUE(tracker.empty()); fail_index++; } } // Test failure during vkCreateInstance and vkCreateDevice to make sure we don't // leak memory if one of the out-of-memory conditions trigger. TEST_F(Allocation, CreateInstanceDeviceIntentionalAllocFail) { auto& driver = env->get_test_icd(); driver.physical_devices.emplace_back("physical_device_0"); driver.physical_devices[0].add_queue_family_properties({VK_QUEUE_GRAPHICS_BIT, 1, 0, {1, 1, 1}}); size_t fail_index = 0; VkResult result = VK_ERROR_OUT_OF_HOST_MEMORY; while (result == VK_ERROR_OUT_OF_HOST_MEMORY && fail_index <= 10000) { MemoryTracker tracker(MemoryTrackerSettings{true, fail_index}); fail_index++; // applies to the next loop InstanceCreateInfo inst_create_info; VkInstance instance; result = env->vulkan_functions.vkCreateInstance(inst_create_info.get(), tracker.get(), &instance); if (result == VK_ERROR_OUT_OF_HOST_MEMORY) { ASSERT_TRUE(tracker.empty()); continue; } uint32_t physical_count = 1; uint32_t returned_physical_count = 0; result = env->vulkan_functions.vkEnumeratePhysicalDevices(instance, &returned_physical_count, nullptr); if (result == VK_ERROR_OUT_OF_HOST_MEMORY) { env->vulkan_functions.vkDestroyInstance(instance, tracker.get()); ASSERT_TRUE(tracker.empty()); continue; } ASSERT_EQ(physical_count, returned_physical_count); VkPhysicalDevice physical_device; result = env->vulkan_functions.vkEnumeratePhysicalDevices(instance, &returned_physical_count, &physical_device); if (result == VK_ERROR_OUT_OF_HOST_MEMORY) { env->vulkan_functions.vkDestroyInstance(instance, tracker.get()); ASSERT_TRUE(tracker.empty()); continue; } ASSERT_EQ(physical_count, returned_physical_count); uint32_t family_count = 1; uint32_t returned_family_count = 0; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, nullptr); ASSERT_EQ(returned_family_count, family_count); VkQueueFamilyProperties family; env->vulkan_functions.vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &returned_family_count, &family); ASSERT_EQ(returned_family_count, family_count); ASSERT_EQ(family.queueFlags, VK_QUEUE_GRAPHICS_BIT); ASSERT_EQ(family.queueCount, family_count); ASSERT_EQ(family.timestampValidBits, 0); DeviceCreateInfo dev_create_info; DeviceQueueCreateInfo queue_info; queue_info.add_priority(0.0f); dev_create_info.add_device_queue(queue_info); VkDevice device; result = env->vulkan_functions.vkCreateDevice(physical_device, dev_create_info.get(), tracker.get(), &device); if (result == VK_SUCCESS) { env->vulkan_functions.vkDestroyDevice(device, tracker.get()); } env->vulkan_functions.vkDestroyInstance(instance, tracker.get()); ASSERT_TRUE(tracker.empty()); } }