// Time measurements #include // String formatting #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" // 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; // 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 // Enable validation layers only in debug mode #ifdef NDEBUG constexpr bool enableValidationLayers = false; #else constexpr std::array validationLayers = { "VK_LAYER_KHRONOS_validation" }; constexpr bool enableValidationLayers = true; #endif class VulkanApp { public: fn run() -> void { // Create window initWindow(); // Setup vulkan initVulkan(); // Render loop mainLoop(); } private: // GLFW instance vkfw::UniqueInstance mVKFWInstance; // GLFW window vkfw::UniqueWindow mWindow; // Vulkan instance vk::UniqueInstance mInstance; // Debug messenger vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; // Window surface vk::UniqueSurfaceKHR mSurface; // Represents the physical device vk::PhysicalDevice mPhysicalDevice; // Number of MSAA samples vk::SampleCountFlagBits mMsaaSamples; // Represents the logical device vk::UniqueDevice mDevice; vk::Queue mGraphicsQueue; vk::Queue mPresentQueue; vk::UniqueSwapchainKHR mSwapChain; std::vector mSwapChainImages; vk::Format mSwapChainImageFormat; vk::Extent2D mSwapChainExtent; std::vector mSwapChainImageViews; std::vector mSwapChainFramebuffers; vk::UniqueRenderPass mRenderPass; vk::UniqueDescriptorSetLayout mDescriptorSetLayout; vk::UniquePipelineLayout mPipelineLayout; vk::UniquePipeline mGraphicsPipeline; vk::UniqueCommandPool mCommandPool; vk::UniqueImage mColorImage; vk::UniqueDeviceMemory mColorImageMemory; vk::UniqueImageView mColorImageView; vk::UniqueImage mDepthImage; vk::UniqueDeviceMemory mDepthImageMemory; vk::UniqueImageView mDepthImageView; u32 mMipLevels; vk::UniqueImage mTextureImage; vk::UniqueDeviceMemory mTextureImageMemory; vk::UniqueImageView mTextureImageView; vk::UniqueSampler mTextureSampler; std::vector mVertices; std::vector mIndices; vk::UniqueBuffer mVertexBuffer; vk::UniqueDeviceMemory mVertexBufferMemory; vk::UniqueBuffer mIndexBuffer; vk::UniqueDeviceMemory mIndexBufferMemory; std::vector mUniformBuffers; std::vector mUniformBuffersMemory; std::vector mUniformBuffersMapped; vk::UniqueDescriptorPool mDescriptorPool; std::vector mDescriptorSets; std::vector mCommandBuffers; std::vector mImageAvailableSemaphores; std::vector mRenderFinishedSemaphores; std::vector mInFlightFences; bool mFramebufferResized = false; u32 mCurrentFrame = 0; struct QueueFamilyIndices { std::optional graphics_family; std::optional present_family; fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); } }; struct SwapChainSupportDetails { vk::SurfaceCapabilitiesKHR capabilities; std::vector formats; std::vector present_modes; }; struct UniformBufferObject { alignas(16) glm::mat4 model; alignas(16) glm::mat4 view; alignas(16) glm::mat4 proj; }; static fn readFile(const std::string& filename) -> std::vector { std::ifstream file(filename, std::ios::ate | std::ios::binary); if (!file.is_open()) throw std::runtime_error("Failed to open file! " + filename); usize fileSize = static_cast(file.tellg()); std::vector buffer(fileSize); file.seekg(0); file.read(buffer.data(), static_cast(fileSize)); file.close(); return buffer; } fn initWindow() -> void { mVKFWInstance = vkfw::initUnique(); vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone }; mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints); mWindow->setUserPointer(this); mWindow->callbacks()->on_window_resize = [](const vkfw::Window& window, u32 /*width*/, u32 /*height*/) -> void { std::bit_cast(window.getUserPointer())->mFramebufferResized = true; }; } 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(); } fn mainLoop() -> void { while (!mWindow->shouldClose()) { vkfw::pollEvents(); drawFrame(); } mDevice->waitIdle(); } fn cleanupSwapChain() -> void { for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset(); for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset(); mSwapChain.reset(); } fn recreateSwapChain() -> void { u32 width = 0, height = 0; std::tie(width, height) = mWindow->getFramebufferSize(); while (width == 0 || height == 0) { std::tie(width, height) = mWindow->getFramebufferSize(); vkfw::waitEvents(); } mDevice->waitIdle(); cleanupSwapChain(); createSwapChain(); createImageViews(); createColorResources(); createDepthResources(); createFramebuffers(); } fn createInstance() -> void { if (enableValidationLayers && !checkValidationLayerSupport()) throw std::runtime_error("Validation layers requested, but not available!"); vk::ApplicationInfo appInfo { .pApplicationName = "Hello Triangle", .applicationVersion = 1, .pEngineName = "No Engine", .engineVersion = 1, .apiVersion = vk::ApiVersion12 }; // Retrieve extensions using custom function std::vector extensions = getRequiredExtensions(); #ifdef __APPLE__ // Enable the portability extension and set flags 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 vk::InstanceCreateInfo createInfo { #ifdef __APPLE__ .flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR, #endif .pApplicationInfo = &appInfo, .enabledLayerCount = enableValidationLayers ? static_cast(validationLayers.size()) : 0, .ppEnabledLayerNames = enableValidationLayers ? validationLayers.data() : nullptr, .enabledExtensionCount = static_cast(extensions.size()), .ppEnabledExtensionNames = extensions.data() }; #ifndef NDEBUG fmt::println("Available extensions:"); for (const char* extension : extensions) fmt::println("\t{}", extension); #endif mInstance = vk::createInstanceUnique(createInfo); VULKAN_HPP_DEFAULT_DISPATCHER.init(mInstance.get()); } fn setupDebugMessenger() -> void { if (!enableValidationLayers) return; 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); } fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); } fn pickPhysicalDevice() -> void { std::vector devices = mInstance->enumeratePhysicalDevices(); if (devices.empty()) throw std::runtime_error("Failed to find GPUs with Vulkan support!"); #ifndef NDEBUG fmt::println("Available devices:"); #endif for (const vk::PhysicalDevice& device : devices) { #ifndef NDEBUG vk::PhysicalDeviceProperties properties = device.getProperties(); fmt::println("\t{}", properties.deviceName.data()); #endif if (isDeviceSuitable(device)) { mPhysicalDevice = device; mMsaaSamples = getMaxUsableSampleCount(); break; } } if (!mPhysicalDevice) throw std::runtime_error("Failed to find a suitable GPU!"); } fn createLogicalDevice() -> void { QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice); std::vector queueCreateInfos; std::set uniqueQueueFamilies = { qfIndices.graphics_family.value(), qfIndices.present_family.value() }; f32 queuePriority = 1.0F; for (u32 queueFamily : uniqueQueueFamilies) { vk::DeviceQueueCreateInfo queueCreateInfo { .queueFamilyIndex = queueFamily, .queueCount = 1, .pQueuePriorities = &queuePriority }; queueCreateInfos.emplace_back(queueCreateInfo); } 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 }; 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; createImage( mSwapChainExtent.width, mSwapChainExtent.height, 1, mMsaaSamples, colorFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransientAttachment | vk::ImageUsageFlagBits::eColorAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, mColorImage, mColorImageMemory ); mColorImageView = createImageView(mColorImage.get(), colorFormat, vk::ImageAspectFlagBits::eColor, 1); } fn createDepthResources() -> void { vk::Format depthFormat = findDepthFormat(); createImage( mSwapChainExtent.width, mSwapChainExtent.height, 1, mMsaaSamples, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, mDepthImage, mDepthImageMemory ); mDepthImageView = createImageView(mDepthImage.get(), depthFormat, vk::ImageAspectFlagBits::eDepth, 1); } fn findSupportedFormat( const std::vector& candidates, vk::ImageTiling tiling, 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; createBuffer( imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory ); copyData(stagingBufferMemory.get(), imageSize, pixels); 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, mTextureImage, mTextureImageMemory ); 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::UniqueCommandBuffer 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(std::move(commandBuffer)); } fn getMaxUsableSampleCount() -> vk::SampleCountFlagBits { vk::PhysicalDeviceProperties physicalDeviceProperties = mPhysicalDevice.getProperties(); vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts; if (counts & vk::SampleCountFlagBits::e64) return vk::SampleCountFlagBits::e64; if (counts & vk::SampleCountFlagBits::e32) return vk::SampleCountFlagBits::e32; if (counts & vk::SampleCountFlagBits::e16) return vk::SampleCountFlagBits::e16; if (counts & vk::SampleCountFlagBits::e8) return vk::SampleCountFlagBits::e8; if (counts & vk::SampleCountFlagBits::e4) return vk::SampleCountFlagBits::e4; if (counts & vk::SampleCountFlagBits::e2) return vk::SampleCountFlagBits::e2; 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(vk::Image image, vk::Format format, vk::ImageAspectFlags aspectFlags, 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( u32 width, u32 height, u32 mipLevels, vk::SampleCountFlagBits numSamples, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, vk::UniqueImage& image, vk::UniqueDeviceMemory& imageMemory ) -> void { 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, }; image = mDevice->createImageUnique(imageInfo); vk::MemoryRequirements memRequirements = mDevice->getImageMemoryRequirements(image.get()); vk::MemoryAllocateInfo allocInfo { .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties), }; imageMemory = mDevice->allocateMemoryUnique(allocInfo); mDevice->bindImageMemory(image.get(), imageMemory.get(), 0); } fn transitionImageLayout( vk::Image image, vk::ImageLayout oldLayout, vk::ImageLayout newLayout, u32 mipLevels ) -> void { vk::UniqueCommandBuffer commandBuffer = beginSingleTimeCommands(); vk::ImageMemoryBarrier barrier { .oldLayout = oldLayout, .newLayout = newLayout, .srcQueueFamilyIndex = vk::QueueFamilyIgnored, .dstQueueFamilyIndex = vk::QueueFamilyIgnored, .image = image, // clang-format off .subresourceRange = { .aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = 1 } // clang-format on }; vk::PipelineStageFlags sourceStage; vk::PipelineStageFlags destinationStage; 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 { throw std::invalid_argument("Unsupported layout transition!"); } commandBuffer->pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier); endSingleTimeCommands(std::move(commandBuffer)); } fn copyBufferToImage(vk::Buffer buffer, vk::Image image, u32 width, u32 height) -> void { vk::UniqueCommandBuffer 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(std::move(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; createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory ); copyData(stagingBufferMemory.get(), bufferSize, mVertices.data()); createBuffer( bufferSize, vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal, mVertexBuffer, mVertexBufferMemory ); 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; createBuffer( bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory ); copyData(stagingBufferMemory.get(), bufferSize, mIndices.data()); createBuffer( bufferSize, vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal, mIndexBuffer, mIndexBufferMemory ); 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++) { createBuffer( bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, mUniformBuffers[i], mUniformBuffersMemory[i] ); mUniformBuffersMapped[i] = mDevice->mapMemory(mUniformBuffersMemory[i].get(), 0, bufferSize); } } fn createDescriptorPool() -> void { std::array poolSizes = { { { .type = vk::DescriptorType::eUniformBuffer, .descriptorCount = MAX_FRAMES_IN_FLIGHT }, { .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 = { { { .dstSet = mDescriptorSets[i], .dstBinding = 0, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eUniformBuffer, .pBufferInfo = &bufferInfo, }, { .dstSet = mDescriptorSets[i], .dstBinding = 1, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &imageInfo, } } }; mDevice->updateDescriptorSets(descriptorWrites, {}); } } fn createBuffer( vk::DeviceSize deviceSize, vk::BufferUsageFlags bufferUsageFlags, vk::MemoryPropertyFlags memoryPropertyFlags, vk::UniqueBuffer& buffer, vk::UniqueDeviceMemory& bufferMemory ) -> void { vk::BufferCreateInfo bufferInfo { .size = deviceSize, .usage = bufferUsageFlags, .sharingMode = vk::SharingMode::eExclusive, }; buffer = mDevice->createBufferUnique(bufferInfo); vk::MemoryRequirements memRequirements = mDevice->getBufferMemoryRequirements(buffer.get()); vk::MemoryAllocateInfo allocInfo { .allocationSize = memRequirements.size, .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, memoryPropertyFlags), }; bufferMemory = mDevice->allocateMemoryUnique(allocInfo); mDevice->bindBufferMemory(buffer.get(), bufferMemory.get(), 0); } fn beginSingleTimeCommands() -> vk::UniqueCommandBuffer { vk::CommandBufferAllocateInfo allocInfo { .commandPool = mCommandPool.get(), .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1, }; vk::UniqueCommandBuffer commandBuffer = std::move(mDevice->allocateCommandBuffersUnique(allocInfo)[0]); vk::CommandBufferBeginInfo beginInfo { .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; commandBuffer->begin(beginInfo); return commandBuffer; } fn endSingleTimeCommands(vk::UniqueCommandBuffer commandBuffer) -> void { commandBuffer->end(); vk::SubmitInfo submitInfo { .commandBufferCount = 1, .pCommandBuffers = &commandBuffer.get() }; mGraphicsQueue.submit(submitInfo, nullptr); mGraphicsQueue.waitIdle(); } fn copyData(vk::DeviceMemory stagingBufferMemory, vk::DeviceSize bufferSize, const void* src) -> void { void* data = mDevice->mapMemory(stagingBufferMemory, 0, bufferSize); memcpy(data, src, static_cast(bufferSize)); mDevice->unmapMemory(stagingBufferMemory); } fn copyBuffer(vk::Buffer srcBuffer, vk::Buffer dstBuffer, vk::DeviceSize deviceSize) -> void { vk::UniqueCommandBuffer commandBuffer = beginSingleTimeCommands(); vk::BufferCopy copyRegion { .size = deviceSize }; commandBuffer->copyBuffer(srcBuffer, dstBuffer, 1, ©Region); endSingleTimeCommands(std::move(commandBuffer)); } fn findMemoryType(u32 typeFilter, vk::MemoryPropertyFlags properties) -> u32 { vk::PhysicalDeviceMemoryProperties memProperties = mPhysicalDevice.getMemoryProperties(); for (u32 i = 0; i < memProperties.memoryTypeCount; i++) if ((typeFilter & (1 << i)) && (memProperties.memoryTypes.at(i).propertyFlags & properties) == properties) return i; throw std::runtime_error("Failed to find a suitable memory type!"); } fn createCommandBuffers() -> void { mCommandBuffers.resize(MAX_FRAMES_IN_FLIGHT); vk::CommandBufferAllocateInfo allocInfo { .commandPool = mCommandPool.get(), .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = static_cast(mCommandBuffers.size()) }; mCommandBuffers = mDevice->allocateCommandBuffersUnique(allocInfo); } fn recordCommandBuffer(vk::CommandBuffer commandBuffer, u32 imageIndex) -> void { vk::CommandBufferBeginInfo beginInfo {}; commandBuffer.begin(beginInfo); std::array clearValues { { { .color = { .float32 = std::array { 0.0F, 0.0F, 0.0F, 1.0F } } }, { .depthStencil = { .depth = 1.0F, .stencil = 0 } } } }; vk::RenderPassBeginInfo renderPassInfo { .renderPass = mRenderPass.get(), .framebuffer = mSwapChainFramebuffers[imageIndex].get(), .renderArea = { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent }, .clearValueCount = static_cast(clearValues.size()), .pClearValues = clearValues.data() }; commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline); commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mGraphicsPipeline.get()); 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, }; vk::Rect2D scissor { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent, }; commandBuffer.setViewport(0, viewport); commandBuffer.setScissor(0, scissor); commandBuffer.bindVertexBuffers(0, mVertexBuffer.get(), { 0 }); commandBuffer.bindIndexBuffer(mIndexBuffer.get(), 0, vk::IndexType::eUint32); commandBuffer.bindDescriptorSets( vk::PipelineBindPoint::eGraphics, mPipelineLayout.get(), 0, 1, &mDescriptorSets[mCurrentFrame], 0, nullptr ); commandBuffer.drawIndexed(static_cast(mIndices.size()), 1, 0, 0, 0); commandBuffer.endRenderPass(); commandBuffer.end(); } fn createSyncObjects() -> void { mImageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); mRenderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); mInFlightFences.resize(MAX_FRAMES_IN_FLIGHT); vk::SemaphoreCreateInfo semaphoreInfo {}; vk::FenceCreateInfo fenceInfo { .flags = vk::FenceCreateFlagBits::eSignaled }; 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(u32 currentImage) -> void { static auto StartTime = std::chrono::high_resolution_clock::now(); auto currentTime = std::chrono::high_resolution_clock::now(); f32 time = std::chrono::duration(currentTime - StartTime).count(); UniformBufferObject ubo { .model = glm::rotate(glm::mat4(1.0F), time * glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F)), .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)), .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; memcpy(mUniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); } fn drawFrame() -> void { try { vk::Result result = mDevice->waitForFences(mInFlightFences[mCurrentFrame].get(), vk::Bool32(vk::True), UINT64_MAX); if (result != vk::Result::eSuccess) throw std::runtime_error("Failed to wait for fences!"); vk::Result imageIndexResult = vk::Result::eSuccess; u32 imageIndexValue = 0; std::tie(imageIndexResult, imageIndexValue) = mDevice->acquireNextImageKHR( mSwapChain.get(), UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame].get(), nullptr ); if (imageIndexResult == vk::Result::eErrorOutOfDateKHR) { recreateSwapChain(); return; } if (imageIndexResult != vk::Result::eSuccess && imageIndexResult != vk::Result::eSuboptimalKHR) throw std::runtime_error("Failed to acquire swap chain image!"); updateUniformBuffer(mCurrentFrame); mDevice->resetFences(mInFlightFences[mCurrentFrame].get()); mCommandBuffers[mCurrentFrame]->reset(vk::CommandBufferResetFlagBits::eReleaseResources); 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(), }; mGraphicsQueue.submit(submitInfo, mInFlightFences[mCurrentFrame].get()); vk::PresentInfoKHR presentInfo { .waitSemaphoreCount = 1, .pWaitSemaphores = &mRenderFinishedSemaphores[mCurrentFrame].get(), .swapchainCount = 1, .pSwapchains = &mSwapChain.get(), .pImageIndices = &imageIndexValue, }; vk::Result presentResult = mPresentQueue.presentKHR(presentInfo); if (presentResult == vk::Result::eErrorOutOfDateKHR || presentResult == vk::Result::eSuboptimalKHR || mFramebufferResized) { mFramebufferResized = false; recreateSwapChain(); } else if (presentResult != vk::Result::eSuccess) throw std::runtime_error("Failed to present swap chain image!"); mCurrentFrame = (mCurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT; } catch (vk::OutOfDateKHRError& /*err*/) { mFramebufferResized = false; recreateSwapChain(); return; } } fn createShaderModule(const std::vector& code) -> vk::UniqueShaderModule { vk::ShaderModuleCreateInfo createInfo { .codeSize = code.size(), .pCode = std::bit_cast(code.data()) }; return mDevice->createShaderModuleUnique(createInfo); } static fn chooseSwapSurfaceFormat(const std::vector& availableFormats ) -> vk::SurfaceFormatKHR { for (const auto& availableFormat : availableFormats) if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) return availableFormat; return availableFormats[0]; } static fn chooseSwapPresentMode(const std::vector& availablePresentModes ) -> vk::PresentModeKHR { for (const auto& availablePresentMode : availablePresentModes) if (availablePresentMode == vk::PresentModeKHR::eMailbox) return availablePresentMode; return vk::PresentModeKHR::eFifo; } fn chooseSwapExtent(const vk::SurfaceCapabilitiesKHR capabilities) -> vk::Extent2D { if (capabilities.currentExtent.width != UINT32_MAX) return capabilities.currentExtent; u32 width = 0, height = 0; std::tie(width, height) = mWindow->getFramebufferSize(); vk::Extent2D actualExtent = { .width = width, .height = height }; actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); return actualExtent; } fn querySwapChainSupport(vk::PhysicalDevice device) -> SwapChainSupportDetails { SwapChainSupportDetails details; details.capabilities = device.getSurfaceCapabilitiesKHR(mSurface.get()); details.formats = device.getSurfaceFormatsKHR(mSurface.get()); details.present_modes = device.getSurfacePresentModesKHR(mSurface.get()); return details; } fn isDeviceSuitable(vk::PhysicalDevice device) -> bool { QueueFamilyIndices qfIndices = findQueueFamilies(device); bool extensionsSupported = checkDeviceExtensionSupport(device); bool swapChainAdequate = false; if (extensionsSupported) { SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty(); } vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures(); return qfIndices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; } static fn checkDeviceExtensionSupport(vk::PhysicalDevice device) -> bool { std::vector availableExtensions = device.enumerateDeviceExtensionProperties(); std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); for (const vk::ExtensionProperties& extension : availableExtensions) requiredExtensions.erase(extension.extensionName); return requiredExtensions.empty(); } fn findQueueFamilies(vk::PhysicalDevice device) -> QueueFamilyIndices { QueueFamilyIndices qfIndices; std::vector queueFamilies = device.getQueueFamilyProperties(); for (u32 i = 0; i < queueFamilies.size(); i++) { if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics) qfIndices.graphics_family = i; vk::Bool32 queuePresentSupport = device.getSurfaceSupportKHR(i, mSurface.get()); if (queuePresentSupport) qfIndices.present_family = i; if (qfIndices.isComplete()) break; } return qfIndices; } static fn getRequiredExtensions() -> std::vector { std::span extensionsSpan = vkfw::getRequiredInstanceExtensions(); std::vector extensions(extensionsSpan.begin(), extensionsSpan.end()); if (enableValidationLayers) extensions.emplace_back(vk::EXTDebugUtilsExtensionName); return extensions; } static fn checkValidationLayerSupport() -> bool { std::vector availableLayers = vk::enumerateInstanceLayerProperties(); for (const char* layerName : validationLayers) { bool layerFound = false; for (const vk::LayerProperties& layerProperties : availableLayers) if (strcmp(layerName, layerProperties.layerName) == 0) { layerFound = true; break; } if (!layerFound) return false; } return true; } static VKAPI_ATTR fn VKAPI_CALL debugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/, VkDebugUtilsMessageTypeFlagsEXT /*messageType*/, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* /*pUserData*/ ) -> vk::Bool32 { fmt::println("Validation layer: {}", pCallbackData->pMessage); return vk::False; } }; fn main() -> i32 { VULKAN_HPP_DEFAULT_DISPATCHER.init(); VulkanApp app; try { app.run(); } catch (const std::exception& e) { fmt::println("{}", e.what()); return EXIT_FAILURE; } return EXIT_SUCCESS; }