diff --git a/src/main.cpp b/src/main.cpp index 2506513..b860068 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,71 +1,61 @@ -// Time measurements -#include -// String formatting -#include -// Unordered sets -#include +// Include necessary headers +#include // For time-related functions +#include // For string formatting +#include // For unordered_set container -// Make depth go from 0 to 1 instead of -1 to 1 -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -// Provides various math-related data structures -#include +// 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 // Include GLM for mathematics operations -// Used to load models +// TinyObjLoader for loading 3D models #define TINYOBJLOADER_IMPLEMENTATION #include -// Dynamic function loading -#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1 -// Use the beta extensions -#define VK_ENABLE_BETA_EXTENSIONS -// Use {} instead of () -#define VULKAN_HPP_NO_CONSTRUCTORS -// Vulkan itself -#include +// 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 // Include Vulkan C++ bindings -// Needed for dynamic function loading to work +// Necessary for dynamic dispatch to work VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE -// Type aliases -#include "util/types.h" +// 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 -// STB image helper -#include "util/unique_image.h" - -// Vertex class -#include "util/vertex.h" - -// ImGui +// ImGui headers for GUI #include #include #include -// Use {} instead of () -#define VKFW_NO_STRUCT_CONSTRUCTORS -// GLFW C++ wrapper -#include "vkfw.hpp" +// GLFW configuration and inclusion +#define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs +#include "vkfw.hpp" // Include GLFW C++ bindings -// Initial width and height +// Constants for window dimensions constexpr i32 WIDTH = 800; constexpr i32 HEIGHT = 600; -// Paths for the model and texture to render +// File paths for 3D model and texture constexpr const char* MODEL_PATH = "models/viking_room.obj"; constexpr const char* TEXTURE_PATH = "textures/viking_room.png"; -// Paths for the shaders +// File paths for shader programs constexpr const char* FRAGMENT_SHADER_PATH = "shaders/frag.spv"; constexpr const char* VERTEX_SHADER_PATH = "shaders/vert.spv"; -// Maximum number of frames to be worked on at the same time +// Maximum number of frames that can be processed concurrently constexpr i32 MAX_FRAMES_IN_FLIGHT = 2; -// Enable validation layers only in debug mode +// Validation layers for debug builds #ifndef NDEBUG constexpr std::array validationLayers = { "VK_LAYER_KHRONOS_validation" }; #endif -// macOS requires the portability extension to be enabled +// Required device extensions (platform-specific) #ifdef __APPLE__ constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName, vk::KHRPortabilitySubsetExtensionName }; @@ -73,54 +63,64 @@ constexpr std::array deviceExtensions = { vk::KHRSwapchainExtens constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName }; #endif +// Main application class class VulkanApp { 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. + */ fn run() -> void { - // Create window - initWindow(); - // Setup vulkan - initVulkan(); - // Render loop - mainLoop(); + initWindow(); // Initialize the application window + initVulkan(); // Initialize Vulkan + mainLoop(); // Enter the main rendering loop - cleanupSwapChain(); + 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; + } + + // Shut down ImGui ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); } private: - // Instances - vkfw::UniqueInstance mVKFWInstance; // Unique handle to the GLFW instance - vkfw::UniqueWindow mWindow; // Unique handle to the GLFW window - vk::UniqueInstance mInstance; // Unique handle to the Vulkan instance + // GLFW and Vulkan instance handles + vkfw::UniqueInstance mVKFWInstance; // GLFW instance + vkfw::UniqueWindow mWindow; // Application window + vk::UniqueInstance mInstance; // Vulkan instance - // Debug messenger for Vulkan validation layers - vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; // Unique handle to the debug messenger + // Debug messenger for validation layers + vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; - // Surface for rendering - vk::UniqueSurfaceKHR mSurface; // Unique handle to the Vulkan surface + // Vulkan surface for rendering + vk::UniqueSurfaceKHR mSurface; - // Physical device and logical device - vk::PhysicalDevice mPhysicalDevice; // Handle to the selected physical device (GPU) - vk::SampleCountFlagBits mMsaaSamples; // Multisample anti-aliasing (MSAA) sample count - vk::UniqueDevice mDevice; // Unique handle to the logical device + // Vulkan devices and queues + vk::PhysicalDevice mPhysicalDevice; // Physical GPU + vk::SampleCountFlagBits mMsaaSamples; // Multisampling count + vk::UniqueDevice mDevice; // Logical Vulkan device - // Vulkan queues for graphics and presentation - vk::Queue mGraphicsQueue; // Handle to the graphics queue - vk::Queue mPresentQueue; // Handle to the presentation queue + vk::Queue mGraphicsQueue; // Queue for graphics commands + vk::Queue mPresentQueue; // Queue for presentation commands - // Swapchain and related resources - vk::UniqueSwapchainKHR mSwapChain; // The swapchain handle - std::vector mSwapChainImages; // Images in the swapchain - vk::Format mSwapChainImageFormat; // Format of the swapchain images - vk::Extent2D mSwapChainExtent; // Dimensions of the swapchain images - std::vector mSwapChainImageViews; // Image views for the swapchain images - std::vector mSwapChainFramebuffers; // Framebuffers for the swapchain images + // Swap chain and related resources + vk::UniqueSwapchainKHR mSwapChain; // Swap chain for frame buffering + std::vector mSwapChainImages; // Images in the swap chain + vk::Format mSwapChainImageFormat; // Format of swap chain images + vk::Extent2D mSwapChainExtent; // Dimensions of swap chain images + std::vector mSwapChainImageViews; // Image views for swap chain images + std::vector mSwapChainFramebuffers; // Framebuffers for rendering - // Render pass and pipeline configurations - vk::UniqueRenderPass mRenderPass; // Render pass configuration + // Render pass and pipeline objects + vk::UniqueRenderPass mRenderPass; // Render pass definition vk::UniqueDescriptorSetLayout mDescriptorSetLayout; // Descriptor set layout vk::UniquePipelineLayout mPipelineLayout; // Pipeline layout vk::UniquePipeline mGraphicsPipeline; // Graphics pipeline @@ -128,63 +128,64 @@ class VulkanApp { // Command pool for allocating command buffers vk::UniqueCommandPool mCommandPool; - // Color image resources - vk::UniqueImage mColorImage; // Color image - vk::UniqueDeviceMemory mColorImageMemory; // Memory for the color image - vk::UniqueImageView mColorImageView; // Image view for the color image + // Multisampling color resources + vk::UniqueImage mColorImage; + vk::UniqueDeviceMemory mColorImageMemory; + vk::UniqueImageView mColorImageView; - // Depth image resources - vk::UniqueImage mDepthImage; // Depth image - vk::UniqueDeviceMemory mDepthImageMemory; // Memory for the depth image - vk::UniqueImageView mDepthImageView; // Image view for the depth image + // Depth buffer resources + vk::UniqueImage mDepthImage; + vk::UniqueDeviceMemory mDepthImageMemory; + vk::UniqueImageView mDepthImageView; // Texture resources u32 mMipLevels; // Number of mipmap levels vk::UniqueImage mTextureImage; // Texture image - vk::UniqueDeviceMemory mTextureImageMemory; // Memory for the texture image - vk::UniqueImageView mTextureImageView; // Image view for the texture image - vk::UniqueSampler mTextureSampler; // Sampler for the texture + vk::UniqueDeviceMemory mTextureImageMemory; // Memory for texture image + vk::UniqueImageView mTextureImageView; // Image view for texture + vk::UniqueSampler mTextureSampler; // Sampler for texture - // Vertex and index buffers - std::vector mVertices; // Vertex data - std::vector mIndices; // Index data - vk::UniqueBuffer mVertexBuffer; // Vertex buffer - vk::UniqueDeviceMemory mVertexBufferMemory; // Memory for the vertex buffer - vk::UniqueBuffer mIndexBuffer; // Index buffer - vk::UniqueDeviceMemory mIndexBufferMemory; // Memory for the index buffer + // Vertex and index data + std::vector mVertices; // Vertex data for the model + std::vector 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 - std::vector mUniformBuffers; // Uniform buffers - std::vector mUniformBuffersMemory; // Memory for the uniform buffers - std::vector mUniformBuffersMapped; // Mapped pointers for the uniform buffers + // Uniform buffers for shader parameters + std::vector mUniformBuffers; + std::vector mUniformBuffersMemory; + std::vector mUniformBuffersMapped; - // Descriptor pool and sets - vk::UniqueDescriptorPool mDescriptorPool; // Descriptor pool - vk::UniqueDescriptorPool mImGuiDescriptorPool; // Descriptor pool for ImGui - std::vector mDescriptorSets; // Descriptor sets + // Descriptor pools and sets + vk::UniqueDescriptorPool mDescriptorPool; // Descriptor pool for the application + vk::UniqueDescriptorPool mImGuiDescriptorPool; // Separate descriptor pool for ImGui + std::vector mDescriptorSets; // Descriptor sets for binding resources - // Command buffers - std::vector mCommandBuffers; // Command buffers + // Command buffers for recording drawing commands + std::vector mCommandBuffers; // Synchronization primitives - std::vector mImageAvailableSemaphores; // Semaphores for image availability - std::vector mRenderFinishedSemaphores; // Semaphores for render completion - std::vector mInFlightFences; // Fences for in-flight frames + std::vector + mImageAvailableSemaphores; // Signals that an image is available for rendering + std::vector mRenderFinishedSemaphores; // Signals that rendering has finished + std::vector mInFlightFences; // Ensures CPU-GPU synchronization - // Framebuffer resize flag and current frame index + // State variables bool mFramebufferResized = false; // Flag indicating if the framebuffer was resized - u32 mCurrentFrame = 0; // Index of the current frame + u32 mCurrentFrame = 0; // Index of the current frame being rendered - // Struct to hold queue family indices + // Helper struct to store queue family indices struct QueueFamilyIndices { - std::optional graphics_family; // Index of the graphics queue family - std::optional present_family; // Index of the presentation queue family + std::optional graphics_family; // Index of graphics queue family + std::optional present_family; // Index of presentation queue family - // Check if both queue families are available + // Check if all required queue families are found fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); } }; - // Struct to hold swapchain support details + // Helper struct to store swap chain support details struct SwapChainSupportDetails { vk::SurfaceCapabilitiesKHR capabilities; // Surface capabilities std::vector formats; // Supported surface formats @@ -193,80 +194,149 @@ class VulkanApp { // Struct to hold uniform buffer object data struct UniformBufferObject { - alignas(16) glm::mat4 model; // Model matrix - alignas(16) glm::mat4 view; // View matrix + alignas(16) glm::mat4 model; // Model transformation matrix + alignas(16) glm::mat4 view; // View transformation matrix alignas(16) glm::mat4 proj; // Projection matrix }; - // Read a file into a vector of chars + /** + * @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 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. + */ static fn readFile(const std::string& filename) -> std::vector { // Open the file in binary mode std::ifstream file(filename, std::ios::binary); - // Make sure the file was opened + // Check if the file was successfully opened if (!file) throw std::runtime_error("Failed to open file: " + filename); - // Read the contents of the file into a vector + // Read the entire file content into a vector + // This constructor reads from the start (first iterator) to the end (default constructor) of the stream std::vector buffer((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return buffer; } - // Create a GLFW window + /** + * @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. + */ fn initWindow() -> void { // Initialize GLFW mVKFWInstance = vkfw::initUnique(); - // Set window hints - vkfw::WindowHints hints { - .clientAPI = vkfw::ClientAPI::eNone, // No client API (Vulkan) - }; + // Set window creation hints + vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone }; // No OpenGL context // Create the window mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints); - // Set the window user pointer to this class + // Set the user pointer to this instance, allowing us to access it in callbacks mWindow->setUserPointer(this); - // Set the framebuffer resize callback + // Set up the window resize callback mWindow->callbacks()->on_window_resize = [](const vkfw::Window& window, usize /*width*/, usize /*height*/) -> void { - // Set the framebuffer resized flag + // Set the framebuffer resized flag when the window is resized std::bit_cast(window.getUserPointer())->mFramebufferResized = true; }; } - // Initialize Vulkan + /** + * @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. + */ fn initVulkan() -> void { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createDescriptorSetLayout(); - createGraphicsPipeline(); - createCommandPool(); - createColorResources(); - createDepthResources(); - createFramebuffers(); - createTextureImage(); - createTextureImageView(); - createTextureSampler(); - loadModel(); - createVertexBuffer(); - createIndexBuffer(); - createUniformBuffers(); - createDescriptorPool(); - createDescriptorSets(); - createCommandBuffers(); - createSyncObjects(); - initImGui(); + 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 } + /** + * @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. + */ fn initImGui() -> void { // Create ImGui context IMGUI_CHECKVERSION(); @@ -315,10 +385,18 @@ class VulkanApp { ImGui_ImplVulkan_Init(&initInfo); } - // Main loop + /** + * @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. + */ fn mainLoop() -> void { // While the window is open, while (!mWindow->shouldClose()) { + // Update the FPS counter + updateFPS(mWindow.get(), "Vulkan"); + // Poll for events vkfw::pollEvents(); @@ -330,7 +408,11 @@ class VulkanApp { mDevice->waitIdle(); } - // Clean up the swap chain + /** + * @brief Cleans up the swap chain resources. + * + * This function destroys the framebuffers and image views associated with the swap chain. + */ fn cleanupSwapChain() -> void { for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset(); for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset(); @@ -338,7 +420,12 @@ class VulkanApp { mSwapChain.reset(); } - // Recreate the swap chain + /** + * @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. + */ fn recreateSwapChain() -> void { // Get the new width and height auto [width, height] = mWindow->getFramebufferSize(); @@ -363,7 +450,11 @@ class VulkanApp { createFramebuffers(); } - // Create a Vulkan instance + /** + * @brief Creates the Vulkan instance. + * + * This function sets up the Vulkan instance, including application info, extensions, and validation layers. + */ fn createInstance() -> void { #ifndef NDEBUG // Make sure validation layers are supported @@ -430,7 +521,11 @@ class VulkanApp { VULKAN_HPP_DEFAULT_DISPATCHER.init(mInstance.get()); } - // Create the debug messenger + /** + * @brief Sets up the debug messenger for Vulkan validation layers. + * + * This function creates a debug messenger that handles validation layer messages in debug builds. + */ fn setupDebugMessenger() -> void { #ifdef NDEBUG return; @@ -449,10 +544,19 @@ class VulkanApp { mDebugMessenger = mInstance->createDebugUtilsMessengerEXTUnique(messengerCreateInfo, nullptr); } - // Create the GLFW window surface + /** + * @brief Creates the window surface for rendering. + * + * This function creates a Vulkan surface for the GLFW window, which is used for presenting rendered images. + */ fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); } - // Choose the best-suited physical device + /** + * @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. + */ fn pickPhysicalDevice() -> void { // Get all physical devices std::vector devices = mInstance->enumeratePhysicalDevices(); @@ -485,7 +589,12 @@ class VulkanApp { throw std::runtime_error("Failed to find a suitable GPU!"); } - // Create the logical device + /** + * @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. + */ fn createLogicalDevice() -> void { // Get the queue families QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice); @@ -501,7 +610,7 @@ class VulkanApp { f32 queuePriority = 1.0F; // For each unique queue family, create a new queue - for (u32 queueFamily : uniqueQueueFamilies) { + for (const u32& queueFamily : uniqueQueueFamilies) { vk::DeviceQueueCreateInfo queueCreateInfo { .queueFamilyIndex = queueFamily, .queueCount = 1, @@ -530,6 +639,12 @@ class VulkanApp { mPresentQueue = mDevice->getQueue(qfIndices.present_family.value(), 0); } + /** + * @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. + */ fn createSwapChain() -> void { SwapChainSupportDetails swapChainSupport = querySwapChainSupport(mPhysicalDevice); @@ -577,6 +692,12 @@ class VulkanApp { 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. + */ fn createImageViews() -> void { mSwapChainImageViews.resize(mSwapChainImages.size()); @@ -585,6 +706,12 @@ class VulkanApp { 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. + */ fn createRenderPass() -> void { vk::AttachmentDescription colorAttachment { .format = mSwapChainImageFormat, @@ -672,6 +799,12 @@ class VulkanApp { mRenderPass = mDevice->createRenderPassUnique(renderPassInfo); } + /** + * @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, @@ -699,6 +832,13 @@ class VulkanApp { 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. + */ fn createGraphicsPipeline() -> void { std::vector vertShaderCode = readFile(VERTEX_SHADER_PATH); std::vector fragShaderCode = readFile(FRAGMENT_SHADER_PATH); @@ -823,6 +963,12 @@ class VulkanApp { mGraphicsPipeline = std::move(graphicsPipelineValue); } + /** + * @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. + */ fn createFramebuffers() -> void { mSwapChainFramebuffers.resize(mSwapChainImageViews.size()); @@ -842,6 +988,12 @@ class VulkanApp { } } + /** + * @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. + */ fn createCommandPool() -> void { QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice); @@ -851,6 +1003,11 @@ class VulkanApp { mCommandPool = mDevice->createCommandPoolUnique(poolInfo); } + /** + * @brief Creates resources for color attachment. + * + * This function creates the image, memory, and view for the color attachment used in multisampling. + */ fn createColorResources() -> void { vk::Format colorFormat = mSwapChainImageFormat; @@ -868,6 +1025,11 @@ class VulkanApp { 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. + */ fn createDepthResources() -> void { vk::Format depthFormat = findDepthFormat(); @@ -885,6 +1047,17 @@ class VulkanApp { mDepthImageView = createImageView(mDepthImage.get(), depthFormat, vk::ImageAspectFlagBits::eDepth, 1); } + /** + * @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. + */ fn findSupportedFormat( const std::vector& candidates, const vk::ImageTiling& tiling, @@ -903,6 +1076,13 @@ class VulkanApp { 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. + */ fn findDepthFormat() -> vk::Format { return findSupportedFormat( { vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint }, @@ -911,10 +1091,23 @@ class VulkanApp { ); } + /** + * @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. + */ 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. + */ fn createTextureImage() -> void { stb::UniqueImage image(TEXTURE_PATH); @@ -963,6 +1156,18 @@ class VulkanApp { 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. + */ fn generateMipmaps(vk::Image image, vk::Format imageFormat, i32 texWidth, i32 texHeight, u32 mipLevels) -> void { vk::FormatProperties formatProperties = mPhysicalDevice.getFormatProperties(imageFormat); @@ -1070,6 +1275,14 @@ class VulkanApp { endSingleTimeCommands(commandBuffer); } + /** + * @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. + */ fn getMaxUsableSampleCount() -> vk::SampleCountFlagBits { vk::PhysicalDeviceProperties physicalDeviceProperties = mPhysicalDevice.getProperties(); @@ -1092,12 +1305,23 @@ class VulkanApp { 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. + */ fn createTextureImageView() -> void { mTextureImageView = createImageView( mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels ); } + /** + * @brief Creates the texture sampler. + * + * This function creates a sampler object that defines how the texture should be sampled in shaders. + */ fn createTextureSampler() -> void { vk::PhysicalDeviceProperties properties = mPhysicalDevice.getProperties(); @@ -1122,23 +1346,42 @@ class VulkanApp { 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 + */ fn createImageView( const vk::Image& image, const vk::Format& format, const vk::ImageAspectFlags& aspectFlags, const u32& mipLevels ) -> vk::UniqueImageView { - vk::ImageViewCreateInfo viewInfo { - .image = image, .viewType = vk::ImageViewType::e2D, .format = format, .subresourceRange = { - .aspectMask = aspectFlags, - .baseMipLevel = 0, - .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = 1, - } - }; - - return mDevice->createImageViewUnique(viewInfo); + return mDevice->createImageViewUnique({ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + .aspectMask = aspectFlags, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); } fn createImage( @@ -1264,6 +1507,12 @@ class VulkanApp { endSingleTimeCommands(commandBuffer); } + /** + * @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. + */ fn loadModel() -> void { tinyobj::attrib_t attrib; std::vector shapes; @@ -1300,6 +1549,12 @@ class VulkanApp { } } + /** + * @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 { vk::DeviceSize bufferSize = sizeof(mVertices[0]) * mVertices.size(); @@ -1326,6 +1581,12 @@ class VulkanApp { 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. + */ fn createIndexBuffer() -> void { vk::DeviceSize bufferSize = sizeof(mIndices[0]) * mIndices.size(); @@ -1352,6 +1613,12 @@ class VulkanApp { 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); @@ -1370,6 +1637,12 @@ class VulkanApp { } } + /** + * @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 { std::array poolSizes = { vk::DescriptorPoolSize { @@ -1391,6 +1664,12 @@ class VulkanApp { 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 layouts(MAX_FRAMES_IN_FLIGHT, mDescriptorSetLayout.get()); @@ -1438,6 +1717,16 @@ class VulkanApp { } } + /** + * @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. + */ fn createBuffer( const vk::DeviceSize& deviceSize, const vk::BufferUsageFlags& bufferUsageFlags, @@ -1471,7 +1760,13 @@ class VulkanApp { return { std::move(buffer), std::move(bufferMemory) }; } - // Create the command buffers + /** + * @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. + */ fn beginSingleTimeCommands() -> vk::CommandBuffer { // Define the command buffer allocation info vk::CommandBufferAllocateInfo allocInfo { @@ -1492,7 +1787,14 @@ class VulkanApp { return commandBuffer; } - // End the single time command buffer + /** + * @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. + */ fn endSingleTimeCommands(const vk::CommandBuffer& commandBuffer) -> void { // End the command buffer commandBuffer.end(); @@ -1510,7 +1812,15 @@ class VulkanApp { mDevice->freeCommandBuffers(mCommandPool.get(), commandBuffer); } - // Copy data from src to dst + /** + * @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. + */ fn copyData(const vk::DeviceMemory& stagingBufferMemory, const vk::DeviceSize& bufferSize, const void* src) -> void { // Map the memory @@ -1523,7 +1833,15 @@ class VulkanApp { mDevice->unmapMemory(stagingBufferMemory); } - // Copy buffer from src to dst + /** + * @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. + */ fn copyBuffer(const vk::Buffer& srcBuffer, const vk::Buffer& dstBuffer, const vk::DeviceSize& deviceSize) -> void { // Begin a single time command buffer @@ -1539,7 +1857,15 @@ class VulkanApp { endSingleTimeCommands(commandBuffer); } - // Find the memory type of the physical device + /** + * @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. + */ fn findMemoryType(const u32& typeFilter, const vk::MemoryPropertyFlags& properties) -> u32 { // Get the memory properties of the physical device vk::PhysicalDeviceMemoryProperties memProperties = mPhysicalDevice.getMemoryProperties(); @@ -1554,7 +1880,12 @@ class VulkanApp { throw std::runtime_error("Failed to find a suitable memory type!"); } - // Create the command buffers + /** + * @brief Creates command buffers. + * + * This function allocates command buffers from the command pool. One command buffer is + * allocated for each frame in flight. + */ fn createCommandBuffers() -> void { // Resize the command buffers to hold the maximum number of frames in flight mCommandBuffers.resize(MAX_FRAMES_IN_FLIGHT); @@ -1570,7 +1901,14 @@ class VulkanApp { mCommandBuffers = mDevice->allocateCommandBuffersUnique(allocInfo); } - // Record the command buffer + /** + * @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. + */ fn recordCommandBuffer(const vk::CommandBuffer& commandBuffer, const u32& imageIndex) -> void { // Define the command buffer begin info vk::CommandBufferBeginInfo beginInfo {}; @@ -1664,7 +2002,12 @@ class VulkanApp { commandBuffer.end(); } - // Create the semaphores and fences + /** + * @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. + */ fn createSyncObjects() -> void { // Resize the vectors to hold the maximum number of frames in flight mImageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); @@ -1683,6 +2026,15 @@ class VulkanApp { } } + /** + * @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. + */ fn updateUniformBuffer(const u32& currentImage) -> void { // For convenience using namespace std::chrono; @@ -1720,7 +2072,16 @@ class VulkanApp { memcpy(mUniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); } - // Draw a frame to the window + /** + * @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 + */ fn drawFrame() -> void { try { // Wait for the fence to signal that the frame is finished @@ -1806,7 +2167,14 @@ class VulkanApp { } } - // Create the shader module + /** + * @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. + */ fn createShaderModule(const std::vector& code) -> vk::UniqueShaderModule { vk::ShaderModuleCreateInfo createInfo { .codeSize = code.size(), @@ -1816,7 +2184,15 @@ class VulkanApp { return mDevice->createShaderModuleUnique(createInfo); } - // Choose the color format for the swap chain + /** + * @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. + */ static fn chooseSwapSurfaceFormat(const std::vector& availableFormats ) -> vk::SurfaceFormatKHR { // If SRGB is available, use it @@ -1829,7 +2205,15 @@ class VulkanApp { return availableFormats[0]; } - // Choose the swap chain presentation mode + /** + * @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. + */ static fn chooseSwapPresentMode(const std::vector& availablePresentModes ) -> vk::PresentModeKHR { // Check if mailbox mode is available (adaptive sync) @@ -1841,7 +2225,15 @@ class VulkanApp { return vk::PresentModeKHR::eFifo; } - // Choose the swap chain extent (resolution) + /** + * @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. + */ 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 @@ -1859,7 +2251,15 @@ class VulkanApp { }; } - // Check if the swap chain is adequate + /** + * @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. + */ fn querySwapChainSupport(const vk::PhysicalDevice& device) -> SwapChainSupportDetails { return { .capabilities = device.getSurfaceCapabilitiesKHR(mSurface.get()), @@ -1868,7 +2268,15 @@ class VulkanApp { }; } - // Check if the device is suitable for the application + /** + * @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. + */ fn isDeviceSuitable(const vk::PhysicalDevice& device) -> bool { // Get the queue families that support the required operations QueueFamilyIndices qfIndices = findQueueFamilies(device); @@ -1893,7 +2301,15 @@ class VulkanApp { supportedFeatures.samplerAnisotropy; } - // Check if the device supports the required extensions + /** + * @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. + */ static fn checkDeviceExtensionSupport(const vk::PhysicalDevice& device) -> bool { // Get the available extensions std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); @@ -1909,7 +2325,15 @@ class VulkanApp { return requiredExtensions.empty(); } - // Find the queue families that support the required operations + /** + * @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. + */ fn findQueueFamilies(const vk::PhysicalDevice& device) -> QueueFamilyIndices { // Create a struct to store the queue family indices QueueFamilyIndices qfIndices; @@ -1938,7 +2362,14 @@ class VulkanApp { return qfIndices; } - // Check if all requested layers are available + /** + * @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. + */ static fn checkValidationLayerSupport() -> bool { std::vector availableLayers = vk::enumerateInstanceLayerProperties(); @@ -1955,12 +2386,20 @@ class VulkanApp { return true; // All validation layers are available } - // Callback function used to report validation layer messages + /** + * @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. + */ static VKAPI_ATTR fn VKAPI_CALL debugCallback( - VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/, // Severity: verbose, info, warning, error - VkDebugUtilsMessageTypeFlagsEXT /*messageType*/, // Type: general, validation, performance - const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, // Message details - void* /*pUserData*/ // Optional user data + VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/, + VkDebugUtilsMessageTypeFlagsEXT /*messageType*/, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* /*pUserData*/ ) -> vk::Bool32 { // Print the message to the console // Because pCallbackData already gives the message severity @@ -1971,6 +2410,15 @@ class VulkanApp { } }; +/** + * @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. + */ fn main() -> i32 { // Initialize dynamic function dispatcher VULKAN_HPP_DEFAULT_DISPATCHER.init();