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:

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

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

    material->CreateDescriptorSet(renderer.CreateDescriptorSetLayout(), renderer.CreateDescriptorPool(1), mvpBuffer, sizeof(MVP));
    // 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();
    material->UpdateBufferBinding(descriptorSet, mvpBuffer, device, sizeof(MVP));
    renderer.CreateGraphicsPipeline(mesh, material);
    vkCmdBindPipeline(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.GetPipeline().get()->GetPipeline());
    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)
    delete mesh;
    delete material;
    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);
}

Mesh* GameObject::GetMesh()
{
    return mesh;
}
Material* GameObject::GetMaterial()
{
    return material;
}

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



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

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

Here is some relevant code from the Renderer class:

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

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

void Renderer::Initialize(GLFWwindow* window)
{
	this->window = window;
	CreateInstance();
	CreateSurface();
	ChoosePhysicalDevice();
	CreateDevice();
	CreateSwapchain();
	CreateRenderPass();
	CreateCommandPool();
	CreateFramebuffers();
	CreateSyncObjects();
}

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

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

	CleanupFramebuffers();
	CleanupRenderPass();
	CleanupSyncObjects();
	CleanupCommandBuffers();
	CleanupCommandPool();
	CleanupImageViews();
	CleanupSwapchain();
	if (device != VK_NULL_HANDLE) {
		CleanupDevice();
	}
	DestroySurface();
	CleanupInstance();

	shutdownInProgress = false;
}

I am getting the following error in console when closing the application:

VUID-vkDestroyBuffer-buffer-00922(ERROR / SPEC): msgNum: -464217071 - Validation Error: [ VUID-vkDestroyBuffer-buffer-00922 ] Object 0: handle = 0x1f956bf58f0, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xe4549c11 | Cannot call vkDestroyBuffer on VkBuffer 0x27d60e0000000019[] that is currently in use by a command buffer. The Vulkan spec states: All submitted commands that refer to buffer, either directly or via a VkBufferView, must have completed execution (https://vulkan.lunarg.com/doc/view/1.3.239.0/windows/1.3-extensions/vkspec.html#VUID-vkDestroyBuffer-buffer-00922)
    Objects: 1
        [0] 0x1f956bf58f0, type: 3, name: NULL

What is causing it and how can I modify the code to fix it?