vulkan-test/src/main.cpp

2446 lines
91 KiB
C++
Raw Normal View History

// Include necessary headers
#include <chrono> // For time-related functions
#include <fmt/format.h> // For string formatting
#include <unordered_set> // For unordered_set container
// GLM (OpenGL Mathematics) configuration
#define GLM_FORCE_DEPTH_ZERO_TO_ONE // Use Vulkan's depth range (0 to 1) instead of OpenGL's (-1 to 1)
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES // Force GLM to use aligned types
#include <glm/glm.hpp> // Include GLM for mathematics operations
// TinyObjLoader for loading 3D models
2024-10-11 20:04:28 -04:00
#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>
// Vulkan configuration and inclusion
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1 // Use dynamic dispatch for Vulkan functions
#define VK_ENABLE_BETA_EXTENSIONS // Enable beta Vulkan extensions
#define VULKAN_HPP_NO_CONSTRUCTORS // Use aggregate initialization for Vulkan structs
#include <vulkan/vulkan.hpp> // Include Vulkan C++ bindings
2024-09-29 23:02:04 -04:00
// Necessary for dynamic dispatch to work
2024-10-06 12:26:44 -04:00
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
// Include custom utility headers
#include "util/types.h" // Custom type definitions
#include "util/unique_image.h" // Custom image handling utilities
#include "util/vertex.h" // Custom vertex structure definition
2024-10-12 15:39:20 -04:00
// ImGui headers for GUI
2024-10-15 19:49:39 -04:00
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>
// GLFW configuration and inclusion
#define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs
#include "vkfw.hpp" // Include GLFW C++ bindings
2024-09-25 23:03:56 -04:00
// Constants for window dimensions
constexpr i32 WIDTH = 800;
constexpr i32 HEIGHT = 600;
2024-09-25 23:03:56 -04:00
// File paths for 3D model and texture
2024-10-11 20:04:28 -04:00
constexpr const char* MODEL_PATH = "models/viking_room.obj";
constexpr const char* TEXTURE_PATH = "textures/viking_room.png";
// File paths for shader programs
2024-10-12 15:39:20 -04:00
constexpr const char* FRAGMENT_SHADER_PATH = "shaders/frag.spv";
constexpr const char* VERTEX_SHADER_PATH = "shaders/vert.spv";
2024-10-05 23:08:12 -04:00
// Maximum number of frames that can be processed concurrently
2024-10-12 15:39:20 -04:00
constexpr i32 MAX_FRAMES_IN_FLIGHT = 2;
// Validation layers for debug builds
2024-10-14 23:32:52 -04:00
#ifndef NDEBUG
constexpr std::array<const char*, 1> validationLayers = { "VK_LAYER_KHRONOS_validation" };
#endif
// Required device extensions (platform-specific)
#ifdef __APPLE__
constexpr std::array<const char*, 2> deviceExtensions = { vk::KHRSwapchainExtensionName,
vk::KHRPortabilitySubsetExtensionName };
#else
constexpr std::array<const char*, 1> deviceExtensions = { vk::KHRSwapchainExtensionName };
#endif
// Main application class
2024-09-29 23:11:12 -04:00
class VulkanApp {
2024-09-25 23:03:56 -04:00
public:
/**
* @brief Runs the Vulkan application.
*
* This function initializes the application window, sets up Vulkan, and enters the main rendering loop.
* It also cleans up resources when the application is closed.
*/
2024-09-30 00:57:13 -04:00
fn run() -> void {
initWindow(); // Initialize the application window
initVulkan(); // Initialize Vulkan
mainLoop(); // Enter the main rendering loop
2024-10-15 19:49:39 -04:00
cleanupSwapChain(); // Clean up swap chain resources
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
if (mUniformBuffersMapped[i]) {
mDevice->unmapMemory(mUniformBuffersMemory[i].get());
mUniformBuffersMapped[i] = nullptr;
}
2024-10-15 19:49:39 -04:00
// Shut down ImGui
2024-10-15 19:49:39 -04:00
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
2024-09-25 23:03:56 -04:00
}
private:
// GLFW and Vulkan instance handles
vkfw::UniqueInstance mVKFWInstance; // GLFW instance
vkfw::UniqueWindow mWindow; // Application window
vk::UniqueInstance mInstance; // Vulkan instance
// Debug messenger for validation layers
vk::UniqueDebugUtilsMessengerEXT mDebugMessenger;
// Vulkan surface for rendering
vk::UniqueSurfaceKHR mSurface;
// Vulkan devices and queues
vk::PhysicalDevice mPhysicalDevice; // Physical GPU
vk::SampleCountFlagBits mMsaaSamples; // Multisampling count
vk::UniqueDevice mDevice; // Logical Vulkan device
vk::Queue mGraphicsQueue; // Queue for graphics commands
vk::Queue mPresentQueue; // Queue for presentation commands
// Swap chain and related resources
vk::UniqueSwapchainKHR mSwapChain; // Swap chain for frame buffering
std::vector<vk::Image> mSwapChainImages; // Images in the swap chain
vk::Format mSwapChainImageFormat; // Format of swap chain images
vk::Extent2D mSwapChainExtent; // Dimensions of swap chain images
std::vector<vk::UniqueImageView> mSwapChainImageViews; // Image views for swap chain images
std::vector<vk::UniqueFramebuffer> mSwapChainFramebuffers; // Framebuffers for rendering
// Render pass and pipeline objects
vk::UniqueRenderPass mRenderPass; // Render pass definition
2024-10-15 20:38:05 -04:00
vk::UniqueDescriptorSetLayout mDescriptorSetLayout; // Descriptor set layout
vk::UniquePipelineLayout mPipelineLayout; // Pipeline layout
vk::UniquePipeline mGraphicsPipeline; // Graphics pipeline
// Command pool for allocating command buffers
vk::UniqueCommandPool mCommandPool;
2024-10-01 18:54:41 -04:00
// Multisampling color resources
vk::UniqueImage mColorImage;
vk::UniqueDeviceMemory mColorImageMemory;
vk::UniqueImageView mColorImageView;
2024-10-15 20:38:05 -04:00
// Depth buffer resources
vk::UniqueImage mDepthImage;
vk::UniqueDeviceMemory mDepthImageMemory;
vk::UniqueImageView mDepthImageView;
2024-10-15 20:38:05 -04:00
// Texture resources
u32 mMipLevels; // Number of mipmap levels
vk::UniqueImage mTextureImage; // Texture image
vk::UniqueDeviceMemory mTextureImageMemory; // Memory for texture image
vk::UniqueImageView mTextureImageView; // Image view for texture
vk::UniqueSampler mTextureSampler; // Sampler for texture
// Vertex and index data
std::vector<Vertex> mVertices; // Vertex data for the model
std::vector<u32> mIndices; // Index data for the model
vk::UniqueBuffer mVertexBuffer; // Buffer for vertex data
vk::UniqueDeviceMemory mVertexBufferMemory; // Memory for vertex buffer
vk::UniqueBuffer mIndexBuffer; // Buffer for index data
vk::UniqueDeviceMemory mIndexBufferMemory; // Memory for index buffer
// Uniform buffers for shader parameters
std::vector<vk::UniqueBuffer> mUniformBuffers;
std::vector<vk::UniqueDeviceMemory> mUniformBuffersMemory;
std::vector<void*> mUniformBuffersMapped;
// Descriptor pools and sets
vk::UniqueDescriptorPool mDescriptorPool; // Descriptor pool for the application
vk::UniqueDescriptorPool mImGuiDescriptorPool; // Separate descriptor pool for ImGui
std::vector<vk::DescriptorSet> mDescriptorSets; // Descriptor sets for binding resources
// Command buffers for recording drawing commands
std::vector<vk::UniqueCommandBuffer> mCommandBuffers;
2024-10-15 20:38:05 -04:00
// Synchronization primitives
std::vector<vk::UniqueSemaphore>
mImageAvailableSemaphores; // Signals that an image is available for rendering
std::vector<vk::UniqueSemaphore> mRenderFinishedSemaphores; // Signals that rendering has finished
std::vector<vk::UniqueFence> mInFlightFences; // Ensures CPU-GPU synchronization
2024-10-15 20:38:05 -04:00
// State variables
2024-10-15 20:38:05 -04:00
bool mFramebufferResized = false; // Flag indicating if the framebuffer was resized
u32 mCurrentFrame = 0; // Index of the current frame being rendered
2024-10-15 20:38:05 -04:00
// Helper struct to store queue family indices
2024-09-30 00:31:08 -04:00
struct QueueFamilyIndices {
std::optional<u32> graphics_family; // Index of graphics queue family
std::optional<u32> present_family; // Index of presentation queue family
2024-09-30 00:31:08 -04:00
// Check if all required queue families are found
2024-09-30 00:57:13 -04:00
fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); }
2024-09-30 00:31:08 -04:00
};
2024-09-28 21:55:26 -04:00
// Helper struct to store swap chain support details
struct SwapChainSupportDetails {
2024-10-15 20:38:05 -04:00
vk::SurfaceCapabilitiesKHR capabilities; // Surface capabilities
std::vector<vk::SurfaceFormatKHR> formats; // Supported surface formats
std::vector<vk::PresentModeKHR> present_modes; // Supported presentation modes
};
2024-10-15 20:38:05 -04:00
// Struct to hold uniform buffer object data
struct UniformBufferObject {
alignas(16) glm::mat4 model; // Model transformation matrix
alignas(16) glm::mat4 view; // View transformation matrix
2024-10-15 20:38:05 -04:00
alignas(16) glm::mat4 proj; // Projection matrix
};
/**
* @brief Reads the contents of a file into a vector of chars.
*
* @param filename The path to the file to be read.
* @return std::vector<char> A vector containing the file's contents.
* @throws std::runtime_error if the file cannot be opened.
*
* This function opens the specified file in binary mode, reads its entire contents
* into a vector of chars, and returns that vector. It's commonly used for reading
* shader files or other binary data needed by the application.
*/
2024-10-01 18:30:31 -04:00
static fn readFile(const std::string& filename) -> std::vector<char> {
2024-10-14 23:32:52 -04:00
// Open the file in binary mode
std::ifstream file(filename, std::ios::binary);
2024-10-01 18:30:31 -04:00
// Check if the file was successfully opened
2024-10-14 23:32:52 -04:00
if (!file)
throw std::runtime_error("Failed to open file: " + filename);
2024-10-01 18:30:31 -04:00
// Read the entire file content into a vector
// This constructor reads from the start (first iterator) to the end (default constructor) of the stream
2024-10-14 23:32:52 -04:00
std::vector<char> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
2024-10-01 18:30:31 -04:00
return buffer;
}
/**
* @brief Update the FPS counter in the window title.
*
* @param window The window to update.
* @param baseTitle The base title string to prepend the FPS to.
*
* This function updates the window title with the current frames per second.
*/
static fn updateFPS(const vkfw::Window& window, const std::string& baseTitle) -> void {
static int FrameCount = 0;
static double LastTime = glfwGetTime();
static double Fps = 0.0;
// Get the current time
double currentTime = glfwGetTime();
FrameCount++;
// If one second has passed, calculate the FPS
if (currentTime - LastTime >= 0.1) {
Fps = FrameCount / (currentTime - LastTime);
LastTime = currentTime;
FrameCount = 0;
// Update window title
std::string newTitle = format("{} - {:.0F} FPS", baseTitle, Fps);
glfwSetWindowTitle(window, newTitle.c_str());
}
}
/**
* @brief Initializes the application window using GLFW.
*
* This function performs the following tasks:
* 1. Initializes the GLFW library.
* 2. Creates a window with the specified dimensions and title.
* 3. Sets up a callback for window resize events.
*
* The window is created without a default OpenGL context, as we'll be using Vulkan.
*/
2024-09-30 00:57:13 -04:00
fn initWindow() -> void {
2024-10-14 23:32:52 -04:00
// Initialize GLFW
2024-10-12 15:39:20 -04:00
mVKFWInstance = vkfw::initUnique();
2024-09-25 23:03:56 -04:00
// Set window creation hints
vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone }; // No OpenGL context
2024-09-30 16:46:17 -04:00
2024-10-14 23:32:52 -04:00
// Create the window
mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints);
2024-10-14 23:32:52 -04:00
// Set the user pointer to this instance, allowing us to access it in callbacks
2024-10-05 23:08:12 -04:00
mWindow->setUserPointer(this);
2024-10-14 23:32:52 -04:00
// Set up the window resize callback
2024-10-12 15:39:20 -04:00
mWindow->callbacks()->on_window_resize =
2024-10-14 23:32:52 -04:00
[](const vkfw::Window& window, usize /*width*/, usize /*height*/) -> void {
// Set the framebuffer resized flag when the window is resized
2024-10-12 15:39:20 -04:00
std::bit_cast<VulkanApp*>(window.getUserPointer())->mFramebufferResized = true;
};
2024-09-25 23:03:56 -04:00
}
/**
* @brief Initializes Vulkan by setting up all necessary components.
*
* This function calls a series of helper functions to set up the Vulkan environment.
* It creates and configures all the Vulkan objects needed for rendering, including:
* - Vulkan instance
* - Debug messenger (for validation layers)
* - Surface (for presenting rendered images)
* - Physical and logical devices
* - Swap chain
* - Render pass and graphics pipeline
* - Buffers and images
* - Synchronization objects
*
* The order of these function calls is important, as many Vulkan objects depend on
* others that must be created first.
*/
2024-09-30 00:57:13 -04:00
fn initVulkan() -> void {
createInstance(); // Create the Vulkan instance
setupDebugMessenger(); // Set up debug messaging (validation layers)
createSurface(); // Create the window surface
pickPhysicalDevice(); // Select a suitable GPU
createLogicalDevice(); // Create a logical device from the chosen GPU
createSwapChain(); // Create the swap chain for presenting images
createImageViews(); // Create image views for the swap chain images
createRenderPass(); // Set up the render pass
createDescriptorSetLayout(); // Create the descriptor set layout
createGraphicsPipeline(); // Create the graphics pipeline
createCommandPool(); // Create a command pool for allocating command buffers
createColorResources(); // Create resources for multisampling
createDepthResources(); // Create resources for depth testing
createFramebuffers(); // Create framebuffers for rendering
createTextureImage(); // Load and create the texture image
createTextureImageView(); // Create an image view for the texture
createTextureSampler(); // Create a sampler for the texture
loadModel(); // Load the 3D model
createVertexBuffer(); // Create a buffer for vertex data
createIndexBuffer(); // Create a buffer for index data
createUniformBuffers(); // Create uniform buffers for shader parameters
createDescriptorPool(); // Create a descriptor pool
createDescriptorSets(); // Allocate and update descriptor sets
createCommandBuffers(); // Create command buffers for rendering commands
createSyncObjects(); // Create synchronization objects (semaphores and fences)
initImGui(); // Initialize Dear ImGui for GUI rendering
2024-10-15 19:49:39 -04:00
}
/**
* @brief Initializes the ImGui library for use with GLFW and Vulkan.
*
* This function sets up ImGui for rendering with Vulkan and GLFW. It creates the ImGui context,
* sets up the style, initializes ImGui for GLFW and Vulkan, and creates a descriptor pool for ImGui.
*/
2024-10-17 23:14:22 -04:00
fn initImGui() -> void {
2024-10-15 19:49:39 -04:00
// Create ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
2024-10-18 13:35:59 -04:00
// Remember to use a reference here, otherwise
// the ImGui::GetIO() function will return a
// copy and the changes won't be saved.
ImGuiIO& imGuiIO = ImGui::GetIO();
// Disable writing imgui.ini
imGuiIO.IniFilename = nullptr;
2024-10-15 19:49:39 -04:00
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// Initialize ImGui for GLFW and Vulkan
ImGui_ImplGlfw_InitForVulkan(mWindow.get(), true);
2024-10-15 21:53:36 -04:00
vk::DescriptorPoolSize descriptorPoolSize = {
.type = vk::DescriptorType::eCombinedImageSampler,
.descriptorCount = 1,
};
2024-10-15 19:49:39 -04:00
2024-10-15 21:53:36 -04:00
vk::DescriptorPoolCreateInfo poolInfo {
.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
.maxSets = 1,
.poolSizeCount = 1,
.pPoolSizes = &descriptorPoolSize,
};
2024-10-15 19:49:39 -04:00
2024-10-15 21:53:36 -04:00
mImGuiDescriptorPool = mDevice->createDescriptorPoolUnique(poolInfo);
ImGui_ImplVulkan_InitInfo initInfo = {
.Instance = mInstance.get(),
.PhysicalDevice = mPhysicalDevice,
.Device = mDevice.get(),
.QueueFamily = findQueueFamilies(mPhysicalDevice).graphics_family.value(),
.Queue = mGraphicsQueue,
.DescriptorPool = mImGuiDescriptorPool.get(),
.RenderPass = mRenderPass.get(),
2024-10-18 13:35:50 -04:00
.MinImageCount = MAX_FRAMES_IN_FLIGHT,
2024-10-15 21:53:36 -04:00
.ImageCount = static_cast<uint32_t>(mSwapChainImages.size()),
.MSAASamples = static_cast<VkSampleCountFlagBits>(mMsaaSamples),
.PipelineCache = VK_NULL_HANDLE,
.Subpass = 0,
.UseDynamicRendering = false,
.PipelineRenderingCreateInfo = {},
.Allocator = nullptr,
.CheckVkResultFn = nullptr,
.MinAllocationSize = 0,
};
ImGui_ImplVulkan_Init(&initInfo);
2024-09-29 23:02:04 -04:00
}
2024-09-28 14:54:39 -04:00
/**
* @brief The main rendering loop of the application.
*
* This function contains the main loop that runs while the window is open. It continuously
* polls for events and draws frames until the window is closed.
*/
2024-09-30 00:57:13 -04:00
fn mainLoop() -> void {
2024-10-14 23:32:52 -04:00
// While the window is open,
2024-10-01 18:54:41 -04:00
while (!mWindow->shouldClose()) {
// Update the FPS counter
updateFPS(mWindow.get(), "Vulkan");
2024-10-14 23:32:52 -04:00
// Poll for events
2024-10-10 21:36:42 -04:00
vkfw::pollEvents();
2024-10-14 23:32:52 -04:00
// Draw a frame
2024-10-01 18:54:41 -04:00
drawFrame();
}
2024-10-14 23:32:52 -04:00
// Wait for the device to finish
2024-10-06 18:14:15 -04:00
mDevice->waitIdle();
2024-09-28 14:54:39 -04:00
}
/**
* @brief Cleans up the swap chain resources.
*
* This function destroys the framebuffers and image views associated with the swap chain.
*/
2024-10-05 23:08:12 -04:00
fn cleanupSwapChain() -> void {
2024-10-11 19:09:37 -04:00
for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset();
for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset();
2024-10-05 23:08:12 -04:00
mSwapChain.reset();
}
/**
* @brief Recreates the swap chain.
*
* This function is called when the swap chain needs to be recreated, such as when the window is resized.
* It cleans up the old swap chain and creates a new one with updated properties.
*/
2024-10-05 23:08:12 -04:00
fn recreateSwapChain() -> void {
2024-10-14 23:32:52 -04:00
// Get the new width and height
auto [width, height] = mWindow->getFramebufferSize();
2024-10-05 23:08:12 -04:00
2024-10-14 23:32:52 -04:00
// If the width or height are 0, wait for events
2024-10-05 23:08:12 -04:00
while (width == 0 || height == 0) {
std::tie(width, height) = mWindow->getFramebufferSize();
vkfw::waitEvents();
}
2024-10-14 23:32:52 -04:00
// Wait for the device to finish
2024-10-06 18:14:15 -04:00
mDevice->waitIdle();
2024-10-05 23:08:12 -04:00
2024-10-14 23:32:52 -04:00
// Clean up the swap chain
2024-10-05 23:08:12 -04:00
cleanupSwapChain();
2024-10-14 23:32:52 -04:00
// Create a new swap chain
2024-10-05 23:08:12 -04:00
createSwapChain();
createImageViews();
2024-10-11 21:14:36 -04:00
createColorResources();
2024-10-11 19:09:37 -04:00
createDepthResources();
2024-10-05 23:08:12 -04:00
createFramebuffers();
}
/**
* @brief Creates the Vulkan instance.
*
* This function sets up the Vulkan instance, including application info, extensions, and validation layers.
*/
2024-09-25 23:03:56 -04:00
fn createInstance() -> void {
2024-10-14 23:32:52 -04:00
#ifndef NDEBUG
// Make sure validation layers are supported
if (!checkValidationLayerSupport())
2024-09-30 21:21:38 -04:00
throw std::runtime_error("Validation layers requested, but not available!");
2024-10-14 23:32:52 -04:00
#endif
// Application metadata
vk::ApplicationInfo appInfo {
.pApplicationName = "Hello Triangle",
.applicationVersion = 1,
.pEngineName = "No Engine",
.engineVersion = 1,
.apiVersion = vk::ApiVersion12,
};
2024-09-25 23:03:56 -04:00
2024-10-14 23:32:52 -04:00
// Get the required extensions
std::span<const char*> extensionsSpan = vkfw::getRequiredInstanceExtensions();
2024-09-25 23:03:56 -04:00
2024-10-14 23:32:52 -04:00
// Convert the span to a vector
std::vector extensions(extensionsSpan.begin(), extensionsSpan.end());
#ifndef NDEBUG
// Add the debug utils extension
extensions.emplace_back(vk::EXTDebugUtilsExtensionName);
#endif
2024-09-26 19:56:19 -04:00
#ifdef __APPLE__
2024-10-14 23:32:52 -04:00
// Add the portability extension
2024-10-01 14:15:39 -04:00
extensions.emplace_back(vk::KHRPortabilityEnumerationExtensionName);
2024-10-14 23:32:52 -04:00
// Technically deprecated but Vulkan complains if I don't include it for macOS,
// so instead of using the vk::KHRPortabilitySubsetExtensionName, I just use
2024-10-01 14:15:39 -04:00
// the direct string.
extensions.emplace_back("VK_KHR_get_physical_device_properties2");
2024-09-30 21:21:38 -04:00
#endif
2024-09-30 00:31:08 -04:00
2024-10-14 23:32:52 -04:00
// Create the instance
2024-09-29 23:05:15 -04:00
vk::InstanceCreateInfo createInfo {
2024-10-01 16:57:40 -04:00
#ifdef __APPLE__
2024-10-14 23:32:52 -04:00
// Enable the portability extension
2024-10-01 16:57:40 -04:00
.flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
#endif
2024-10-14 23:32:52 -04:00
.pApplicationInfo = &appInfo,
#ifdef NDEBUG
.enabledLayerCount = 0,
.ppEnabledLayerNames = nullptr,
#else
.enabledLayerCount = static_cast<u32>(validationLayers.size()),
.ppEnabledLayerNames = validationLayers.data(),
#endif
.enabledExtensionCount = static_cast<u32>(extensions.size()),
2024-09-30 00:31:08 -04:00
.ppEnabledExtensionNames = extensions.data()
2024-09-29 23:05:15 -04:00
};
2024-09-26 17:18:45 -04:00
2024-09-30 00:31:08 -04:00
#ifndef NDEBUG
2024-09-29 23:02:04 -04:00
fmt::println("Available extensions:");
for (const char* extension : extensions) fmt::println("\t{}", extension);
2024-09-30 00:31:08 -04:00
#endif
2024-09-25 23:03:56 -04:00
2024-10-14 23:32:52 -04:00
// Create the instance
2024-10-06 18:14:15 -04:00
mInstance = vk::createInstanceUnique(createInfo);
2024-10-06 17:34:25 -04:00
2024-10-14 23:32:52 -04:00
// Load the instance functions
2024-10-06 12:26:44 -04:00
VULKAN_HPP_DEFAULT_DISPATCHER.init(mInstance.get());
2024-09-28 14:54:39 -04:00
}
/**
* @brief Sets up the debug messenger for Vulkan validation layers.
*
* This function creates a debug messenger that handles validation layer messages in debug builds.
*/
2024-09-30 00:57:13 -04:00
fn setupDebugMessenger() -> void {
2024-10-14 23:32:52 -04:00
#ifdef NDEBUG
return;
#endif
2024-09-28 18:13:24 -04:00
2024-09-30 00:31:08 -04:00
vk::DebugUtilsMessengerCreateInfoEXT messengerCreateInfo {
.messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose |
vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning |
vk::DebugUtilsMessageSeverityFlagBitsEXT::eError,
.messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral |
vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation |
vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance,
2024-10-12 15:39:20 -04:00
.pfnUserCallback = debugCallback
2024-09-30 00:31:08 -04:00
};
2024-09-28 18:13:24 -04:00
2024-10-06 18:14:15 -04:00
mDebugMessenger = mInstance->createDebugUtilsMessengerEXTUnique(messengerCreateInfo, nullptr);
2024-09-30 00:31:08 -04:00
}
2024-09-28 18:13:24 -04:00
/**
* @brief Creates the window surface for rendering.
*
* This function creates a Vulkan surface for the GLFW window, which is used for presenting rendered images.
*/
2024-09-30 16:46:17 -04:00
fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); }
2024-09-30 00:57:13 -04:00
/**
* @brief Selects a suitable physical device (GPU) for Vulkan.
*
* This function enumerates available physical devices and selects the first one that meets
* the application's requirements.
*/
2024-09-30 00:31:08 -04:00
fn pickPhysicalDevice() -> void {
2024-10-14 23:32:52 -04:00
// Get all physical devices
2024-10-06 18:14:15 -04:00
std::vector<vk::PhysicalDevice> devices = mInstance->enumeratePhysicalDevices();
2024-09-30 00:31:08 -04:00
2024-10-14 23:32:52 -04:00
// Make sure there are supported devices
2024-10-06 18:14:15 -04:00
if (devices.empty())
2024-09-30 00:31:08 -04:00
throw std::runtime_error("Failed to find GPUs with Vulkan support!");
#ifndef NDEBUG
fmt::println("Available devices:");
#endif
2024-10-14 23:32:52 -04:00
// For each device,
2024-10-06 18:14:15 -04:00
for (const vk::PhysicalDevice& device : devices) {
2024-09-30 00:31:08 -04:00
#ifndef NDEBUG
vk::PhysicalDeviceProperties properties = device.getProperties();
2024-09-30 19:49:44 -04:00
fmt::println("\t{}", properties.deviceName.data());
2024-09-30 00:31:08 -04:00
#endif
2024-10-14 23:32:52 -04:00
// Set the first suitable device as the physical device
2024-09-30 00:31:08 -04:00
if (isDeviceSuitable(device)) {
mPhysicalDevice = device;
2024-10-11 21:14:36 -04:00
mMsaaSamples = getMaxUsableSampleCount();
2024-09-30 00:31:08 -04:00
break;
}
2024-09-28 18:13:24 -04:00
}
2024-10-14 23:32:52 -04:00
// If no suitable device was found, throw an error
2024-09-30 00:31:08 -04:00
if (!mPhysicalDevice)
throw std::runtime_error("Failed to find a suitable GPU!");
}
/**
* @brief Creates the logical device and retrieves queue handles.
*
* This function creates a logical device from the selected physical device, enabling required
* features and retrieving handles to the graphics and presentation queues.
*/
2024-09-30 00:57:13 -04:00
fn createLogicalDevice() -> void {
2024-10-14 23:32:52 -04:00
// Get the queue families
2024-10-10 18:51:20 -04:00
QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice);
2024-09-30 00:57:13 -04:00
std::vector<vk::DeviceQueueCreateInfo> queueCreateInfos;
2024-10-14 23:32:52 -04:00
std::set<u32> uniqueQueueFamilies = {
qfIndices.graphics_family.value(),
qfIndices.present_family.value(),
};
// Set the queue priority
2024-09-30 00:57:13 -04:00
f32 queuePriority = 1.0F;
2024-10-14 23:32:52 -04:00
// For each unique queue family, create a new queue
for (const u32& queueFamily : uniqueQueueFamilies) {
2024-10-14 23:32:52 -04:00
vk::DeviceQueueCreateInfo queueCreateInfo {
.queueFamilyIndex = queueFamily,
.queueCount = 1,
.pQueuePriorities = &queuePriority,
};
2024-09-30 00:57:13 -04:00
2024-10-01 14:15:39 -04:00
queueCreateInfos.emplace_back(queueCreateInfo);
2024-09-30 00:57:13 -04:00
}
2024-10-14 23:32:52 -04:00
// Enable anisotropic filtering
2024-10-11 00:42:58 -04:00
vk::PhysicalDeviceFeatures deviceFeatures {
.samplerAnisotropy = vk::True,
};
2024-09-30 00:57:13 -04:00
2024-10-14 23:32:52 -04:00
vk::DeviceCreateInfo createInfo {
.queueCreateInfoCount = static_cast<u32>(queueCreateInfos.size()),
.pQueueCreateInfos = queueCreateInfos.data(),
.enabledExtensionCount = static_cast<u32>(deviceExtensions.size()),
.ppEnabledExtensionNames = deviceExtensions.data(),
.pEnabledFeatures = &deviceFeatures,
};
2024-09-30 00:57:13 -04:00
2024-10-14 23:32:52 -04:00
// Create the logical device and set the graphics and present queues
2024-10-06 18:14:15 -04:00
mDevice = mPhysicalDevice.createDeviceUnique(createInfo);
2024-10-10 18:51:20 -04:00
mGraphicsQueue = mDevice->getQueue(qfIndices.graphics_family.value(), 0);
mPresentQueue = mDevice->getQueue(qfIndices.present_family.value(), 0);
2024-09-30 00:57:13 -04:00
}
/**
* @brief Creates the swap chain for image presentation.
*
* This function sets up the swap chain, which is a queue of images that can be presented to the screen.
* It determines the format, presentation mode, and extent of the swap chain images.
*/
2024-10-01 16:57:40 -04:00
fn createSwapChain() -> void {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(mPhysicalDevice);
vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
vk::PresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.present_modes);
vk::Extent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
u32 imageCount = swapChainSupport.capabilities.minImageCount + 1;
if (swapChainSupport.capabilities.maxImageCount > 0 &&
imageCount > swapChainSupport.capabilities.maxImageCount)
imageCount = swapChainSupport.capabilities.maxImageCount;
2024-10-10 18:51:20 -04:00
QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice);
2024-10-14 23:32:52 -04:00
std::array<u32, 2> queueFamilyIndices = {
qfIndices.graphics_family.value(),
qfIndices.present_family.value(),
};
2024-10-01 16:57:40 -04:00
vk::SwapchainCreateInfoKHR createInfo {
.surface = mSurface.get(),
.minImageCount = imageCount,
.imageFormat = surfaceFormat.format,
.imageColorSpace = surfaceFormat.colorSpace,
.imageExtent = extent,
.imageArrayLayers = 1,
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
2024-10-10 18:51:20 -04:00
.imageSharingMode = qfIndices.graphics_family != qfIndices.present_family ? vk::SharingMode::eConcurrent
: vk::SharingMode::eExclusive,
.queueFamilyIndexCount =
static_cast<u32>(qfIndices.graphics_family != qfIndices.present_family ? 2 : 0),
2024-10-01 16:57:40 -04:00
.pQueueFamilyIndices =
2024-10-10 18:51:20 -04:00
qfIndices.graphics_family != qfIndices.present_family ? queueFamilyIndices.data() : nullptr,
2024-10-01 16:57:40 -04:00
.preTransform = swapChainSupport.capabilities.currentTransform,
.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
.presentMode = presentMode,
2024-10-01 18:30:31 -04:00
.clipped = vk::True,
2024-10-01 16:57:40 -04:00
.oldSwapchain = nullptr,
};
2024-10-06 18:14:15 -04:00
mSwapChain = mDevice->createSwapchainKHRUnique(createInfo);
2024-10-06 17:34:25 -04:00
2024-10-06 18:14:15 -04:00
mSwapChainImages = mDevice->getSwapchainImagesKHR(mSwapChain.get());
2024-10-01 16:57:40 -04:00
mSwapChainImageFormat = surfaceFormat.format;
mSwapChainExtent = extent;
}
/**
* @brief Creates image views for the swap chain images.
*
* This function creates a view for each image in the swap chain, which is used to access the image
* contents.
*/
2024-10-01 17:06:14 -04:00
fn createImageViews() -> void {
mSwapChainImageViews.resize(mSwapChainImages.size());
2024-10-11 19:09:37 -04:00
for (u32 i = 0; i < mSwapChainImages.size(); i++)
mSwapChainImageViews[i] =
2024-10-11 20:50:23 -04:00
createImageView(mSwapChainImages[i], mSwapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1);
}
/**
* @brief Creates the render pass.
*
* This function sets up the render pass, which describes the structure of rendering operations,
* including the number and format of attachments used during rendering.
*/
2024-10-01 18:30:31 -04:00
fn createRenderPass() -> void {
2024-10-14 23:32:52 -04:00
vk::AttachmentDescription colorAttachment {
.format = mSwapChainImageFormat,
.samples = mMsaaSamples,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::eColorAttachmentOptimal,
};
vk::AttachmentDescription depthAttachment {
.format = findDepthFormat(),
.samples = mMsaaSamples,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eDontCare,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal,
};
vk::AttachmentDescription colorAttachmentResolve {
.format = mSwapChainImageFormat,
.samples = vk::SampleCountFlagBits::e1,
.loadOp = vk::AttachmentLoadOp::eDontCare,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::ePresentSrcKHR,
};
vk::AttachmentReference colorAttachmentRef {
.attachment = 0,
.layout = vk::ImageLayout::eColorAttachmentOptimal,
};
vk::AttachmentReference depthAttachmentRef {
.attachment = 1,
.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal,
};
vk::AttachmentReference colorAttachmentResolveRef {
.attachment = 2,
.layout = vk::ImageLayout::eColorAttachmentOptimal,
};
vk::SubpassDescription subpass {
.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = 1,
.pColorAttachments = &colorAttachmentRef,
.pResolveAttachments = &colorAttachmentResolveRef,
.pDepthStencilAttachment = &depthAttachmentRef,
};
vk::SubpassDependency dependency {
.srcSubpass = vk::SubpassExternal,
.dstSubpass = {},
.srcStageMask =
vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests,
.dstStageMask =
vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests,
.srcAccessMask = {},
.dstAccessMask =
vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite,
};
std::array<vk::AttachmentDescription, 3> attachments = {
colorAttachment,
depthAttachment,
colorAttachmentResolve,
};
vk::RenderPassCreateInfo renderPassInfo {
.attachmentCount = static_cast<u32>(attachments.size()),
.pAttachments = attachments.data(),
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 1,
.pDependencies = &dependency,
};
2024-10-01 18:30:31 -04:00
2024-10-06 18:14:15 -04:00
mRenderPass = mDevice->createRenderPassUnique(renderPassInfo);
2024-10-01 18:30:31 -04:00
}
/**
* @brief Creates the descriptor set layout.
*
* This function defines the layout of descriptor sets used in the shader, specifying the types
* of resources that will be accessed by the shader.
*/
fn createDescriptorSetLayout() -> void {
vk::DescriptorSetLayoutBinding uboLayoutBinding {
.binding = 0,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eVertex,
.pImmutableSamplers = nullptr,
};
2024-10-11 00:42:58 -04:00
vk::DescriptorSetLayoutBinding samplerLayoutBinding {
.binding = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eFragment,
.pImmutableSamplers = nullptr,
};
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = { uboLayoutBinding, samplerLayoutBinding };
vk::DescriptorSetLayoutCreateInfo layoutInfo {
2024-10-11 00:42:58 -04:00
.bindingCount = static_cast<u32>(bindings.size()),
.pBindings = bindings.data(),
};
mDescriptorSetLayout = mDevice->createDescriptorSetLayoutUnique(layoutInfo);
}
/**
* @brief Creates the graphics pipeline.
*
* This function sets up the entire graphics pipeline, including shader stages, vertex input,
* input assembly, viewport, rasterization, multisampling, depth testing, color blending, and dynamic
* states.
*/
2024-10-01 18:30:31 -04:00
fn createGraphicsPipeline() -> void {
2024-10-12 15:39:20 -04:00
std::vector<char> vertShaderCode = readFile(VERTEX_SHADER_PATH);
std::vector<char> fragShaderCode = readFile(FRAGMENT_SHADER_PATH);
2024-10-01 18:30:31 -04:00
vk::UniqueShaderModule vertShaderModule = createShaderModule(vertShaderCode);
vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode);
2024-10-14 23:32:52 -04:00
vk::PipelineShaderStageCreateInfo vertShaderStageInfo {
.stage = vk::ShaderStageFlagBits::eVertex,
.module = vertShaderModule.get(),
.pName = "main",
};
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
vk::PipelineShaderStageCreateInfo fragShaderStageInfo {
.stage = vk::ShaderStageFlagBits::eFragment,
.module = fragShaderModule.get(),
.pName = "main",
};
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
std::array<vk::PipelineShaderStageCreateInfo, 2> shaderStages = {
vertShaderStageInfo,
fragShaderStageInfo,
};
2024-10-01 18:30:31 -04:00
vk::VertexInputBindingDescription bindingDescription = Vertex::getBindingDescription();
2024-10-11 00:42:58 -04:00
std::array<vk::VertexInputAttributeDescription, 3> attributeDescriptions =
Vertex::getAttributeDescriptions();
2024-10-01 18:30:31 -04:00
vk::PipelineVertexInputStateCreateInfo vertexInputInfo {
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &bindingDescription,
.vertexAttributeDescriptionCount = static_cast<u32>(attributeDescriptions.size()),
2024-10-14 23:32:52 -04:00
.pVertexAttributeDescriptions = attributeDescriptions.data(),
2024-10-01 18:30:31 -04:00
};
2024-10-14 23:32:52 -04:00
vk::PipelineInputAssemblyStateCreateInfo inputAssembly {
.topology = vk::PrimitiveTopology::eTriangleList,
.primitiveRestartEnable = vk::False,
};
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
vk::PipelineViewportStateCreateInfo viewportState {
.viewportCount = 1,
.scissorCount = 1,
};
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
vk::PipelineRasterizationStateCreateInfo rasterizer {
.depthClampEnable = vk::False,
.rasterizerDiscardEnable = vk::False,
.polygonMode = vk::PolygonMode::eFill,
.cullMode = vk::CullModeFlagBits::eBack,
.frontFace = vk::FrontFace::eCounterClockwise,
.depthBiasEnable = vk::False,
.lineWidth = 1.0F,
};
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
vk::PipelineMultisampleStateCreateInfo multisampling {
.rasterizationSamples = mMsaaSamples,
.sampleShadingEnable = vk::False,
};
2024-10-11 19:09:37 -04:00
2024-10-14 23:32:52 -04:00
vk::PipelineDepthStencilStateCreateInfo depthStencil {
.depthTestEnable = vk::True,
.depthWriteEnable = vk::True,
.depthCompareOp = vk::CompareOp::eLess,
.depthBoundsTestEnable = vk::False,
.stencilTestEnable = vk::False,
};
2024-10-01 18:30:31 -04:00
vk::PipelineColorBlendAttachmentState colorBlendAttachment {
.blendEnable = vk::False,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
2024-10-14 23:32:52 -04:00
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
2024-10-01 18:30:31 -04:00
};
vk::PipelineColorBlendStateCreateInfo colorBlending {
.logicOpEnable = vk::False,
.logicOp = vk::LogicOp::eCopy,
.attachmentCount = 1,
.pAttachments = &colorBlendAttachment,
2024-10-14 23:32:52 -04:00
.blendConstants = std::array<float, 4> { 0.0F, 0.0F, 0.0F, 0.0F },
2024-10-01 18:30:31 -04:00
};
2024-10-01 18:30:31 -04:00
std::vector<vk::DynamicState> dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor };
2024-10-14 23:32:52 -04:00
vk::PipelineDynamicStateCreateInfo dynamicState {
.dynamicStateCount = static_cast<u32>(dynamicStates.size()),
.pDynamicStates = dynamicStates.data(),
};
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
vk::PipelineLayoutCreateInfo pipelineLayoutInfo {
.setLayoutCount = 1,
.pSetLayouts = &mDescriptorSetLayout.get(),
};
2024-10-01 18:30:31 -04:00
2024-10-06 18:14:15 -04:00
mPipelineLayout = mDevice->createPipelineLayoutUnique(pipelineLayoutInfo);
2024-10-01 18:30:31 -04:00
2024-10-14 23:32:52 -04:00
vk::GraphicsPipelineCreateInfo pipelineInfo {
.stageCount = static_cast<u32>(shaderStages.size()),
.pStages = shaderStages.data(),
.pVertexInputState = &vertexInputInfo,
.pInputAssemblyState = &inputAssembly,
.pViewportState = &viewportState,
.pRasterizationState = &rasterizer,
.pMultisampleState = &multisampling,
.pDepthStencilState = &depthStencil,
.pColorBlendState = &colorBlending,
.pDynamicState = &dynamicState,
.layout = mPipelineLayout.get(),
.renderPass = mRenderPass.get(),
.subpass = 0,
};
2024-10-01 18:30:31 -04:00
2024-10-06 17:34:25 -04:00
vk::Result graphicsPipelineResult = vk::Result::eSuccess;
vk::UniquePipeline graphicsPipelineValue;
std::tie(graphicsPipelineResult, graphicsPipelineValue) =
mDevice->createGraphicsPipelineUnique(nullptr, pipelineInfo).asTuple();
if (graphicsPipelineResult != vk::Result::eSuccess)
throw std::runtime_error("Failed to create graphics pipeline!");
mGraphicsPipeline = std::move(graphicsPipelineValue);
2024-10-01 18:30:31 -04:00
}
/**
* @brief Creates framebuffers for the swap chain images.
*
* This function creates a framebuffer for each image view in the swap chain, attaching
* the color, depth, and resolve attachments.
*/
2024-10-01 18:33:05 -04:00
fn createFramebuffers() -> void {
mSwapChainFramebuffers.resize(mSwapChainImageViews.size());
for (usize i = 0; i < mSwapChainImageViews.size(); i++) {
2024-10-11 21:14:36 -04:00
std::array<vk::ImageView, 3> attachments = { mColorImageView.get(),
mDepthImageView.get(),
mSwapChainImageViews[i].get() };
2024-10-11 19:09:37 -04:00
vk::FramebufferCreateInfo framebufferInfo { .renderPass = mRenderPass.get(),
.attachmentCount = static_cast<u32>(attachments.size()),
.pAttachments = attachments.data(),
.width = mSwapChainExtent.width,
.height = mSwapChainExtent.height,
.layers = 1 };
2024-10-01 18:33:05 -04:00
2024-10-06 18:14:15 -04:00
mSwapChainFramebuffers[i] = mDevice->createFramebufferUnique(framebufferInfo);
2024-10-01 18:33:05 -04:00
}
}
/**
* @brief Creates the command pool.
*
* This function creates a command pool, which is used to manage the memory used to store
* the buffers from which command buffer memory is allocated.
*/
2024-10-01 18:54:41 -04:00
fn createCommandPool() -> void {
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice);
2024-10-12 15:39:20 -04:00
vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
.queueFamilyIndex = queueFamilyIndices.graphics_family.value() };
2024-10-01 18:54:41 -04:00
2024-10-06 18:14:15 -04:00
mCommandPool = mDevice->createCommandPoolUnique(poolInfo);
2024-10-01 18:54:41 -04:00
}
/**
* @brief Creates resources for color attachment.
*
* This function creates the image, memory, and view for the color attachment used in multisampling.
*/
2024-10-11 21:14:36 -04:00
fn createColorResources() -> void {
vk::Format colorFormat = mSwapChainImageFormat;
2024-10-14 23:32:52 -04:00
std::tie(mColorImage, mColorImageMemory) = createImage(
2024-10-11 21:14:36 -04:00
mSwapChainExtent.width,
mSwapChainExtent.height,
1,
mMsaaSamples,
colorFormat,
vk::ImageTiling::eOptimal,
vk::ImageUsageFlagBits::eTransientAttachment | vk::ImageUsageFlagBits::eColorAttachment,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eDeviceLocal
2024-10-11 21:14:36 -04:00
);
mColorImageView = createImageView(mColorImage.get(), colorFormat, vk::ImageAspectFlagBits::eColor, 1);
}
/**
* @brief Creates resources for depth attachment.
*
* This function creates the image, memory, and view for the depth attachment used in depth testing.
*/
2024-10-11 19:09:37 -04:00
fn createDepthResources() -> void {
vk::Format depthFormat = findDepthFormat();
2024-10-14 23:32:52 -04:00
std::tie(mDepthImage, mDepthImageMemory) = createImage(
2024-10-11 19:09:37 -04:00
mSwapChainExtent.width,
mSwapChainExtent.height,
2024-10-11 20:50:23 -04:00
1,
2024-10-11 21:14:36 -04:00
mMsaaSamples,
2024-10-11 19:09:37 -04:00
depthFormat,
vk::ImageTiling::eOptimal,
vk::ImageUsageFlagBits::eDepthStencilAttachment,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eDeviceLocal
2024-10-11 19:09:37 -04:00
);
2024-10-11 20:50:23 -04:00
mDepthImageView = createImageView(mDepthImage.get(), depthFormat, vk::ImageAspectFlagBits::eDepth, 1);
2024-10-11 19:09:37 -04:00
}
/**
* @brief Finds a supported format from a list of candidates.
*
* @param candidates A vector of candidate formats to check.
* @param tiling The desired tiling arrangement of the format.
* @param features The required format features.
* @return The first supported format from the list of candidates.
*
* This function iterates through a list of candidate formats and returns the first one
* that is supported with the specified tiling and features.
*/
2024-10-11 19:09:37 -04:00
fn findSupportedFormat(
const std::vector<vk::Format>& candidates,
2024-10-14 23:32:52 -04:00
const vk::ImageTiling& tiling,
const vk::FormatFeatureFlags& features
2024-10-11 19:09:37 -04:00
) -> vk::Format {
for (vk::Format format : candidates) {
vk::FormatProperties props = mPhysicalDevice.getFormatProperties(format);
2024-10-14 23:32:52 -04:00
2024-10-11 19:09:37 -04:00
if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features)
return format;
2024-10-14 23:32:52 -04:00
2024-10-11 19:09:37 -04:00
if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features)
return format;
}
throw std::runtime_error("Failed to find supported format!");
}
/**
* @brief Finds a supported depth format.
*
* @return A supported depth format.
*
* This function tries to find a supported depth format from a predefined list of candidates.
*/
2024-10-11 19:09:37 -04:00
fn findDepthFormat() -> vk::Format {
return findSupportedFormat(
{ vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint },
vk::ImageTiling::eOptimal,
vk::FormatFeatureFlagBits::eDepthStencilAttachment
);
}
/**
* @brief Checks if a format has a stencil component.
*
* @param format The format to check.
* @return True if the format has a stencil component, false otherwise.
*/
2024-10-11 19:09:37 -04:00
static fn hasStencilComponent(vk::Format format) {
return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint;
}
/**
* @brief Creates the texture image.
*
* This function loads an image from a file, creates a staging buffer, transfers the image data
* to the staging buffer, creates the final texture image, and copies the data from the staging
* buffer to the texture image. It also generates mipmaps for the texture.
*/
2024-10-11 00:42:58 -04:00
fn createTextureImage() -> void {
2024-10-12 00:07:13 -04:00
stb::UniqueImage image(TEXTURE_PATH);
2024-10-11 20:50:23 -04:00
2024-10-12 00:07:13 -04:00
u8* pixels = image.getData();
i32 texWidth = image.getWidth(), texHeight = image.getHeight();
2024-10-11 00:42:58 -04:00
vk::DeviceSize imageSize =
static_cast<vk::DeviceSize>(texWidth) * static_cast<vk::DeviceSize>(texHeight) * 4;
2024-10-11 20:50:23 -04:00
mMipLevels = static_cast<u32>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;
2024-10-11 00:42:58 -04:00
if (!pixels)
throw std::runtime_error("Failed to load texture image!");
vk::UniqueBuffer stagingBuffer;
vk::UniqueDeviceMemory stagingBufferMemory;
2024-10-14 23:32:52 -04:00
std::tie(stagingBuffer, stagingBufferMemory) = createBuffer(
2024-10-11 00:42:58 -04:00
imageSize,
vk::BufferUsageFlagBits::eTransferSrc,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
2024-10-11 00:42:58 -04:00
);
copyData(stagingBufferMemory.get(), imageSize, pixels);
2024-10-14 23:32:52 -04:00
std::tie(mTextureImage, mTextureImageMemory) = createImage(
2024-10-11 00:42:58 -04:00
static_cast<u32>(texWidth),
static_cast<u32>(texHeight),
2024-10-11 20:50:23 -04:00
mMipLevels,
2024-10-11 21:14:36 -04:00
vk::SampleCountFlagBits::e1,
2024-10-11 00:42:58 -04:00
vk::Format::eR8G8B8A8Srgb,
vk::ImageTiling::eOptimal,
2024-10-11 20:50:23 -04:00
vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eDeviceLocal
2024-10-11 00:42:58 -04:00
);
transitionImageLayout(
2024-10-11 20:50:23 -04:00
mTextureImage.get(), vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, mMipLevels
2024-10-11 00:42:58 -04:00
);
copyBufferToImage(
stagingBuffer.get(), mTextureImage.get(), static_cast<u32>(texWidth), static_cast<u32>(texHeight)
);
2024-10-11 20:50:23 -04:00
generateMipmaps(mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mMipLevels);
}
/**
* @brief Generates mipmaps for a texture image.
*
* @param image The image for which to generate mipmaps.
* @param imageFormat The format of the image.
* @param texWidth The width of the texture.
* @param texHeight The height of the texture.
* @param mipLevels The number of mipmap levels to generate.
*
* This function generates mipmaps for the given texture image by repeatedly scaling down
* the image by half until reaching the smallest mip level.
*/
2024-10-11 20:50:23 -04:00
fn generateMipmaps(vk::Image image, vk::Format imageFormat, i32 texWidth, i32 texHeight, u32 mipLevels)
-> void {
vk::FormatProperties formatProperties = mPhysicalDevice.getFormatProperties(imageFormat);
if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear))
throw std::runtime_error("Texture image format does not support linear blitting!");
2024-10-14 23:32:52 -04:00
vk::CommandBuffer commandBuffer = beginSingleTimeCommands();
2024-10-11 20:50:23 -04:00
vk::ImageMemoryBarrier barrier {
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.image = image,
.subresourceRange = { .aspectMask = vk::ImageAspectFlagBits::eColor,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1 }
};
i32 mipWidth = texWidth;
i32 mipHeight = texHeight;
for (u32 i = 1; i < mipLevels; i++) {
barrier.subresourceRange.baseMipLevel = i - 1;
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal;
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead;
2024-10-14 23:32:52 -04:00
commandBuffer.pipelineBarrier(
2024-10-11 20:50:23 -04:00
vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eTransfer,
{},
nullptr,
nullptr,
barrier
);
vk::ImageBlit blit {
.srcSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor,
.mipLevel = i - 1,
.baseArrayLayer = 0,
.layerCount = 1 },
2024-10-11 22:34:15 -04:00
.srcOffsets = std::array<vk::Offset3D, 2> { { { .x = 0, .y = 0, .z = 0 },
{ .x = mipWidth, .y = mipHeight, .z = 1 } } },
2024-10-11 20:50:23 -04:00
.dstSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor,
.mipLevel = i,
.baseArrayLayer = 0,
.layerCount = 1 },
2024-10-12 15:39:20 -04:00
.dstOffsets = std::array<vk::Offset3D, 2> { vk::Offset3D {
.x = 0,
.y = 0,
.z = 0,
}, vk::Offset3D {
.x = mipWidth > 1 ? mipWidth / 2 : 1,
.y = mipHeight > 1 ? mipHeight / 2 : 1,
.z = 1,
} }
2024-10-11 20:50:23 -04:00
};
2024-10-14 23:32:52 -04:00
commandBuffer.blitImage(
2024-10-11 20:50:23 -04:00
image,
vk::ImageLayout::eTransferSrcOptimal,
image,
vk::ImageLayout::eTransferDstOptimal,
blit,
vk::Filter::eLinear
);
barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
2024-10-14 23:32:52 -04:00
commandBuffer.pipelineBarrier(
2024-10-11 20:50:23 -04:00
vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eFragmentShader,
{},
nullptr,
nullptr,
barrier
);
if (mipWidth > 1)
mipWidth /= 2;
if (mipHeight > 1)
mipHeight /= 2;
}
barrier.subresourceRange.baseMipLevel = mMipLevels - 1;
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
2024-10-14 23:32:52 -04:00
commandBuffer.pipelineBarrier(
2024-10-11 20:50:23 -04:00
vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eFragmentShader,
{},
nullptr,
nullptr,
barrier
2024-10-11 00:42:58 -04:00
);
2024-10-11 20:50:23 -04:00
2024-10-14 23:32:52 -04:00
endSingleTimeCommands(commandBuffer);
2024-10-11 00:42:58 -04:00
}
/**
* @brief Gets the maximum usable sample count for multisampling.
*
* @return The maximum sample count supported by the device for both color and depth.
*
* This function determines the highest sample count that is supported by the device
* for both color and depth attachments.
*/
2024-10-11 21:14:36 -04:00
fn getMaxUsableSampleCount() -> vk::SampleCountFlagBits {
vk::PhysicalDeviceProperties physicalDeviceProperties = mPhysicalDevice.getProperties();
vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts &
physicalDeviceProperties.limits.framebufferDepthSampleCounts;
2024-10-14 23:32:52 -04:00
// Define an array of sample counts in descending order
const std::array<vk::SampleCountFlagBits, 7> sampleCounts = {
vk::SampleCountFlagBits::e64, vk::SampleCountFlagBits::e32, vk::SampleCountFlagBits::e16,
vk::SampleCountFlagBits::e8, vk::SampleCountFlagBits::e4, vk::SampleCountFlagBits::e2,
vk::SampleCountFlagBits::e1,
};
// Loop through the array and return the first supported sample count
for (const vk::SampleCountFlagBits& count : sampleCounts)
if (counts & count)
return count;
2024-10-11 21:14:36 -04:00
2024-10-14 23:32:52 -04:00
// Return e1 if no other sample count is supported
2024-10-11 21:14:36 -04:00
return vk::SampleCountFlagBits::e1;
}
/**
* @brief Creates the texture image view.
*
* This function creates an image view for the texture image, which can be used
* to access the texture in shaders.
*/
2024-10-11 00:42:58 -04:00
fn createTextureImageView() -> void {
2024-10-11 20:50:23 -04:00
mTextureImageView = createImageView(
mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels
);
2024-10-11 00:42:58 -04:00
}
/**
* @brief Creates the texture sampler.
*
* This function creates a sampler object that defines how the texture should be sampled in shaders.
*/
2024-10-11 00:42:58 -04:00
fn createTextureSampler() -> void {
vk::PhysicalDeviceProperties properties = mPhysicalDevice.getProperties();
vk::SamplerCreateInfo samplerInfo {
.magFilter = vk::Filter::eLinear,
.minFilter = vk::Filter::eLinear,
.mipmapMode = vk::SamplerMipmapMode::eLinear,
.addressModeU = vk::SamplerAddressMode::eRepeat,
.addressModeV = vk::SamplerAddressMode::eRepeat,
.addressModeW = vk::SamplerAddressMode::eRepeat,
.mipLodBias = 0.0F,
.anisotropyEnable = vk::False,
.maxAnisotropy = properties.limits.maxSamplerAnisotropy,
.compareEnable = vk::False,
.compareOp = vk::CompareOp::eAlways,
.minLod = 0.0F,
2024-10-11 20:50:23 -04:00
.maxLod = static_cast<f32>(mMipLevels),
2024-10-11 00:42:58 -04:00
.borderColor = vk::BorderColor::eIntOpaqueBlack,
.unnormalizedCoordinates = vk::False,
};
mTextureSampler = mDevice->createSamplerUnique(samplerInfo);
}
/**
* @brief Creates a Vulkan image view.
*
* This function creates and returns a unique Vulkan image view using the provided parameters.
*
* @param image The Vulkan image for which to create the view.
* @param format The format of the image.
* @param aspectFlags The aspect flags for the image view.
* @param mipLevels The number of mip levels for the image view.
*
* @return vk::UniqueImageView A unique handle to the created Vulkan image view.
*
* @details
* The function creates an image view with the following properties:
* - 2D view type
* - Subresource range starting from base mip level 0
* - Single array layer starting from base array layer 0
*/
2024-10-14 23:32:52 -04:00
fn createImageView(
const vk::Image& image,
const vk::Format& format,
const vk::ImageAspectFlags& aspectFlags,
const u32& mipLevels
) -> vk::UniqueImageView {
return mDevice->createImageViewUnique({
.image = image,
.viewType = vk::ImageViewType::e2D,
.format = format,
.subresourceRange = {
.aspectMask = aspectFlags,
.baseMipLevel = 0,
.levelCount = mipLevels,
.baseArrayLayer = 0,
.layerCount = 1,
},
});
2024-10-11 00:42:58 -04:00
}
fn createImage(
2024-10-14 23:32:52 -04:00
const u32& width,
const u32& height,
const u32& mipLevels,
const vk::SampleCountFlagBits& numSamples,
const vk::Format& format,
const vk::ImageTiling& tiling,
const vk::ImageUsageFlags& usage,
const vk::MemoryPropertyFlags& properties
) -> std::pair<vk::UniqueImage, vk::UniqueDeviceMemory> {
// Define the image creation info
2024-10-11 00:42:58 -04:00
vk::ImageCreateInfo imageInfo {
.imageType = vk::ImageType::e2D,
.format = format,
.extent = { .width = width, .height = height, .depth = 1 },
2024-10-11 20:50:23 -04:00
.mipLevels = mipLevels,
2024-10-11 00:42:58 -04:00
.arrayLayers = 1,
2024-10-11 21:14:36 -04:00
.samples = numSamples,
2024-10-11 00:42:58 -04:00
.tiling = tiling,
.usage = usage,
.sharingMode = vk::SharingMode::eExclusive,
.initialLayout = vk::ImageLayout::eUndefined,
};
2024-10-14 23:32:52 -04:00
// Create the image
vk::UniqueImage image = mDevice->createImageUnique(imageInfo);
2024-10-11 00:42:58 -04:00
2024-10-14 23:32:52 -04:00
// Get the memory requirements for the image
2024-10-11 00:42:58 -04:00
vk::MemoryRequirements memRequirements = mDevice->getImageMemoryRequirements(image.get());
2024-10-14 23:32:52 -04:00
// Memory allocation info
2024-10-11 00:42:58 -04:00
vk::MemoryAllocateInfo allocInfo {
.allocationSize = memRequirements.size,
.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties),
};
2024-10-14 23:32:52 -04:00
// Allocate memory
vk::UniqueDeviceMemory imageMemory = mDevice->allocateMemoryUnique(allocInfo);
2024-10-11 00:42:58 -04:00
2024-10-14 23:32:52 -04:00
// Bind the allocated memory to the image
2024-10-11 00:42:58 -04:00
mDevice->bindImageMemory(image.get(), imageMemory.get(), 0);
2024-10-14 23:32:52 -04:00
// Return the unique image
return { std::move(image), std::move(imageMemory) };
2024-10-11 00:42:58 -04:00
}
2024-10-15 20:38:05 -04:00
// Transition image between layouts
2024-10-11 00:42:58 -04:00
fn transitionImageLayout(
2024-10-14 23:32:52 -04:00
const vk::Image& image,
const vk::ImageLayout& oldLayout,
const vk::ImageLayout& newLayout,
const u32& mipLevels
2024-10-11 00:42:58 -04:00
) -> void {
2024-10-15 20:38:05 -04:00
// Create a command buffer
2024-10-14 23:32:52 -04:00
vk::CommandBuffer commandBuffer = beginSingleTimeCommands();
2024-10-11 00:42:58 -04:00
2024-10-15 20:38:05 -04:00
// Define the image memory barrier
2024-10-11 00:42:58 -04:00
vk::ImageMemoryBarrier barrier {
.oldLayout = oldLayout,
.newLayout = newLayout,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.image = image,
2024-10-14 23:32:52 -04:00
.subresourceRange = {
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = mipLevels,
.baseArrayLayer = 0,
.layerCount = 1,
},
2024-10-11 00:42:58 -04:00
};
2024-10-15 20:38:05 -04:00
// Define the source and destination stages
2024-10-11 00:42:58 -04:00
vk::PipelineStageFlags sourceStage;
vk::PipelineStageFlags destinationStage;
2024-10-15 20:38:05 -04:00
// Define the access masks
2024-10-11 00:42:58 -04:00
if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) {
barrier.srcAccessMask = {};
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
sourceStage = vk::PipelineStageFlagBits::eTopOfPipe;
destinationStage = vk::PipelineStageFlagBits::eTransfer;
} else if (oldLayout == vk::ImageLayout::eTransferDstOptimal &&
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
sourceStage = vk::PipelineStageFlagBits::eTransfer;
destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
} else {
2024-10-15 20:38:05 -04:00
// Ensure that the layout transition is supported
2024-10-11 00:42:58 -04:00
throw std::invalid_argument("Unsupported layout transition!");
}
2024-10-15 20:38:05 -04:00
// Record the pipeline barrier
2024-10-14 23:32:52 -04:00
commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier);
2024-10-11 00:42:58 -04:00
2024-10-15 20:38:05 -04:00
// End the command buffer
2024-10-14 23:32:52 -04:00
endSingleTimeCommands(commandBuffer);
2024-10-11 00:42:58 -04:00
}
2024-10-14 23:32:52 -04:00
fn copyBufferToImage(const vk::Buffer& buffer, const vk::Image& image, const u32& width, const u32& height)
-> void {
vk::CommandBuffer commandBuffer = beginSingleTimeCommands();
2024-10-11 00:42:58 -04:00
vk::BufferImageCopy region {
.bufferOffset = 0,
.bufferRowLength = 0,
.bufferImageHeight = 0,
.imageSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1 },
2024-10-11 22:34:15 -04:00
.imageOffset = { .x = 0, .y = 0, .z = 0 },
.imageExtent = { .width = width, .height = height, .depth = 1 },
2024-10-11 00:42:58 -04:00
};
2024-10-14 23:32:52 -04:00
commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, 1, &region);
2024-10-11 00:42:58 -04:00
2024-10-14 23:32:52 -04:00
endSingleTimeCommands(commandBuffer);
2024-10-11 00:42:58 -04:00
}
/**
* @brief Loads the 3D model.
*
* This function loads a 3D model from an OBJ file, extracting vertex and index data.
* It also removes duplicate vertices to optimize the model.
*/
2024-10-11 20:04:28 -04:00
fn loadModel() -> void {
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string warn, err;
if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH))
throw std::runtime_error(warn + err);
std::unordered_map<Vertex, u32> uniqueVertices {};
for (const tinyobj::shape_t& shape : shapes) {
for (const tinyobj::index_t& index : shape.mesh.indices) {
Vertex vertex {
.pos = {
2024-10-11 22:34:15 -04:00
attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 0)],
attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 1)],
attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 2)],
2024-10-11 20:04:28 -04:00
},
.color = { 1.0F, 1.0F, 1.0F },
.tex_coord = {
2024-10-11 22:34:15 -04:00
attrib.texcoords[static_cast<u32>((2 * index.texcoord_index) + 0)],
1.0F - attrib.texcoords[static_cast<u32>((2 * index.texcoord_index) + 1)],
2024-10-14 23:32:52 -04:00
},
2024-10-11 20:04:28 -04:00
};
if (!uniqueVertices.contains(vertex)) {
uniqueVertices[vertex] = static_cast<u32>(mVertices.size());
mVertices.push_back(vertex);
}
mIndices.push_back(uniqueVertices[vertex]);
}
}
}
/**
* @brief Creates the vertex buffer.
*
* This function creates a vertex buffer on the GPU and transfers vertex data from CPU memory.
* It uses a staging buffer for the transfer to allow for better performance.
*/
fn createVertexBuffer() -> void {
2024-10-11 20:04:28 -04:00
vk::DeviceSize bufferSize = sizeof(mVertices[0]) * mVertices.size();
2024-10-10 16:04:46 -04:00
vk::UniqueBuffer stagingBuffer;
vk::UniqueDeviceMemory stagingBufferMemory;
2024-10-14 23:32:52 -04:00
std::tie(stagingBuffer, stagingBufferMemory) = createBuffer(
2024-10-10 16:04:46 -04:00
bufferSize,
vk::BufferUsageFlagBits::eTransferSrc,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
2024-10-10 16:04:46 -04:00
);
2024-10-11 20:04:28 -04:00
copyData(stagingBufferMemory.get(), bufferSize, mVertices.data());
2024-10-10 16:04:46 -04:00
2024-10-14 23:32:52 -04:00
std::tie(mVertexBuffer, mVertexBufferMemory) = createBuffer(
2024-10-10 16:04:46 -04:00
bufferSize,
vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eDeviceLocal
2024-10-10 16:04:46 -04:00
);
copyBuffer(stagingBuffer.get(), mVertexBuffer.get(), bufferSize);
stagingBuffer.reset();
stagingBufferMemory.reset();
}
/**
* @brief Creates the index buffer.
*
* This function creates an index buffer on the GPU and transfers index data from CPU memory.
* It uses a staging buffer for the transfer to allow for better performance.
*/
2024-10-10 18:51:20 -04:00
fn createIndexBuffer() -> void {
2024-10-11 20:04:28 -04:00
vk::DeviceSize bufferSize = sizeof(mIndices[0]) * mIndices.size();
2024-10-10 18:51:20 -04:00
vk::UniqueBuffer stagingBuffer;
vk::UniqueDeviceMemory stagingBufferMemory;
2024-10-14 23:32:52 -04:00
std::tie(stagingBuffer, stagingBufferMemory) = createBuffer(
2024-10-10 18:51:20 -04:00
bufferSize,
vk::BufferUsageFlagBits::eTransferSrc,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
2024-10-10 18:51:20 -04:00
);
2024-10-11 20:04:28 -04:00
copyData(stagingBufferMemory.get(), bufferSize, mIndices.data());
2024-10-10 18:51:20 -04:00
2024-10-14 23:32:52 -04:00
std::tie(mIndexBuffer, mIndexBufferMemory) = createBuffer(
2024-10-10 18:51:20 -04:00
bufferSize,
vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eTransferDst,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eDeviceLocal
2024-10-10 18:51:20 -04:00
);
copyBuffer(stagingBuffer.get(), mIndexBuffer.get(), bufferSize);
stagingBuffer.reset();
stagingBufferMemory.reset();
}
/**
* @brief Creates uniform buffers.
*
* This function creates uniform buffers for each frame in flight. These buffers are used
* to pass uniform data (like transformation matrices) to shaders.
*/
fn createUniformBuffers() -> void {
vk::DeviceSize bufferSize = sizeof(UniformBufferObject);
mUniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
mUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
mUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);
for (usize i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
2024-10-14 23:32:52 -04:00
std::tie(mUniformBuffers[i], mUniformBuffersMemory[i]) = createBuffer(
bufferSize,
vk::BufferUsageFlagBits::eUniformBuffer,
2024-10-14 23:32:52 -04:00
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
);
mUniformBuffersMapped[i] = mDevice->mapMemory(mUniformBuffersMemory[i].get(), 0, bufferSize);
}
}
/**
* @brief Creates the descriptor pool.
*
* This function creates a descriptor pool from which descriptor sets can be allocated.
* The pool is sized to accommodate the number of frames in flight.
*/
fn createDescriptorPool() -> void {
2024-10-11 00:42:58 -04:00
std::array<vk::DescriptorPoolSize, 2> poolSizes = {
2024-10-14 23:32:52 -04:00
vk::DescriptorPoolSize {
.type = vk::DescriptorType::eUniformBuffer,
.descriptorCount = MAX_FRAMES_IN_FLIGHT,
},
vk::DescriptorPoolSize {
.type = vk::DescriptorType::eCombinedImageSampler,
.descriptorCount = MAX_FRAMES_IN_FLIGHT,
},
};
vk::DescriptorPoolCreateInfo poolInfo {
2024-10-11 00:42:58 -04:00
.maxSets = MAX_FRAMES_IN_FLIGHT,
.poolSizeCount = static_cast<u32>(poolSizes.size()),
.pPoolSizes = poolSizes.data(),
};
mDescriptorPool = mDevice->createDescriptorPoolUnique(poolInfo);
}
/**
* @brief Creates descriptor sets.
*
* This function allocates and updates descriptor sets for each frame in flight.
* These sets bind the uniform buffers and texture sampler to the shader.
*/
fn createDescriptorSets() -> void {
std::vector<vk::DescriptorSetLayout> layouts(MAX_FRAMES_IN_FLIGHT, mDescriptorSetLayout.get());
vk::DescriptorSetAllocateInfo allocInfo {
.descriptorPool = mDescriptorPool.get(),
.descriptorSetCount = static_cast<u32>(MAX_FRAMES_IN_FLIGHT),
.pSetLayouts = layouts.data(),
};
mDescriptorSets = mDevice->allocateDescriptorSets(allocInfo);
for (usize i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vk::DescriptorBufferInfo bufferInfo {
.buffer = mUniformBuffers[i].get(),
.offset = 0,
.range = sizeof(UniformBufferObject),
};
2024-10-11 00:42:58 -04:00
vk::DescriptorImageInfo imageInfo {
.sampler = mTextureSampler.get(),
.imageView = mTextureImageView.get(),
.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal,
};
std::array<vk::WriteDescriptorSet, 2> descriptorWrites = {
2024-10-14 23:32:52 -04:00
vk::WriteDescriptorSet {
.dstSet = mDescriptorSets[i],
.dstBinding = 0,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &bufferInfo,
},
vk::WriteDescriptorSet {
.dstSet = mDescriptorSets[i],
.dstBinding = 1,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &imageInfo,
},
};
2024-10-11 00:42:58 -04:00
mDevice->updateDescriptorSets(descriptorWrites, {});
}
}
/**
* @brief Creates a Vulkan buffer.
*
* @param deviceSize The size of the buffer to create.
* @param bufferUsageFlags The usage flags for the buffer.
* @param memoryPropertyFlags The desired properties of the memory to be allocated.
* @return A pair containing the created buffer and its associated device memory.
*
* This function creates a Vulkan buffer with the specified size, usage, and memory properties.
*/
2024-10-10 16:04:46 -04:00
fn createBuffer(
2024-10-14 23:32:52 -04:00
const vk::DeviceSize& deviceSize,
const vk::BufferUsageFlags& bufferUsageFlags,
const vk::MemoryPropertyFlags& memoryPropertyFlags
) -> std::pair<vk::UniqueBuffer, vk::UniqueDeviceMemory> {
vk::BufferCreateInfo bufferInfo {
2024-10-10 16:04:46 -04:00
.size = deviceSize,
.usage = bufferUsageFlags,
.sharingMode = vk::SharingMode::eExclusive,
};
2024-10-14 23:32:52 -04:00
// Create the buffer
vk::UniqueBuffer buffer = mDevice->createBufferUnique(bufferInfo);
2024-10-14 23:32:52 -04:00
// Get the memory requirements for the buffer
2024-10-10 16:04:46 -04:00
vk::MemoryRequirements memRequirements = mDevice->getBufferMemoryRequirements(buffer.get());
2024-10-14 23:32:52 -04:00
// Memory allocation info
vk::MemoryAllocateInfo allocInfo {
.allocationSize = memRequirements.size,
2024-10-10 16:04:46 -04:00
.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, memoryPropertyFlags),
};
2024-10-14 23:32:52 -04:00
// Allocate memory
vk::UniqueDeviceMemory bufferMemory = mDevice->allocateMemoryUnique(allocInfo);
2024-10-10 16:04:46 -04:00
2024-10-14 23:32:52 -04:00
// Bind the allocated memory to the buffer
2024-10-10 16:04:46 -04:00
mDevice->bindBufferMemory(buffer.get(), bufferMemory.get(), 0);
2024-10-14 23:32:52 -04:00
// Return both the unique buffer and its associated memory
return { std::move(buffer), std::move(bufferMemory) };
2024-10-10 16:04:46 -04:00
}
/**
* @brief Begins a single-time command buffer.
*
* @return A command buffer ready for recording commands.
*
* This function allocates and begins a command buffer for one-time use operations.
*/
2024-10-14 23:32:52 -04:00
fn beginSingleTimeCommands() -> vk::CommandBuffer {
// Define the command buffer allocation info
2024-10-10 16:04:46 -04:00
vk::CommandBufferAllocateInfo allocInfo {
.commandPool = mCommandPool.get(),
.level = vk::CommandBufferLevel::ePrimary,
.commandBufferCount = 1,
};
2024-10-14 23:32:52 -04:00
// Allocate the command buffer
vk::CommandBuffer commandBuffer = mDevice->allocateCommandBuffers(allocInfo)[0];
2024-10-10 16:04:46 -04:00
2024-10-14 23:32:52 -04:00
// Define the command buffer begin info
2024-10-10 16:04:46 -04:00
vk::CommandBufferBeginInfo beginInfo { .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit };
2024-10-14 23:32:52 -04:00
// Begin the command buffer
commandBuffer.begin(beginInfo);
2024-10-10 16:04:46 -04:00
2024-10-11 00:42:58 -04:00
return commandBuffer;
}
2024-10-10 16:04:46 -04:00
/**
* @brief Ends and submits a single-time command buffer.
*
* @param commandBuffer The command buffer to end and submit.
*
* This function ends the recording of a single-time command buffer, submits it to the queue,
* and waits for it to complete before freeing the command buffer.
*/
2024-10-14 23:32:52 -04:00
fn endSingleTimeCommands(const vk::CommandBuffer& commandBuffer) -> void {
// End the command buffer
commandBuffer.end();
2024-10-14 23:32:52 -04:00
// Define the submit info
vk::SubmitInfo submitInfo { .commandBufferCount = 1, .pCommandBuffers = &commandBuffer };
2024-10-14 23:32:52 -04:00
// Submit the command buffer
2024-10-10 16:04:46 -04:00
mGraphicsQueue.submit(submitInfo, nullptr);
2024-10-14 23:32:52 -04:00
// Wait for the queue to finish
2024-10-10 16:04:46 -04:00
mGraphicsQueue.waitIdle();
2024-10-14 23:32:52 -04:00
// Free the command buffer
mDevice->freeCommandBuffers(mCommandPool.get(), commandBuffer);
}
/**
* @brief Copies data to a mapped memory.
*
* @param stagingBufferMemory The device memory to copy to.
* @param bufferSize The size of the data to copy.
* @param src Pointer to the source data.
*
* This function maps a memory, copies data to it, and then unmaps the memory.
*/
2024-10-14 23:32:52 -04:00
fn copyData(const vk::DeviceMemory& stagingBufferMemory, const vk::DeviceSize& bufferSize, const void* src)
-> void {
// Map the memory
2024-10-11 00:42:58 -04:00
void* data = mDevice->mapMemory(stagingBufferMemory, 0, bufferSize);
2024-10-14 23:32:52 -04:00
// Copy the data with memcpy - memcpy(dst, src, size)
2024-10-11 00:42:58 -04:00
memcpy(data, src, static_cast<usize>(bufferSize));
2024-10-14 23:32:52 -04:00
// Unmap the memory
2024-10-11 00:42:58 -04:00
mDevice->unmapMemory(stagingBufferMemory);
}
/**
* @brief Copies data from one buffer to another.
*
* @param srcBuffer The source buffer.
* @param dstBuffer The destination buffer.
* @param deviceSize The size of data to copy.
*
* This function records and submits a command to copy data between two buffers.
*/
2024-10-14 23:32:52 -04:00
fn copyBuffer(const vk::Buffer& srcBuffer, const vk::Buffer& dstBuffer, const vk::DeviceSize& deviceSize)
-> void {
// Begin a single time command buffer
vk::CommandBuffer commandBuffer = beginSingleTimeCommands();
2024-10-11 00:42:58 -04:00
2024-10-14 23:32:52 -04:00
// Define the copy region
2024-10-11 00:42:58 -04:00
vk::BufferCopy copyRegion { .size = deviceSize };
2024-10-14 23:32:52 -04:00
// Copy the buffer
commandBuffer.copyBuffer(srcBuffer, dstBuffer, 1, &copyRegion);
2024-10-11 00:42:58 -04:00
2024-10-14 23:32:52 -04:00
// End the single time command buffer
endSingleTimeCommands(commandBuffer);
2024-10-11 00:42:58 -04:00
}
/**
* @brief Finds a memory type index that satisfies the given properties.
*
* @param typeFilter A bit field of memory types that are suitable for the buffer.
* @param properties The properties the memory type must have.
* @return The index of a suitable memory type.
*
* This function finds a memory type that satisfies both the type filter and the desired properties.
*/
2024-10-14 23:32:52 -04:00
fn findMemoryType(const u32& typeFilter, const vk::MemoryPropertyFlags& properties) -> u32 {
// Get the memory properties of the physical device
vk::PhysicalDeviceMemoryProperties memProperties = mPhysicalDevice.getMemoryProperties();
2024-10-14 23:32:52 -04:00
// Loop through the memory types and find the one that matches the filter
for (u32 idx = 0; idx < memProperties.memoryTypeCount; idx++)
if ((typeFilter & (1 << idx)) &&
(memProperties.memoryTypes.at(idx).propertyFlags & properties) == properties)
return idx;
2024-10-14 23:32:52 -04:00
// Throw an error if no suitable memory type is found
throw std::runtime_error("Failed to find a suitable memory type!");
}
/**
* @brief Creates command buffers.
*
* This function allocates command buffers from the command pool. One command buffer is
* allocated for each frame in flight.
*/
2024-10-05 23:08:12 -04:00
fn createCommandBuffers() -> void {
2024-10-14 23:32:52 -04:00
// Resize the command buffers to hold the maximum number of frames in flight
2024-10-05 23:08:12 -04:00
mCommandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
2024-10-14 23:32:52 -04:00
// Define the command buffer allocation info
vk::CommandBufferAllocateInfo allocInfo {
.commandPool = mCommandPool.get(),
.level = vk::CommandBufferLevel::ePrimary,
.commandBufferCount = static_cast<u32>(mCommandBuffers.size()),
};
2024-10-01 18:54:41 -04:00
2024-10-14 23:32:52 -04:00
// Allocate the command buffers
2024-10-06 18:14:15 -04:00
mCommandBuffers = mDevice->allocateCommandBuffersUnique(allocInfo);
2024-10-01 18:54:41 -04:00
}
/**
* @brief Records a command buffer.
*
* @param commandBuffer The command buffer to record into.
* @param imageIndex The index of the swap chain image to render to.
*
* This function records drawing commands into the given command buffer.
*/
2024-10-14 23:32:52 -04:00
fn recordCommandBuffer(const vk::CommandBuffer& commandBuffer, const u32& imageIndex) -> void {
// Define the command buffer begin info
2024-10-01 18:54:41 -04:00
vk::CommandBufferBeginInfo beginInfo {};
2024-10-14 23:32:52 -04:00
// Begin the command buffer
2024-10-06 18:14:15 -04:00
commandBuffer.begin(beginInfo);
2024-10-01 18:54:41 -04:00
2024-10-14 23:32:52 -04:00
// Define the render pass begin info
2024-10-11 19:09:37 -04:00
std::array<vk::ClearValue, 2> clearValues {
2024-10-14 23:32:52 -04:00
// Set the color buffer to black
vk::ClearValue { .color = { .uint32 = std::array<u32, 4> { 0, 0, 0, 255 } } },
// Set the depth buffer to 1.0F
vk::ClearValue { .depthStencil = { .depth = 1.0F, .stencil = 0 } },
2024-10-11 19:09:37 -04:00
};
2024-10-01 18:54:41 -04:00
2024-10-14 23:32:52 -04:00
// Define the render pass info
2024-10-01 18:54:41 -04:00
vk::RenderPassBeginInfo renderPassInfo {
2024-10-14 23:32:52 -04:00
// Render pass itself
.renderPass = mRenderPass.get(),
// Current framebuffer
.framebuffer = mSwapChainFramebuffers[imageIndex].get(),
// Render area (entire framebuffer)
.renderArea = { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent },
// Clear values for the attachments
2024-10-11 19:09:37 -04:00
.clearValueCount = static_cast<u32>(clearValues.size()),
2024-10-14 23:32:52 -04:00
.pClearValues = clearValues.data(),
2024-10-01 18:54:41 -04:00
};
2024-10-14 23:32:52 -04:00
// Begin the render pass
2024-10-01 18:54:41 -04:00
commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
2024-10-14 23:32:52 -04:00
// Bind the graphics pipeline to the command buffer
2024-10-01 18:54:41 -04:00
commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mGraphicsPipeline.get());
2024-10-14 23:32:52 -04:00
// Create the viewport and scissor.
// If the scissor is smaller than the
// viewport, then it will be clipped.
2024-10-01 18:54:41 -04:00
vk::Viewport viewport {
.x = 0.0F,
.y = 0.0F,
.width = static_cast<f32>(mSwapChainExtent.width),
.height = static_cast<f32>(mSwapChainExtent.height),
.minDepth = 0.0F,
.maxDepth = 1.0F,
};
2024-10-14 23:32:52 -04:00
// Create the scissor
2024-10-01 18:54:41 -04:00
vk::Rect2D scissor {
2024-10-11 22:34:15 -04:00
.offset = { .x = 0, .y = 0 },
2024-10-01 18:54:41 -04:00
.extent = mSwapChainExtent,
};
2024-10-14 23:32:52 -04:00
// Set the viewport and scissor
2024-10-01 19:00:31 -04:00
commandBuffer.setViewport(0, viewport);
2024-10-01 18:54:41 -04:00
commandBuffer.setScissor(0, scissor);
2024-10-14 23:32:52 -04:00
// Bind the vertex buffer
commandBuffer.bindVertexBuffers(0, mVertexBuffer.get(), { 0 });
2024-10-14 23:32:52 -04:00
// Bind the index buffer
2024-10-11 20:04:28 -04:00
commandBuffer.bindIndexBuffer(mIndexBuffer.get(), 0, vk::IndexType::eUint32);
2024-10-10 18:51:20 -04:00
2024-10-14 23:32:52 -04:00
// Bind the descriptor sets
commandBuffer.bindDescriptorSets(
vk::PipelineBindPoint::eGraphics,
mPipelineLayout.get(),
0,
1,
&mDescriptorSets[mCurrentFrame],
0,
nullptr
);
2024-10-14 23:32:52 -04:00
// Draw the indexed vertices
2024-10-11 20:04:28 -04:00
commandBuffer.drawIndexed(static_cast<u32>(mIndices.size()), 1, 0, 0, 0);
2024-10-01 18:54:41 -04:00
2024-10-15 19:49:39 -04:00
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Your ImGui code here
ImGui::ShowDemoWindow();
ImGui::Render();
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer);
2024-10-14 23:32:52 -04:00
// End the render pass
2024-10-01 18:54:41 -04:00
commandBuffer.endRenderPass();
2024-10-14 23:32:52 -04:00
// End the command buffer
2024-10-06 18:14:15 -04:00
commandBuffer.end();
2024-10-01 18:54:41 -04:00
}
/**
* @brief Creates synchronization objects for frame rendering.
*
* This function creates semaphores and fences used for synchronizing operations
* between the CPU and GPU, and between different stages of rendering.
*/
2024-10-01 18:54:41 -04:00
fn createSyncObjects() -> void {
2024-10-14 23:32:52 -04:00
// Resize the vectors to hold the maximum number of frames in flight
2024-10-05 23:08:12 -04:00
mImageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
mRenderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
mInFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
2024-10-14 23:32:52 -04:00
// Create the semaphore and fence info
2024-10-01 18:54:41 -04:00
vk::SemaphoreCreateInfo semaphoreInfo {};
vk::FenceCreateInfo fenceInfo { .flags = vk::FenceCreateFlagBits::eSignaled };
2024-10-14 23:32:52 -04:00
// Loop through the maximum number of frames in flight and create the semaphores and fences
2024-10-05 23:08:12 -04:00
for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
2024-10-06 18:14:15 -04:00
mImageAvailableSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo);
mRenderFinishedSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo);
mInFlightFences[idx] = mDevice->createFenceUnique(fenceInfo);
2024-10-05 23:08:12 -04:00
}
2024-10-01 18:54:41 -04:00
}
/**
* @brief Updates the uniform buffer for the current frame.
*
* @param currentImage The index of the current swap chain image.
*
* This function updates the uniform buffer object (UBO) with new transformation
* matrices for each frame. It calculates a new model matrix based on time,
* and updates the view and projection matrices.
*/
2024-10-14 23:32:52 -04:00
fn updateUniformBuffer(const u32& currentImage) -> void {
2024-10-15 20:38:05 -04:00
// For convenience
2024-10-14 23:32:52 -04:00
using namespace std::chrono;
2024-10-15 20:38:05 -04:00
using time_point = high_resolution_clock::time_point;
// Time of the program start
2024-10-14 23:32:52 -04:00
static time_point StartTime = high_resolution_clock::now();
2024-10-15 20:38:05 -04:00
// Current time
2024-10-14 23:32:52 -04:00
time_point currentTime = high_resolution_clock::now();
2024-10-15 20:38:05 -04:00
// Time since the program started
f32 time = duration<f32, seconds::period>(currentTime - StartTime).count();
// Uniform buffer object
UniformBufferObject ubo {
2024-10-15 20:38:05 -04:00
// Model matrix - glm::rotate(matrix, angle, axis)
.model = glm::rotate(glm::mat4(1.0F), time * glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F)),
2024-10-15 20:38:05 -04:00
// View matrix - glm::lookAt(eye, center, up)
.view =
glm::lookAt(glm::vec3(2.0F, 2.0F, 2.0F), glm::vec3(0.0F, 0.0F, 0.0F), glm::vec3(0.0F, 0.0F, 1.0F)),
2024-10-15 20:38:05 -04:00
// Projection matrix - glm::perspective(fov, aspect, near, far)
.proj = glm::perspective(
glm::radians(45.0F),
static_cast<f32>(mSwapChainExtent.width) / static_cast<f32>(mSwapChainExtent.height),
0.1F,
10.0F
)
};
// Flip the Y axis, because glm was designed for OpenGL
ubo.proj[1][1] *= -1;
2024-10-15 20:38:05 -04:00
// Copy the uniform buffer object to the mapped memory
memcpy(mUniformBuffersMapped[currentImage], &ubo, sizeof(ubo));
}
/**
* @brief Renders a single frame.
*
* This function performs all the steps necessary to render a single frame:
* 1. Waits for the previous frame to finish
* 2. Acquires an image from the swap chain
* 3. Records a command buffer
* 4. Submits the command buffer
* 5. Presents the swap chain image
*/
2024-10-01 18:54:41 -04:00
fn drawFrame() -> void {
2024-10-06 18:34:42 -04:00
try {
2024-10-15 20:38:05 -04:00
// Wait for the fence to signal that the frame is finished
2024-10-06 18:34:42 -04:00
vk::Result result =
mDevice->waitForFences(mInFlightFences[mCurrentFrame].get(), vk::Bool32(vk::True), UINT64_MAX);
2024-10-01 18:54:41 -04:00
2024-10-15 20:38:05 -04:00
// Make sure the result is successful
2024-10-06 18:34:42 -04:00
if (result != vk::Result::eSuccess)
throw std::runtime_error("Failed to wait for fences!");
2024-10-01 18:54:41 -04:00
2024-10-15 20:38:05 -04:00
// Acquire the next image from the swap chain
auto [imageIndexResult, imageIndexValue] = mDevice->acquireNextImageKHR(
2024-10-06 18:34:42 -04:00
mSwapChain.get(), UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame].get(), nullptr
);
2024-10-05 23:08:12 -04:00
2024-10-15 20:38:05 -04:00
// Check if the swap chain needs to be recreated
2024-10-06 18:34:42 -04:00
if (imageIndexResult == vk::Result::eErrorOutOfDateKHR) {
recreateSwapChain();
return;
}
2024-10-05 23:08:12 -04:00
2024-10-15 20:38:05 -04:00
// Check if the image index is valid
2024-10-06 18:34:42 -04:00
if (imageIndexResult != vk::Result::eSuccess && imageIndexResult != vk::Result::eSuboptimalKHR)
throw std::runtime_error("Failed to acquire swap chain image!");
2024-10-01 18:54:41 -04:00
2024-10-15 20:38:05 -04:00
// Update the uniform buffer with the current image
updateUniformBuffer(mCurrentFrame);
2024-10-15 20:38:05 -04:00
// Reset the current fence
2024-10-06 18:34:42 -04:00
mDevice->resetFences(mInFlightFences[mCurrentFrame].get());
2024-10-01 18:54:41 -04:00
2024-10-15 20:38:05 -04:00
// Reset the current command buffer
2024-10-06 18:34:42 -04:00
mCommandBuffers[mCurrentFrame]->reset(vk::CommandBufferResetFlagBits::eReleaseResources);
2024-10-15 20:38:05 -04:00
// Define the command buffer submit info
2024-10-06 18:34:42 -04:00
recordCommandBuffer(mCommandBuffers[mCurrentFrame].get(), imageIndexValue);
2024-10-01 18:54:41 -04:00
2024-10-06 18:34:42 -04:00
std::array<vk::PipelineStageFlags, 1> waitStages = {
vk::PipelineStageFlagBits::eColorAttachmentOutput
};
2024-10-01 18:54:41 -04:00
2024-10-06 18:34:42 -04:00
vk::SubmitInfo submitInfo {
.waitSemaphoreCount = 1,
.pWaitSemaphores = &mImageAvailableSemaphores[mCurrentFrame].get(),
.pWaitDstStageMask = waitStages.data(),
.commandBufferCount = 1,
.pCommandBuffers = &mCommandBuffers[mCurrentFrame].get(),
.signalSemaphoreCount = 1,
.pSignalSemaphores = &mRenderFinishedSemaphores[mCurrentFrame].get(),
};
2024-10-01 18:54:41 -04:00
2024-10-15 20:38:05 -04:00
// Submit the graphics queue
2024-10-06 18:34:42 -04:00
mGraphicsQueue.submit(submitInfo, mInFlightFences[mCurrentFrame].get());
2024-10-01 18:54:41 -04:00
2024-10-06 18:34:42 -04:00
vk::PresentInfoKHR presentInfo {
.waitSemaphoreCount = 1,
.pWaitSemaphores = &mRenderFinishedSemaphores[mCurrentFrame].get(),
.swapchainCount = 1,
.pSwapchains = &mSwapChain.get(),
.pImageIndices = &imageIndexValue,
};
2024-10-01 18:54:41 -04:00
2024-10-15 20:38:05 -04:00
// Present the swap chain image
2024-10-06 18:14:15 -04:00
vk::Result presentResult = mPresentQueue.presentKHR(presentInfo);
2024-10-15 20:38:05 -04:00
// Check if the swap chain needs to be recreated
2024-10-06 18:14:15 -04:00
if (presentResult == vk::Result::eErrorOutOfDateKHR || presentResult == vk::Result::eSuboptimalKHR ||
mFramebufferResized) {
mFramebufferResized = false;
recreateSwapChain();
2024-10-15 20:38:05 -04:00
} else if (presentResult != vk::Result::eSuccess) {
// Throw if present failed
2024-10-06 18:14:15 -04:00
throw std::runtime_error("Failed to present swap chain image!");
2024-10-15 20:38:05 -04:00
}
2024-10-06 18:14:15 -04:00
2024-10-15 20:38:05 -04:00
// Increment the current frame (or loop back to 0)
2024-10-06 18:14:15 -04:00
mCurrentFrame = (mCurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
2024-10-07 16:04:49 -04:00
} catch (vk::OutOfDateKHRError& /*err*/) {
2024-10-15 20:38:05 -04:00
// Recreate the swap chain if it's out of date
2024-10-05 23:08:12 -04:00
mFramebufferResized = false;
recreateSwapChain();
2024-10-06 18:14:15 -04:00
return;
2024-10-05 23:08:12 -04:00
}
2024-10-01 18:54:41 -04:00
}
/**
* @brief Creates a shader module from compiled shader code.
*
* @param code A vector of chars containing the compiled shader code.
* @return A unique shader module.
*
* This function takes compiled shader code and creates a Vulkan shader module from it.
*/
2024-10-01 18:30:31 -04:00
fn createShaderModule(const std::vector<char>& code) -> vk::UniqueShaderModule {
2024-10-14 23:32:52 -04:00
vk::ShaderModuleCreateInfo createInfo {
.codeSize = code.size(),
.pCode = std::bit_cast<const u32*>(code.data()),
};
2024-10-01 18:30:31 -04:00
2024-10-06 18:14:15 -04:00
return mDevice->createShaderModuleUnique(createInfo);
2024-10-01 18:30:31 -04:00
}
/**
* @brief Chooses the best surface format for the swap chain.
*
* @param availableFormats A vector of available surface formats.
* @return The chosen surface format.
*
* This function selects the best surface format from the available options,
* preferring SRGB color space when available.
*/
2024-10-05 23:08:12 -04:00
static fn chooseSwapSurfaceFormat(const std::vector<vk::SurfaceFormatKHR>& availableFormats
) -> vk::SurfaceFormatKHR {
2024-10-14 23:32:52 -04:00
// If SRGB is available, use it
for (const vk::SurfaceFormatKHR& availableFormat : availableFormats)
2024-10-01 16:57:40 -04:00
if (availableFormat.format == vk::Format::eB8G8R8A8Srgb &&
availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear)
return availableFormat;
2024-10-14 23:32:52 -04:00
// Otherwise, use the first available format
2024-10-01 16:57:40 -04:00
return availableFormats[0];
}
/**
* @brief Chooses the best presentation mode for the swap chain.
*
* @param availablePresentModes A vector of available presentation modes.
* @return The chosen presentation mode.
*
* This function selects the best presentation mode from the available options,
* preferring mailbox mode (triple buffering) when available.
*/
2024-10-05 23:08:12 -04:00
static fn chooseSwapPresentMode(const std::vector<vk::PresentModeKHR>& availablePresentModes
) -> vk::PresentModeKHR {
2024-10-14 23:32:52 -04:00
// Check if mailbox mode is available (adaptive sync)
for (const vk::PresentModeKHR& availablePresentMode : availablePresentModes)
2024-10-01 16:57:40 -04:00
if (availablePresentMode == vk::PresentModeKHR::eMailbox)
return availablePresentMode;
2024-10-14 23:32:52 -04:00
// If mailbox mode is not available, use FIFO mode (vsync)
2024-10-01 16:57:40 -04:00
return vk::PresentModeKHR::eFifo;
}
/**
* @brief Chooses the swap extent (resolution) for the swap chain.
*
* @param capabilities The surface capabilities of the device.
* @return The chosen swap extent.
*
* This function determines the resolution of the swap chain images,
* taking into account the current window size and device limitations.
*/
2024-10-14 23:32:52 -04:00
fn chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) -> vk::Extent2D {
// If the resolution is not UINT32_MAX, return it
// Otherwise, we need to set the resolution manually
2024-10-01 16:57:40 -04:00
if (capabilities.currentExtent.width != UINT32_MAX)
return capabilities.currentExtent;
2024-10-14 23:32:52 -04:00
// Get the window's resolution
2024-10-01 16:57:40 -04:00
u32 width = 0, height = 0;
std::tie(width, height) = mWindow->getFramebufferSize();
2024-10-14 23:32:52 -04:00
// Return the resolution clamped to the supported range
return {
.width = std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
.height = std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height),
};
2024-10-01 16:57:40 -04:00
}
/**
* @brief Queries the swap chain support details for a physical device.
*
* @param device The physical device to query.
* @return A SwapChainSupportDetails struct containing the support information.
*
* This function retrieves information about the swap chain support,
* including surface capabilities, formats, and presentation modes.
*/
2024-10-14 23:32:52 -04:00
fn querySwapChainSupport(const vk::PhysicalDevice& device) -> SwapChainSupportDetails {
return {
.capabilities = device.getSurfaceCapabilitiesKHR(mSurface.get()),
.formats = device.getSurfaceFormatsKHR(mSurface.get()),
.present_modes = device.getSurfacePresentModesKHR(mSurface.get()),
};
2024-10-01 17:06:14 -04:00
}
/**
* @brief Checks if a physical device is suitable for the application.
*
* @param device The physical device to check.
* @return True if the device is suitable, false otherwise.
*
* This function checks if a physical device meets all the requirements
* of the application, including queue families, extensions, and features.
*/
2024-10-14 23:32:52 -04:00
fn isDeviceSuitable(const vk::PhysicalDevice& device) -> bool {
// Get the queue families that support the required operations
2024-10-10 18:51:20 -04:00
QueueFamilyIndices qfIndices = findQueueFamilies(device);
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// Check if the device supports the required extensions
2024-10-01 17:06:14 -04:00
bool extensionsSupported = checkDeviceExtensionSupport(device);
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
2024-10-14 23:32:52 -04:00
// Check if the swap chain is adequate (make sure it has
// at least one supported format and presentation mode)
2024-10-01 17:06:14 -04:00
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty();
}
2024-10-14 23:32:52 -04:00
// Check if the device supports the required features
2024-10-11 00:42:58 -04:00
vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures();
2024-10-14 23:32:52 -04:00
// If the device supports everything required, return true
2024-10-11 00:42:58 -04:00
return qfIndices.isComplete() && extensionsSupported && swapChainAdequate &&
supportedFeatures.samplerAnisotropy;
2024-10-01 17:06:14 -04:00
}
/**
* @brief Checks if a device supports all required extensions.
*
* @param device The physical device to check.
* @return True if all required extensions are supported, false otherwise.
*
* This function verifies that a physical device supports all the
* extensions required by the application.
*/
2024-10-14 23:32:52 -04:00
static fn checkDeviceExtensionSupport(const vk::PhysicalDevice& device) -> bool {
// Get the available extensions
2024-10-06 18:14:15 -04:00
std::vector<vk::ExtensionProperties> availableExtensions = device.enumerateDeviceExtensionProperties();
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// Create a set of required extension names
2024-10-01 17:06:14 -04:00
std::set<string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());
2024-10-14 23:32:52 -04:00
// Remove each required extension from the set of available extensions
2024-10-06 18:14:15 -04:00
for (const vk::ExtensionProperties& extension : availableExtensions)
2024-10-06 17:34:25 -04:00
requiredExtensions.erase(extension.extensionName);
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// If the set is empty, all required extensions are supported
2024-10-01 17:06:14 -04:00
return requiredExtensions.empty();
}
/**
* @brief Finds queue families that support required operations.
*
* @param device The physical device to check.
* @return A QueueFamilyIndices struct with the found queue family indices.
*
* This function finds queue families that support graphics operations
* and presentation to the window surface.
*/
2024-10-14 23:32:52 -04:00
fn findQueueFamilies(const vk::PhysicalDevice& device) -> QueueFamilyIndices {
// Create a struct to store the queue family indices
2024-10-10 18:51:20 -04:00
QueueFamilyIndices qfIndices;
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// Get the queue family properties
2024-10-01 17:06:14 -04:00
std::vector<vk::QueueFamilyProperties> queueFamilies = device.getQueueFamilyProperties();
2024-10-14 23:32:52 -04:00
// For every queue family,
2024-10-01 17:06:14 -04:00
for (u32 i = 0; i < queueFamilies.size(); i++) {
2024-10-14 23:32:52 -04:00
// Check if the queue family supports the required operations
2024-10-01 17:06:14 -04:00
if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics)
2024-10-10 18:51:20 -04:00
qfIndices.graphics_family = i;
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// Check if the queue family supports presentation
2024-10-06 18:14:15 -04:00
vk::Bool32 queuePresentSupport = device.getSurfaceSupportKHR(i, mSurface.get());
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// If the queue family supports presentation, set the present family index
2024-10-06 18:14:15 -04:00
if (queuePresentSupport)
2024-10-10 18:51:20 -04:00
qfIndices.present_family = i;
2024-10-01 17:06:14 -04:00
2024-10-14 23:32:52 -04:00
// If the queue family supports both operations, we're done
2024-10-10 18:51:20 -04:00
if (qfIndices.isComplete())
2024-10-01 17:06:14 -04:00
break;
}
2024-10-10 18:51:20 -04:00
return qfIndices;
2024-10-01 17:06:14 -04:00
}
/**
* @brief Checks if all requested validation layers are available.
*
* @return True if all validation layers are available, false otherwise.
*
* This function verifies that all requested validation layers are
* supported by the Vulkan implementation.
*/
2024-09-30 00:31:08 -04:00
static fn checkValidationLayerSupport() -> bool {
2024-10-06 18:14:15 -04:00
std::vector<vk::LayerProperties> availableLayers = vk::enumerateInstanceLayerProperties();
2024-09-30 00:31:08 -04:00
2024-10-14 23:32:52 -04:00
// Create a set of available layer names
std::unordered_set<std::string_view> availableLayerNames;
for (const vk::LayerProperties& layerProperties : availableLayers)
availableLayerNames.emplace(layerProperties.layerName);
2024-09-30 00:31:08 -04:00
2024-10-14 23:32:52 -04:00
// Check if each validation layer is available
for (const std::string_view layerName : validationLayers)
if (availableLayerNames.find(layerName) == availableLayerNames.end())
return false; // Layer not found
2024-09-30 00:31:08 -04:00
2024-10-14 23:32:52 -04:00
return true; // All validation layers are available
2024-09-30 00:31:08 -04:00
}
/**
* @brief Debug callback function for Vulkan validation layers.
*
* @param pCallbackData Pointer to a structure containing the message details.
* @return vk::False to indicate the call should not be aborted.
*
* This function is called by Vulkan to report debug messages from
* validation layers. It prints the message to the console.
*/
2024-09-28 18:13:24 -04:00
static VKAPI_ATTR fn VKAPI_CALL debugCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/,
VkDebugUtilsMessageTypeFlagsEXT /*messageType*/,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* /*pUserData*/
2024-09-30 00:57:13 -04:00
) -> vk::Bool32 {
2024-10-14 23:32:52 -04:00
// Print the message to the console
// Because pCallbackData already gives the message severity
// and type, we don't need to print them, so they're unused.
2024-09-29 23:02:04 -04:00
fmt::println("Validation layer: {}", pCallbackData->pMessage);
2024-09-28 18:13:24 -04:00
2024-09-30 21:34:25 -04:00
return vk::False;
2024-09-25 23:03:56 -04:00
}
};
/**
* @brief The main function of the application.
*
* @return 0 if the application runs successfully, non-zero otherwise.
*
* This function initializes the Vulkan dynamic dispatcher, creates an instance
* of the VulkanApp class, and runs the application. It catches and reports any
* exceptions that occur during execution.
*/
2024-09-28 21:55:26 -04:00
fn main() -> i32 {
2024-10-14 23:32:52 -04:00
// Initialize dynamic function dispatcher
2024-10-12 15:39:20 -04:00
VULKAN_HPP_DEFAULT_DISPATCHER.init();
2024-10-06 12:26:44 -04:00
2024-10-14 23:32:52 -04:00
// Create an app instance
2024-09-29 23:11:12 -04:00
VulkanApp app;
2024-09-25 23:03:56 -04:00
try {
app.run();
} catch (const std::exception& e) {
2024-10-05 23:08:12 -04:00
fmt::println("{}", e.what());
2024-09-25 23:03:56 -04:00
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}