I’m building a video game engine using C++ as the coding language and Vulkan for graphics. I am trying to set up a generic renderer using Vulkan that is flexible and will render objects based on a vector that is supplied to it. The renderer will also handle the creation of the window using GLFW and use GLM for all relevant math calls. I am using the ASSIMP library to load 3d models and animations.

I am in the process of converting the code to make use of vulkan.hpp and RAII. I've been making some changes to the Material class. Here is some relevant code:

Engine.h:

#pragma once

#include "Window.h"
#include "Renderer.h"
#include "Scene.h"
#include <chrono>
#include <thread>

class Engine
{
public:
	Engine();
	~Engine();

	void Run();
	void Shutdown();

	int MaxFPS = 60;

private:

	void Initialize();
	void MainLoop();
	void Update(float deltaTime);
	void Render();

	Window window;
	Renderer renderer;
	Scene scene;
};

Engine.cpp:

#include "Engine.h"
#include "Terrain.h"
#include <iostream>

Engine::Engine()
{
	Initialize();
}

Engine::~Engine()
{
	Shutdown();
}

void Engine::Run()
{
	MainLoop();

}

void Engine::Initialize()
{
	// Initialize window, renderer, and scene
	window.Initialize();
	renderer.Initialize(window.GetWindow());
	scene.Initialize();

	VkDescriptorSetLayout descriptorSetLayout = renderer.CreateDescriptorSetLayout();
	//VkDescriptorPool descriptorPool = renderer.CreateDescriptorPool(1); // Assuming only one terrain object

	//VkDescriptorSetLayout samplerDescriptorSetLayout = renderer.CreateSamplerDescriptorSetLayout(); // Use this new method to create a separate descriptor layout.
	vk::UniqueDescriptorPool descriptorPool = renderer.CreateDescriptorPool(1);


	// Create a simple square tile GameObject
	GameObject* squareTile = new GameObject();
	squareTile->Initialize();

	// Define the cube’s vertices and indices
	std::vector<Vertex> vertices = {
		{ {-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f} },  // bottom-left-back
		{ { 0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f} },  // bottom-right-back
		{ { 0.5f,  0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f} },  // top-right-back
		{ {-0.5f,  0.5f, -0.5f}, {1.0f, 1.0f, 0.0f}, {0.0f, 1.0f} },  // top-left-back
		{ {-0.5f, -0.5f,  0.5f}, {0.0f, 1.0f, 1.0f}, {1.0f, 0.0f} },  // bottom-left-front
		{ { 0.5f, -0.5f,  0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f} },  // bottom-right-front
		{ { 0.5f,  0.5f,  0.5f}, {1.0f, 0.0f, 1.0f}, {0.0f, 1.0f} },  // top-right-front
		{ {-0.5f,  0.5f,  0.5f}, {0.0f, 0.0f, 0.0f}, {1.0f, 1.0f} },  // top-left-front
	};

	std::vector<uint32_t> indices = {
		0, 1, 2, 2, 3, 0,  // back face
		0, 3, 7, 7, 4, 0,  // left face
		4, 7, 6, 6, 5, 4,  // front face
		1, 5, 6, 6, 2, 1,  // right face
		3, 2, 6, 6, 7, 3,  // top face
		0, 4, 5, 5, 1, 0   // bottom face
	};

	// Initialize mesh and material for the square tile
	squareTile->GetMesh()->Initialize(vertices, indices, *renderer.GetDevice(), *renderer.GetPhysicalDevice(), *renderer.GetCommandPool(), *renderer.GetGraphicsQueue());
	squareTile->GetMaterial()->Initialize("C:/shaders/vert_depth.spv", "C:/shaders/frag_depth.spv", "C:/textures/texture.jpg", *renderer.GetDevice(), descriptorSetLayout, descriptorPool.get(), *renderer.GetPhysicalDevice(), *renderer.GetCommandPool(), *renderer.GetGraphicsQueue());
	squareTile->Initialize2(renderer);
	// Add the square tile GameObject to the scene
	scene.AddGameObject(squareTile);

	/*Terrain terrain(0,10,1,renderer.GetDevice(), renderer.GetPhysicalDevice(), renderer.GetCommandPool(), renderer.GetGraphicsQueue());
	terrain.GenerateTerrain(descriptorSetLayout, samplerDescriptorSetLayout, descriptorPool);*/

	//scene.AddGameObject(terrain.GetTerrainObject());
	float deltaTime = window.GetDeltaTime();

}

void Engine::MainLoop()
{
	while (!window.ShouldClose())
	{
		window.PollEvents();

		float deltaTime = window.GetDeltaTime();
		Update(deltaTime);
		Render();

		auto sleep_duration = std::chrono::milliseconds(1000 / MaxFPS);
		std::this_thread::sleep_for(sleep_duration);
	}
}

void Engine::Update(float deltaTime)
{
	scene.Update(deltaTime);
}

void Engine::Render()
{
	renderer.BeginFrame();
	scene.Render(renderer);
	renderer.EndFrame();
}

void Engine::Shutdown()
{
	vkDeviceWaitIdle(*renderer.GetDevice());
	// Clean up resources in reverse order
	scene.Shutdown();
	renderer.Shutdown();
	window.Shutdown();
}

Material.h:

#pragma once

#include <vulkan/vulkan.hpp>
#include "Texture.h"
#include "Shader.h"
#include <stdexcept>
#include <memory> // Don’t forget to include <memory>
#include <array>
#include <iostream>

// Add this struct outside the Material class, possibly at the top of Material.cpp
struct ShaderDeleter {
	void operator()(Shader* shaderPtr) {
		if (shaderPtr != nullptr) {
			Shader::cleanup(shaderPtr);
		}
	}
};

class Material
{
public:
	Material() = default;
	~Material();

	void Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, vk::Device device, vk::DescriptorSetLayout descriptorSetLayout, vk::DescriptorPool descriptorPool, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue);
	void Cleanup();

	void LoadTexture(const std::string& filename, vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue);
	void LoadShaders(const std::string& vertFilename, const std::string& fragFilename, vk::Device device);

	void UpdateBufferBinding(VkBuffer newBuffer, vk::Device device, vk::DeviceSize devicesize);
	vk::DescriptorSet GetDescriptorSet() const;
	vk::PipelineLayout GetPipelineLayout() const;

	std::shared_ptr <Shader> GetvertexShader();
	std::shared_ptr <Shader> GetfragmentShader();
	void CreateDescriptorSet(VkBuffer uniformBuffer, vk::DeviceSize bufferSize);

private:
	vk::Device device;
	std::shared_ptr <Shader> vertexShader;
	std::shared_ptr <Shader> fragmentShader;
	std::shared_ptr<Texture> texture;


	void CreatePipelineLayout(vk::DescriptorSetLayout descriptorSetLayout);


	vk::DescriptorSet descriptorSet;
	vk::PipelineLayout pipelineLayout;

	vk::DescriptorSetLayout descriptorSetLayout;// = VK_NULL_HANDLE;

	vk::DescriptorPool descriptorPool;

	void CleanupDescriptorSetLayout();
};


Material.cpp:

#include "Material.h"

Material::~Material()
{
	Cleanup();
}

void Material::Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, vk::Device device, vk::DescriptorSetLayout descriptorSetLayout, vk::DescriptorPool descriptorPool, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue)
{
	this->device = device;
	this->descriptorPool = descriptorPool;//vk::UniqueDescriptorPool(descriptorPool, device); // Assign unique descriptor pool
	this->descriptorSetLayout = descriptorSetLayout;

	// Load shaders and texture
	LoadTexture(texturePath, device, physicalDevice, commandPool, graphicsQueue);
	LoadShaders(vertShaderPath, fragShaderPath, device);

	// Create descriptor set and pipeline layout
	CreatePipelineLayout(descriptorSetLayout);
}

void Material::CreateDescriptorSet(VkBuffer uniformBuffer, vk::DeviceSize bufferSize)
{
	vk::DescriptorSetAllocateInfo allocInfo{};
	allocInfo.setDescriptorPool(descriptorPool);
	allocInfo.setDescriptorSetCount(1);
	allocInfo.setPSetLayouts(&descriptorSetLayout);

	//descriptorSet = std::move(device.allocateDescriptorSetsUnique(allocInfo)[0]);
	auto descriptorSets = device.allocateDescriptorSets(allocInfo);
	if (descriptorSets.empty()) {
		throw std::runtime_error("Failed to allocate descriptor set!");
	}

	descriptorSet = descriptorSets[0];
	if (!descriptorSet) {
		throw std::runtime_error("Failed to allocate descriptor set!");
	}

	std::cout << "Descriptor set allocated: " << descriptorSet << std::endl;

	vk::DescriptorBufferInfo bufferInfo{};
	bufferInfo.buffer = uniformBuffer;
	bufferInfo.offset = 0;
	bufferInfo.range = bufferSize;

	vk::DescriptorImageInfo imageInfo{};
	imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
	imageInfo.imageView = texture->GetImageView();
	imageInfo.sampler = texture->GetSampler();

	std::array<vk::WriteDescriptorSet, 2> descriptorWrites{};

	descriptorWrites[0].setDstSet(descriptorSet);
	descriptorWrites[0].setDstBinding(0);
	descriptorWrites[0].setDstArrayElement(0);
	descriptorWrites[0].setDescriptorType(vk::DescriptorType::eUniformBuffer);
	descriptorWrites[0].setDescriptorCount(1);
	descriptorWrites[0].setPBufferInfo(&bufferInfo);

	descriptorWrites[1].setDstSet(descriptorSet);
	descriptorWrites[1].setDstBinding(1);
	descriptorWrites[1].setDstArrayElement(0);
	descriptorWrites[1].setDescriptorType(vk::DescriptorType::eCombinedImageSampler);
	descriptorWrites[1].setDescriptorCount(1);
	descriptorWrites[1].setPImageInfo(&imageInfo);

	device.updateDescriptorSets(static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
	std::cout << "Descriptor set updated: " << descriptorSet << std::endl;
}

void Material::CreatePipelineLayout(vk::DescriptorSetLayout descriptorSetLayout)
{
	vk::PipelineLayoutCreateInfo pipelineLayoutInfo{};
	pipelineLayoutInfo.setSetLayouts(descriptorSetLayout);

	pipelineLayout = device.createPipelineLayout(pipelineLayoutInfo);
}

void Material::Cleanup()
{
	if (vertexShader) {
		Shader::cleanup(vertexShader.get());
	}

	if (fragmentShader) {
		Shader::cleanup(fragmentShader.get());
	}

	if (texture) {
		texture->Cleanup();
		texture.reset();
	}

	CleanupDescriptorSetLayout();
	// Be sure to destroy the descriptor set if necessary
	// Note: If the descriptor pool is being destroyed, you don’t need to free individual descriptor sets
}

vk::DescriptorSet Material::GetDescriptorSet() const
{
	return descriptorSet;
}

vk::PipelineLayout Material::GetPipelineLayout() const
{
	return pipelineLayout;
}

std::shared_ptr <Shader> Material::GetvertexShader()
{
	return vertexShader;
}

std::shared_ptr <Shader> Material::GetfragmentShader()
{
	return fragmentShader;
}

void Material::LoadTexture(const std::string& filename, vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue)
{
	texture = std::make_shared<Texture>();
	texture->LoadFromFile(filename, device, physicalDevice, commandPool, graphicsQueue);
}

void Material::LoadShaders(const std::string& vertFilename, const std::string& fragFilename, vk::Device device)
{
	vertexShader = std::make_shared<Shader>();
	fragmentShader = std::make_shared<Shader>();
	vertexShader->loadFromFile(vertFilename, device, vk::ShaderStageFlagBits::eVertex);
	fragmentShader->loadFromFile(fragFilename, device, vk::ShaderStageFlagBits::eFragment);
}

void Material::UpdateBufferBinding(VkBuffer newBuffer, vk::Device device, vk::DeviceSize devicesize)
{
	vk::DescriptorBufferInfo bufferInfo{};
	bufferInfo.buffer = newBuffer;
	bufferInfo.offset = 0;
	bufferInfo.range = devicesize;

	vk::DescriptorImageInfo imageInfo{};
	imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
	imageInfo.imageView = texture->GetImageView();
	imageInfo.sampler = texture->GetSampler();

	std::array<vk::WriteDescriptorSet, 2> descriptorWrites{};
	descriptorWrites[0].setDstSet(descriptorSet);
	descriptorWrites[0].setDstBinding(0);
	descriptorWrites[0].setDstArrayElement(0);
	descriptorWrites[0].setDescriptorType(vk::DescriptorType::eUniformBuffer);
	descriptorWrites[0].setDescriptorCount(1);
	descriptorWrites[0].setPBufferInfo(&bufferInfo);

	descriptorWrites[1].setDstSet(descriptorSet);
	descriptorWrites[1].setDstBinding(1);
	descriptorWrites[1].setDstArrayElement(0);
	descriptorWrites[1].setDescriptorType(vk::DescriptorType::eCombinedImageSampler);
	descriptorWrites[1].setDescriptorCount(1);
	descriptorWrites[1].setPImageInfo(&imageInfo);

	device.updateDescriptorSets(static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
	std::cout << "Descriptor was updated" << std::endl;
}

void Material::CleanupDescriptorSetLayout()
{
	if (descriptorSetLayout != vk::DescriptorSetLayout{}) {
		device.destroyDescriptorSetLayout(descriptorSetLayout, nullptr);
		descriptorSetLayout = vk::DescriptorSetLayout{};
		std::cout << "Descriptor set layout destroyed" << std::endl;
	}
}

A portion of Renderer.cpp:

vk::UniqueDescriptorPool Renderer::CreateDescriptorPool(uint32_t maxSets) {
    vk::DescriptorPoolSize poolSize{};
    poolSize.type = vk::DescriptorType::eUniformBuffer;
    poolSize.descriptorCount = maxSets;

    // Add an additional descriptor pool size for COMBINED_IMAGE_SAMPLER
    vk::DescriptorPoolSize samplerPoolSize{};
    samplerPoolSize.type = vk::DescriptorType::eCombinedImageSampler;
    samplerPoolSize.descriptorCount = maxSets;

    std::array<vk::DescriptorPoolSize, 2> poolSizes = { poolSize, samplerPoolSize };

    vk::DescriptorPoolCreateInfo poolInfo{};
    poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet;
    poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
    poolInfo.pPoolSizes = poolSizes.data();
    poolInfo.maxSets = maxSets;

    return device.createDescriptorPoolUnique(poolInfo);
}

The code is failing in Material::UpdateBufferBindings at the following line:

device.updateDescriptorSets(static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);

I am getting the following errors in the log:

VUID-VkWriteDescriptorSet-dstSet-00320(ERROR / SPEC): msgNum: 148949623 - Validation Error: [ VUID-VkWriteDescriptorSet-dstSet-00320 ] Object 0: handle = 0x28335d888e0, type = VK_OBJECT_TYPE_INSTANCE; | MessageID = 0x8e0ca77 | Invalid VkDescriptorSet Object 0xa43473000000002d. The Vulkan spec states: dstSet must be a valid VkDescriptorSet handle (https://vulkan.lunarg.com/doc/view/1.3.239.0/windows/1.3-extensions/vkspec.html#VUID-VkWriteDescriptorSet-dstSet-00320)
    Objects: 1
        [0] 0x28335d888e0, type: 1, name: NULL
VUID-VkWriteDescriptorSet-dstSet-00320(ERROR / SPEC): msgNum: 148949623 - Validation Error: [ VUID-VkWriteDescriptorSet-dstSet-00320 ] Object 0: handle = 0x28335d888e0, type = VK_OBJECT_TYPE_INSTANCE; | MessageID = 0x8e0ca77 | Invalid VkDescriptorSet Object 0xa43473000000002d. The Vulkan spec states: dstSet must be a valid VkDescriptorSet handle (https://vulkan.lunarg.com/doc/view/1.3.239.0/windows/1.3-extensions/vkspec.html#VUID-VkWriteDescriptorSet-dstSet-00320)
    Objects: 1
        [0] 0x28335d888e0, type: 1, name: NULL
UNASSIGNED-CoreValidation-DrawState-InvalidDescriptorSet(ERROR / SPEC): msgNum: 1515374316 - Validation Error: [ UNASSIGNED-CoreValidation-DrawState-InvalidDescriptorSet ] Object 0: handle = 0xa43473000000002d, type = VK_OBJECT_TYPE_DESCRIPTOR_SET; | MessageID = 0x5a52c6ec | Cannot call vkUpdateDescriptorSets() on VkDescriptorSet 0xa43473000000002d[] that has not been allocated in pDescriptorWrites[0].
    Objects: 1
        [0] 0xa43473000000002d, type: 23, name: NULL

Can you see what might be causing the error and how to fix it?