// Time measurements #include // String formatting #include // Unordered sets #include // Make depth go from 0 to 1 instead of -1 to 1 #define GLM_FORCE_DEPTH_ZERO_TO_ONE // Provides various math-related data structures #include // Used to load models #define TINYOBJLOADER_IMPLEMENTATION #include // Dynamic function loading #define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1 // Use the beta extensions #define VK_ENABLE_BETA_EXTENSIONS // Use {} instead of () #define VULKAN_HPP_NO_CONSTRUCTORS // Vulkan itself #include // Needed for dynamic function loading to work VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE // Type aliases #include "util/types.h" // STB image helper #include "util/unique_image.h" // Vertex class #include "util/vertex.h" // ImGui #include #include #include // Use {} instead of () #define VKFW_NO_STRUCT_CONSTRUCTORS // GLFW C++ wrapper #include "vkfw.hpp" // Initial width and height constexpr i32 WIDTH = 800; constexpr i32 HEIGHT = 600; // Paths for the model and texture to render constexpr const char* MODEL_PATH = "models/viking_room.obj"; constexpr const char* TEXTURE_PATH = "textures/viking_room.png"; // Paths for the shaders constexpr const char* FRAGMENT_SHADER_PATH = "shaders/frag.spv"; constexpr const char* VERTEX_SHADER_PATH = "shaders/vert.spv"; // Maximum number of frames to be worked on at the same time constexpr i32 MAX_FRAMES_IN_FLIGHT = 2; // Enable validation layers only in debug mode #ifndef NDEBUG constexpr std::array validationLayers = { "VK_LAYER_KHRONOS_validation" }; #endif // macOS requires the portability extension to be enabled #ifdef __APPLE__ constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName, vk::KHRPortabilitySubsetExtensionName }; #else constexpr std::array deviceExtensions = { vk::KHRSwapchainExtensionName }; #endif class VulkanApp { public: fn run() -> void { // Create window initWindow(); // Setup vulkan initVulkan(); // Render loop mainLoop(); cleanupSwapChain(); ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); } private: // Instances vkfw::UniqueInstance mVKFWInstance; // Unique handle to the GLFW instance vkfw::UniqueWindow mWindow; // Unique handle to the GLFW window vk::UniqueInstance mInstance; // Unique handle to the Vulkan instance // Debug messenger for Vulkan validation layers vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; // Unique handle to the debug messenger // Surface for rendering vk::UniqueSurfaceKHR mSurface; // Unique handle to the Vulkan surface // Physical device and logical device vk::PhysicalDevice mPhysicalDevice; // Handle to the selected physical device (GPU) vk::SampleCountFlagBits mMsaaSamples; // Multisample anti-aliasing (MSAA) sample count vk::UniqueDevice mDevice; // Unique handle to the logical device // Vulkan queues for graphics and presentation vk::Queue mGraphicsQueue; // Handle to the graphics queue vk::Queue mPresentQueue; // Handle to the presentation queue // Swapchain and related resources vk::UniqueSwapchainKHR mSwapChain; // The swapchain handle std::vector mSwapChainImages; // Images in the swapchain vk::Format mSwapChainImageFormat; // Format of the swapchain images vk::Extent2D mSwapChainExtent; // Dimensions of the swapchain images std::vector mSwapChainImageViews; // Image views for the swapchain images std::vector mSwapChainFramebuffers; // Framebuffers for the swapchain images // Render pass and pipeline configurations vk::UniqueRenderPass mRenderPass; // Render pass configuration 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; // Color image resources vk::UniqueImage mColorImage; // Color image vk::UniqueDeviceMemory mColorImageMemory; // Memory for the color image vk::UniqueImageView mColorImageView; // Image view for the color image // Depth image resources vk::UniqueImage mDepthImage; // Depth image vk::UniqueDeviceMemory mDepthImageMemory; // Memory for the depth image vk::UniqueImageView mDepthImageView; // Image view for the depth image // Texture resources u32 mMipLevels; // Number of mipmap levels vk::UniqueImage mTextureImage; // Texture image vk::UniqueDeviceMemory mTextureImageMemory; // Memory for the texture image vk::UniqueImageView mTextureImageView; // Image view for the texture image vk::UniqueSampler mTextureSampler; // Sampler for the texture // Vertex and index buffers std::vector mVertices; // Vertex data std::vector mIndices; // Index data vk::UniqueBuffer mVertexBuffer; // Vertex buffer vk::UniqueDeviceMemory mVertexBufferMemory; // Memory for the vertex buffer vk::UniqueBuffer mIndexBuffer; // Index buffer vk::UniqueDeviceMemory mIndexBufferMemory; // Memory for the index buffer // Uniform buffers std::vector mUniformBuffers; // Uniform buffers std::vector mUniformBuffersMemory; // Memory for the uniform buffers std::vector mUniformBuffersMapped; // Mapped pointers for the uniform buffers // Descriptor pool and sets vk::UniqueDescriptorPool mDescriptorPool; // Descriptor pool vk::UniqueDescriptorPool mImGuiDescriptorPool; // Descriptor pool for ImGui std::vector mDescriptorSets; // Descriptor sets // Command buffers std::vector mCommandBuffers; // Command buffers // Synchronization primitives std::vector mImageAvailableSemaphores; // Semaphores for image availability std::vector mRenderFinishedSemaphores; // Semaphores for render completion std::vector mInFlightFences; // Fences for in-flight frames // Framebuffer resize flag and current frame index bool mFramebufferResized = false; // Flag indicating if the framebuffer was resized u32 mCurrentFrame = 0; // Index of the current frame // Struct to hold queue family indices struct QueueFamilyIndices { std::optional graphics_family; // Index of the graphics queue family std::optional present_family; // Index of the presentation queue family // Check if both queue families are available fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); } }; // Struct to hold swapchain 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 matrix alignas(16) glm::mat4 view; // View matrix alignas(16) glm::mat4 proj; // Projection matrix }; // Read a file into a vector of chars static fn readFile(const std::string& filename) -> std::vector { // Open the file in binary mode std::ifstream file(filename, std::ios::binary); // Make sure the file was opened if (!file) throw std::runtime_error("Failed to open file: " + filename); // Read the contents of the file into a vector std::vector buffer((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return buffer; } // Create a GLFW window fn initWindow() -> void { // Initialize GLFW mVKFWInstance = vkfw::initUnique(); // Set window hints vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone, // No client API (Vulkan) }; // Create the window mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints); // Set the window user pointer to this class mWindow->setUserPointer(this); // Set the framebuffer resize callback mWindow->callbacks()->on_window_resize = [](const vkfw::Window& window, usize /*width*/, usize /*height*/) -> void { // Set the framebuffer resized flag std::bit_cast(window.getUserPointer())->mFramebufferResized = true; }; } // Initialize Vulkan fn initVulkan() -> void { createInstance(); setupDebugMessenger(); createSurface(); pickPhysicalDevice(); createLogicalDevice(); createSwapChain(); createImageViews(); createRenderPass(); createDescriptorSetLayout(); createGraphicsPipeline(); createCommandPool(); createColorResources(); createDepthResources(); createFramebuffers(); createTextureImage(); createTextureImageView(); createTextureSampler(); loadModel(); createVertexBuffer(); createIndexBuffer(); createUniformBuffers(); createDescriptorPool(); createDescriptorSets(); createCommandBuffers(); createSyncObjects(); initImGui(); } void initImGui() { // 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 = 1, .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); } // Main loop fn mainLoop() -> void { // While the window is open, while (!mWindow->shouldClose()) { // Poll for events vkfw::pollEvents(); // Draw a frame drawFrame(); } // Wait for the device to finish mDevice->waitIdle(); } // Clean up the swap chain fn cleanupSwapChain() -> void { for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset(); for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset(); mSwapChain.reset(); } // Recreate the swap chain 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(); } // Create a Vulkan instance 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()); } // Create the debug messenger 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); } // Create the GLFW window surface fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); } // Choose the best-suited physical device 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!"); } // Create the logical device 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 (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); } 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; } 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); } 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); } 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); } 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); } 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); } } fn createCommandPool() -> void { QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice); vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = queueFamilyIndices.graphics_family.value() }; mCommandPool = mDevice->createCommandPoolUnique(poolInfo); } 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); } 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); } 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!"); } fn findDepthFormat() -> vk::Format { return findSupportedFormat( { vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint }, vk::ImageTiling::eOptimal, vk::FormatFeatureFlagBits::eDepthStencilAttachment ); } static fn hasStencilComponent(vk::Format format) { return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; } 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); } 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); } 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; } fn createTextureImageView() -> void { mTextureImageView = createImageView( mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels ); } 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); } fn createImageView( const vk::Image& image, const vk::Format& format, const vk::ImageAspectFlags& aspectFlags, const u32& mipLevels ) -> vk::UniqueImageView { vk::ImageViewCreateInfo viewInfo { .image = image, .viewType = vk::ImageViewType::e2D, .format = format, .subresourceRange = { .aspectMask = aspectFlags, .baseMipLevel = 0, .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = 1, } }; return mDevice->createImageViewUnique(viewInfo); } 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); } 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]); } } } 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(); } 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(); } 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); } } 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); } 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, {}); } } 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) }; } // Create the command buffers 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; } // End the single time 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); } // Copy data from src to dst 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); } // Copy buffer from src to dst 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); } // Find the memory type of the physical device 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!"); } // Create the command buffers 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); } // Record the 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(); } // Create the semaphores and fences 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); } } 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)); } // Draw a frame to the window 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; } } // Create the shader module fn createShaderModule(const std::vector& code) -> vk::UniqueShaderModule { vk::ShaderModuleCreateInfo createInfo { .codeSize = code.size(), .pCode = std::bit_cast(code.data()), }; return mDevice->createShaderModuleUnique(createInfo); } // Choose the color format for the swap chain 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]; } // Choose the swap chain presentation mode 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; } // Choose the swap chain extent (resolution) 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), }; } // Check if the swap chain is adequate fn querySwapChainSupport(const vk::PhysicalDevice& device) -> SwapChainSupportDetails { return { .capabilities = device.getSurfaceCapabilitiesKHR(mSurface.get()), .formats = device.getSurfaceFormatsKHR(mSurface.get()), .present_modes = device.getSurfacePresentModesKHR(mSurface.get()), }; } // Check if the device is suitable for the application 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; } // Check if the device supports the required extensions 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(); } // Find the queue families that support the required operations 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; } // Check if all requested layers are available 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 } // Callback function used to report validation layer messages static VKAPI_ATTR fn VKAPI_CALL debugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/, // Severity: verbose, info, warning, error VkDebugUtilsMessageTypeFlagsEXT /*messageType*/, // Type: general, validation, performance const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, // Message details void* /*pUserData*/ // Optional user data ) -> 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; } }; 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; }