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

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, VkDescriptorSetLayout descriptorSetLayout, VkDescriptorSetLayout samplerDescriptorSetLayout, 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();
};

Material.cpp:

#include "Material.h"

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

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

void Material::Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, VkDevice device, VkDescriptorSetLayout descriptorSetLayout, VkDescriptorSetLayout samplerDescriptorSetLayout, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
    this->device = device;

    this->descriptorSetLayout = descriptorSetLayout;
    this->samplerDescriptorSetLayout = samplerDescriptorSetLayout;

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

    // Create descriptor set and pipeline layout
    //CreateDescriptorSet(samplerDescriptorSetLayout, descriptorPool);
    CreatePipelineLayout(descriptorSetLayout);
}

void Material::CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool, VkBuffer uniformBuffer, VkDeviceSize bufferSize)
{
    VkDescriptorSetAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
    allocInfo.descriptorPool = descriptorPool;
    allocInfo.descriptorSetCount = 1;
    allocInfo.pSetLayouts = &descriptorSetLayout;

    if (vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet) != VK_SUCCESS) {
        throw std::runtime_error("Failed to allocate descriptor sets!");
    }

    VkDescriptorImageInfo imageInfo{};
    imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageInfo.imageView = texture->GetImageView();
    imageInfo.sampler = texture->GetSampler();

    VkDescriptorBufferInfo bufferInfo{};
    bufferInfo.buffer = uniformBuffer;
    bufferInfo.offset = 0;
    bufferInfo.range = bufferSize;

    std::array<VkWriteDescriptorSet, 2> descriptorWrites{};

    descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrites[0].dstSet = descriptorSet;
    descriptorWrites[0].dstBinding = 0;
    descriptorWrites[0].dstArrayElement = 0;
    descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    descriptorWrites[0].descriptorCount = 1;
    descriptorWrites[0].pBufferInfo = &bufferInfo;

    descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrites[1].dstSet = descriptorSet;
    descriptorWrites[1].dstBinding = 1;
    descriptorWrites[1].dstArrayElement = 0;
    descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorWrites[1].descriptorCount = 1;
    descriptorWrites[1].pImageInfo = &imageInfo;

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

void Material::CreatePipelineLayout(VkDescriptorSetLayout descriptorSetLayout)
{
    VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipelineLayoutInfo.setLayoutCount = 1;
    pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

    if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
        throw std::runtime_error("Failed to create pipeline layout!");
    }
}



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
}

VkDescriptorSet Material::GetDescriptorSet() const
{
	return descriptorSet;
}

VkPipelineLayout 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, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
    texture = std::shared_ptr<Texture>(new Texture{}, [device](Texture* textureToDelete) {
        textureToDelete->Cleanup();
        delete textureToDelete;
        });
    texture->LoadFromFile(filename, device, physicalDevice, commandPool, graphicsQueue);
}

void Material::LoadShaders(const std::string& vertFilename, const std::string& fragFilename, VkDevice device)
{
    vertexShader = std::shared_ptr<Shader>(new Shader, ShaderDeleter());
    fragmentShader = std::shared_ptr<Shader>(new Shader, ShaderDeleter());
    vertexShader->LoadFromFile(vertFilename, device, VK_SHADER_STAGE_VERTEX_BIT);
    fragmentShader->LoadFromFile(fragFilename, device, VK_SHADER_STAGE_FRAGMENT_BIT);
}

void Material::UpdateBufferBinding(VkDescriptorSet descriptorSet, VkBuffer newBuffer, VkDevice device, VkDeviceSize devicesize)
{
    VkDescriptorBufferInfo bufferInfo{};
    bufferInfo.buffer = newBuffer;
    bufferInfo.offset = 0;
    bufferInfo.range = devicesize;

    VkDescriptorImageInfo imageInfo{};
    imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageInfo.imageView = texture->GetImageView();
    imageInfo.sampler = texture->GetSampler();

    std::array<VkWriteDescriptorSet, 2> descriptorWrites{};
    descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrites[0].dstSet = descriptorSet;
    descriptorWrites[0].dstBinding = 0;
    descriptorWrites[0].dstArrayElement = 0;
    descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    descriptorWrites[0].descriptorCount = 1;
    descriptorWrites[0].pBufferInfo = &bufferInfo;

    descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrites[1].dstSet = descriptorSet;
    descriptorWrites[1].dstBinding = 1;
    descriptorWrites[1].dstArrayElement = 0;
    descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorWrites[1].descriptorCount = 1;
    descriptorWrites[1].pImageInfo = &imageInfo;

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

void Material::CleanupDescriptorSetLayout()
{
    if (device != VK_NULL_HANDLE && descriptorSetLayout != VK_NULL_HANDLE)
    {
        vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
        descriptorSetLayout = VK_NULL_HANDLE;
    }
}

void Material::CleanupSamplerDescriptorSetLayout()
{
    if (device != VK_NULL_HANDLE && samplerDescriptorSetLayout != VK_NULL_HANDLE)
    {
        vkDestroyDescriptorSetLayout(device, samplerDescriptorSetLayout, nullptr);
        samplerDescriptorSetLayout = VK_NULL_HANDLE;
    }
}

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

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)
	{
		gameObject->Shutdown(); // Make sure to call Shutdown() on the game objects
		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)
    if (material) {
        material->Cleanup();
        delete material;
        material = nullptr;
    }

    if (mesh) {
        delete mesh;
        mesh = nullptr;
    }

    this->initialized = false;
}

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:

Texture::Texture()
	: device(VK_NULL_HANDLE), image(VK_NULL_HANDLE), imageMemory(VK_NULL_HANDLE), imageView(VK_NULL_HANDLE), sampler(VK_NULL_HANDLE)
{
}

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


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

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::CleanupDevice()
{
	// Reset the pipeline first to ensure it’s destroyed before the device
	pipeline.reset();

	// Destroy the logical device
	vkDestroyDevice(device, nullptr);
}


I am getting this error in Renderer::CleanupDevice when trying to close the application:

VUID-vkDestroyDevice-device-00378(ERROR / SPEC): msgNum: 1901072314 - Validation Error: [ VUID-vkDestroyDevice-device-00378 ] Object 0: handle = 0x250768af840, type = VK_OBJECT_TYPE_DEVICE; Object 1: handle = 0x149d740000000087, type = VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT; | MessageID = 0x71500fba | OBJ ERROR : For VkDevice 0x250768af840[], VkDescriptorSetLayout 0x149d740000000087[] has not been destroyed. The Vulkan spec states: All child objects created on device must have been destroyed prior to destroying device (https://vulkan.lunarg.com/doc/view/1.3.239.0/windows/1.3-extensions/vkspec.html#VUID-vkDestroyDevice-device-00378)
    Objects: 2
        [0] 0x250768af840, type: 3, name: NULL
        [1] 0x149d740000000087, type: 20, name: NULL

What might be causing this issue and how can I fix it? I have checked through breakpoints in debug mode that Materials::Cleanup is destroying the descriptorsetlayouts.