// Include necessary headers #include // For time-related functions #include // For string formatting #include // 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 // Include GLM for mathematics operations // TinyObjLoader for loading 3D models #define TINYOBJLOADER_IMPLEMENTATION #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 // Necessary for dynamic dispatch to work 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 // ImGui headers for GUI #include #include #include // GLFW configuration and inclusion #define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs #include "vkfw.hpp" // Include GLFW C++ bindings // Constants for window dimensions constexpr i32 WIDTH = 800; constexpr i32 HEIGHT = 600; // 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"; // 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 that can be processed concurrently constexpr i32 MAX_FRAMES_IN_FLIGHT = 2; // Validation layers for debug builds #ifndef NDEBUG constexpr std::array validationLayers = { "VK_LAYER_KHRONOS_validation" }; #endif // Required device extensions (platform-specific) #ifdef __APPLE__ constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName, vk::KHRPortabilitySubsetExtensionName }; #else 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 { initWindow(); // Initialize the application window initVulkan(); // Initialize Vulkan mainLoop(); // Enter the main rendering loop 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: // 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 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 objects vk::UniqueRenderPass mRenderPass; // Render pass definition 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; // Multisampling color resources vk::UniqueImage mColorImage; vk::UniqueDeviceMemory mColorImageMemory; vk::UniqueImageView mColorImageView; // 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 texture image vk::UniqueImageView mTextureImageView; // Image view for texture vk::UniqueSampler mTextureSampler; // Sampler for texture // 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 for shader parameters std::vector mUniformBuffers; std::vector mUniformBuffersMemory; std::vector mUniformBuffersMapped; // 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 for recording drawing commands std::vector mCommandBuffers; // Synchronization primitives 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 // State variables bool mFramebufferResized = false; // Flag indicating if the framebuffer was resized u32 mCurrentFrame = 0; // Index of the current frame being rendered // Helper struct to store queue family indices struct QueueFamilyIndices { std::optional graphics_family; // Index of graphics queue family std::optional present_family; // Index of presentation queue family // Check if all required queue families are found fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); } }; // Helper struct to store swap chain support details struct SwapChainSupportDetails { vk::SurfaceCapabilitiesKHR capabilities; // Surface capabilities std::vector formats; // Supported surface formats std::vector present_modes; // Supported presentation modes }; // 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 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 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); // Check if the file was successfully opened if (!file) throw std::runtime_error("Failed to open file: " + filename); // 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; } /** * @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 creation hints vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone }; // No OpenGL context // Create the window mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints); // Set the user pointer to this instance, allowing us to access it in callbacks mWindow->setUserPointer(this); // 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 when the window is resized std::bit_cast(window.getUserPointer())->mFramebufferResized = true; }; } /** * @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(); // 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(); ImGui::CreateContext(); // Setup Dear ImGui style ImGui::StyleColorsDark(); // Initialize ImGui for GLFW and Vulkan ImGui_ImplGlfw_InitForVulkan(mWindow.get(), true); vk::DescriptorPoolSize descriptorPoolSize = { .type = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, }; vk::DescriptorPoolCreateInfo poolInfo { .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, .maxSets = 1, .poolSizeCount = 1, .pPoolSizes = &descriptorPoolSize, }; 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(), .MinImageCount = MAX_FRAMES_IN_FLIGHT, .ImageCount = static_cast(mSwapChainImages.size()), .MSAASamples = static_cast(mMsaaSamples), .PipelineCache = VK_NULL_HANDLE, .Subpass = 0, .UseDynamicRendering = false, .PipelineRenderingCreateInfo = {}, .Allocator = nullptr, .CheckVkResultFn = nullptr, .MinAllocationSize = 0, }; ImGui_ImplVulkan_Init(&initInfo); } /** * @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(); // Draw a frame drawFrame(); } // Wait for the device to finish mDevice->waitIdle(); } /** * @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(); 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. */ fn recreateSwapChain() -> void { // Get the new width and height auto [width, height] = mWindow->getFramebufferSize(); // If the width or height are 0, wait for events while (width == 0 || height == 0) { std::tie(width, height) = mWindow->getFramebufferSize(); vkfw::waitEvents(); } // Wait for the device to finish mDevice->waitIdle(); // Clean up the swap chain cleanupSwapChain(); // Create a new swap chain createSwapChain(); createImageViews(); createColorResources(); createDepthResources(); createFramebuffers(); } /** * @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 if (!checkValidationLayerSupport()) throw std::runtime_error("Validation layers requested, but not available!"); #endif // Application metadata vk::ApplicationInfo appInfo { .pApplicationName = "Hello Triangle", .applicationVersion = 1, .pEngineName = "No Engine", .engineVersion = 1, .apiVersion = vk::ApiVersion12, }; // Get the required extensions std::span extensionsSpan = vkfw::getRequiredInstanceExtensions(); // 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 #ifdef __APPLE__ // Add the portability extension extensions.emplace_back(vk::KHRPortabilityEnumerationExtensionName); // Technically deprecated but Vulkan complains if I don't include it for macOS, // so instead of using the vk::KHRPortabilitySubsetExtensionName, I just use // the direct string. extensions.emplace_back("VK_KHR_get_physical_device_properties2"); #endif // Create the instance vk::InstanceCreateInfo createInfo { #ifdef __APPLE__ // Enable the portability extension .flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR, #endif .pApplicationInfo = &appInfo, #ifdef NDEBUG .enabledLayerCount = 0, .ppEnabledLayerNames = nullptr, #else .enabledLayerCount = static_cast(validationLayers.size()), .ppEnabledLayerNames = validationLayers.data(), #endif .enabledExtensionCount = static_cast(extensions.size()), .ppEnabledExtensionNames = extensions.data() }; #ifndef NDEBUG fmt::println("Available extensions:"); for (const char* extension : extensions) fmt::println("\t{}", extension); #endif // Create the instance mInstance = vk::createInstanceUnique(createInfo); // Load the instance functions VULKAN_HPP_DEFAULT_DISPATCHER.init(mInstance.get()); } /** * @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; #endif vk::DebugUtilsMessengerCreateInfoEXT messengerCreateInfo { .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError, .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance, .pfnUserCallback = debugCallback }; mDebugMessenger = mInstance->createDebugUtilsMessengerEXTUnique(messengerCreateInfo, nullptr); } /** * @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()); } /** * @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(); // Make sure there are supported devices if (devices.empty()) throw std::runtime_error("Failed to find GPUs with Vulkan support!"); #ifndef NDEBUG fmt::println("Available devices:"); #endif // For each device, for (const vk::PhysicalDevice& device : devices) { #ifndef NDEBUG vk::PhysicalDeviceProperties properties = device.getProperties(); fmt::println("\t{}", properties.deviceName.data()); #endif // Set the first suitable device as the physical device if (isDeviceSuitable(device)) { mPhysicalDevice = device; mMsaaSamples = getMaxUsableSampleCount(); break; } } // If no suitable device was found, throw an error 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. */ fn createLogicalDevice() -> void { // Get the queue families QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice); std::vector queueCreateInfos; std::set uniqueQueueFamilies = { qfIndices.graphics_family.value(), qfIndices.present_family.value(), }; // Set the queue priority f32 queuePriority = 1.0F; // For each unique queue family, create a new queue for (const u32& queueFamily : uniqueQueueFamilies) { vk::DeviceQueueCreateInfo queueCreateInfo { .queueFamilyIndex = queueFamily, .queueCount = 1, .pQueuePriorities = &queuePriority, }; queueCreateInfos.emplace_back(queueCreateInfo); } // Enable anisotropic filtering vk::PhysicalDeviceFeatures deviceFeatures { .samplerAnisotropy = vk::True, }; vk::DeviceCreateInfo createInfo { .queueCreateInfoCount = static_cast(queueCreateInfos.size()), .pQueueCreateInfos = queueCreateInfos.data(), .enabledExtensionCount = static_cast(deviceExtensions.size()), .ppEnabledExtensionNames = deviceExtensions.data(), .pEnabledFeatures = &deviceFeatures, }; // Create the logical device and set the graphics and present queues mDevice = mPhysicalDevice.createDeviceUnique(createInfo); mGraphicsQueue = mDevice->getQueue(qfIndices.graphics_family.value(), 0); 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); 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; QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice); std::array queueFamilyIndices = { qfIndices.graphics_family.value(), qfIndices.present_family.value(), }; vk::SwapchainCreateInfoKHR createInfo { .surface = mSurface.get(), .minImageCount = imageCount, .imageFormat = surfaceFormat.format, .imageColorSpace = surfaceFormat.colorSpace, .imageExtent = extent, .imageArrayLayers = 1, .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, .imageSharingMode = qfIndices.graphics_family != qfIndices.present_family ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive, .queueFamilyIndexCount = static_cast(qfIndices.graphics_family != qfIndices.present_family ? 2 : 0), .pQueueFamilyIndices = qfIndices.graphics_family != qfIndices.present_family ? queueFamilyIndices.data() : nullptr, .preTransform = swapChainSupport.capabilities.currentTransform, .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, .presentMode = presentMode, .clipped = vk::True, .oldSwapchain = nullptr, }; mSwapChain = mDevice->createSwapchainKHRUnique(createInfo); mSwapChainImages = mDevice->getSwapchainImagesKHR(mSwapChain.get()); 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. */ fn createImageViews() -> void { mSwapChainImageViews.resize(mSwapChainImages.size()); for (u32 i = 0; i < mSwapChainImages.size(); i++) mSwapChainImageViews[i] = 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, .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 attachments = { colorAttachment, depthAttachment, colorAttachmentResolve, }; vk::RenderPassCreateInfo renderPassInfo { .attachmentCount = static_cast(attachments.size()), .pAttachments = attachments.data(), .subpassCount = 1, .pSubpasses = &subpass, .dependencyCount = 1, .pDependencies = &dependency, }; 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, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eVertex, .pImmutableSamplers = nullptr, }; vk::DescriptorSetLayoutBinding samplerLayoutBinding { .binding = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr, }; std::array bindings = { uboLayoutBinding, samplerLayoutBinding }; vk::DescriptorSetLayoutCreateInfo layoutInfo { .bindingCount = static_cast(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. */ fn createGraphicsPipeline() -> void { std::vector vertShaderCode = readFile(VERTEX_SHADER_PATH); std::vector fragShaderCode = readFile(FRAGMENT_SHADER_PATH); vk::UniqueShaderModule vertShaderModule = createShaderModule(vertShaderCode); vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode); vk::PipelineShaderStageCreateInfo vertShaderStageInfo { .stage = vk::ShaderStageFlagBits::eVertex, .module = vertShaderModule.get(), .pName = "main", }; vk::PipelineShaderStageCreateInfo fragShaderStageInfo { .stage = vk::ShaderStageFlagBits::eFragment, .module = fragShaderModule.get(), .pName = "main", }; std::array shaderStages = { vertShaderStageInfo, fragShaderStageInfo, }; vk::VertexInputBindingDescription bindingDescription = Vertex::getBindingDescription(); std::array attributeDescriptions = Vertex::getAttributeDescriptions(); vk::PipelineVertexInputStateCreateInfo vertexInputInfo { .vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &bindingDescription, .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), .pVertexAttributeDescriptions = attributeDescriptions.data(), }; vk::PipelineInputAssemblyStateCreateInfo inputAssembly { .topology = vk::PrimitiveTopology::eTriangleList, .primitiveRestartEnable = vk::False, }; vk::PipelineViewportStateCreateInfo viewportState { .viewportCount = 1, .scissorCount = 1, }; 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, }; vk::PipelineMultisampleStateCreateInfo multisampling { .rasterizationSamples = mMsaaSamples, .sampleShadingEnable = vk::False, }; vk::PipelineDepthStencilStateCreateInfo depthStencil { .depthTestEnable = vk::True, .depthWriteEnable = vk::True, .depthCompareOp = vk::CompareOp::eLess, .depthBoundsTestEnable = vk::False, .stencilTestEnable = vk::False, }; vk::PipelineColorBlendAttachmentState colorBlendAttachment { .blendEnable = vk::False, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, }; vk::PipelineColorBlendStateCreateInfo colorBlending { .logicOpEnable = vk::False, .logicOp = vk::LogicOp::eCopy, .attachmentCount = 1, .pAttachments = &colorBlendAttachment, .blendConstants = std::array { 0.0F, 0.0F, 0.0F, 0.0F }, }; std::vector dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; vk::PipelineDynamicStateCreateInfo dynamicState { .dynamicStateCount = static_cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data(), }; vk::PipelineLayoutCreateInfo pipelineLayoutInfo { .setLayoutCount = 1, .pSetLayouts = &mDescriptorSetLayout.get(), }; mPipelineLayout = mDevice->createPipelineLayoutUnique(pipelineLayoutInfo); vk::GraphicsPipelineCreateInfo pipelineInfo { .stageCount = static_cast(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, }; 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); } /** * @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()); for (usize i = 0; i < mSwapChainImageViews.size(); i++) { std::array attachments = { mColorImageView.get(), mDepthImageView.get(), mSwapChainImageViews[i].get() }; vk::FramebufferCreateInfo framebufferInfo { .renderPass = mRenderPass.get(), .attachmentCount = static_cast(attachments.size()), .pAttachments = attachments.data(), .width = mSwapChainExtent.width, .height = mSwapChainExtent.height, .layers = 1 }; mSwapChainFramebuffers[i] = mDevice->createFramebufferUnique(framebufferInfo); } } /** * @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); vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueFamilyIndices.graphics_family.value() }; 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; std::tie(mColorImage, mColorImageMemory) = createImage( mSwapChainExtent.width, mSwapChainExtent.height, 1, mMsaaSamples, colorFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransientAttachment | vk::ImageUsageFlagBits::eColorAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal ); 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(); std::tie(mDepthImage, mDepthImageMemory) = createImage( mSwapChainExtent.width, mSwapChainExtent.height, 1, mMsaaSamples, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal ); 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, const vk::FormatFeatureFlags& features ) -> vk::Format { for (vk::Format format : candidates) { vk::FormatProperties props = mPhysicalDevice.getFormatProperties(format); if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) return format; 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. */ 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. */ 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); u8* pixels = image.getData(); i32 texWidth = image.getWidth(), texHeight = image.getHeight(); vk::DeviceSize imageSize = static_cast(texWidth) * static_cast(texHeight) * 4; mMipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; if (!pixels) throw std::runtime_error("Failed to load texture image!"); vk::UniqueBuffer stagingBuffer; vk::UniqueDeviceMemory stagingBufferMemory; std::tie(stagingBuffer, stagingBufferMemory) = createBuffer( imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); copyData(stagingBufferMemory.get(), imageSize, pixels); std::tie(mTextureImage, mTextureImageMemory) = createImage( static_cast(texWidth), static_cast(texHeight), mMipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal ); transitionImageLayout( mTextureImage.get(), vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, mMipLevels ); copyBufferToImage( stagingBuffer.get(), mTextureImage.get(), static_cast(texWidth), static_cast(texHeight) ); 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); if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear)) throw std::runtime_error("Texture image format does not support linear blitting!"); vk::CommandBuffer commandBuffer = beginSingleTimeCommands(); 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; commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, nullptr, barrier ); vk::ImageBlit blit { .srcSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = i - 1, .baseArrayLayer = 0, .layerCount = 1 }, .srcOffsets = std::array { { { .x = 0, .y = 0, .z = 0 }, { .x = mipWidth, .y = mipHeight, .z = 1 } } }, .dstSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = i, .baseArrayLayer = 0, .layerCount = 1 }, .dstOffsets = std::array { vk::Offset3D { .x = 0, .y = 0, .z = 0, }, vk::Offset3D { .x = mipWidth > 1 ? mipWidth / 2 : 1, .y = mipHeight > 1 ? mipHeight / 2 : 1, .z = 1, } } }; commandBuffer.blitImage( 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; commandBuffer.pipelineBarrier( 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; commandBuffer.pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr, barrier ); 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(); vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts; // Define an array of sample counts in descending order const std::array 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; // Return e1 if no other sample count is supported 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(); 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, .maxLod = static_cast(mMipLevels), .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 */ 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, }, }); } fn createImage( 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 { // Define the image creation info vk::ImageCreateInfo imageInfo { .imageType = vk::ImageType::e2D, .format = format, .extent = { .width = width, .height = height, .depth = 1 }, .mipLevels = mipLevels, .arrayLayers = 1, .samples = numSamples, .tiling = tiling, .usage = usage, .sharingMode = vk::SharingMode::eExclusive, .initialLayout = vk::ImageLayout::eUndefined, }; // Create the image vk::UniqueImage image = mDevice->createImageUnique(imageInfo); // Get the memory requirements for the image vk::MemoryRequirements memRequirements = mDevice->getImageMemoryRequirements(image.get()); // Memory allocation info vk::MemoryAllocateInfo allocInfo { .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties), }; // Allocate memory vk::UniqueDeviceMemory imageMemory = mDevice->allocateMemoryUnique(allocInfo); // Bind the allocated memory to the image mDevice->bindImageMemory(image.get(), imageMemory.get(), 0); // Return the unique image return { std::move(image), std::move(imageMemory) }; } // Transition image between layouts fn transitionImageLayout( const vk::Image& image, const vk::ImageLayout& oldLayout, const vk::ImageLayout& newLayout, const u32& mipLevels ) -> void { // Create a command buffer vk::CommandBuffer commandBuffer = beginSingleTimeCommands(); // Define the image memory barrier vk::ImageMemoryBarrier barrier { .oldLayout = oldLayout, .newLayout = newLayout, .srcQueueFamilyIndex = vk::QueueFamilyIgnored, .dstQueueFamilyIndex = vk::QueueFamilyIgnored, .image = image, .subresourceRange = { .aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = 1, }, }; // Define the source and destination stages vk::PipelineStageFlags sourceStage; vk::PipelineStageFlags destinationStage; // Define the access masks 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 { // Ensure that the layout transition is supported throw std::invalid_argument("Unsupported layout transition!"); } // Record the pipeline barrier commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier); // End the command buffer endSingleTimeCommands(commandBuffer); } fn copyBufferToImage(const vk::Buffer& buffer, const vk::Image& image, const u32& width, const u32& height) -> void { vk::CommandBuffer commandBuffer = beginSingleTimeCommands(); vk::BufferImageCopy region { .bufferOffset = 0, .bufferRowLength = 0, .bufferImageHeight = 0, .imageSubresource = { .aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1 }, .imageOffset = { .x = 0, .y = 0, .z = 0 }, .imageExtent = { .width = width, .height = height, .depth = 1 }, }; commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, 1, ®ion); 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; std::vector materials; std::string warn, err; if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH)) throw std::runtime_error(warn + err); std::unordered_map uniqueVertices {}; for (const tinyobj::shape_t& shape : shapes) { for (const tinyobj::index_t& index : shape.mesh.indices) { Vertex vertex { .pos = { attrib.vertices[static_cast((3 * index.vertex_index) + 0)], attrib.vertices[static_cast((3 * index.vertex_index) + 1)], attrib.vertices[static_cast((3 * index.vertex_index) + 2)], }, .color = { 1.0F, 1.0F, 1.0F }, .tex_coord = { attrib.texcoords[static_cast((2 * index.texcoord_index) + 0)], 1.0F - attrib.texcoords[static_cast((2 * index.texcoord_index) + 1)], }, }; if (!uniqueVertices.contains(vertex)) { uniqueVertices[vertex] = static_cast(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 { vk::DeviceSize bufferSize = sizeof(mVertices[0]) * mVertices.size(); vk::UniqueBuffer stagingBuffer; vk::UniqueDeviceMemory stagingBufferMemory; std::tie(stagingBuffer, stagingBufferMemory) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); copyData(stagingBufferMemory.get(), bufferSize, mVertices.data()); std::tie(mVertexBuffer, mVertexBufferMemory) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal ); 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. */ fn createIndexBuffer() -> void { vk::DeviceSize bufferSize = sizeof(mIndices[0]) * mIndices.size(); vk::UniqueBuffer stagingBuffer; vk::UniqueDeviceMemory stagingBufferMemory; std::tie(stagingBuffer, stagingBufferMemory) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); copyData(stagingBufferMemory.get(), bufferSize, mIndices.data()); std::tie(mIndexBuffer, mIndexBufferMemory) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal ); 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++) { std::tie(mUniformBuffers[i], mUniformBuffersMemory[i]) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, 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 { std::array poolSizes = { 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 { .maxSets = MAX_FRAMES_IN_FLIGHT, .poolSizeCount = static_cast(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 layouts(MAX_FRAMES_IN_FLIGHT, mDescriptorSetLayout.get()); vk::DescriptorSetAllocateInfo allocInfo { .descriptorPool = mDescriptorPool.get(), .descriptorSetCount = static_cast(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), }; vk::DescriptorImageInfo imageInfo { .sampler = mTextureSampler.get(), .imageView = mTextureImageView.get(), .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal, }; std::array descriptorWrites = { 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, }, }; 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. */ fn createBuffer( const vk::DeviceSize& deviceSize, const vk::BufferUsageFlags& bufferUsageFlags, const vk::MemoryPropertyFlags& memoryPropertyFlags ) -> std::pair { vk::BufferCreateInfo bufferInfo { .size = deviceSize, .usage = bufferUsageFlags, .sharingMode = vk::SharingMode::eExclusive, }; // Create the buffer vk::UniqueBuffer buffer = mDevice->createBufferUnique(bufferInfo); // Get the memory requirements for the buffer vk::MemoryRequirements memRequirements = mDevice->getBufferMemoryRequirements(buffer.get()); // Memory allocation info vk::MemoryAllocateInfo allocInfo { .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, memoryPropertyFlags), }; // Allocate memory vk::UniqueDeviceMemory bufferMemory = mDevice->allocateMemoryUnique(allocInfo); // Bind the allocated memory to the buffer mDevice->bindBufferMemory(buffer.get(), bufferMemory.get(), 0); // Return both the unique buffer and its associated memory return { std::move(buffer), std::move(bufferMemory) }; } /** * @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 { .commandPool = mCommandPool.get(), .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1, }; // Allocate the command buffer vk::CommandBuffer commandBuffer = mDevice->allocateCommandBuffers(allocInfo)[0]; // Define the command buffer begin info vk::CommandBufferBeginInfo beginInfo { .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; // Begin the command buffer commandBuffer.begin(beginInfo); return commandBuffer; } /** * @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(); // Define the submit info vk::SubmitInfo submitInfo { .commandBufferCount = 1, .pCommandBuffers = &commandBuffer }; // Submit the command buffer mGraphicsQueue.submit(submitInfo, nullptr); // Wait for the queue to finish mGraphicsQueue.waitIdle(); // 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. */ fn copyData(const vk::DeviceMemory& stagingBufferMemory, const vk::DeviceSize& bufferSize, const void* src) -> void { // Map the memory void* data = mDevice->mapMemory(stagingBufferMemory, 0, bufferSize); // Copy the data with memcpy - memcpy(dst, src, size) memcpy(data, src, static_cast(bufferSize)); // Unmap the memory 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. */ 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(); // Define the copy region vk::BufferCopy copyRegion { .size = deviceSize }; // Copy the buffer commandBuffer.copyBuffer(srcBuffer, dstBuffer, 1, ©Region); // End the single time command buffer endSingleTimeCommands(commandBuffer); } /** * @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(); // 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; // 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. */ fn createCommandBuffers() -> void { // Resize the command buffers to hold the maximum number of frames in flight mCommandBuffers.resize(MAX_FRAMES_IN_FLIGHT); // Define the command buffer allocation info vk::CommandBufferAllocateInfo allocInfo { .commandPool = mCommandPool.get(), .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = static_cast(mCommandBuffers.size()), }; // Allocate the command buffers mCommandBuffers = mDevice->allocateCommandBuffersUnique(allocInfo); } /** * @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 {}; // Begin the command buffer commandBuffer.begin(beginInfo); // Define the render pass begin info std::array clearValues { // Set the color buffer to black vk::ClearValue { .color = { .uint32 = std::array { 0, 0, 0, 255 } } }, // Set the depth buffer to 1.0F vk::ClearValue { .depthStencil = { .depth = 1.0F, .stencil = 0 } }, }; // Define the render pass info vk::RenderPassBeginInfo renderPassInfo { // 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 .clearValueCount = static_cast(clearValues.size()), .pClearValues = clearValues.data(), }; // Begin the render pass commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); // Bind the graphics pipeline to the command buffer commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mGraphicsPipeline.get()); // Create the viewport and scissor. // If the scissor is smaller than the // viewport, then it will be clipped. vk::Viewport viewport { .x = 0.0F, .y = 0.0F, .width = static_cast(mSwapChainExtent.width), .height = static_cast(mSwapChainExtent.height), .minDepth = 0.0F, .maxDepth = 1.0F, }; // Create the scissor vk::Rect2D scissor { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent, }; // Set the viewport and scissor commandBuffer.setViewport(0, viewport); commandBuffer.setScissor(0, scissor); // Bind the vertex buffer commandBuffer.bindVertexBuffers(0, mVertexBuffer.get(), { 0 }); // Bind the index buffer commandBuffer.bindIndexBuffer(mIndexBuffer.get(), 0, vk::IndexType::eUint32); // Bind the descriptor sets commandBuffer.bindDescriptorSets( vk::PipelineBindPoint::eGraphics, mPipelineLayout.get(), 0, 1, &mDescriptorSets[mCurrentFrame], 0, nullptr ); // Draw the indexed vertices commandBuffer.drawIndexed(static_cast(mIndices.size()), 1, 0, 0, 0); ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // Your ImGui code here ImGui::ShowDemoWindow(); ImGui::Render(); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer); // End the render pass commandBuffer.endRenderPass(); // End the command buffer commandBuffer.end(); } /** * @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); mRenderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); mInFlightFences.resize(MAX_FRAMES_IN_FLIGHT); // Create the semaphore and fence info vk::SemaphoreCreateInfo semaphoreInfo {}; vk::FenceCreateInfo fenceInfo { .flags = vk::FenceCreateFlagBits::eSignaled }; // Loop through the maximum number of frames in flight and create the semaphores and fences for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) { mImageAvailableSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo); mRenderFinishedSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo); mInFlightFences[idx] = mDevice->createFenceUnique(fenceInfo); } } /** * @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; using time_point = high_resolution_clock::time_point; // Time of the program start static time_point StartTime = high_resolution_clock::now(); // Current time time_point currentTime = high_resolution_clock::now(); // Time since the program started f32 time = duration(currentTime - StartTime).count(); // Uniform buffer object UniformBufferObject ubo { // 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)), // 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)), // Projection matrix - glm::perspective(fov, aspect, near, far) .proj = glm::perspective( glm::radians(45.0F), static_cast(mSwapChainExtent.width) / static_cast(mSwapChainExtent.height), 0.1F, 10.0F ) }; // Flip the Y axis, because glm was designed for OpenGL ubo.proj[1][1] *= -1; // 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 */ fn drawFrame() -> void { try { // Wait for the fence to signal that the frame is finished vk::Result result = mDevice->waitForFences(mInFlightFences[mCurrentFrame].get(), vk::Bool32(vk::True), UINT64_MAX); // Make sure the result is successful if (result != vk::Result::eSuccess) throw std::runtime_error("Failed to wait for fences!"); // Acquire the next image from the swap chain auto [imageIndexResult, imageIndexValue] = mDevice->acquireNextImageKHR( mSwapChain.get(), UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame].get(), nullptr ); // Check if the swap chain needs to be recreated if (imageIndexResult == vk::Result::eErrorOutOfDateKHR) { recreateSwapChain(); return; } // Check if the image index is valid if (imageIndexResult != vk::Result::eSuccess && imageIndexResult != vk::Result::eSuboptimalKHR) throw std::runtime_error("Failed to acquire swap chain image!"); // Update the uniform buffer with the current image updateUniformBuffer(mCurrentFrame); // Reset the current fence mDevice->resetFences(mInFlightFences[mCurrentFrame].get()); // Reset the current command buffer mCommandBuffers[mCurrentFrame]->reset(vk::CommandBufferResetFlagBits::eReleaseResources); // Define the command buffer submit info recordCommandBuffer(mCommandBuffers[mCurrentFrame].get(), imageIndexValue); std::array waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput }; vk::SubmitInfo submitInfo { .waitSemaphoreCount = 1, .pWaitSemaphores = &mImageAvailableSemaphores[mCurrentFrame].get(), .pWaitDstStageMask = waitStages.data(), .commandBufferCount = 1, .pCommandBuffers = &mCommandBuffers[mCurrentFrame].get(), .signalSemaphoreCount = 1, .pSignalSemaphores = &mRenderFinishedSemaphores[mCurrentFrame].get(), }; // Submit the graphics queue mGraphicsQueue.submit(submitInfo, mInFlightFences[mCurrentFrame].get()); vk::PresentInfoKHR presentInfo { .waitSemaphoreCount = 1, .pWaitSemaphores = &mRenderFinishedSemaphores[mCurrentFrame].get(), .swapchainCount = 1, .pSwapchains = &mSwapChain.get(), .pImageIndices = &imageIndexValue, }; // Present the swap chain image vk::Result presentResult = mPresentQueue.presentKHR(presentInfo); // Check if the swap chain needs to be recreated if (presentResult == vk::Result::eErrorOutOfDateKHR || presentResult == vk::Result::eSuboptimalKHR || mFramebufferResized) { mFramebufferResized = false; recreateSwapChain(); } else if (presentResult != vk::Result::eSuccess) { // Throw if present failed throw std::runtime_error("Failed to present swap chain image!"); } // Increment the current frame (or loop back to 0) mCurrentFrame = (mCurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT; } catch (vk::OutOfDateKHRError& /*err*/) { // Recreate the swap chain if it's out of date mFramebufferResized = false; recreateSwapChain(); return; } } /** * @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(), .pCode = std::bit_cast(code.data()), }; return mDevice->createShaderModuleUnique(createInfo); } /** * @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 for (const vk::SurfaceFormatKHR& availableFormat : availableFormats) if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) return availableFormat; // Otherwise, use the first available format 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. */ static fn chooseSwapPresentMode(const std::vector& availablePresentModes ) -> vk::PresentModeKHR { // Check if mailbox mode is available (adaptive sync) for (const vk::PresentModeKHR& availablePresentMode : availablePresentModes) if (availablePresentMode == vk::PresentModeKHR::eMailbox) return availablePresentMode; // If mailbox mode is not available, use FIFO mode (vsync) 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. */ 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 if (capabilities.currentExtent.width != UINT32_MAX) return capabilities.currentExtent; // Get the window's resolution u32 width = 0, height = 0; std::tie(width, height) = mWindow->getFramebufferSize(); // 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), }; } /** * @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()), .formats = device.getSurfaceFormatsKHR(mSurface.get()), .present_modes = device.getSurfacePresentModesKHR(mSurface.get()), }; } /** * @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); // Check if the device supports the required extensions bool extensionsSupported = checkDeviceExtensionSupport(device); bool swapChainAdequate = false; if (extensionsSupported) { SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); // Check if the swap chain is adequate (make sure it has // at least one supported format and presentation mode) swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty(); } // Check if the device supports the required features vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures(); // If the device supports everything required, return true return qfIndices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; } /** * @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(); // Create a set of required extension names std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); // Remove each required extension from the set of available extensions for (const vk::ExtensionProperties& extension : availableExtensions) requiredExtensions.erase(extension.extensionName); // If the set is empty, all required extensions are supported 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. */ fn findQueueFamilies(const vk::PhysicalDevice& device) -> QueueFamilyIndices { // Create a struct to store the queue family indices QueueFamilyIndices qfIndices; // Get the queue family properties std::vector queueFamilies = device.getQueueFamilyProperties(); // For every queue family, for (u32 i = 0; i < queueFamilies.size(); i++) { // Check if the queue family supports the required operations if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) qfIndices.graphics_family = i; // Check if the queue family supports presentation vk::Bool32 queuePresentSupport = device.getSurfaceSupportKHR(i, mSurface.get()); // If the queue family supports presentation, set the present family index if (queuePresentSupport) qfIndices.present_family = i; // If the queue family supports both operations, we're done if (qfIndices.isComplete()) break; } return qfIndices; } /** * @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(); // Create a set of available layer names std::unordered_set availableLayerNames; for (const vk::LayerProperties& layerProperties : availableLayers) availableLayerNames.emplace(layerProperties.layerName); // 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 return true; // All validation layers are available } /** * @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*/, VkDebugUtilsMessageTypeFlagsEXT /*messageType*/, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* /*pUserData*/ ) -> vk::Bool32 { // 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. fmt::println("Validation layer: {}", pCallbackData->pMessage); return vk::False; } }; /** * @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(); // Create an app instance VulkanApp app; try { app.run(); } catch (const std::exception& e) { fmt::println("{}", e.what()); return EXIT_FAILURE; } return EXIT_SUCCESS; }