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. Can you update the code for the Material class below to make use of this functionality, while also aligning with the updated Shader class?

Shader.h:

#pragma once

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

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

	void loadFromFile(const std::string& filename, vk::Device device, vk::ShaderStageFlagBits stage);
	vk::PipelineShaderStageCreateInfo getPipelineShaderStageCreateInfo() const;
	static void cleanup(Shader* shader);

private:
	vk::Device m_device;
	vk::UniqueShaderModule m_shaderModule;
	vk::ShaderStageFlagBits m_stage;
};

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, 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(VkBuffer newBuffer, VkDevice device, VkDeviceSize devicesize);
    VkDescriptorSet GetDescriptorSet() const;
    VkPipelineLayout GetPipelineLayout() const;

    std::shared_ptr <Shader> GetvertexShader();
    std::shared_ptr <Shader> GetfragmentShader();
    void CreateDescriptorSet(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;

    VkDescriptorPool descriptorPool;

    void CleanupDescriptorSetLayout();
};

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, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
    this->device = device;

    this->descriptorSetLayout = descriptorSetLayout;
    this->descriptorPool = descriptorPool;

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

    if (descriptorPool != VK_NULL_HANDLE) {
        vkDestroyDescriptorPool(device, descriptorPool, nullptr);
        descriptorPool = VK_NULL_HANDLE;
    }

    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
}

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