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.

Here is a portion of the code:

Renderer.h:

#pragma once

#include <vulkan/vulkan.h>
#include "Window.h"
#include <vector>
#include <stdexcept>
#include <set>
#include <optional>
#include <iostream>
#include "Pipeline.h"
#include "Material.h"
#include "Mesh.h"

struct QueueFamilyIndices
{
	std::optional<uint32_t> graphicsFamily;
	std::optional<uint32_t> presentFamily;

	bool IsComplete()
	{
		return graphicsFamily.has_value() && presentFamily.has_value();
	}
};

struct SwapChainSupportDetails {
	VkSurfaceCapabilitiesKHR capabilities;
	std::vector<VkSurfaceFormatKHR> formats;
	std::vector<VkPresentModeKHR> presentModes;
};

struct MVP {
	glm::mat4 model;
	glm::mat4 view;
	glm::mat4 projection;
};


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

	void Initialize(GLFWwindow* window);
	void Shutdown();

	void BeginFrame();
	void EndFrame();

	VkDescriptorSetLayout CreateDescriptorSetLayout();
	VkDescriptorPool CreateDescriptorPool(uint32_t maxSets);
	VkDevice* GetDevice();
	VkPhysicalDevice* GetPhysicalDevice();
	VkCommandPool* GetCommandPool();
	VkQueue* GetGraphicsQueue();
	VkCommandBuffer* GetCurrentCommandBuffer();
	std::shared_ptr<Pipeline> GetPipeline();
	void CreateGraphicsPipeline(Mesh* mesh, Material* material);
	VkDescriptorSetLayout CreateSamplerDescriptorSetLayout();
	std::pair<VkBuffer, VkDeviceMemory> RequestMvpBuffer();

private:

	bool isShutDown = false;

	static const uint32_t kMvpBufferCount = 3;
	std::vector<VkBuffer> mvpBuffers;
	std::vector<VkDeviceMemory> mvpBufferMemory;
	uint32_t currentMvpBufferIndex = 0;

	bool shutdownInProgress;
	uint32_t currentCmdBufferIndex = 0;
	std::vector<size_t> currentFramePerImage;

	std::vector<VkImage> swapChainImages;
	std::vector<VkImageView> swapChainImageViews;
	VkExtent2D swapChainExtent;
	VkRenderPass renderPass;
	uint32_t imageIndex;
	std::shared_ptr<Pipeline> pipeline;

	VkFormat swapChainImageFormat;

	std::vector<VkCommandBuffer> commandBuffers;

	void CreateImageViews();
	void CleanupImageViews();
	void CreateRenderPass();
	void CleanupRenderPass();

	void CreateSurface();
	void DestroySurface();

	void CreateInstance();
	void CleanupInstance();
	void ChoosePhysicalDevice();
	void CreateDevice();
	void CleanupDevice();
	void CreateSwapchain();
	void CleanupSwapchain();
	void CreateCommandPool();
	void CleanupCommandPool();
	void CreateFramebuffers();
	void CleanupFramebuffers();

	void CreateCommandBuffers();
	void CleanupCommandBuffers();

	void Present();

	

	

	GLFWwindow* window;
	VkInstance instance = VK_NULL_HANDLE;
	VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
	VkDevice device = VK_NULL_HANDLE;

	VkSurfaceKHR surface;
	VkSwapchainKHR swapchain;
	VkCommandPool commandPool;
	VkCommandBuffer currentCommandBuffer;

	std::vector<VkFramebuffer> framebuffers;

	// Additional Vulkan objects needed for rendering…

	const uint32_t kMaxFramesInFlight = 2;
	std::vector<VkSemaphore> imageAvailableSemaphores;
	std::vector<VkSemaphore> renderFinishedSemaphores;
	std::vector<VkFence> inFlightFences;
	size_t currentFrame;

	VkQueue graphicsQueue;
	VkQueue presentQueue;

	void CreateSyncObjects();
	void CleanupSyncObjects();

	SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device, VkSurfaceKHR surface);
	VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats);
	VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes);
	VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, GLFWwindow* window);

	std::vector<const char*> deviceExtensions = {
		VK_KHR_SWAPCHAIN_EXTENSION_NAME
	};

	std::vector<const char*> CheckPhysicalDeviceExtensionSupport(VkPhysicalDevice physicalDevice);
	QueueFamilyIndices GetQueueFamilyIndices(VkPhysicalDevice physicalDevice);
};

A portion of Renderer.cpp:

Renderer::Renderer() : currentFrame(0), shutdownInProgress(false)
{
}

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

void Renderer::Initialize(GLFWwindow* window)
{
	this->window = window;
	CreateInstance();
	CreateSurface();
	ChoosePhysicalDevice();
	CreateDevice();
	// Create the MVP buffers
mvpBuffers.resize(kMvpBufferCount);
mvpBufferMemory.resize(kMvpBufferCount);

for (uint32_t i = 0; i < kMvpBufferCount; ++i)
{
    BufferUtils::CreateBuffer(device, *GetPhysicalDevice(),
        sizeof(MVP), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        mvpBuffers[i], mvpBufferMemory[i]);
}
	CreateSwapchain();
	CreateRenderPass();
	CreateCommandPool();
	CreateFramebuffers();
	CreateSyncObjects();
}

void Renderer::Shutdown()
{
	if (isShutDown) {
		return;
	}

	if (shutdownInProgress) {
		return;
	}

	shutdownInProgress = true;
	if (device != VK_NULL_HANDLE) {
		vkDeviceWaitIdle(device);
	}

	CleanupFramebuffers();
	CleanupRenderPass();
	CleanupSyncObjects();
	CleanupCommandPool();
	CleanupImageViews();
	CleanupSwapchain();
	// Destroy the MVP buffers
	for (uint32_t i = 0; i < kMvpBufferCount; ++i)
	{
		vkDestroyBuffer(device, mvpBuffers[i], nullptr);
		vkFreeMemory(device, mvpBufferMemory[i], nullptr);
	}
	if (device != VK_NULL_HANDLE) {
		CleanupDevice();
	}
	DestroySurface();
	CleanupInstance();

	shutdownInProgress = false;
	isShutDown = true;
}

void Renderer::CleanupCommandBuffers()
{
	vkFreeCommandBuffers(device, commandPool, static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data());
}

void Renderer::CleanupCommandPool()
{
	// Clean up Vulkan command pool
	CleanupCommandBuffers(); // Add this line to clean up command buffers before destroying the command pool
	vkDestroyCommandPool(device, commandPool, nullptr);
}

void Renderer::CleanupDevice()
{
	// Destroy the logical device
	vkDestroyDevice(device, nullptr);
}

void Renderer::CleanupFramebuffers()
{
	for (auto framebuffer : framebuffers)
	{
		if (framebuffer != VK_NULL_HANDLE)
		{
			vkDestroyFramebuffer(device, framebuffer, nullptr);
			framebuffer = VK_NULL_HANDLE;
		}
	}
	framebuffers.clear(); // Make sure to clear the framebuffers vector after destroying each framebuffer
}

void Renderer::CleanupImageViews()
{
	for (auto imageView : swapChainImageViews)
	{
		vkDestroyImageView(device, imageView, nullptr);
	}
	swapChainImageViews.clear();
}

void Renderer::CleanupInstance()
{
	// Destroy the Vulkan instance
	vkDestroyInstance(instance, nullptr);
}

void Renderer::CleanupRenderPass()
{
	vkDestroyRenderPass(device, renderPass, nullptr);
}

void Renderer::CleanupSwapchain()
{
	// Clean up Vulkan swapchain
	if (swapchain != VK_NULL_HANDLE)
	{
		vkDestroySwapchainKHR(device, swapchain, nullptr);
		swapchain = VK_NULL_HANDLE;
	}
}

void Renderer::CleanupSyncObjects()
{
	for (size_t i = 0; i < kMaxFramesInFlight; ++i)
	{
		if (renderFinishedSemaphores[i] != VK_NULL_HANDLE)
			vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
		if (imageAvailableSemaphores[i] != VK_NULL_HANDLE)
			vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
		if (inFlightFences[i] != VK_NULL_HANDLE)
			vkDestroyFence(device, inFlightFences[i], nullptr);
	}
}

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 square’s vertices and indices
	std::vector<Vertex> vertices = {
		{ { 0.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },  // Bottom left
		{ { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } },  // Bottom right
		{ { 1.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } },  // Top right
		{ { 0.0f, 1.0f, 0.0f }, { 1.0f, 1.0f, 0.0f } },  // Top left
	};

	std::vector<uint32_t> indices = {
		0, 1, 2,  // First triangle
		0, 2, 3   // Second triangle
	};

	// 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_depth2.spv", "C:/shaders/frag_depth2.spv", "C:/textures/texture.jpg", *renderer.GetDevice(), descriptorSetLayout, samplerDescriptorSetLayout, descriptorPool, *renderer.GetPhysicalDevice(), *renderer.GetCommandPool(), *renderer.GetGraphicsQueue());

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

Scene.h:

#pragma once

#include <vector>
#include "GameObject.h"
#include "Camera.h"
#include "Renderer.h"

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

	void Initialize();
	void Update(float deltaTime);
	void Render(Renderer& renderer);
	void Shutdown();

	void AddGameObject(GameObject* gameObject);
	Camera& GetCamera();
	float temp;

private:
	std::vector<GameObject*> gameObjects;
	Camera camera;
};

Scene.cpp:

#include "Scene.h"

Scene::Scene()
{
}

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

void Scene::Initialize()
{
	// Initialize camera and game objects
	//camera.SetPosition(glm::vec3(0.0f, 0.0f, -5.0f));

	camera.Initialize(800.0f / 600.0f);
	camera.SetPosition(glm::vec3(0.0f, 0.0f, 5.0f));
	camera.SetRotation(glm::vec3(0.0f, 0.0f, 0.0f));

	// Add initial game objects
}

void Scene::Update(float deltaTime)
{
	// Update game objects and camera
	for (GameObject* gameObject : gameObjects)
	{
		gameObject->Update(deltaTime);
		temp = temp + 0.01;
	}
}

void Scene::Render(Renderer& renderer)
{
	// Render game objects
	for (GameObject* gameObject : gameObjects)
	{
		gameObject->Render(renderer, camera);
	}
	//camera.SetRotation(glm::vec3(0, 0, temp));

	// Submit rendering-related commands to Vulkan queues
}

void Scene::Shutdown()
{
	// Clean up game objects
	for (GameObject* gameObject : gameObjects)
	{
		delete gameObject;
		gameObject = nullptr;
	}
	gameObjects.clear();
	camera.Shutdown();
}

void Scene::AddGameObject(GameObject* gameObject)
{
	gameObjects.push_back(gameObject);
}

Camera& Scene::GetCamera()
{
	return camera;
}

GameObject.h:

#pragma once

#include <glm/glm.hpp>
#include "Mesh.h"
#include "Material.h"
#include "Camera.h"
#include "Renderer.h"

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

	void Initialize();
	void Update(float deltaTime);
	void Render(Renderer& renderer, const Camera& camera);
	void Shutdown();

	void SetPosition(const glm::vec3& position);
	void SetRotation(const glm::vec3& rotation);
	void SetScale(const glm::vec3& scale);

	Mesh* GetMesh();
	Material* GetMaterial();

private:
	glm::mat4 modelMatrix;
	glm::vec3 position;
	glm::vec3 rotation;
	glm::vec3 scale;

	Mesh* mesh;
	Material* material;

	bool initialized = false;

	void UpdateModelMatrix();
};

A portion of GameObject.cpp:

#include "GameObject.h"
#include <glm/gtc/matrix_transform.hpp>

GameObject::GameObject()
	: position(glm::vec3(0.0f, 0.0f, 0.0f)), rotation(glm::vec3(0.0f, 0.0f, 0.0f)), scale(1.0f)
{
}

GameObject::~GameObject()
{
    if (initialized)
    {
        Shutdown();
    }
}

void GameObject::Initialize()
{
    mesh = new Mesh{};
    material = new Material{};
    SetScale(glm::vec3(1.0f));
    this->initialized = true;
}

void GameObject::Update(float deltaTime)
{
	// Update position, rotation, scale, and other properties
	// Example: Rotate the object around the Y-axis
	rotation.y += deltaTime * glm::radians(90.0f);
	UpdateModelMatrix();
}

void GameObject::Shutdown()
{
	// Clean up resources, if necessary
	// (depending on how Mesh and Material resources are managed)
    delete mesh;
    delete material;
    this->initialized = false;
}


Material.h:

#pragma once

#include <vulkan/vulkan.h>
#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();
    ~Material();

    void Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, VkDevice device, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
    void Cleanup();

    void LoadTexture(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
    void LoadShaders(const std::string& vertFilename, const std::string& fragFilename, VkDevice device);

    void UpdateBufferBinding(VkDescriptorSet descriptorSet, VkBuffer newBuffer, VkDevice device, VkDeviceSize devicesize);
    VkDescriptorSet GetDescriptorSet() const;
    VkPipelineLayout GetPipelineLayout() const;

    std::shared_ptr <Shader> GetvertexShader();
    std::shared_ptr <Shader> GetfragmentShader();
    void CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool, VkBuffer uniformBuffer, VkDeviceSize bufferSize);

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

    
    void CreatePipelineLayout(VkDescriptorSetLayout descriptorSetLayout);
    

    VkDescriptorSet descriptorSet;
    VkPipelineLayout pipelineLayout;

    VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
    VkDescriptorSetLayout samplerDescriptorSetLayout = VK_NULL_HANDLE;

    void CleanupDescriptorSetLayout();
    void CleanupSamplerDescriptorSetLayout();
};

A portion of Material.cpp:

Material::Material()
    : device(VK_NULL_HANDLE), descriptorSet(VK_NULL_HANDLE), pipelineLayout(VK_NULL_HANDLE)
{
}

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

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

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

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

    if (pipelineLayout != VK_NULL_HANDLE) {
        vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
        pipelineLayout = VK_NULL_HANDLE;
    }

    CleanupDescriptorSetLayout();
    CleanupSamplerDescriptorSetLayout();
    // 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
}

Texture.h:

#pragma once

#include <vulkan/vulkan.h>

#include "stb_image.h" // Include the stb_image header
#include "BufferUtils.h"
#include <string>

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

	void LoadFromFile(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);

	VkImageView GetImageView() const;
	VkSampler GetSampler() const;
	void Cleanup();

private:
	VkDevice device;
	VkImage image;
	VkDeviceMemory imageMemory;
	VkImageView imageView;
	VkSampler sampler;
	VkPhysicalDevice physicalDevice;
	VkCommandPool commandPool;
	VkQueue graphicsQueue;
	bool initialized = false;

	void CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties);
	void TransitionImageLayout(VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels, VkSampleCountFlagBits numSamples);
	void CreateImageView(VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels);
	void CreateSampler(uint32_t mipLevels);
	void CopyBufferToImage(VkBuffer buffer, uint32_t width, uint32_t height);
	

	// Additional helper functions for texture loading…
};

A portion of Texture.cpp:

void Texture::Cleanup()
{
    if (!initialized) {
        return;
    }

    if (sampler != VK_NULL_HANDLE) {
        vkDestroySampler(device, sampler, nullptr);
        sampler = VK_NULL_HANDLE;
    }

    if (imageView != VK_NULL_HANDLE) {
        vkDestroyImageView(device, imageView, nullptr);
        imageView = VK_NULL_HANDLE;
    }

    if (image != VK_NULL_HANDLE) {
        vkDestroyImage(device, image, nullptr);
        image = VK_NULL_HANDLE;
    }

    if (imageMemory != VK_NULL_HANDLE) {
        vkFreeMemory(device, imageMemory, nullptr);
        imageMemory = VK_NULL_HANDLE;
    }

    initialized = false;
}

Pipeline.h:

#pragma once

#include <vulkan/vulkan.h>
#include <vector>
#include <array>
#include <stdexcept>
#include "Shader.h"

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

	void CreateGraphicsPipeline(const std::vector<VkVertexInputBindingDescription>& vertexBindingDescriptions,
		const std::vector<VkVertexInputAttributeDescription>& vertexAttributeDescriptions,
		VkExtent2D swapchainExtent,
		const std::vector<Shader*>& shaders,
		VkRenderPass renderPass,
		VkPipelineLayout pipelineLayout,
		VkDevice device);

	void Cleanup();

	VkPipeline GetPipeline() const;

	bool IsInitialized() const;

private:
	VkDevice device;
	VkPipeline pipeline;
	bool initialized;

	void CreateShaderStages(const std::vector<Shader*>& shaders, std::vector<VkPipelineShaderStageCreateInfo>& shaderStages);
};

A portion of code from Pipeline.cpp:

Pipeline::Pipeline()
	: device(VK_NULL_HANDLE), pipeline(VK_NULL_HANDLE), initialized(false)
{
}

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

void Pipeline::CreateGraphicsPipeline(const std::vector<VkVertexInputBindingDescription>& vertexBindingDescriptions,
	const std::vector<VkVertexInputAttributeDescription>& vertexAttributeDescriptions,
	VkExtent2D swapchainExtent,
	const std::vector<Shader*>& shaders,
	VkRenderPass renderPass,
	VkPipelineLayout pipelineLayout,
	VkDevice device)
{
	this->device = device;

	VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
	vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
	vertexInputInfo.vertexBindingDescriptionCount = static_cast<uint32_t>(vertexBindingDescriptions.size());
	vertexInputInfo.pVertexBindingDescriptions = vertexBindingDescriptions.data();
	vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexAttributeDescriptions.size());
	vertexInputInfo.pVertexAttributeDescriptions = vertexAttributeDescriptions.data();

	VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
	inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
	inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
	inputAssembly.primitiveRestartEnable = VK_FALSE;

	VkViewport viewport{};
	viewport.x = 0.0f;
	viewport.y = 0.0f;
	viewport.width = static_cast<float>(swapchainExtent.width);
	viewport.height = static_cast<float>(swapchainExtent.height);
	viewport.minDepth = 0.0f;
	viewport.maxDepth = 1.0f;

	VkRect2D scissor{};
	scissor.offset = { 0, 0 };
	scissor.extent = swapchainExtent;

	VkPipelineViewportStateCreateInfo viewportState{};
	viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
	viewportState.viewportCount = 1;
	viewportState.pViewports = &viewport;
	viewportState.scissorCount = 1;
	viewportState.pScissors = &scissor;

	VkPipelineRasterizationStateCreateInfo rasterizer{};
	rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
	rasterizer.depthClampEnable = VK_FALSE;
	rasterizer.rasterizerDiscardEnable = VK_FALSE;
	rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
	rasterizer.lineWidth = 1.0f;
	rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
	rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
	rasterizer.depthBiasEnable = VK_FALSE;

	VkPipelineMultisampleStateCreateInfo multisampling{};
	multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
	multisampling.sampleShadingEnable = VK_FALSE;
	multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

	VkPipelineColorBlendAttachmentState colorBlendAttachment{};
	colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
	colorBlendAttachment.blendEnable = VK_FALSE;

	VkPipelineColorBlendStateCreateInfo colorBlending{};
	colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
	colorBlending.logicOpEnable = VK_FALSE;
	colorBlending.logicOp = VK_LOGIC_OP_COPY;
	colorBlending.attachmentCount = 1;
	colorBlending.pAttachments = &colorBlendAttachment;
	colorBlending.blendConstants[0] = 0.0f;
	colorBlending.blendConstants[1] = 0.0f;
	colorBlending.blendConstants[2] = 0.0f;
	colorBlending.blendConstants[3] = 0.0f;

	std::vector<VkPipelineShaderStageCreateInfo> shaderStages;
	CreateShaderStages(shaders, shaderStages);

	VkGraphicsPipelineCreateInfo pipelineCreateInfo{};
	pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
	pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
	pipelineCreateInfo.pStages = shaderStages.data();
	pipelineCreateInfo.pVertexInputState = &vertexInputInfo;
	pipelineCreateInfo.pInputAssemblyState = &inputAssembly;
	pipelineCreateInfo.pViewportState = &viewportState;
	pipelineCreateInfo.pRasterizationState = &rasterizer;
	pipelineCreateInfo.pMultisampleState = &multisampling;
	pipelineCreateInfo.pColorBlendState = &colorBlending;
	pipelineCreateInfo.layout = pipelineLayout;
	pipelineCreateInfo.renderPass = renderPass;
	pipelineCreateInfo.subpass = 0;

	if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &pipeline) != VK_SUCCESS)
	{
		throw std::runtime_error("Failed to create graphics pipeline.");
	}

	initialized = true;
}

I am getting the following error in Pipeline::CreateGraphicsPipeline:

VUID-VkPipelineLayoutCreateInfo-pSetLayouts-06561(ERROR / SPEC): msgNum: -2012114744 - Validation Error: [ VUID-VkPipelineLayoutCreateInfo-pSetLayouts-06561 ] Object 0: handle = 0x208787c0ce0, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0x881190c8 | vkCreatePipelineLayout(): pSetLayouts[0] is VK_NULL_HANDLE, but VK_EXT_graphics_pipeline_library is not enabled. The Vulkan spec states: Elements of pSetLayouts must be valid VkDescriptorSetLayout objects (https://vulkan.lunarg.com/doc/view/1.3.239.0/windows/1.3-extensions/vkspec.html#VUID-VkPipelineLayoutCreateInfo-pSetLayouts-06561)
    Objects: 1
        [0] 0x208787c0ce0, type: 3, name: NULL

The error seems to have occured when trying to resolve an issue with cleaning up the DescriptorSets in the Material class. Something may have gone wrong in implementing that fix.

Do you know what might be causing it and how to change the code to fix it?