Texture corruption when window is resized

Hello,

I’m having trouble with a nasty bug in my Vulkan application. I am using Vulkan memory allocator (VMA) library for memory management. To begin with, my application runs as expected:

I would like to add links to more picture here, but this form doesn’t allow me to :_(

When I minimize and re-open the window from task bar, it is still rendered corectly. If I resize the window however, the texture image is rendered incorrectly:

This texture corruption is irreversible. When I increase the size of my window further, the corruption increases:

First I thought the depth buffer causes problems. However, if I change the fragment shader code to this:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 1) uniform sampler2D texSampler;

layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

I get a correctly rendered image. These colored squads will be rendered correctly, no matter how I change the window size. I therefore conclude that it’s a problem with the texture memory.

Let’s take a look at the texture creation:

#include "VulkanTextureManager.hpp"
#include "../error-handling/VulkanErrorHandling.hpp"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"


namespace inexor {
namespace vulkan_renderer {

	
	VulkanTextureManager::VulkanTextureManager()
	{
	}

	
	VulkanTextureManager::~VulkanTextureManager()
	{
	}


	VkResult VulkanTextureManager::initialise(const VkDevice& device, const VkPhysicalDevice& graphics_card, const std::shared_ptr<VulkanDebugMarkerManager> debug_marker_manager,  const VmaAllocator& vma_allocator, const uint32_t& transfer_queue_family_index, const VkQueue& data_transfer_queue)
	{
		assert(device);
		assert(vma_allocator);
		assert(data_transfer_queue);
		assert(debug_marker_manager);
		assert(graphics_card);

		this->device = device;
		this->vma_allocator = vma_allocator;
		this->data_transfer_queue = data_transfer_queue;
		this->dbg_marker_manager = debug_marker_manager;
		this->graphics_card = graphics_card;

		spdlog::debug("Initialising Vulkan texture buffer manager.");
		spdlog::debug("Creating command pool for texture buffer manager.");

		VkCommandPoolCreateInfo command_pool_create_info = {};

		command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
		command_pool_create_info.pNext = nullptr;
		command_pool_create_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; // TODO: Do we need this?

		// This might be a distinct data transfer queue.
		command_pool_create_info.queueFamilyIndex = transfer_queue_family_index;

		// Create a second command pool for all commands that are going to be executed in the data transfer queue.
		VkResult result = vkCreateCommandPool(device, &command_pool_create_info, nullptr, &data_transfer_command_pool);
		vulkan_error_check(result);
		
		// Give this command pool an appropriate name.
		dbg_marker_manager->set_object_name(device, (uint64_t)(data_transfer_command_pool), VK_DEBUG_REPORT_OBJECT_TYPE_COMMAND_POOL_EXT, "Command pool for VulkanTextureManager.");
		
		VkCommandBufferAllocateInfo command_buffer_allocate_info = {};

		command_buffer_allocate_info.sType              = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
		command_buffer_allocate_info.commandPool        = data_transfer_command_pool;
		command_buffer_allocate_info.level              = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
		command_buffer_allocate_info.commandBufferCount = 1;

		spdlog::debug("Allocating command buffers for texture buffer manager.");

		// Allocate a command buffer for data transfer commands.
		result = vkAllocateCommandBuffers(device, &command_buffer_allocate_info, &data_transfer_command_buffer);
		vulkan_error_check(result);

		// Give this command pool an appropriate name.
		dbg_marker_manager->set_object_name(device, (uint64_t)(data_transfer_command_buffer), VK_DEBUG_REPORT_OBJECT_TYPE_COMMAND_BUFFER_EXT, "Command buffer for VulkanTextureManager.");

		return result;
	}


	VkResult VulkanTextureManager::create_texture_buffer(const std::string& texture_name, InexorBuffer& buffer_object, const VkBufferUsageFlags& buffer_usage, const VmaMemoryUsage& memory_usage)
	{
		assert(vma_allocator);
		assert(dbg_marker_manager);
		
		spdlog::debug("Creating data buffer for texture '" + texture_name + "'.");

		buffer_object.create_info.sType            = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
		buffer_object.create_info.size             = buffer_object.size;
		buffer_object.create_info.usage            = buffer_usage;
		buffer_object.create_info.sharingMode      = VK_SHARING_MODE_EXCLUSIVE;

		buffer_object.allocation_create_info.usage     = memory_usage;
		buffer_object.allocation_create_info.flags     = VMA_ALLOCATION_CREATE_MAPPED_BIT|VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT;
		buffer_object.allocation_create_info.pUserData = "Inexor-TODO!";

		VkResult result = vmaCreateBuffer(vma_allocator, &buffer_object.create_info, &buffer_object.allocation_create_info, &buffer_object.buffer, &buffer_object.allocation, &buffer_object.allocation_info);
		vulkan_error_check(result);
		
		// Give this texture data buffer an appropriate name.
		const std::string data_buffer_name = "Data buffer for texture '" + texture_name + "'.";

		dbg_marker_manager->set_object_name(device, (uint64_t)(buffer_object.buffer), VK_DEBUG_REPORT_OBJECT_TYPE_BUFFER_EXT, data_buffer_name.c_str());

		return result;
	}
	

	VkResult VulkanTextureManager::create_texture_image(std::shared_ptr<InexorTexture> texture, const uint32_t& texture_width, const uint32_t& texture_height, const VkFormat& format, const VkImageTiling& tiling, const VmaMemoryUsage& memory_usage, const VkBufferUsageFlags& buffer_usage, const VkImageUsageFlags& image_usage_flags)
	{
		texture->image_create_info = {};

		texture->image_create_info.sType         = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
		texture->image_create_info.imageType     = VK_IMAGE_TYPE_2D;
		texture->image_create_info.extent.width  = texture_width;
		texture->image_create_info.extent.height = texture_height;
		texture->image_create_info.extent.depth  = 1;
		texture->image_create_info.mipLevels     = 1;
		texture->image_create_info.arrayLayers   = 1;
		texture->image_create_info.format        = format;
		texture->image_create_info.tiling        = tiling;
		texture->image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
		texture->image_create_info.usage         = image_usage_flags;
		texture->image_create_info.samples       = VK_SAMPLE_COUNT_1_BIT;
		texture->image_create_info.sharingMode   = VK_SHARING_MODE_EXCLUSIVE;
				
		// Image creation does not allocate memory for the image automatically.
		// This is done in the following code part:

		texture->allocation_create_info.usage     = memory_usage;
		texture->allocation_create_info.flags     = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT;
		texture->allocation_create_info.pUserData = "test";
		
		VkResult result = vmaCreateImage(vma_allocator, &texture->image_create_info, &texture->allocation_create_info, &texture->image, &texture->allocation, nullptr);
		vulkan_error_check(result);

		// Assign an appropriate name to this image view.
		std::string image_name = "Image for texture '" + texture->texture_name + "'";

		dbg_marker_manager->set_object_name(device, (uint64_t)(texture->image), VK_DEBUG_REPORT_OBJECT_TYPE_IMAGE_EXT, image_name.c_str());

		return VK_SUCCESS;
	}
	

	VkResult VulkanTextureManager::begin_single_time_commands()
	{
		spdlog::debug("Started recording command buffer for single command.");

		VkCommandBufferAllocateInfo command_buffer_allocate_info = {};

		command_buffer_allocate_info.sType              = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
		command_buffer_allocate_info.pNext              = nullptr;
		command_buffer_allocate_info.commandBufferCount = 1;
		command_buffer_allocate_info.commandPool        = data_transfer_command_pool;
		command_buffer_allocate_info.level              = VK_COMMAND_BUFFER_LEVEL_PRIMARY;

		// We have to allocate the command buffer every time this function is
		// called because we call vkFreeCommandBuffers in end_single_time_commands.
		VkResult result = vkAllocateCommandBuffers(device, &command_buffer_allocate_info, &data_transfer_command_buffer);
		vulkan_error_check(result);

		// TODO: Assign memory marker to data_transfer_command_buffer!

		VkCommandBufferBeginInfo command_buffer_begin_info = {};

		command_buffer_begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
		command_buffer_begin_info.pNext = nullptr;

		// We’re only going to use the command buffer once and wait with returning from the function until
		// the copy operation has finished executing. It’s good practice to tell the driver about our intent
		// using VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT.
		command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

		// Begin recording of the command buffer.
		result = vkBeginCommandBuffer(data_transfer_command_buffer, &command_buffer_begin_info);
		vulkan_error_check(result);

		return result;
	}


	VkResult VulkanTextureManager::end_single_time_commands()
	{
		spdlog::debug("Ended recording command buffer for single time commands.");

		VkResult result = vkEndCommandBuffer(data_transfer_command_buffer);
		vulkan_error_check(result);

		VkSubmitInfo submit_info = {};

		submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
		submit_info.commandBufferCount = 1;
		submit_info.pCommandBuffers = &data_transfer_command_buffer;

		result = vkQueueSubmit(data_transfer_queue, 1, &submit_info, VK_NULL_HANDLE);
		vulkan_error_check(result);

		// TODO: Fence!
		result = vkQueueWaitIdle(data_transfer_queue);
		vulkan_error_check(result);
		
		spdlog::debug("Destroying command buffer again.");

		// Because we destroy the command buffer after submission, we have to allocate it every time begin_single_commands is called.
		vkFreeCommandBuffers(device, data_transfer_command_pool, 1, &data_transfer_command_buffer);

		return VK_SUCCESS;
	}
	
	
	VkResult VulkanTextureManager::create_texture_image_view(std::shared_ptr<InexorTexture> texture, const VkFormat& format)
	{
		texture->view_create_info = {};

		texture->view_create_info.sType                           = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
		texture->view_create_info.image                           = texture->image;
		texture->view_create_info.viewType                        = VK_IMAGE_VIEW_TYPE_2D;
		texture->view_create_info.format                          = format;
		texture->view_create_info.subresourceRange.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
		texture->view_create_info.subresourceRange.baseMipLevel   = 0;
		texture->view_create_info.subresourceRange.levelCount     = 1;
		texture->view_create_info.subresourceRange.baseArrayLayer = 0;
		texture->view_create_info.subresourceRange.layerCount     = 1;
		
		VkResult result = vkCreateImageView(device, &texture->view_create_info, nullptr, &texture->view);
		vulkan_error_check(result);

		return VK_SUCCESS;
	}


	VkResult VulkanTextureManager::create_texture_from_file(const std::string& texture_name, const std::string& file_name, std::shared_ptr<InexorTexture> texture)
	{
		int texture_width = 0;
		int texture_height = 0;
		int texture_channels = 0;
		
		// TODO: Check if texture with this name does already exist.

		const VkFormat format = VK_FORMAT_R8G8B8A8_UNORM;

		spdlog::debug("Loading texture {}.", file_name);
		
		// Load the texture file using stb_image library.
		// Force stb_image to load an alpha channel as well.
		stbi_uc* pixels = stbi_load(file_name.c_str(), &texture_width, &texture_height, &texture_channels, STBI_rgb_alpha);

		if(!pixels)
		{
			spdlog::error("Texture {} could not be loaded!", file_name);
			return VK_ERROR_INITIALIZATION_FAILED;
		}

		spdlog::debug("Texture width: {}, height: {}", texture_width, texture_height);

		// Note: Inexor vulkan-renderer does not intend to support linear tiled textures because it is not advisable to do so!

		// Create a staging buffer for the texture.
		VkBuffer texture_staging_buffer = VK_NULL_HANDLE;

		// Calculate the memory size of the texture.
		// We need 4 times the size since we have 4 channels: red, green, blue and alpha channel.
		VkDeviceSize texture_memory_size = 4 * texture_width * texture_height;
		
		// Store the name of the texture.
		texture->texture_name = texture_name;
		texture->size = texture_memory_size;

		// Create a staging vertex buffer.
		InexorBuffer staging_buffer_for_texture(texture_memory_size);

		// Create a staging buffer for the texture.
		// This buffer is used as a transfer source for the buffer copy
		// TODO: Use generalized buffer creation manager?
		VkResult result = create_texture_buffer(texture_name, staging_buffer_for_texture, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
		vulkan_error_check(result);

		// Copy memory to staging buffer.
		// Vma library already ensures that the memory is mapped for us!
		std::memcpy(staging_buffer_for_texture.allocation_info.pMappedData, pixels, static_cast<std::size_t>(texture_memory_size));

		// We now can discard the image data since we copied it already.
		stbi_image_free(pixels);

		result = create_texture_image(texture, texture_width, texture_height, format, VK_IMAGE_TILING_OPTIMAL, VMA_MEMORY_USAGE_GPU_ONLY, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_IMAGE_USAGE_TRANSFER_DST_BIT|VK_IMAGE_USAGE_SAMPLED_BIT);
		vulkan_error_check(result);


		transition_image_layout(texture->image, format, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
		
		copy_buffer_to_image(staging_buffer_for_texture.buffer, texture->image, texture_width, texture_height);

		transition_image_layout(texture->image, format, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);


		// Destroy staging buffer.
		vmaDestroyBuffer(vma_allocator, staging_buffer_for_texture.buffer, texture->allocation);

		// Free memory of the texture's staging buffer.
		vmaFreeMemory(vma_allocator, staging_buffer_for_texture.allocation);


		/// Create an image view so shaders can access this texture.
		create_texture_image_view(texture, format);

		// Create a texture sampler so shaders can access this texture.
		create_texture_sampler(texture);

		// Update the texture's descriptor.
		texture->update_descriptor();

		// TODO: Check if texture with this name does already exist.
		textures.insert({texture_name, texture});

		return VK_SUCCESS;
	}


	VkResult VulkanTextureManager::create_texture_sampler(std::shared_ptr<InexorTexture> texture)
	{
		VkSamplerCreateInfo sampler_create_info = {};

		sampler_create_info.sType        = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
		sampler_create_info.magFilter    = VK_FILTER_LINEAR;
		sampler_create_info.minFilter    = VK_FILTER_LINEAR;
		sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
		sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
		sampler_create_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;

		// These two fields specify if anisotropic filtering should be used.
		// There is no reason not to use this unless performance is a concern.
		// The maxAnisotropy field limits the amount of texel samples that can
		// be used to calculate the final color. A lower value results in better
		// performance, but lower quality results. There is no graphics hardware
		// available today that will use more than 16 samples, because the difference
		// is negligible beyond that point.
		sampler_create_info.anisotropyEnable = VK_TRUE;
		sampler_create_info.maxAnisotropy    = 16;
		
		// The borderColor field specifies which color is returned when sampling beyond
		// the image with clamp to border addressing mode. It is possible to return black,
		// white or transparent in either float or int formats. You cannot specify an arbitrary color.
		sampler_create_info.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;

		// The unnormalizedCoordinates field specifies which coordinate system you
		// want to use to address texels in an image. If this field is VK_TRUE, then you
		// can simply use coordinates within the [0, texWidth) and [0, texHeight)
		// range. If it is VK_FALSE, then the texels are addressed using the [0, 1) range
		// on all axes. Real-world applications almost always use normalized coordinates,
		// because then it’s possible to use textures of varying resolutions with the exact
		// same coordinates.
		sampler_create_info.unnormalizedCoordinates = VK_FALSE;

		sampler_create_info.compareEnable = VK_FALSE;

		// TODO: Verify if we should use something else than this.
		sampler_create_info.compareOp     = VK_COMPARE_OP_ALWAYS;

		sampler_create_info.mipmapMode    = VK_SAMPLER_MIPMAP_MODE_LINEAR;
		sampler_create_info.mipLodBias    = 0.0f;
		sampler_create_info.minLod        = 0.0f;
		sampler_create_info.maxLod        = 0.0f;


		VkPhysicalDeviceFeatures device_features;
		vkGetPhysicalDeviceFeatures(graphics_card, &device_features);
		
		VkPhysicalDeviceProperties graphics_card_properties;
		vkGetPhysicalDeviceProperties(graphics_card, &graphics_card_properties);


		if(VK_TRUE == device_features.samplerAnisotropy)
		{
			// Anisotropic filtering is available.
			sampler_create_info.maxAnisotropy = graphics_card_properties.limits.maxSamplerAnisotropy;
			sampler_create_info.anisotropyEnable = VK_TRUE;
		}
		else
		{
			// The device does not support anisotropic filtering
			sampler_create_info.maxAnisotropy = 1.0;
			sampler_create_info.anisotropyEnable = VK_FALSE;
		}

		VkResult result = vkCreateSampler(device, &sampler_create_info, nullptr, &texture->sampler);
		vulkan_error_check(result);

		// Give this texture sampler an appropriate name.
		const std::string texture_sampler_name = "Texture sampler for texture '" + texture->texture_name + "'.";

		dbg_marker_manager->set_object_name(device, (uint64_t)(texture->sampler), VK_DEBUG_REPORT_OBJECT_TYPE_SAMPLER_EXT, texture_sampler_name.c_str());

		return VK_SUCCESS;
	}


	VkResult VulkanTextureManager::transition_image_layout(VkImage& image, VkFormat format, VkImageLayout old_layout, VkImageLayout new_layout)
	{
		// Start the recording of a command buffer.
		begin_single_time_commands();

		VkImageMemoryBarrier barrier = {};

		barrier.sType                           = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
		barrier.oldLayout                       = old_layout;
		barrier.newLayout                       = new_layout;
		barrier.srcQueueFamilyIndex             = VK_QUEUE_FAMILY_IGNORED;
		barrier.dstQueueFamilyIndex             = VK_QUEUE_FAMILY_IGNORED;
		barrier.image                           = image;
		barrier.subresourceRange.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
		barrier.subresourceRange.baseMipLevel   = 0;
		barrier.subresourceRange.levelCount     = 1;
		barrier.subresourceRange.baseArrayLayer = 0;
		barrier.subresourceRange.layerCount     = 1;

		VkPipelineStageFlags source_stage;
		VkPipelineStageFlags destination_stage;

		if(VK_IMAGE_LAYOUT_UNDEFINED == old_layout && VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL == new_layout)
		{
			// 
			barrier.srcAccessMask = 0;
			barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

			source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
			destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
		}
		else if(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL == old_layout && VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL == new_layout)
		{
			// 
			barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
			barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
			
			source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
			destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
		}
		else
		{
			spdlog::error("Error: unsupported layout transition!");
			return VK_ERROR_INITIALIZATION_FAILED; 
		}

		spdlog::debug("Recording pipeline barrier for image layer transition");

		// 
		vkCmdPipelineBarrier(data_transfer_command_buffer, source_stage, destination_stage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
		
		// End the recording of a command buffer.
		end_single_time_commands();

		return VK_SUCCESS;
	}
	
	
	VkResult VulkanTextureManager::copy_buffer_to_image(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height)
	{
		// Start the recording of a command buffer.
		begin_single_time_commands();

		VkBufferImageCopy buffer_image_region = {};

		buffer_image_region.bufferOffset                    = 0;
		buffer_image_region.bufferRowLength                 = 0;
		buffer_image_region.bufferImageHeight               = 0;
		buffer_image_region.imageSubresource.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
		buffer_image_region.imageSubresource.mipLevel       = 0;
		buffer_image_region.imageSubresource.baseArrayLayer = 0;
		buffer_image_region.imageSubresource.layerCount     = 1;
		buffer_image_region.imageOffset                     = {0, 0, 0};
		buffer_image_region.imageExtent                     = {width, height, 1};

		vkCmdCopyBufferToImage(data_transfer_command_buffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1, &buffer_image_region);

		// End the recording of a command buffer.
		end_single_time_commands();

		return VK_SUCCESS;
	}
	
	
	VkImageView VulkanTextureManager::get_texture_view(const std::string& texture_name)
	{
		// TODO: Check if index exists.
		return textures[texture_name]->view;
	}
    
	
	VkSampler VulkanTextureManager::get_texture_sampler(const std::string& texture_name)
	{
		// TODO: Check if index exists.
		return textures[texture_name]->sampler;
	}


	void VulkanTextureManager::shutdown_textures()
	{
		std::unordered_map<std::string, std::shared_ptr<InexorTexture>>::iterator texture_iterator;
	
		// Iterate through all textures and destroy them.
		for(texture_iterator = textures.begin(); texture_iterator != textures.end(); texture_iterator++)
		{
			texture_iterator->second->destroy_texture(device, vma_allocator);
		}
		
		// Destroy command pool for texture data transfer.
		vkDestroyCommandPool(device, data_transfer_command_pool, nullptr);
	}


};
};

This code recreates the swapchain:

VkResult VulkanRenderer::recreate_swapchain(std::vector<InexorMeshBuffer>& mesh_buffers)
{
	assert(device);

	int current_window_width = 0;
	int current_window_height = 0;

	// If window is minimized, wait until it is visible again.
	while(current_window_width == 0 || current_window_height == 0)
	{
		glfwGetFramebufferSize(window, &current_window_width, &current_window_height);
		glfwWaitEvents();
	}

	vkDeviceWaitIdle(device);
	
	spdlog::debug("Recreating the swapchain.");

	// Cleanup only neccesary parts.
	cleanup_swapchain();

	VkResult result = create_swapchain();
	if(VK_SUCCESS != result) return result;

	result = create_image_views();
	if(VK_SUCCESS != result) return result;
	
	result = create_pipeline();
	if(VK_SUCCESS != result) return result;
	
	result = create_depth_buffer();
	if(VK_SUCCESS != result) return result;
	
	result = create_frame_buffers();
	if(VK_SUCCESS != result) return result;
	
	result = create_uniform_buffers();
	if(VK_SUCCESS != result) return result;

	result = create_descriptor_pool();
	if(VK_SUCCESS != result) return result;
	
	result = create_descriptor_sets();
	if(VK_SUCCESS != result) return result;
	
	result = create_command_buffers();
	if(VK_SUCCESS != result) return result;

	result = record_command_buffers(mesh_buffers);
	if(VK_SUCCESS != result) return result;

	calculate_memory_budget();

	return VK_SUCCESS;
}

This code cleans up the swapchain:

VkResult VulkanRenderer::cleanup_swapchain()
{
	spdlog::debug("Cleaning up swapchain.");
	
	spdlog::debug("Waiting for device to be idle.");
	
	vkDeviceWaitIdle(device);

	spdlog::debug("Device is idle.");
	spdlog::debug("Destroying frame buffer.");
	
	if(frame_buffers.size() > 0)
	{
		for(auto frame_buffer : frame_buffers)
		{
			if(VK_NULL_HANDLE != frame_buffer)
			{
				vkDestroyFramebuffer(device, frame_buffer, nullptr);
				frame_buffer = VK_NULL_HANDLE;
			}
		}

		frame_buffers.clear();
	}
	
	spdlog::debug("Destroying command buffers.");

	// We do not need to reset the command buffers explicitly, since it is covered by vkDestroyCommandPool.
	if(command_buffers.size() > 0)
	{
		// The size of the command buffer is equal to the number of image in swapchain.
		vkFreeCommandBuffers(device, command_pool, static_cast<uint32_t>(command_buffers.size()), command_buffers.data());

		// Don't forget to free the memory.
		command_buffers.clear();
	}

	if(VK_NULL_HANDLE != depth_buffer.image_view)
	{
		vkDestroyImageView(device, depth_buffer.image_view, nullptr);
		//depth_buffer.image_view = VK_NULL_HANDLE;
	}

	if(VK_NULL_HANDLE != depth_buffer.image)
	{
		vmaDestroyImage(vma_allocator, depth_buffer.image, depth_buffer.allocation);
		//depth_buffer.image = VK_NULL_HANDLE;
	}

	spdlog::debug("Destroying pipeline.");

	if(VK_NULL_HANDLE != pipeline)
	{
		vkDestroyPipeline(device, pipeline, nullptr);
		pipeline = VK_NULL_HANDLE;
	}

	spdlog::debug("Destroying pipeline layout.");
	
	if(VK_NULL_HANDLE != pipeline_layout)
	{
		vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
		pipeline_layout = VK_NULL_HANDLE;
	}
	
	spdlog::debug("Destroying render pass.");

	if(VK_NULL_HANDLE != render_pass)
	{
		vkDestroyRenderPass(device, render_pass, nullptr);
		render_pass = VK_NULL_HANDLE;
	}

	spdlog::debug("Destroying image views.");
	
	if(swapchain_image_views.size() > 0)
	{
		for(auto image_view : swapchain_image_views)
		{
			if(VK_NULL_HANDLE != image_view)
			{
				vkDestroyImageView(device, image_view, nullptr);
				image_view = VK_NULL_HANDLE;
			}
		}

		swapchain_image_views.clear();
	}
	
	swapchain_images.clear();

	spdlog::debug("Destroying swapchain.");

	if(VK_NULL_HANDLE != swapchain)
	{
		vkDestroySwapchainKHR(device, swapchain, nullptr);
		swapchain = VK_NULL_HANDLE;
	}
	
	spdlog::debug("Destroying uniform buffers.");
	
	for(std::size_t i=0; i<number_of_images_in_swapchain; i++)
	{
		vkDestroyBuffer(device, uniform_buffers[i].buffer, nullptr);
		vmaFreeMemory(vma_allocator, uniform_buffers[i].allocation);
	}

	uniform_buffers.clear();

	spdlog::debug("Destroying descriptor pool.");

	vkDestroyDescriptorPool(device, descriptor_pool, nullptr);

	return VK_SUCCESS;
}

I know it’s a lot of code I’m not expecting anyone to read through it. How would you suggest to find a solution to this? Here is some more information:

  • No validation errors are reported when texture image is corrupted!
  • No Vulkan memory allocator messages are repoted either

Thank you

So this is RenderDoc’s output of the texture:

This is the correctly rendered image after I changed the fragment shader code:

This means the depth buffer is not corrupted.

I found the solution to the problem.
It’s quite a long list of problems actually.

bye.

Feel free to share your fixes. You could save someone else some time in tracking down similar artifacts.

Ok.
I highly recommend to use Vulkan memory allocator when starting with Vulkan. It is an excellent and battle-tested library which is used in big engines as well as far as I know.

VMA comes with useful debugging tools like memory playback which you should enable during development:

VmaRecordSettings vma_record_settings;

vma_record_settings.pFilePath = "vma_replay.csv";
// Close stream after every write operation in case we are expecting program to crash any time.
// Note that this can decrease performance though!
vma_record_settings.flags     = VMA_RECORD_FLUSH_AFTER_CALL_BIT;

VmaAllocatorCreateInfo allocator_info = {};

allocator_info.physicalDevice  = selected_graphics_card;
allocator_info.device          = device;
allocator_info.pRecordSettings = &vma_record_settings;

VkResult result = vmaCreateAllocator(&allocator_info, &vma_allocator);
vulkan_error_check(result);

The first problem of my program came from not freeing uniform buffer’s and depth buffer’s memory when recreating the swapchain:

VkResult result = vmaCreateImage(vma_allocator, &depth_buffer_image_create_info, &depth_buffer.allocation_create_info, &depth_buffer.image, &depth_buffer.allocation, nullptr);

Note how the last parameter is nullptr. Seeing nullptr as parameter is not uncommon when dealing with Vulkan API related code, but in this case the VMA memory allocation is not stored, which means that the buffer can’t be freed correctly.

This is how it should be done:

VkResult result = vmaCreateImage(vma_allocator, &depth_buffer_image_create_info, &depth_buffer.allocation_create_info, &depth_buffer.image, &depth_buffer.allocation, &depth_buffer.allocation_info);

I somehow managed to do the same mistake when freeing the buffers as well. This code is wrong:

vmaDestroyBuffer(vma_allocator, uniform_buffers[i].buffer, nullptr);

It should be:

vmaDestroyBuffer(vma_allocator, uniform_buffers[i].buffer, uniform_buffers[i].allocation);

I managed to track the issue because I enabled VMA memory recording and playback. It is highly recommended by me to give memory allocations a distinct name, so they can be tracked by name in the allocation replay:

depth_buffer.allocation_create_info.usage     = VMA_MEMORY_USAGE_GPU_ONLY;
depth_buffer.allocation_create_info.flags     = VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT;	
// Give this depth buffer a custom name so we can see it in VMA replay.
depth_buffer.allocation_create_info.pUserData = "Depth buffer image.";

Because memory was not freed before the program shut down, I got the following error message by VMA:

The allocation/deallocation mismatch can be seen in the VMA memory replay. Notice how the resources are nicely named because we specified pUserData in the allocation:

Vulkan Memory Allocator,Calls recording
1,8
Config,Begin
VulkanApiVersion,1,0
PhysicalDevice,apiVersion,4198495
PhysicalDevice,driverVersion,1783087104
PhysicalDevice,vendorID,4318
PhysicalDevice,deviceID,4052
PhysicalDevice,deviceType,2
PhysicalDevice,deviceName,GeForce GTX 660M
PhysicalDeviceLimits,maxMemoryAllocationCount,4096
PhysicalDeviceLimits,bufferImageGranularity,65536
PhysicalDeviceLimits,nonCoherentAtomSize,64
PhysicalDeviceMemory,HeapCount,2
PhysicalDeviceMemory,Heap,0,size,2075918336
PhysicalDeviceMemory,Heap,0,flags,1
PhysicalDeviceMemory,Heap,1,size,8265048064
PhysicalDeviceMemory,Heap,1,flags,0
PhysicalDeviceMemory,TypeCount,11
PhysicalDeviceMemory,Type,0,heapIndex,1
PhysicalDeviceMemory,Type,0,propertyFlags,0
PhysicalDeviceMemory,Type,1,heapIndex,1
PhysicalDeviceMemory,Type,1,propertyFlags,0
PhysicalDeviceMemory,Type,2,heapIndex,1
PhysicalDeviceMemory,Type,2,propertyFlags,0
PhysicalDeviceMemory,Type,3,heapIndex,1
PhysicalDeviceMemory,Type,3,propertyFlags,0
PhysicalDeviceMemory,Type,4,heapIndex,1
PhysicalDeviceMemory,Type,4,propertyFlags,0
PhysicalDeviceMemory,Type,5,heapIndex,1
PhysicalDeviceMemory,Type,5,propertyFlags,0
PhysicalDeviceMemory,Type,6,heapIndex,1
PhysicalDeviceMemory,Type,6,propertyFlags,0
PhysicalDeviceMemory,Type,7,heapIndex,0
PhysicalDeviceMemory,Type,7,propertyFlags,1
PhysicalDeviceMemory,Type,8,heapIndex,0
PhysicalDeviceMemory,Type,8,propertyFlags,1
PhysicalDeviceMemory,Type,9,heapIndex,1
PhysicalDeviceMemory,Type,9,propertyFlags,6
PhysicalDeviceMemory,Type,10,heapIndex,1
PhysicalDeviceMemory,Type,10,propertyFlags,14
Extension,VK_KHR_dedicated_allocation,0
Extension,VK_KHR_bind_memory2,0
Extension,VK_EXT_memory_budget,0
Extension,VK_AMD_device_coherent_memory,0
Macro,VMA_DEBUG_ALWAYS_DEDICATED_MEMORY,0
Macro,VMA_DEBUG_ALIGNMENT,1
Macro,VMA_DEBUG_MARGIN,16
Macro,VMA_DEBUG_INITIALIZE_ALLOCATIONS,1
Macro,VMA_DEBUG_DETECT_CORRUPTION,1
Macro,VMA_DEBUG_GLOBAL_MUTEX,0
Macro,VMA_DEBUG_MIN_BUFFER_IMAGE_GRANULARITY,1
Macro,VMA_SMALL_HEAP_MAX_SIZE,1073741824
Macro,VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE,268435456
Config,End
8644,0.002,0,vmaCreateAllocator
8644,0.134,0,vmaCreateImage,0,1,126,800,600,1,1,1,1,0,32,0,0,32,1,0,0,0,0000000000000000,0000000007F66FA0,Depth buffer image.
8644,0.302,0,vmaCreateBuffer,0,4194304,1,0,36,3,0,0,0,0000000000000000,0000000007F66FE8,example_texture_1
8644,0.304,0,vmaCreateImage,0,1,37,1024,1024,1,1,1,1,0,6,0,0,32,1,0,0,0,0000000000000000,0000000007F67030,example_texture_1
8644,0.388,0,vmaDestroyBuffer,0000000007F66FE8
8644,0.652,0,vmaCreateBuffer,0,192,16,0,36,3,0,0,0,0000000000000000,0000000007F66FE8,Uniform buffer #0
8644,0.663,0,vmaCreateBuffer,0,192,16,0,36,3,0,0,0,0000000000000000,0000000007F67078,Uniform buffer #1
8644,0.673,0,vmaCreateBuffer,0,192,16,0,36,3,0,0,0,0000000000000000,0000000007F670C0,Uniform buffer #2
8644,0.737,0,vmaCreateBuffer,0,128,1,0,36,2,0,0,0,0000000000000000,0000000007F67108,Example vertex buffer 1
8644,0.752,0,vmaCreateBuffer,0,192,1,0,36,2,0,0,0,0000000000000000,0000000007F67150,Example vertex buffer 1
8644,0.762,0,vmaCreateBuffer,0,128,130,0,36,2,0,0,0,0000000000000000,0000000007F67198,Example vertex buffer 1
8644,0.767,0,vmaCreateBuffer,0,192,66,0,36,2,0,0,0,0000000000000000,0000000007F671E0,Example vertex buffer 1
8644,0.817,0,vmaDestroyBuffer,0000000007F67108
8644,0.821,0,vmaDestroyBuffer,0000000007F67150
8644,0.842,0,vmaCreateBuffer,0,128,1,0,36,2,0,0,0,0000000000000000,0000000007F67150,Example vertex buffer 2
8644,0.855,0,vmaCreateBuffer,0,192,1,0,36,2,0,0,0,0000000000000000,0000000007F67108,Example vertex buffer 2
8644,0.866,0,vmaCreateBuffer,0,128,130,0,36,2,0,0,0,0000000000000000,0000000007F67228,Example vertex buffer 2
8644,0.870,0,vmaCreateBuffer,0,192,66,0,36,2,0,0,0,0000000000000000,0000000007F67270,Example vertex buffer 2
8644,0.920,0,vmaDestroyBuffer,0000000007F67150
8644,0.926,0,vmaDestroyBuffer,0000000007F67108
8644,1.113,0,vmaDestroyBuffer,0000000007F66FE8
8644,1.113,0,vmaDestroyBuffer,0000000007F67078
8644,1.113,0,vmaDestroyBuffer,0000000007F670C0
8644,1.195,0,vmaCreateImage,0,1,126,800,600,1,1,1,1,0,32,0,0,32,1,0,0,0,0000000000000000,0000000007F670C0,Depth buffer image.
8644,1.235,0,vmaCreateBuffer,0,192,16,0,36,3,0,0,0,0000000000000000,0000000007F67078,Uniform buffer #0
8644,1.246,0,vmaCreateBuffer,0,192,16,0,36,3,0,0,0,0000000000000000,0000000007F66FE8,Uniform buffer #1
8644,1.255,0,vmaCreateBuffer,0,192,16,0,36,3,0,0,0,0000000000000000,0000000007F67108,Uniform buffer #2
8644,2.432,0,vmaDestroyBuffer,0000000007F67078
8644,2.432,0,vmaDestroyBuffer,0000000007F66FE8
8644,2.432,0,vmaDestroyBuffer,0000000007F67108
8644,2.450,0,vmaDestroyImage,0000000007F67030
8644,2.469,0,vmaDestroyBuffer,0000000007F67198
8644,2.474,0,vmaDestroyBuffer,0000000007F671E0
8644,2.484,0,vmaDestroyBuffer,0000000007F67228
8644,2.489,0,vmaDestroyBuffer,0000000007F67270
8644,2.617,0,vmaDestroyAllocator

Thats about it, cu

Very interesting. That’s good advice! Thanks for following up!

Interesting, but how does a mem-leak explain mem-corruption?

Thats a good question. I don’t certainly know. Since this is a small resource, VMA will try to sub-allocate memory for it from a big block of 256 MB (I guess). Since I was allocating but not re-allocating, some memory might got shifted around?

I don’t know to be honest.

One more thing: VMA can also dump the current memory footprint into a .JSON file which can be converted into a .PNG image using VmaDumpVis.py (included in VMA github repository).

In the following example, I deliberately forget to destroy depth buffer and uniform buffers when recreating the swapchain. In the dump files you can see some unwanted changes in memory:

(from the docs:)
purple: Image with OPTIMAL tiling and usage containing DEPTH_STENCIL_ATTACHMENT.
cyan blue: Image with OPTIMAL tiling and usage containing SAMPLED.

So that is very useful