// Include necessary headers #include // For time-related functions #define FMT_HEADER_ONLY #include // For string formatting #include // For shader compilation #include // For unordered_set container #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 // 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 VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE // Include custom utility headers #include "camera/camera.hpp" // Camera class #include "init/debug_messenger.hpp" #include "init/device_manager.hpp" #include "init/vulkan_instance.hpp" #include "structs/camera_info.hpp" #include "structs/light_info.hpp" #include "structs/queue_family_indices.hpp" #include "structs/swap_chain_support_details.hpp" #include "structs/uniform_buffer_object.hpp" #include "util/constants.hpp" // Constants definitions #include "util/crosshair.hpp" // Crosshair definitions #include "util/shaders.hpp" // Compiled shader code #include "util/types.hpp" // Custom type definitions #include "util/unique_image.hpp" // Custom image handling utilities #include "util/vertex.hpp" // Custom vertex structure definition #include "window/window_manager.hpp" // Window manager // ImGui headers for GUI #include "gui/imgui_manager.hpp" #include "vkfw.hpp" using namespace constants; /** * @brief The Vulkan application class. * * This class encapsulates the entire Vulkan application, managing the Vulkan * instance, window, rendering loop, and resource cleanup. It handles the * initialization of Vulkan, manages resources, and orchestrates rendering. */ 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 mImGuiManager.cleanup(); } private: vkfw::UniqueInstance mVKFWInstance; ///< GLFW instance vkfw::UniqueWindow mWindow; ///< Application window vk::UniqueInstance mInstance; ///< Vulkan instance vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; ///< Debug messenger vk::UniqueSurfaceKHR mSurface; ///< Vulkan surface for rendering 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 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 vk::UniqueRenderPass mRenderPass; ///< Render pass definition vk::UniqueDescriptorSetLayout mDescriptorSetLayout; ///< Descriptor set layout vk::UniquePipelineLayout mPipelineLayout; ///< Pipeline layout vk::UniquePipeline mGraphicsPipeline; ///< Graphics pipeline vk::UniquePipeline mOldPipeline; ///< Previous graphics pipeline for safe deletion vk::UniquePipelineLayout mCrosshairPipelineLayout; vk::UniquePipeline mCrosshairPipeline; vk::UniqueBuffer mCrosshairVertexBuffer; vk::UniqueDeviceMemory mCrosshairVertexBufferMemory; vk::UniqueBuffer mCrosshairIndexBuffer; vk::UniqueDeviceMemory mCrosshairIndexBufferMemory; vk::UniqueCommandPool mCommandPool; ///< Command pool for allocating command buffers vk::UniqueImage mColorImage; ///< Color image vk::UniqueDeviceMemory mColorImageMemory; ///< Memory for color image vk::UniqueImageView mColorImageView; ///< Image view for color image vk::UniqueImage mDepthImage; ///< Depth image vk::UniqueDeviceMemory mDepthImageMemory; ///< Memory for depth image vk::UniqueImageView mDepthImageView; ///< Image view for depth image 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 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 std::vector mUniformBuffers; ///< Uniform buffers for shader parameters std::vector mUniformBuffersMemory; ///< Memory for uniform buffers std::vector mUniformBuffersMapped; ///< Mapped uniform buffers std::vector mLightUniformBuffers; ///< Uniform buffers for light parameters std::vector mLightUniformBuffersMemory; ///< Memory for light uniform buffers std::vector mLightUniformBuffersMapped; ///< Mapped light uniform buffers std::vector mCameraUniformBuffers; ///< Uniform buffers for camera parameters std::vector mCameraUniformBuffersMemory; ///< Memory for camera uniform buffers std::vector mCameraUniformBuffersMapped; ///< Mapped camera uniform buffers vk::UniqueDescriptorPool mDescriptorPool; ///< Descriptor pool for the application std::vector mDescriptorSets; ///< Descriptor sets for binding resources std::vector mCommandBuffers; ///< Command buffers for drawing commands Camera mCamera; ///< Camera object // Light settings LightInfo mLightSettings { .position = glm::vec3(2.0F, 2.0F, 2.0F), .color = glm::vec3(1.0F, 1.0F, 1.0F), .ambient_strength = 0.1F, .specular_strength = 0.5F, }; // Mouse input tracking bool mFirstMouse = true; ///< Flag for first mouse movement f64 mLastX = WIDTH / 2.0; ///< Last mouse X position f64 mLastY = HEIGHT / 2.0; ///< Last mouse Y position bool mCursorCaptured = true; ///< Flag indicating if cursor is captured // ImGui-related state f32 mCameraSpeed = CAMERA_SPEED; ///< Current camera speed f32 mFieldOfView = 90.0F; ///< Current field of view bool mWireframeMode = false; ///< Wireframe rendering mode f32 mLineWidth = 1.0F; ///< Line width for wireframe mode f32 mMaxLineWidth = 1.0F; ///< Maximum supported line width bool mWideLineSupport = false; ///< Whether wide lines are supported 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 std::vector mImagesInFlight; ///< Tracks which fences are in use by which swap chain images bool mFramebufferResized = false; ///< Flag indicating if the framebuffer was resized u32 mCurrentFrame = 0; ///< Index of the current frame being rendered glm::mat4 mView; ///< View matrix ImGuiManager mImGuiManager; ///< ImGui manager for GUI rendering static fn processInput(vkfw::Window& window, Camera& camera, const f32& deltaTime, const f32& cameraSpeed) -> void { if (window.getKey(vkfw::Key::eW) == vkfw::eTrue) camera.moveForward(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eA) == vkfw::eTrue) camera.moveLeft(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eS) == vkfw::eTrue) camera.moveBackward(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eD) == vkfw::eTrue) camera.moveRight(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eSpace) == vkfw::eTrue) camera.moveUp(static_cast(deltaTime * cameraSpeed)); if (window.getKey(vkfw::Key::eLeftShift) == vkfw::eTrue) camera.moveDown(static_cast(deltaTime * cameraSpeed)); } /** * @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 and create window std::tie(mVKFWInstance, mWindow) = WindowManager::create( "Vulkan", { .on_cursor_move = [this](const vkfw::Window& /*window*/, f64 mouseX, f64 mouseY) -> void { if (!mCursorCaptured) return; // Skip camera movement when cursor is not captured if (mFirstMouse) { mLastX = mouseX; mLastY = mouseY; mFirstMouse = false; return; } f64 xoffset = mouseX - mLastX; f64 yoffset = mLastY - mouseY; // Reversed since y-coordinates range from bottom to top mLastX = mouseX; mLastY = mouseY; mCamera.rotate(-xoffset, yoffset); // Invert xoffset for correct horizontal movement }, .on_key = [this]( const vkfw::Window& window, const vkfw::Key& key, const i32& /*scancode*/, const vkfw::KeyAction& action, const vkfw::ModifierKeyFlags& /*mods*/ ) -> void { if (key == vkfw::Key::eEscape && action == vkfw::KeyAction::ePress) { mCursorCaptured = false; window.set(vkfw::CursorMode::eNormal); } if (key == vkfw::Key::eR && action == vkfw::KeyAction::ePress) { try { mDevice->waitIdle(); createGraphicsPipeline(); fmt::println("Shaders reloaded successfully!"); } catch (const std::exception& e) { fmt::println(stderr, "Failed to reload shaders: {}", e.what()); } } }, .on_mouse_button = [this]( const vkfw::Window& window, const vkfw::MouseButton& button, const vkfw::MouseButtonAction& action, const vkfw::ModifierKeyFlags& /*mods*/ ) -> void { if (button == vkfw::MouseButton::eLeft && action == vkfw::MouseButtonAction::ePress && !mCursorCaptured) { // Only capture cursor if click is not on ImGui window if (!ImGui::GetIO().WantCaptureMouse) { mCursorCaptured = true; mFirstMouse = true; // Reset first mouse flag to avoid jumps window.set(vkfw::CursorMode::eDisabled); } } }, .on_window_resize = [this](const vkfw::Window& /*window*/, usize /*width*/, usize /*height*/) -> void { // Set the framebuffer resized flag when the window is resized 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 createCrosshairPipeline(); // Create the crosshair 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 createLightUniformBuffers(); // Create uniform buffers for light parameters createCameraUniformBuffers(); // Create uniform buffers for camera parameters createDescriptorPool(); // Create a descriptor pool createDescriptorSets(); // Allocate and update descriptor sets createCommandBuffers(); // Create command buffers for rendering commands createCrosshairBuffers(); // Create crosshair buffers createSyncObjects(); // Create synchronization objects (semaphores and fences) // Initialize ImGui mImGuiManager.init( mInstance.get(), mPhysicalDevice, mDevice.get(), DeviceManager::findQueueFamilies(mPhysicalDevice, mSurface.get()).graphics_family.value(), mGraphicsQueue, mRenderPass.get(), MAX_FRAMES_IN_FLIGHT, static_cast(mSwapChainImages.size()), mMsaaSamples, mWindow.get() ); } /** * @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 { f64 lastFrame = 0.0; f64 deltaTime = 0.0; f64 lastFpsUpdate = 0.0; i32 frameCounter = 0; while (!mWindow->shouldClose()) { f64 currentFrame = vkfw::getTime(); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; processInput(mWindow.get(), mCamera, static_cast(deltaTime), mCameraSpeed); mView = mCamera.getViewMatrix(); updateFrameStats(lastFpsUpdate, frameCounter); vkfw::pollEvents(); mImGuiManager.newFrame(); bool needsPipelineRecreation = false; mImGuiManager.renderControls( mCameraSpeed, mFieldOfView, mWireframeMode, mLineWidth, mMaxLineWidth, mWideLineSupport, mCamera, mLightSettings, needsPipelineRecreation ); mImGuiManager.renderMemoryUsage(sizeof(Vertex), sizeof(uint32_t), mVertices.size(), mIndices.size()); if (needsPipelineRecreation) { mDevice->waitIdle(); createGraphicsPipeline(); } mImGuiManager.render(); drawFrame(); } mDevice->waitIdle(); } fn updateFrameStats(f64& lastFpsUpdate, i32& frameCounter) -> void { f64 currentFrame = vkfw::getTime(); if (currentFrame - lastFpsUpdate > 1.0) { WindowManager::setTitle( mWindow.get(), fmt::format("Vulkan - {:.0f}FPS", static_cast(frameCounter / (currentFrame - lastFpsUpdate))) ); lastFpsUpdate = currentFrame; frameCounter = 0; } ++frameCounter; } /** * @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 { i32 width = 0, height = 0; while (width == 0 || height == 0) { std::tie(width, height) = WindowManager::getFramebufferSize(mWindow.get()); vkfw::waitEvents(); } mDevice->waitIdle(); cleanupSwapChain(); createSwapChain(); createImageViews(); createRenderPass(); createGraphicsPipeline(); createCrosshairPipeline(); createColorResources(); createDepthResources(); createFramebuffers(); createUniformBuffers(); createLightUniformBuffers(); createCameraUniformBuffers(); createDescriptorPool(); createDescriptorSets(); createCommandBuffers(); createCrosshairBuffers(); mImagesInFlight.resize(mSwapChainImages.size(), nullptr); } /** * @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 mInstance = VulkanInstance::create(); } /** * @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 DebugMessenger::Config config { .severity_flags = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError, .type_flags = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance, .use_stderr_for_errors = true, }; auto result = DebugMessenger::create(mInstance.get(), config); if (!result) { throw std::runtime_error(result.error()); } mDebugMessenger = std::move(*result); } /** * @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 { auto result = DeviceManager::pickPhysicalDevice(mInstance.get(), mSurface.get(), std::span(deviceExtensions)); if (!result) { throw std::runtime_error(result.error()); } const auto& info = *result; mPhysicalDevice = info.physical_device; mMsaaSamples = info.msaa_samples; mMaxLineWidth = info.max_line_width; mWideLineSupport = info.wide_line_support; } /** * @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 { auto result = DeviceManager::createLogicalDevice( mPhysicalDevice, mSurface.get(), std::span(deviceExtensions), enableValidationLayers, std::span(validationLayers) ); if (!result) { throw std::runtime_error(result.error()); } auto info = std::move(*result); mDevice = std::move(info.device); mGraphicsQueue = info.graphics_queue; mPresentQueue = info.present_queue; } /** * @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 = DeviceManager::findQueueFamilies(mPhysicalDevice, mSurface.get()); 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 lightLayoutBinding { .binding = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr, }; vk::DescriptorSetLayoutBinding cameraLayoutBinding { .binding = 2, .descriptorType = vk::DescriptorType::eUniformBuffer, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr, }; vk::DescriptorSetLayoutBinding samplerLayoutBinding { .binding = 3, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment, .pImmutableSamplers = nullptr, }; std::array bindings = { uboLayoutBinding, lightLayoutBinding, cameraLayoutBinding, 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 = ShaderCompiler::getCompiledShader(VERTEX_SHADER_PATH, shaderc_shader_kind::shaderc_vertex_shader); std::vector fragShaderCode = ShaderCompiler::getCompiledShader(FRAGMENT_SHADER_PATH, shaderc_shader_kind::shaderc_fragment_shader); 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 = mWireframeMode ? vk::PolygonMode::eLine : vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eBack, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = vk::False, .lineWidth = mWireframeMode ? mLineWidth : 1.0F, // Thicker lines in wireframe mode }; 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, .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); } fn createCrosshairPipeline() -> void { // Create pipeline layout (no descriptor sets or push constants needed) vk::PipelineLayoutCreateInfo pipelineLayoutInfo {}; mCrosshairPipelineLayout = mDevice->createPipelineLayoutUnique(pipelineLayoutInfo); // Load shaders std::vector vertShaderCode = ShaderCompiler::getCompiledShader(CROSSHAIR_VERTEX_SHADER_PATH, shaderc_vertex_shader); std::vector fragShaderCode = ShaderCompiler::getCompiledShader(CROSSHAIR_FRAGMENT_SHADER_PATH, shaderc_fragment_shader); 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 }; // Vertex input vk::VertexInputBindingDescription bindingDescription = CrosshairVertex::getBindingDescription(); std::array attributeDescriptions = CrosshairVertex::getAttributeDescriptions(); vk::PipelineVertexInputStateCreateInfo vertexInputInfo { .vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &bindingDescription, .vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()), .pVertexAttributeDescriptions = attributeDescriptions.data() }; // Input assembly vk::PipelineInputAssemblyStateCreateInfo inputAssembly { .topology = vk::PrimitiveTopology::eLineList, .primitiveRestartEnable = false }; // Viewport and scissor vk::PipelineViewportStateCreateInfo viewportState { .viewportCount = 1, .scissorCount = 1 }; // Rasterization vk::PipelineRasterizationStateCreateInfo rasterizer { .depthClampEnable = false, .rasterizerDiscardEnable = false, .polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = false, .lineWidth = 1.0F }; // Multisampling vk::PipelineMultisampleStateCreateInfo multisampling { .rasterizationSamples = mMsaaSamples, .sampleShadingEnable = false }; // Color blending vk::PipelineColorBlendAttachmentState colorBlendAttachment { .blendEnable = false, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA }; vk::PipelineColorBlendStateCreateInfo colorBlending { .logicOpEnable = false, .attachmentCount = 1, .pAttachments = &colorBlendAttachment }; // Dynamic state std::array dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; vk::PipelineDynamicStateCreateInfo dynamicState { .dynamicStateCount = static_cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data() }; // Depth and stencil vk::PipelineDepthStencilStateCreateInfo depthStencil { .depthTestEnable = false, .depthWriteEnable = false, .depthCompareOp = vk::CompareOp::eLess, .depthBoundsTestEnable = false, .stencilTestEnable = false }; // Create the pipeline 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 = mCrosshairPipelineLayout.get(), .renderPass = mRenderPass.get(), .subpass = 0 }; mCrosshairPipeline = mDevice->createGraphicsPipelineUnique(nullptr, pipelineInfo).value; } fn createCrosshairBuffers() -> void { // Create vertex buffer vk::DeviceSize bufferSize = sizeof(crosshairVertices[0]) * crosshairVertices.size(); std::pair stagingBuffer = createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); void* data = mDevice->mapMemory(stagingBuffer.second.get(), 0, bufferSize); memcpy(data, crosshairVertices.data(), bufferSize); mDevice->unmapMemory(stagingBuffer.second.get()); auto [vertexBuffer, vertexBufferMemory] = createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal ); copyBuffer(stagingBuffer.first.get(), vertexBuffer.get(), bufferSize); mCrosshairVertexBuffer = std::move(vertexBuffer); mCrosshairVertexBufferMemory = std::move(vertexBufferMemory); // Create index buffer bufferSize = sizeof(crosshairIndices[0]) * crosshairIndices.size(); std::pair stagingBufferIndices = createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); data = mDevice->mapMemory(stagingBufferIndices.second.get(), 0, bufferSize); memcpy(data, crosshairIndices.data(), bufferSize); mDevice->unmapMemory(stagingBufferIndices.second.get()); auto [indexBuffer, indexBufferMemory] = createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal ); copyBuffer(stagingBufferIndices.first.get(), indexBuffer.get(), bufferSize); mCrosshairIndexBuffer = std::move(indexBuffer); mCrosshairIndexBufferMemory = std::move(indexBufferMemory); } /** * @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 = DeviceManager::findQueueFamilies(mPhysicalDevice, mSurface.get()); 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(const 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 { std::filesystem::path texturePath = std::filesystem::current_path() / "textures" / "viking_room.png"; stb::UniqueImage image(texturePath); u8* pixels = image.getData(); i32 texWidth = image.getWidth(), texHeight = image.getHeight(); mMipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; if (!pixels) throw std::runtime_error("Failed to load texture image!"); vk::DeviceSize imageSize = static_cast(texWidth) * static_cast(texHeight) * 4; 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( const vk::Image& image, const vk::Format& imageFormat, const i32& texWidth, const i32& texHeight, const 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; string warn, err; std::filesystem::path modelPath = std::filesystem::current_path() / "models" / "viking_room.obj"; if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, modelPath.string().c_str())) 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)], }, .normal = { attrib.normals[static_cast((3 * index.normal_index) + 0)], attrib.normals[static_cast((3 * index.normal_index) + 1)], attrib.normals[static_cast((3 * index.normal_index) + 2)], }, }; 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 idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) { std::tie(mUniformBuffers[idx], mUniformBuffersMemory[idx]) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); mUniformBuffersMapped[idx] = mDevice->mapMemory(mUniformBuffersMemory[idx].get(), 0, bufferSize); } } fn createLightUniformBuffers() -> void { vk::DeviceSize bufferSize = sizeof(LightInfo); mLightUniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); mLightUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); mLightUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) { std::tie(mLightUniformBuffers[idx], mLightUniformBuffersMemory[idx]) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); mLightUniformBuffersMapped[idx] = mDevice->mapMemory(mLightUniformBuffersMemory[idx].get(), 0, bufferSize); } } fn createCameraUniformBuffers() -> void { vk::DeviceSize bufferSize = sizeof(CameraInfo); mCameraUniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); mCameraUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); mCameraUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) { std::tie(mCameraUniformBuffers[idx], mCameraUniformBuffersMemory[idx]) = createBuffer( bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent ); mCameraUniformBuffersMapped[idx] = mDevice->mapMemory(mCameraUniformBuffersMemory[idx].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::eUniformBuffer, .descriptorCount = MAX_FRAMES_IN_FLIGHT, }, 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 idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) { vk::DescriptorBufferInfo uboBufferInfo { .buffer = mUniformBuffers[idx].get(), .offset = 0, .range = sizeof(UniformBufferObject), }; vk::DescriptorBufferInfo lightBufferInfo { .buffer = mLightUniformBuffers[idx].get(), .offset = 0, .range = sizeof(LightInfo), }; vk::DescriptorBufferInfo cameraBufferInfo { .buffer = mCameraUniformBuffers[idx].get(), .offset = 0, .range = sizeof(CameraInfo), }; vk::DescriptorImageInfo imageInfo { .sampler = mTextureSampler.get(), .imageView = mTextureImageView.get(), .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal, }; std::array descriptorWrites = { vk::WriteDescriptorSet { .dstSet = mDescriptorSets[idx], .dstBinding = 0, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &uboBufferInfo, }, vk::WriteDescriptorSet { .dstSet = mDescriptorSets[idx], .dstBinding = 1, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &lightBufferInfo, }, vk::WriteDescriptorSet { .dstSet = mDescriptorSets[idx], .dstBinding = 2, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &cameraBufferInfo, }, vk::WriteDescriptorSet { .dstSet = mDescriptorSets[idx], .dstBinding = 3, .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 { // Begin the command buffer commandBuffer.begin({ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }); // Define clear values for color and depth std::array clearValues = { vk::ClearValue { .color = { std::array { 0.0F, 0.0F, 0.0F, 1.0F } } }, vk::ClearValue { .depthStencil = { 1.0F, 0 } }, }; // Begin the render pass vk::RenderPassBeginInfo renderPassInfo { .renderPass = mRenderPass.get(), .framebuffer = mSwapChainFramebuffers[imageIndex].get(), .renderArea = { .offset = { 0, 0 }, .extent = mSwapChainExtent }, .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 ); UniformBufferObject ubo { // Model matrix - glm::rotate(matrix, angle, axis) .model = glm::mat4(1.0F), // View matrix - glm::lookAt(eye, center, up) .view = mView, // Projection matrix - glm::perspective(fov, aspect, near, far) .proj = glm::perspective( glm::radians(mFieldOfView), static_cast(mSwapChainExtent.width) / static_cast(mSwapChainExtent.height), 0.1F, 100.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[mCurrentFrame], &ubo, sizeof(ubo)); LightInfo lightInfo { .position = mLightSettings.position, .color = mLightSettings.color, .ambient_strength = mLightSettings.ambient_strength, .specular_strength = mLightSettings.specular_strength, }; // Copy the light uniform buffer object to the mapped memory memcpy(mLightUniformBuffersMapped[mCurrentFrame], &lightInfo, sizeof(lightInfo)); CameraInfo cameraInfo { .position = glm::vec3(mCamera.getPosition()), // Use actual camera position }; // Copy the camera uniform buffer object to the mapped memory memcpy(mCameraUniformBuffersMapped[mCurrentFrame], &cameraInfo, sizeof(cameraInfo)); // Example: Add extra clones with different translations std::vector modelMatrices = { glm::translate(glm::mat4(1.0F), glm::vec3(2.0F, 0.0F, 0.0F)), glm::translate(glm::mat4(1.0F), glm::vec3(-2.0F, 0.0F, 0.0F)), glm::translate(glm::mat4(1.0F), glm::vec3(0.0F, 2.0F, 0.0F)) }; for (const glm::mat4& modelMatrix : modelMatrices) { // Update model matrix for each clone ubo.model = modelMatrix; memcpy(mUniformBuffersMapped[mCurrentFrame], &ubo, sizeof(ubo)); // 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); } // Draw the crosshair commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mCrosshairPipeline.get()); commandBuffer.bindVertexBuffers( 0, 1, &mCrosshairVertexBuffer.get(), std::array { 0 }.data() ); commandBuffer.bindIndexBuffer(mCrosshairIndexBuffer.get(), 0, vk::IndexType::eUint16); // Draw the crosshair commandBuffer.drawIndexed(static_cast(crosshairIndices.size()), 1, 0, 0, 0); // Render ImGui if we have a draw data (ImGui::Render was called) if (ImGui::GetDrawData()) 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 = mView, // Projection matrix - glm::perspective(fov, aspect, near, far) .proj = glm::perspective( glm::radians(mFieldOfView), static_cast(mSwapChainExtent.width) / static_cast(mSwapChainExtent.height), 0.1F, 100.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!"); // Clear old pipeline if it exists if (mOldPipeline) { mDevice->waitIdle(); // Wait for all operations to complete mOldPipeline.reset(); } // 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 command buffer 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 (const vk::SystemError& /*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() * sizeof(u32), .pCode = 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 || availablePresentMode == vk::PresentModeKHR::eImmediate) 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) = WindowManager::getFramebufferSize(mWindow.get()); // 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 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; }