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:

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>

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

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;

    // 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()
{
	// Clean up resources, if necessary
	// (depending on how Shader and Texture resources are managed)
}

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, Shader::Cleanup);
	fragmentShader = std::shared_ptr<Shader>(new Shader, Shader::Cleanup);
	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);
}

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

Shader.h:

#pragma once

#include <vulkan/vulkan.h>
#include <string>

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

	void LoadFromFile(const std::string& filename, VkDevice device, VkShaderStageFlagBits stage);
	VkPipelineShaderStageCreateInfo GetPipelineShaderStageCreateInfo() const;
	static void Cleanup(Shader* shader);

private:
	VkDevice device;
	VkShaderModule shaderModule;
	VkShaderStageFlagBits stage;
};

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

The Material::Cleanup method is currently incomplete. Based on the provided code, can you complete the code for this method?