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 the current code for both the Engine and Material classes and some relevant code from Renderer.cpp:

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.
	VkDescriptorPool 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, *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>

// 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::UniqueDescriptorPool 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->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.get());
	allocInfo.setDescriptorSetCount(1);
	allocInfo.setPSetLayouts(&descriptorSetLayout);

	descriptorSet = device.allocateDescriptorSetsUnique(allocInfo)[0].get();

	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);
}

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);
}

void Material::CleanupDescriptorSetLayout()
{
	if (descriptorSetLayout != vk::DescriptorSetLayout{}) {
		device.destroyDescriptorSetLayout(descriptorSetLayout, nullptr);
		descriptorSetLayout = vk::DescriptorSetLayout{};
	}
}

A portion of Renderer.cpp:

vk::DescriptorPool 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.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
    poolInfo.pPoolSizes = poolSizes.data();
    poolInfo.maxSets = maxSets;

    vk::DescriptorPool descriptorPool;
    if (device.createDescriptorPool(&poolInfo, nullptr, &descriptorPool) != vk::Result::eSuccess) {
        throw std::runtime_error("Failed to create descriptor pool!");
    }

    return descriptorPool;
}

I am now getting the following errors in the log:

UNASSIGNED-GeneralParameterError-RequiredParameter(ERROR / SPEC): msgNum: -1711571459 - Validation Error: [ UNASSIGNED-GeneralParameterError-RequiredParameter ] Object 0: handle = 0x1db5cc43360, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x99fb7dfd | vkAllocateDescriptorSets: required parameter pAllocateInfo->descriptorPool specified as VK_NULL_HANDLE
    Objects: 1
        [0] 0x1db5cc43360, type: 3, name: NULL
VUID-VkDescriptorSetAllocateInfo-descriptorPool-parameter(ERROR / SPEC): msgNum: -2127463821 - Validation Error: [ VUID-VkDescriptorSetAllocateInfo-descriptorPool-parameter ] Object 0: handle = 0x1db595d8e30, type = VK_OBJECT_TYPE_INSTANCE; | MessageID = 0x81317a73 | Invalid VkDescriptorPool Object 0x0. The Vulkan spec states: descriptorPool must be a valid VkDescriptorPool handle (https://vulkan.lunarg.com/doc/view/1.3.239.0/windows/1.3-extensions/vkspec.html#VUID-VkDescriptorSetAllocateInfo-descriptorPool-parameter)
    Objects: 1
        [0] 0x1db595d8e30, type: 1, name: NULL

I don't believe the descriptorPool variable is being assigned to the descriptorPool member variable properly during Material::Initialization. Do you know how to modify the code to fix it?