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.

The basic class structure is as follows:

1. Core:

- class Engine
- This is the main class that will initialize, run, and shut down other subsystems of the engine (e.g., Window, Renderer, Input, etc.)

2. Window:

- class Window
- This class will handle window creation, resizing, and destruction using GLFW. It should also handle keyboard and mouse input events.

3. Renderer:

- class Renderer
- This class will manage Vulkan objects (e.g., instance, device, swapchain, command buffers) and rendering pipelines.
- class Shader
- To handle shader modules creation and destruction.
- class Texture
- To handle texture loading and destruction.
- class Pipeline
- To encapsulate the description and creation of a Vulkan graphics or compute pipeline.

4. Scene:

- class Scene
- This class will manage the list of objects in the vector, update their transforms, and send them to the Renderer for rendering.
- class GameObject
- To represent individual objects in the scene, store their transformation matrices, and bind the relevant data like vertices and indices.
- class Camera
- To store and manage camera properties such as position, rotation, view, and projection matrices.
- class Mesh
- To handle mesh data (e.g., vertices, indices, etc.) for individual objects.
- class Material
- To store and manage material properties such as colors, shaders, and textures.

5. Math:

- Utilize GLM library to handle vector and matrix math operations.


Here is some of the relevant header and source file code:

Engine.h:

#pragma once

#include "Window.h"
#include "Renderer.h"
#include "Scene.h"

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

	void Run();
	void Shutdown();

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

	Terrain terrain(0,10,1,renderer.GetDevice(), renderer.GetPhysicalDevice(), renderer.GetCommandPool(), renderer.GetGraphicsQueue());
	terrain.GenerateTerrain(descriptorSetLayout, descriptorPool);

	scene.AddGameObject(terrain.GetTerrainObject());
}

void Engine::MainLoop()
{
	while (!window.ShouldClose())
	{
		window.PollEvents();

		float deltaTime = window.GetDeltaTime();
		Update(deltaTime);
		Render();
	}
}

void Engine::Update(float deltaTime)
{
	scene.Update(deltaTime);
}

void Engine::Render()
{
	renderer.BeginFrame();
	scene.Render(renderer);
	renderer.EndFrame();
}

void Engine::Shutdown()
{
	// 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();

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

	// Add initial game objects
}

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

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

	// 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(const Mesh& mesh, const Material& material);
	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);

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

	Mesh mesh;
	Material material;

	bool initialized = false;

	void UpdateModelMatrix();
};

GameObject.cpp:

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

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

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

void GameObject::Initialize(const Mesh& mesh, const Material& material)
{
	this->mesh = mesh; // copy or share mesh data
	this->material = material; // copy or share material data
    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::Render(Renderer& renderer, const Camera& camera)
{
    // Render this object using the renderer and camera
    VkDevice device = *renderer.GetDevice();

    // Bind mesh vertex and index buffers
    VkBuffer vertexBuffers[] = { mesh.GetVertexBuffer() };
    VkDeviceSize offsets[] = { 0 };
    vkCmdBindVertexBuffers(*renderer.GetCurrentCommandBuffer(), 0, 1, vertexBuffers, offsets);
    vkCmdBindIndexBuffer(*renderer.GetCurrentCommandBuffer(), mesh.GetIndexBuffer(), 0, VK_INDEX_TYPE_UINT32);

    // Update shader uniform buffers with modelMatrix, viewMatrix and projectionMatrix transforms
    struct MVP {
        glm::mat4 model;
        glm::mat4 view;
        glm::mat4 projection;
    } mvp;

    mvp.model = modelMatrix;
    mvp.view = camera.GetViewMatrix();
    mvp.projection = camera.GetProjectionMatrix();

    // Create a new buffer to hold the MVP data temporarily
    VkBuffer mvpBuffer;
    VkDeviceMemory mvpBufferMemory;
    BufferUtils::CreateBuffer(device, *renderer.GetPhysicalDevice(),
        sizeof(MVP), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        mvpBuffer, mvpBufferMemory);

    // Map the MVP data into the buffer and unmap
    void* data = nullptr;
    vkMapMemory(device, mvpBufferMemory, 0, sizeof(MVP), 0, &data);
    memcpy(data, &mvp, sizeof(MVP));
    vkUnmapMemory(device, mvpBufferMemory);

    // TODO: Modify your material, descriptor set, and pipeline to use this new mvpBuffer instead of
    // the default uniform buffer

    // Bind the DescriptorSet associated with the material
    VkDescriptorSet descriptorSet = material.GetDescriptorSet();
    vkCmdBindDescriptorSets(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, material.GetPipelineLayout(), 0, 1, &descriptorSet, 0, nullptr);

    // Call vkCmdDrawIndexed()
    uint32_t numIndices = static_cast<uint32_t>(mesh.GetIndices().size());
    vkCmdDrawIndexed(*renderer.GetCurrentCommandBuffer(), numIndices, 1, 0, 0, 0);

    // Cleanup the temporary buffer
    vkDestroyBuffer(device, mvpBuffer, nullptr);
    vkFreeMemory(device, mvpBufferMemory, nullptr);
}

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

void GameObject::SetPosition(const glm::vec3& position)
{
	this->position = position;
	UpdateModelMatrix();
}

void GameObject::SetRotation(const glm::vec3& rotation)
{
	this->rotation = rotation;
	UpdateModelMatrix();
}

void GameObject::SetScale(const glm::vec3& scale)
{
	this->scale = scale;
	UpdateModelMatrix();
}

void GameObject::UpdateModelMatrix()
{
	modelMatrix = glm::mat4(1.0f);
	modelMatrix = glm::translate(modelMatrix, position);
	modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
	modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
	modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
	modelMatrix = glm::scale(modelMatrix, scale);
}

Terrain.h:

#pragma once

#include "Simplex.h"
#include "Mesh.h"
#include "Material.h"
#include "GameObject.h"

class Terrain {
public:
    Terrain(int seed, int worldSize, float scale, VkDevice* device, VkPhysicalDevice* physicalDevice, VkCommandPool* commandPool, VkQueue* graphicsQueue);
    ~Terrain();

    void GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool);
    GameObject* GetTerrainObject();

private:
    void GenerateHeightMap();
    void ConvertHeightMapToMeshData();

    int seed;
    int worldSize;
    float scale;

    VkDevice* device;
    VkPhysicalDevice* physicalDevice;
    VkCommandPool* commandPool;
    VkQueue* graphicsQueue;

    SimplexNoise noise;
    std::vector<std::vector<float>> heightMap;
    std::vector<Vertex> vertices;
    std::vector<uint32_t> indices;

    Mesh terrainMesh;
    Material terrainMaterial;
    GameObject terrainObject;
};

Terrain.cpp:

#include "Terrain.h"

Terrain::Terrain(int seed, int worldSize, float scale, VkDevice* device, VkPhysicalDevice* physicalDevice, VkCommandPool* commandPool, VkQueue* graphicsQueue)
    : seed(seed), worldSize(worldSize), scale(scale), noise(seed), device(device), physicalDevice(physicalDevice), commandPool(commandPool), graphicsQueue(graphicsQueue) {
}

Terrain::~Terrain() {
    // Cleanup any resources associated with the Terrain class
    //terrainMesh.Cleanup();
    //terrainMaterial.Cleanup();
}

void Terrain::GenerateHeightMap() {
    heightMap = std::vector<std::vector<float>>(worldSize, std::vector<float>(worldSize, 0.0f));

    for (int x = 0; x < worldSize; ++x) {
        for (int z = 0; z < worldSize; ++z) {
            float noiseValue = noise.noise(x * scale, z * scale);
            heightMap[x][z] = noiseValue;
        }
    }
}

void Terrain::ConvertHeightMapToMeshData()
{
    vertices.clear();
    indices.clear();

    // Generate vertices
    for (int x = 0; x < worldSize; ++x)
    {
        for (int z = 0; z < worldSize; ++z)
        {
            Vertex vertex;
            vertex.position = glm::vec3(static_cast<float>(x), heightMap[x][z], static_cast<float>(z));

            // Set color based on height, feel free to customize this as desired
            float color = heightMap[x][z] * 0.5f + 0.5f; // Normalize the height value between 0 and 1
            vertex.color = glm::vec3(color, color, color);

            vertices.push_back(vertex);
        }
    }

    // Generate indices for rendering using triangle strips
    for (int x = 0; x < worldSize - 1; ++x)
    {
        for (int z = 0; z < worldSize - 1; ++z)
        {
            int topLeft = x * worldSize + z;
            int topRight = topLeft + 1;
            int bottomLeft = (x + 1) * worldSize + z;
            int bottomRight = bottomLeft + 1;

            // First triangle
            indices.push_back(topLeft);
            indices.push_back(bottomRight);
            indices.push_back(bottomLeft);

            // Second triangle
            indices.push_back(topLeft);
            indices.push_back(topRight);
            indices.push_back(bottomRight);
        }
    }
}

void Terrain::GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    GenerateHeightMap();
    ConvertHeightMapToMeshData();
    terrainMesh.Initialize(vertices, indices, *device, *physicalDevice, *commandPool, *graphicsQueue);

    // Initialize terrainMaterial with the loaded shaders and texture
    // Assume descriptorSetLayout and descriptorPool are created before this call
    terrainMaterial.Initialize("C:/shaders/vert.spv", "C:/shaders/frag.spv", "C:/textures/texture.jpg", *device, descriptorSetLayout, descriptorPool, *physicalDevice, *commandPool, *graphicsQueue);

    terrainObject.Initialize(terrainMesh, terrainMaterial);
}

GameObject* Terrain::GetTerrainObject() {
    return &terrainObject;
}

Material.h:

#pragma once

#include <vulkan/vulkan.h>
#include "Texture.h"
#include "Shader.h"
#include <stdexcept>
#include <memory> // Don’t forget to include <memory>

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

    VkDescriptorSet GetDescriptorSet() const;
    VkPipelineLayout GetPipelineLayout() const;

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

    void CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool);
    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, 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(descriptorSetLayout, descriptorPool);
    CreatePipelineLayout(descriptorSetLayout);
}

void Material::CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    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();

    VkWriteDescriptorSet descriptorWrite{};
    descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrite.dstSet = descriptorSet;
    descriptorWrite.dstBinding = 0;
    descriptorWrite.dstArrayElement = 0;
    descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorWrite.descriptorCount = 1;
    descriptorWrite.pImageInfo = &imageInfo;

    vkUpdateDescriptorSets(device, 1, &descriptorWrite, 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;
}

void Material::LoadTexture(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	texture = std::shared_ptr<Texture>(new Texture{}, Texture::Cleanup); // Create a new Texture using shared_ptr
	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);
}

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;
	static void Cleanup(Texture* texture);

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

Texture.cpp:

#include "Texture.h"
#include "BufferUtils.h"
#include <iostream>

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

Texture::~Texture()
{
}

void Texture::LoadFromFile(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    // Load image from file using stb_image
    int width, height, channels;
    stbi_uc* pixels = stbi_load(filename.c_str(), &width, &height, &channels, STBI_rgb_alpha);
    if (!pixels)
    {
        throw std::runtime_error("Failed to load texture image!");
    }

    // Calculate the number of mip levels
    uint32_t mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1;

    // Create a buffer to store the image data
    VkDeviceSize imageSize = width * height * 4;
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    // Create and fill the buffer
    // …
    // Create the staging buffer for transferring image data
    VkBufferCreateInfo bufferCreateInfo{};
    bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferCreateInfo.size = imageSize;
    bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
    bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    if (vkCreateBuffer(device, &bufferCreateInfo, nullptr, &stagingBuffer) != VK_SUCCESS) {
        throw std::runtime_error("Failed to create staging buffer!");
    }

    VkMemoryRequirements memoryRequirements;
    vkGetBufferMemoryRequirements(device, stagingBuffer, &memoryRequirements);

    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memoryRequirements.size;
    allocInfo.memoryTypeIndex = BufferUtils::FindMemoryType(physicalDevice, memoryRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

    if (vkAllocateMemory(device, &allocInfo, nullptr, &stagingBufferMemory) != VK_SUCCESS) {
        throw std::runtime_error("Failed to allocate staging buffer memory!");
    }


    // Create the staging buffer and staging buffer memory
    BufferUtils::CreateBuffer(device, physicalDevice, imageSize,VK_BUFFER_USAGE_TRANSFER_SRC_BIT,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,stagingBuffer, stagingBufferMemory);

    vkBindBufferMemory(device, stagingBuffer, stagingBufferMemory, 0);

    // Copy image data to the buffer
    void* bufferData;
    vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &bufferData);
    memcpy(bufferData, pixels, static_cast<size_t>(imageSize));
    vkUnmapMemory(device, stagingBufferMemory);

    // Free the stb_image buffer
    stbi_image_free(pixels);

    // Create vkImage, copy buffer to image, and create imageView and sampler
    // …

    CreateImage(width, height, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL,
        VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
    CreateImageView(VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels);
    CreateSampler(mipLevels);

    CopyBufferToImage(stagingBuffer, width, height);

    // Cleanup the staging buffer and staging buffer memory
    // …

    // Destroy the staging buffer and free the staging buffer memory
    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}

void Texture::Cleanup(Texture* texture)
{
    // Put the content of the old Cleanup() method here
    // Make sure to replace this with texture keyword
    // …
}

VkImageView Texture::GetImageView() const
{
	return imageView;
}

VkSampler Texture::GetSampler() const
{
	return sampler;
}

void Texture::CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties)
{
    VkImageCreateInfo imageInfo{};
    imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
    imageInfo.imageType = VK_IMAGE_TYPE_2D;
    imageInfo.extent.width = width;
    imageInfo.extent.height = height;
    imageInfo.extent.depth = 1;
    imageInfo.mipLevels = mipLevels;
    imageInfo.arrayLayers = 1;
    imageInfo.format = format;
    imageInfo.tiling = tiling;
    imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    imageInfo.usage = usage;
    imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    imageInfo.samples = numSamples;
    imageInfo.flags = 0;

    if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create image!");
    }

    VkMemoryRequirements memRequirements;
    vkGetImageMemoryRequirements(device, image, &memRequirements);

    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = BufferUtils::FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);

    if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to allocate image memory!");
    }

    vkBindImageMemory(device, image, imageMemory, 0);
}

void Texture::TransitionImageLayout(VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels)
{
    // …
    // Create a one-time-use command buffer and record image layout transition commands
    // …

    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.image = image;
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    barrier.subresourceRange.baseMipLevel = 0;
    barrier.subresourceRange.levelCount = mipLevels;
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 1;

    // …
}

void Texture::CreateImageView(VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels)
{
    VkImageViewCreateInfo viewInfo{};
    viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
    viewInfo.image = image;
    viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
    viewInfo.format = format;
    viewInfo.subresourceRange.aspectMask = aspectFlags;
    viewInfo.subresourceRange.baseMipLevel = 0;
    viewInfo.subresourceRange.levelCount = mipLevels;
    viewInfo.subresourceRange.baseArrayLayer = 0;
    viewInfo.subresourceRange.layerCount = 1;

    if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create texture image view!");
    }
}

void Texture::CreateSampler(uint32_t mipLevels)
{
    VkSamplerCreateInfo samplerInfo{};
    samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    samplerInfo.magFilter = VK_FILTER_LINEAR;
    samplerInfo.minFilter = VK_FILTER_LINEAR;
    samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    samplerInfo.anisotropyEnable = VK_TRUE;
    samplerInfo.maxAnisotropy = 16;
    samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
    samplerInfo.unnormalizedCoordinates = VK_FALSE;
    samplerInfo.compareEnable = VK_FALSE;
    samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
    samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    samplerInfo.mipLodBias = 0.0f;
    samplerInfo.minLod = 0.0f;
    samplerInfo.maxLod = static_cast<float>(mipLevels);

    if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler) != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create texture sampler!");
    }
}

void Texture::CopyBufferToImage(VkBuffer buffer, uint32_t width, uint32_t height)
{
    // Create a one-time-use command buffer
    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    // Record buffer to image copy command in the command buffer
    vkBeginCommandBuffer(commandBuffer, &beginInfo);

    VkBufferImageCopy region{};
    region.bufferOffset = 0;
    region.bufferRowLength = 0;
    region.bufferImageHeight = 0;
    region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    region.imageSubresource.mipLevel = 0;
    region.imageSubresource.baseArrayLayer = 0;
    region.imageSubresource.layerCount = 1;
    region.imageOffset = { 0, 0, 0 };
    region.imageExtent = {
        width,
        height,
        1
    };

    vkCmdCopyBufferToImage(
        commandBuffer,
        buffer,
        image,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        1,
        &region
    );

    // End command buffer recording
    vkEndCommandBuffer(commandBuffer);

    // Submit command buffer
    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
    vkQueueWaitIdle(graphicsQueue);

    // Free command buffer
    vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
}



Mesh.h:

#pragma once

#include <vector>
#include <vulkan/vulkan.h>
#include <glm/glm.hpp>
#include "BufferUtils.h"

struct Vertex
{
	glm::vec3 position;
	glm::vec3 color;
};

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

	void Initialize(std::vector<Vertex> vertices, std::vector<uint32_t> indices, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Cleanup();

	const std::vector<Vertex>& GetVertices() const;
	const std::vector<uint32_t>& GetIndices() const;
	VkBuffer GetVertexBuffer() const;
	VkBuffer GetIndexBuffer() const;

	void SetVertices(const std::vector<Vertex>& vertices);
	void SetIndices(const std::vector<uint32_t>& indices);

private:
	VkDevice device;
	std::vector<Vertex> vertices;
	std::vector<uint32_t> indices;

	VkBuffer vertexBuffer;
	VkDeviceMemory vertexBufferMemory;
	VkBuffer indexBuffer;
	VkDeviceMemory indexBufferMemory;
};

Mesh.cpp:

#include "Mesh.h"

Mesh::Mesh()
	: device(VK_NULL_HANDLE), vertexBuffer(VK_NULL_HANDLE), vertexBufferMemory(VK_NULL_HANDLE), indexBuffer(VK_NULL_HANDLE), indexBufferMemory(VK_NULL_HANDLE)
{
}

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

void Mesh::Initialize(std::vector<Vertex> vertices, std::vector<uint32_t> indices, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->vertices = vertices;
	this->indices = indices;
	this->device = device;

	Initialize(device, physicalDevice, commandPool, graphicsQueue);
	// Create vertex buffer and index buffer
	// (assuming you have helper functions CreateBuffer and CopyBuffer)
	// …
}

void Mesh::Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->device = device;

	// Create vertex buffer and index buffer
	// (assuming you have helper functions CreateBuffer and CopyBuffer)
	// …

	// Declare and initialize stagingBuffer and bufferSize here

	VkBuffer stagingBuffer;
	VkDeviceMemory stagingBufferMemory;
	VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingBuffer, stagingBufferMemory);

	void* data;
	vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
	memcpy(data, vertices.data(), (size_t)bufferSize);
	vkUnmapMemory(device, stagingBufferMemory);

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		vertexBuffer, vertexBufferMemory);

	BufferUtils::CopyBuffer(device, commandPool, graphicsQueue, stagingBuffer, vertexBuffer, bufferSize);

	vkDestroyBuffer(device, stagingBuffer, nullptr);
	vkFreeMemory(device, stagingBufferMemory, nullptr);

	bufferSize = sizeof(indices[0]) * indices.size();
	VkBuffer stagingIndexBuffer;
	VkDeviceMemory stagingIndexBufferMemory;

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingIndexBuffer, stagingIndexBufferMemory);

	vkMapMemory(device, stagingIndexBufferMemory, 0, bufferSize, 0, &data);
	memcpy(data, indices.data(), (size_t)bufferSize);
	vkUnmapMemory(device, stagingIndexBufferMemory);

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		indexBuffer, indexBufferMemory);

	BufferUtils::CopyBuffer(device, commandPool, graphicsQueue, stagingIndexBuffer, indexBuffer, bufferSize);

	vkDestroyBuffer(device, stagingIndexBuffer, nullptr);
	vkFreeMemory(device, stagingIndexBufferMemory, nullptr);
}

void Mesh::Cleanup()
{
	if (device != VK_NULL_HANDLE)
	{
		if (vertexBuffer != VK_NULL_HANDLE)
		{
			vkDestroyBuffer(device, vertexBuffer, nullptr);
			vkFreeMemory(device, vertexBufferMemory, nullptr);
			vertexBuffer = VK_NULL_HANDLE;
			vertexBufferMemory = VK_NULL_HANDLE;
		}

		if (indexBuffer != VK_NULL_HANDLE)
		{
			vkDestroyBuffer(device, indexBuffer, nullptr);
			vkFreeMemory(device, indexBufferMemory, nullptr);
			indexBuffer = VK_NULL_HANDLE;
			indexBufferMemory = VK_NULL_HANDLE;
		}
	}
}

const std::vector<Vertex>& Mesh::GetVertices() const
{
	return vertices;
}

const std::vector<uint32_t>& Mesh::GetIndices() const
{
	return indices;
}

VkBuffer Mesh::GetVertexBuffer() const
{
	return vertexBuffer;
}

VkBuffer Mesh::GetIndexBuffer() const
{
	return indexBuffer;
}
void Mesh::SetVertices(const std::vector<Vertex>& vertices)
{
	this->vertices = vertices;
}

void Mesh::SetIndices(const std::vector<uint32_t>& indices)
{
	this->indices = indices;
}

The code is throwing an access violation error in the Mesh::Cleanup. I don't see why this code is even bring called as no Mesh object should be having it's Cleanup method triggered. Do you know what is causing this issue and how to modify the code to fix it?