From e12505698e13c632271c209acd45ac9b6ba83eff Mon Sep 17 00:00:00 2001 From: Sascha Willems Date: Mon, 23 Mar 2026 21:49:26 +0100 Subject: [PATCH 1/3] Add standalone Vulkan application template --- .gitignore | 2 + attachments/template/CMakeLists.txt | 119 +++ attachments/template/main.cpp | 1235 +++++++++++++++++++++++++++ attachments/template/shader.slang | 35 + 4 files changed, 1391 insertions(+) create mode 100644 attachments/template/CMakeLists.txt create mode 100644 attachments/template/main.cpp create mode 100644 attachments/template/shader.slang diff --git a/.gitignore b/.gitignore index c6d15111..46f884b4 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ attachments/simple_engine/android/app/build/** attachments/simple_engine/android/gradle/wrapper/** attachments/simple_engine/android/gradlew attachments/simple_engine/android/gradlew.bat + +attachments/template/build/** \ No newline at end of file diff --git a/attachments/template/CMakeLists.txt b/attachments/template/CMakeLists.txt new file mode 100644 index 00000000..902ad6ae --- /dev/null +++ b/attachments/template/CMakeLists.txt @@ -0,0 +1,119 @@ +cmake_minimum_required (VERSION 3.29) + +set (TEMPLATE_NAME "VulkanTemplate") + +project (${TEMPLATE_NAME}) + +# Add option to enable/disable C++ 20 module +option(ENABLE_CPP20_MODULE "Enable C++ 20 module support for Vulkan" OFF) + +# Enable C++ module dependency scanning only if C++ 20 module is enabled +if(ENABLE_CPP20_MODULE) + set(CMAKE_CXX_SCAN_FOR_MODULES ON) +endif() + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake") + +find_package(glfw3 REQUIRED) +find_package(glm REQUIRED) +find_package(Vulkan 1.4.335 REQUIRED) +find_package(tinyobjloader REQUIRED) +find_package(tinygltf REQUIRED) +find_package(KTX REQUIRED) + +# Set up Vulkan C++ module only if enabled +if(ENABLE_CPP20_MODULE) + add_library(VulkanCppModule) + add_library(Vulkan::cppm ALIAS VulkanCppModule) + + target_compile_definitions(VulkanCppModule PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1) + target_include_directories(VulkanCppModule PUBLIC "${Vulkan_INCLUDE_DIR}") + target_link_libraries(VulkanCppModule PUBLIC Vulkan::Vulkan) + + set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20) + + # Add MSVC-specific compiler options for proper C++ module support + if(MSVC) + target_compile_options(VulkanCppModule PRIVATE + /std:c++latest # Use latest C++ standard for better module support + /permissive- # Standards conformance mode + /Zc:__cplusplus # Enable correct __cplusplus macro + /EHsc # Enable C++ exception handling + /Zc:preprocessor # Use conforming preprocessor + /translateInclude # Automatically translate #include to import for standard library + ) + endif() + + target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${Vulkan_INCLUDE_DIR}" + FILES + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" + ) + + + # Add the vulkan.cppm file directly as a source file + target_sources(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" + ) +else() + # Create a dummy interface library when C++ 20 module is disabled + add_library(VulkanCppModule INTERFACE) + add_library(Vulkan::cppm ALIAS VulkanCppModule) + target_link_libraries(VulkanCppModule INTERFACE Vulkan::Vulkan) + target_compile_definitions(VulkanCppModule + INTERFACE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1 + ) +endif() + +find_package(stb REQUIRED) +set(STB_INCLUDEDIR ${stb_INCLUDE_DIRS}) + +find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin REQUIRED) + +set(TEMPLATE_DIR ${CMAKE_BINARY_DIR}/${TEMPLATE_NAME}) + +add_executable (${TEMPLATE_NAME} main.cpp) +set_target_properties (${TEMPLATE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TEMPLATE_DIR}) +set_target_properties (${TEMPLATE_NAME} PROPERTIES CXX_STANDARD 20) +target_link_libraries (${TEMPLATE_NAME} Vulkan::cppm glfw) +target_include_directories (${TEMPLATE_NAME} PRIVATE ${STB_INCLUDEDIR}) + +# Add compile definition if C++ 20 module is enabled +if(ENABLE_CPP20_MODULE) + target_compile_definitions(${TEMPLATE_NAME} PRIVATE USE_CPP20_MODULES=1) +endif() + +# Define VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS to treat VK_ERROR_OUT_OF_DATE_KHR as a success code +target_compile_definitions(${TEMPLATE_NAME} PRIVATE "VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS" ) + +if(WIN32) + if(${CMAKE_GENERATOR} MATCHES "Visual Studio.*") + set_target_properties(${TEMPLATE_NAME} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${TEMPLATE_DIR}) + endif() +endif() + +# Slang shader +set (SHADER_TARGET ${TEMPLATE_NAME}Shader) +set (SHADERS_DIR ${TEMPLATE_DIR}/shaders) +set (SHADER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/shader.slang") +set (ENTRY_POINTS -entry vertMain -entry fragMain) +add_custom_command(OUTPUT ${SHADERS_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR}) +add_custom_command( + OUTPUT ${SHADERS_DIR}/slang.spv + COMMAND ${SLANGC_EXECUTABLE} ${SHADER_SOURCES} -target spirv -profile spirv_1_4 -emit-spirv-directly -fvk-use-entrypoint-name ${ENTRY_POINTS} -o ${TEMPLATE_DIR}/shaders/slang.spv + WORKING_DIRECTORY ${SHADERS_DIR} + DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} + COMMENT "Compiling Slang Shaders" + VERBATIM +) +add_custom_target(${SHADER_TARGET} DEPENDS ${SHADERS_DIR}/slang.spv) +add_dependencies(${TEMPLATE_NAME} ${SHADER_TARGET}) + +# Assets +set (ASSET_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../assets/") +file (COPY ${ASSET_DIR}/viking_room.obj DESTINATION ${TEMPLATE_DIR}/models) +file (COPY ${ASSET_DIR}/viking_room.png DESTINATION ${TEMPLATE_DIR}/textures) diff --git a/attachments/template/main.cpp b/attachments/template/main.cpp new file mode 100644 index 00000000..0a02d7b1 --- /dev/null +++ b/attachments/template/main.cpp @@ -0,0 +1,1235 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES) +# include +#else +import vulkan_hpp; +#endif + +#define GLFW_INCLUDE_VULKAN // REQUIRED only for GLFW CreateWindowSurface. +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define TINYOBJLOADER_IMPLEMENTATION +#include + +constexpr uint32_t WIDTH = 800; +constexpr uint32_t HEIGHT = 600; +const std::string MODEL_PATH = "models/viking_room.obj"; +const std::string TEXTURE_PATH = "textures/viking_room.png"; +constexpr int MAX_FRAMES_IN_FLIGHT = 2; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation"}; + +#ifdef NDEBUG +constexpr bool enableValidationLayers = false; +#else +constexpr bool enableValidationLayers = true; +#endif + +struct Vertex +{ + glm::vec3 pos; + glm::vec3 color; + glm::vec2 texCoord; + + static vk::VertexInputBindingDescription getBindingDescription() + { + return {0, sizeof(Vertex), vk::VertexInputRate::eVertex}; + } + + static std::array getAttributeDescriptions() + { + return { + vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos)), + vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)), + vk::VertexInputAttributeDescription(2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord))}; + } + + bool operator==(const Vertex &other) const + { + return pos == other.pos && color == other.color && texCoord == other.texCoord; + } +}; + +template <> +struct std::hash +{ + size_t operator()(Vertex const &vertex) const noexcept + { + return ((hash()(vertex.pos) ^ (hash()(vertex.color) << 1)) >> 1) ^ (hash()(vertex.texCoord) << 1); + } +}; + +struct UniformBufferObject +{ + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; + +class VulkanApplication +{ + public: + void run() + { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); + } + + private: + GLFWwindow *window = nullptr; + vk::raii::Context context; + vk::raii::Instance instance = nullptr; + vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; + vk::raii::SurfaceKHR surface = nullptr; + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + uint32_t queueIndex = ~0; + vk::raii::Queue queue = nullptr; + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::SurfaceFormatKHR swapChainSurfaceFormat; + vk::Extent2D swapChainExtent; + std::vector swapChainImageViews; + + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthImageMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + + vk::raii::Image textureImage = nullptr; + vk::raii::DeviceMemory textureImageMemory = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + + std::vector vertices; + std::vector indices; + vk::raii::Buffer vertexBuffer = nullptr; + vk::raii::DeviceMemory vertexBufferMemory = nullptr; + vk::raii::Buffer indexBuffer = nullptr; + vk::raii::DeviceMemory indexBufferMemory = nullptr; + + std::vector uniformBuffers; + std::vector uniformBuffersMemory; + std::vector uniformBuffersMapped; + + vk::raii::DescriptorPool descriptorPool = nullptr; + std::vector descriptorSets; + + vk::raii::CommandPool commandPool = nullptr; + std::vector commandBuffers; + + std::vector presentCompleteSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + uint32_t frameIndex = 0; + + bool framebufferResized = false; + + std::vector requiredDeviceExtension = { + vk::KHRSwapchainExtensionName}; + + void initWindow() + { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); + glfwSetWindowUserPointer(window, this); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); + } + + static void framebufferResizeCallback(GLFWwindow *window, int width, int height) + { + auto app = static_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; + } + + void initVulkan() + { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createDescriptorSetLayout(); + createGraphicsPipeline(); + createCommandPool(); + createDepthResources(); + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + loadModel(); + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + createDescriptorPool(); + createDescriptorSets(); + createCommandBuffers(); + createSyncObjects(); + } + + void mainLoop() + { + while (!glfwWindowShouldClose(window)) + { + glfwPollEvents(); + drawFrame(); + } + + device.waitIdle(); + } + + void cleanupSwapChain() + { + swapChainImageViews.clear(); + swapChain = nullptr; + } + + void cleanup() const + { + glfwDestroyWindow(window); + + glfwTerminate(); + } + + void recreateSwapChain() + { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) + { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + device.waitIdle(); + + cleanupSwapChain(); + createSwapChain(); + createImageViews(); + createDepthResources(); + } + + void createInstance() + { + constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle", + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = vk::ApiVersion14}; + + // Get the required layers + std::vector requiredLayers; + if (enableValidationLayers) + { + requiredLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Check if the required layers are supported by the Vulkan implementation. + auto layerProperties = context.enumerateInstanceLayerProperties(); + auto unsupportedLayerIt = std::ranges::find_if(requiredLayers, + [&layerProperties](auto const &requiredLayer) { + return std::ranges::none_of(layerProperties, + [requiredLayer](auto const &layerProperty) { return strcmp(layerProperty.layerName, requiredLayer) == 0; }); + }); + if (unsupportedLayerIt != requiredLayers.end()) + { + throw std::runtime_error("Required layer not supported: " + std::string(*unsupportedLayerIt)); + } + + // Get the required extensions. + auto requiredExtensions = getRequiredInstanceExtensions(); + + // Check if the required extensions are supported by the Vulkan implementation. + auto extensionProperties = context.enumerateInstanceExtensionProperties(); + auto unsupportedPropertyIt = + std::ranges::find_if(requiredExtensions, + [&extensionProperties](auto const &requiredExtension) { + return std::ranges::none_of(extensionProperties, + [requiredExtension](auto const &extensionProperty) { return strcmp(extensionProperty.extensionName, requiredExtension) == 0; }); + }); + if (unsupportedPropertyIt != requiredExtensions.end()) + { + throw std::runtime_error("Required extension not supported: " + std::string(*unsupportedPropertyIt)); + } + + vk::InstanceCreateInfo createInfo{.pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(requiredLayers.size()), + .ppEnabledLayerNames = requiredLayers.data(), + .enabledExtensionCount = static_cast(requiredExtensions.size()), + .ppEnabledExtensionNames = requiredExtensions.data()}; + instance = vk::raii::Instance(context, createInfo); + } + + void setupDebugMessenger() + { + if (!enableValidationLayers) + return; + + vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError); + vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation); + vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{.messageSeverity = severityFlags, + .messageType = messageTypeFlags, + .pfnUserCallback = &debugCallback}; + debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + } + + void createSurface() + { + VkSurfaceKHR _surface; + if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) + { + throw std::runtime_error("failed to create window surface!"); + } + surface = vk::raii::SurfaceKHR(instance, _surface); + } + + bool isDeviceSuitable(vk::raii::PhysicalDevice const &physicalDevice) + { + // Check if the physicalDevice supports the Vulkan 1.3 API version + bool supportsVulkan1_3 = physicalDevice.getProperties().apiVersion >= VK_API_VERSION_1_3; + + // Check if any of the queue families support graphics operations + auto queueFamilies = physicalDevice.getQueueFamilyProperties(); + bool supportsGraphics = std::ranges::any_of(queueFamilies, [](auto const &qfp) { return !!(qfp.queueFlags & vk::QueueFlagBits::eGraphics); }); + + // Check if all required physicalDevice extensions are available + auto availableDeviceExtensions = physicalDevice.enumerateDeviceExtensionProperties(); + bool supportsAllRequiredExtensions = + std::ranges::all_of(requiredDeviceExtension, + [&availableDeviceExtensions](auto const &requiredDeviceExtension) { + return std::ranges::any_of(availableDeviceExtensions, + [requiredDeviceExtension](auto const &availableDeviceExtension) { return strcmp(availableDeviceExtension.extensionName, requiredDeviceExtension) == 0; }); + }); + + // Check if the physicalDevice supports the required features + auto features = physicalDevice.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().features.samplerAnisotropy && + features.template get().dynamicRendering && + features.template get().extendedDynamicState; + + // Return true if the physicalDevice meets all the criteria + return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures; + } + + void pickPhysicalDevice() + { + std::vector physicalDevices = instance.enumeratePhysicalDevices(); + auto const devIter = std::ranges::find_if(physicalDevices, [&](auto const &physicalDevice) { return isDeviceSuitable(physicalDevice); }); + if (devIter == physicalDevices.end()) + { + throw std::runtime_error("failed to find a suitable GPU!"); + } + physicalDevice = *devIter; + } + + void createLogicalDevice() + { + std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + + // get the first index into queueFamilyProperties which supports both graphics and present + for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) + { + if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && + physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface)) + { + // found a queue family that supports both graphics and present + queueIndex = qfpIndex; + break; + } + } + if (queueIndex == ~0) + { + throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); + } + + // query for Vulkan 1.3 features + vk::StructureChain featureChain = { + {.features = {.samplerAnisotropy = true}}, // vk::PhysicalDeviceFeatures2 + {.synchronization2 = true, .dynamicRendering = true}, // vk::PhysicalDeviceVulkan13Features + {.extendedDynamicState = true} // vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT + }; + + // create a Device + float queuePriority = 0.5f; + vk::DeviceQueueCreateInfo deviceQueueCreateInfo{.queueFamilyIndex = queueIndex, .queueCount = 1, .pQueuePriorities = &queuePriority}; + vk::DeviceCreateInfo deviceCreateInfo{.pNext = &featureChain.get(), + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &deviceQueueCreateInfo, + .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), + .ppEnabledExtensionNames = requiredDeviceExtension.data()}; + + device = vk::raii::Device(physicalDevice, deviceCreateInfo); + queue = vk::raii::Queue(device, queueIndex, 0); + } + + void createSwapChain() + { + vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface); + swapChainExtent = chooseSwapExtent(surfaceCapabilities); + uint32_t minImageCount = chooseSwapMinImageCount(surfaceCapabilities); + + std::vector availableFormats = physicalDevice.getSurfaceFormatsKHR(*surface); + swapChainSurfaceFormat = chooseSwapSurfaceFormat(availableFormats); + + std::vector availablePresentModes = physicalDevice.getSurfacePresentModesKHR(*surface); + vk::PresentModeKHR presentMode = chooseSwapPresentMode(availablePresentModes); + + vk::SwapchainCreateInfoKHR swapChainCreateInfo{.surface = *surface, + .minImageCount = minImageCount, + .imageFormat = swapChainSurfaceFormat.format, + .imageColorSpace = swapChainSurfaceFormat.colorSpace, + .imageExtent = swapChainExtent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, + .imageSharingMode = vk::SharingMode::eExclusive, + .preTransform = surfaceCapabilities.currentTransform, + .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, + .presentMode = presentMode, + .clipped = true}; + + swapChain = vk::raii::SwapchainKHR(device, swapChainCreateInfo); + swapChainImages = swapChain.getImages(); + } + + void createImageViews() + { + assert(swapChainImageViews.empty()); + + vk::ImageViewCreateInfo imageViewCreateInfo{.viewType = vk::ImageViewType::e2D, + .format = swapChainSurfaceFormat.format, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; + for (auto &image : swapChainImages) + { + imageViewCreateInfo.image = image; + swapChainImageViews.emplace_back(device, imageViewCreateInfo); + } + } + + void createDescriptorSetLayout() + { + std::array bindings = { + vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex, nullptr), + vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, nullptr)}; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{.bindingCount = static_cast(bindings.size()), .pBindings = bindings.data()}; + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + } + + void createGraphicsPipeline() + { + vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv")); + + vk::PipelineShaderStageCreateInfo vertShaderStageInfo{.stage = vk::ShaderStageFlagBits::eVertex, .module = shaderModule, .pName = "vertMain"}; + vk::PipelineShaderStageCreateInfo fragShaderStageInfo{.stage = vk::ShaderStageFlagBits::eFragment, .module = shaderModule, .pName = "fragMain"}; + vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; + + auto bindingDescription = Vertex::getBindingDescription(); + auto 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 = vk::SampleCountFlagBits::e1, + .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}; + 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 = &*descriptorSetLayout, .pushConstantRangeCount = 0}; + + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + vk::Format depthFormat = findDepthFormat(); + + vk::StructureChain pipelineCreateInfoChain = { + {.stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = nullptr}, + {.colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainSurfaceFormat.format, .depthAttachmentFormat = depthFormat}}; + + graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineCreateInfoChain.get()); + } + + void createCommandPool() + { + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueIndex}; + commandPool = vk::raii::CommandPool(device, poolInfo); + } + + void createDepthResources() + { + vk::Format depthFormat = findDepthFormat(); + + createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, depthImage, depthImageMemory); + depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); + } + + vk::Format findSupportedFormat(const std::vector &candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) const + { + for (const auto format : candidates) + { + vk::FormatProperties props = physicalDevice.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!"); + } + + [[nodiscard]] vk::Format findDepthFormat() const + { + return findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment); + } + + static bool hasStencilComponent(vk::Format format) + { + return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; + } + + void createTextureImage() + { + int texWidth, texHeight, texChannels; + stbi_uc *pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + vk::DeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) + { + throw std::runtime_error("failed to load texture image!"); + } + + vk::raii::Buffer stagingBuffer({}); + vk::raii::DeviceMemory stagingBufferMemory({}); + createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void *data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels, imageSize); + stagingBufferMemory.unmapMemory(); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, textureImage, textureImageMemory); + + transitionImageLayout(textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); + transitionImageLayout(textureImage, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + } + + void createTextureImageView() + { + textureImageView = createImageView(textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor); + } + + void createTextureSampler() + { + vk::PhysicalDeviceProperties properties = physicalDevice.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::True, + .maxAnisotropy = properties.limits.maxSamplerAnisotropy, + .compareEnable = vk::False, + .compareOp = vk::CompareOp::eAlways}; + textureSampler = vk::raii::Sampler(device, samplerInfo); + } + + vk::raii::ImageView createImageView(vk::raii::Image &image, vk::Format format, vk::ImageAspectFlags aspectFlags) + { + vk::ImageViewCreateInfo viewInfo{ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = {aspectFlags, 0, 1, 0, 1}}; + return vk::raii::ImageView(device, viewInfo); + } + + void createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, vk::raii::Image &image, vk::raii::DeviceMemory &imageMemory) + { + vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined}; + image = vk::raii::Image(device, imageInfo); + + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties)}; + imageMemory = vk::raii::DeviceMemory(device, allocInfo); + image.bindMemory(imageMemory, 0); + } + + void transitionImageLayout(const vk::raii::Image &image, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) + { + auto commandBuffer = beginSingleTimeCommands(); + + vk::ImageMemoryBarrier barrier{ + .oldLayout = oldLayout, + .newLayout = newLayout, + .image = image, + .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; + + 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, {}, {}, nullptr, barrier); + endSingleTimeCommands(*commandBuffer); + } + + void copyBufferToImage(const vk::raii::Buffer &buffer, vk::raii::Image &image, uint32_t width, uint32_t height) + { + std::unique_ptr commandBuffer = beginSingleTimeCommands(); + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1}}; + commandBuffer->copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, {region}); + endSingleTimeCommands(*commandBuffer); + } + + void loadModel() + { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + + if (!LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) + { + throw std::runtime_error(warn + err); + } + + std::unordered_map uniqueVertices{}; + + for (const auto &shape : shapes) + { + for (const auto &index : shape.mesh.indices) + { + Vertex vertex{}; + + vertex.pos = { + attrib.vertices[3 * index.vertex_index + 0], + attrib.vertices[3 * index.vertex_index + 1], + attrib.vertices[3 * index.vertex_index + 2]}; + + vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + 1.0f - attrib.texcoords[2 * index.texcoord_index + 1]}; + + vertex.color = {1.0f, 1.0f, 1.0f}; + + if (!uniqueVertices.contains(vertex)) + { + uniqueVertices[vertex] = static_cast(vertices.size()); + vertices.push_back(vertex); + } + + indices.push_back(uniqueVertices[vertex]); + } + } + } + + void createVertexBuffer() + { + vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + vk::raii::Buffer stagingBuffer({}); + vk::raii::DeviceMemory stagingBufferMemory({}); + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void *dataStaging = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(dataStaging, vertices.data(), bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, vertexBuffer, vertexBufferMemory); + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + } + + void createIndexBuffer() + { + vk::DeviceSize bufferSize = sizeof(indices[0]) * indices.size(); + + vk::raii::Buffer stagingBuffer({}); + vk::raii::DeviceMemory stagingBufferMemory({}); + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); + + void *data = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(data, indices.data(), bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, indexBuffer, indexBufferMemory); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + } + + void createUniformBuffers() + { + uniformBuffers.clear(); + uniformBuffersMemory.clear(); + uniformBuffersMapped.clear(); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + vk::raii::Buffer buffer({}); + vk::raii::DeviceMemory bufferMem({}); + createBuffer(bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, buffer, bufferMem); + uniformBuffers.emplace_back(std::move(buffer)); + uniformBuffersMemory.emplace_back(std::move(bufferMem)); + uniformBuffersMapped.emplace_back(uniformBuffersMemory[i].mapMemory(0, bufferSize)); + } + } + + void createDescriptorPool() + { + std::array poolSize{ + vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, MAX_FRAMES_IN_FLIGHT), + vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, MAX_FRAMES_IN_FLIGHT)}; + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = MAX_FRAMES_IN_FLIGHT, + .poolSizeCount = static_cast(poolSize.size()), + .pPoolSizes = poolSize.data()}; + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + } + + void createDescriptorSets() + { + std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = descriptorPool, + .descriptorSetCount = static_cast(layouts.size()), + .pSetLayouts = layouts.data()}; + + descriptorSets.clear(); + descriptorSets = device.allocateDescriptorSets(allocInfo); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + vk::DescriptorBufferInfo bufferInfo{ + .buffer = uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject)}; + vk::DescriptorImageInfo imageInfo{ + .sampler = textureSampler, + .imageView = textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; + std::array descriptorWrites{ + vk::WriteDescriptorSet{ + .dstSet = descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo}, + vk::WriteDescriptorSet{ + .dstSet = descriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo}}; + device.updateDescriptorSets(descriptorWrites, {}); + } + } + + void createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties, vk::raii::Buffer &buffer, vk::raii::DeviceMemory &bufferMemory) + { + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive}; + buffer = vk::raii::Buffer(device, bufferInfo); + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties)}; + bufferMemory = vk::raii::DeviceMemory(device, allocInfo); + buffer.bindMemory(bufferMemory, 0); + } + + std::unique_ptr beginSingleTimeCommands() + { + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1}; + std::unique_ptr commandBuffer = std::make_unique(std::move(vk::raii::CommandBuffers(device, allocInfo).front())); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}; + commandBuffer->begin(beginInfo); + + return commandBuffer; + } + + void endSingleTimeCommands(const vk::raii::CommandBuffer &commandBuffer) const + { + commandBuffer.end(); + + vk::SubmitInfo submitInfo{.commandBufferCount = 1, .pCommandBuffers = &*commandBuffer}; + queue.submit(submitInfo, nullptr); + queue.waitIdle(); + } + + void copyBuffer(vk::raii::Buffer &srcBuffer, vk::raii::Buffer &dstBuffer, vk::DeviceSize size) + { + vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; + vk::raii::CommandBuffer commandCopyBuffer = std::move(device.allocateCommandBuffers(allocInfo).front()); + commandCopyBuffer.begin(vk::CommandBufferBeginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + commandCopyBuffer.copyBuffer(*srcBuffer, *dstBuffer, vk::BufferCopy{.size = size}); + commandCopyBuffer.end(); + queue.submit(vk::SubmitInfo{.commandBufferCount = 1, .pCommandBuffers = &*commandCopyBuffer}, nullptr); + queue.waitIdle(); + } + + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) + { + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) + { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) + { + return i; + } + } + + throw std::runtime_error("failed to find suitable memory type!"); + } + + void createCommandBuffers() + { + commandBuffers.clear(); + vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = MAX_FRAMES_IN_FLIGHT}; + commandBuffers = vk::raii::CommandBuffers(device, allocInfo); + } + + void recordCommandBuffer(uint32_t imageIndex) + { + auto &commandBuffer = commandBuffers[frameIndex]; + commandBuffer.begin({}); + // Before starting rendering, transition the swapchain image to COLOR_ATTACHMENT_OPTIMAL + transition_image_layout( + swapChainImages[imageIndex], + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, // srcAccessMask (no need to wait for previous operations) + vk::AccessFlagBits2::eColorAttachmentWrite, // dstAccessMask + vk::PipelineStageFlagBits2::eColorAttachmentOutput, // srcStage + vk::PipelineStageFlagBits2::eColorAttachmentOutput, // dstStage + vk::ImageAspectFlagBits::eColor); + // Transition depth image to depth attachment optimal layout + transition_image_layout( + *depthImage, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eDepthAttachmentOptimal, + vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, + vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, + vk::ImageAspectFlagBits::eDepth); + + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor}; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = clearDepth}; + + vk::RenderingInfo renderingInfo = { + .renderArea = {.offset = {0, 0}, .extent = swapChainExtent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo}; + commandBuffer.beginRendering(renderingInfo); + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + commandBuffer.bindVertexBuffers(0, *vertexBuffer, {0}); + commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint32); + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, *descriptorSets[frameIndex], nullptr); + commandBuffer.drawIndexed(indices.size(), 1, 0, 0, 0); + commandBuffer.endRendering(); + // After rendering, transition the swapchain image to PRESENT_SRC + transition_image_layout( + swapChainImages[imageIndex], + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, // srcAccessMask + {}, // dstAccessMask + vk::PipelineStageFlagBits2::eColorAttachmentOutput, // srcStage + vk::PipelineStageFlagBits2::eBottomOfPipe, // dstStage + vk::ImageAspectFlagBits::eColor); + commandBuffer.end(); + } + + void transition_image_layout( + vk::Image image, + vk::ImageLayout old_layout, + vk::ImageLayout new_layout, + vk::AccessFlags2 src_access_mask, + vk::AccessFlags2 dst_access_mask, + vk::PipelineStageFlags2 src_stage_mask, + vk::PipelineStageFlags2 dst_stage_mask, + vk::ImageAspectFlags image_aspect_flags) + { + vk::ImageMemoryBarrier2 barrier = { + .srcStageMask = src_stage_mask, + .srcAccessMask = src_access_mask, + .dstStageMask = dst_stage_mask, + .dstAccessMask = dst_access_mask, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = image_aspect_flags, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1}}; + vk::DependencyInfo dependency_info = { + .dependencyFlags = {}, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier}; + commandBuffers[frameIndex].pipelineBarrier2(dependency_info); + } + + void createSyncObjects() + { + assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty()); + + for (size_t i = 0; i < swapChainImages.size(); i++) + { + renderFinishedSemaphores.emplace_back(device, vk::SemaphoreCreateInfo()); + } + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + presentCompleteSemaphores.emplace_back(device, vk::SemaphoreCreateInfo()); + inFlightFences.emplace_back(device, vk::FenceCreateInfo{.flags = vk::FenceCreateFlagBits::eSignaled}); + } + } + + void updateUniformBuffer(uint32_t currentImage) const + { + static auto startTime = std::chrono::high_resolution_clock::now(); + + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); + + UniformBufferObject ubo{}; + ubo.model = rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.view = 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)); + ubo.proj = glm::perspective(glm::radians(45.0f), static_cast(swapChainExtent.width) / static_cast(swapChainExtent.height), 0.1f, 10.0f); + ubo.proj[1][1] *= -1; + + memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + } + + void drawFrame() + { + // Note: inFlightFences, presentCompleteSemaphores, and commandBuffers are indexed by frameIndex, + // while renderFinishedSemaphores is indexed by imageIndex + auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX); + if (fenceResult != vk::Result::eSuccess) + { + throw std::runtime_error("failed to wait for fence!"); + } + + auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr); + + // Due to VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS being defined, eErrorOutOfDateKHR can be checked as a result + // here and does not need to be caught by an exception. + if (result == vk::Result::eErrorOutOfDateKHR) + { + recreateSwapChain(); + return; + } + // On other success codes than eSuccess and eSuboptimalKHR we just throw an exception. + // On any error code, aquireNextImage already threw an exception. + if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) + { + assert(result == vk::Result::eTimeout || result == vk::Result::eNotReady); + throw std::runtime_error("failed to acquire swap chain image!"); + } + updateUniformBuffer(frameIndex); + + // Only reset the fence if we are submitting work + device.resetFences(*inFlightFences[frameIndex]); + + commandBuffers[frameIndex].reset(); + recordCommandBuffer(imageIndex); + + vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); + const vk::SubmitInfo submitInfo{.waitSemaphoreCount = 1, + .pWaitSemaphores = &*presentCompleteSemaphores[frameIndex], + .pWaitDstStageMask = &waitDestinationStageMask, + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[frameIndex], + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex]}; + queue.submit(submitInfo, *inFlightFences[frameIndex]); + + const vk::PresentInfoKHR presentInfoKHR{.waitSemaphoreCount = 1, + .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex}; + result = queue.presentKHR(presentInfoKHR); + // Due to VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS being defined, eErrorOutOfDateKHR can be checked as a result + // here and does not need to be caught by an exception. + if ((result == vk::Result::eSuboptimalKHR) || (result == vk::Result::eErrorOutOfDateKHR) || framebufferResized) + { + framebufferResized = false; + recreateSwapChain(); + } + else + { + // There are no other success codes than eSuccess; on any error code, presentKHR already threw an exception. + assert(result == vk::Result::eSuccess); + } + frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT; + } + + [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector &code) const + { + vk::ShaderModuleCreateInfo createInfo{.codeSize = code.size(), .pCode = reinterpret_cast(code.data())}; + vk::raii::ShaderModule shaderModule{device, createInfo}; + + return shaderModule; + } + + static uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) + { + auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount); + if ((0 < surfaceCapabilities.maxImageCount) && (surfaceCapabilities.maxImageCount < minImageCount)) + { + minImageCount = surfaceCapabilities.maxImageCount; + } + return minImageCount; + } + + static vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector &availableFormats) + { + assert(!availableFormats.empty()); + const auto formatIt = std::ranges::find_if( + availableFormats, + [](const auto &format) { return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; }); + return formatIt != availableFormats.end() ? *formatIt : availableFormats[0]; + } + + static vk::PresentModeKHR chooseSwapPresentMode(std::vector const &availablePresentModes) + { + assert(std::ranges::any_of(availablePresentModes, [](auto presentMode) { return presentMode == vk::PresentModeKHR::eFifo; })); + return std::ranges::any_of(availablePresentModes, + [](const vk::PresentModeKHR value) { return vk::PresentModeKHR::eMailbox == value; }) ? + vk::PresentModeKHR::eMailbox : + vk::PresentModeKHR::eFifo; + } + + vk::Extent2D chooseSwapExtent(vk::SurfaceCapabilitiesKHR const &capabilities) + { + if (capabilities.currentExtent.width != std::numeric_limits::max()) + { + return capabilities.currentExtent; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + return { + std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)}; + } + + [[nodiscard]] std::vector getRequiredInstanceExtensions() + { + uint32_t glfwExtensionCount = 0; + auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + if (enableValidationLayers) + { + extensions.push_back(vk::EXTDebugUtilsExtensionName); + } + + return extensions; + } + + static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity, vk::DebugUtilsMessageTypeFlagsEXT type, const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData, void *) + { + if (severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError || severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) + { + std::cerr << "validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl; + } + + return vk::False; + } + + static std::vector readFile(const std::string &filename) + { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + if (!file.is_open()) + { + throw std::runtime_error("failed to open file!"); + } + std::vector buffer(file.tellg()); + file.seekg(0, std::ios::beg); + file.read(buffer.data(), static_cast(buffer.size())); + file.close(); + return buffer; + } +}; + +int main() +{ + try + { + VulkanApplication app; + app.run(); + } + catch (const std::exception &e) + { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/attachments/template/shader.slang b/attachments/template/shader.slang new file mode 100644 index 00000000..a4fe6442 --- /dev/null +++ b/attachments/template/shader.slang @@ -0,0 +1,35 @@ +struct VSInput { + float3 inPosition; + float3 inColor; + float2 inTexCoord; +}; + +struct UniformBuffer { + float4x4 model; + float4x4 view; + float4x4 proj; +}; +ConstantBuffer ubo; + +struct VSOutput +{ + float4 pos : SV_Position; + float3 fragColor; + float2 fragTexCoord; +}; + +[shader("vertex")] +VSOutput vertMain(VSInput input) { + VSOutput output; + output.pos = mul(ubo.proj, mul(ubo.view, mul(ubo.model, float4(input.inPosition, 1.0)))); + output.fragColor = input.inColor; + output.fragTexCoord = input.inTexCoord; + return output; +} + +Sampler2D texture; + +[shader("fragment")] +float4 fragMain(VSOutput vertIn) : SV_TARGET { + return texture.Sample(vertIn.fragTexCoord); +} \ No newline at end of file From ffb4a4916986ebed8c3eb15b7dc7e5f13dba220c Mon Sep 17 00:00:00 2001 From: Sascha Willems Date: Tue, 24 Mar 2026 07:05:08 +0100 Subject: [PATCH 2/3] Use first chapter as project template --- attachments/template/CMakeLists.txt | 2 +- attachments/template/main.cpp | 1181 +-------------------------- 2 files changed, 11 insertions(+), 1172 deletions(-) diff --git a/attachments/template/CMakeLists.txt b/attachments/template/CMakeLists.txt index 902ad6ae..6edabec0 100644 --- a/attachments/template/CMakeLists.txt +++ b/attachments/template/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required (VERSION 3.29) -set (TEMPLATE_NAME "VulkanTemplate") +set (TEMPLATE_NAME "VulkanTutorial") project (${TEMPLATE_NAME}) diff --git a/attachments/template/main.cpp b/attachments/template/main.cpp index 0a02d7b1..3b685670 100644 --- a/attachments/template/main.cpp +++ b/attachments/template/main.cpp @@ -1,95 +1,19 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include - #if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES) # include #else import vulkan_hpp; #endif - -#define GLFW_INCLUDE_VULKAN // REQUIRED only for GLFW CreateWindowSurface. #include -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#define GLM_ENABLE_EXPERIMENTAL -#include -#include -#include - -#define STB_IMAGE_IMPLEMENTATION -#include - -#define TINYOBJLOADER_IMPLEMENTATION -#include - -constexpr uint32_t WIDTH = 800; -constexpr uint32_t HEIGHT = 600; -const std::string MODEL_PATH = "models/viking_room.obj"; -const std::string TEXTURE_PATH = "textures/viking_room.png"; -constexpr int MAX_FRAMES_IN_FLIGHT = 2; - -const std::vector validationLayers = { - "VK_LAYER_KHRONOS_validation"}; - -#ifdef NDEBUG -constexpr bool enableValidationLayers = false; -#else -constexpr bool enableValidationLayers = true; -#endif - -struct Vertex -{ - glm::vec3 pos; - glm::vec3 color; - glm::vec2 texCoord; - - static vk::VertexInputBindingDescription getBindingDescription() - { - return {0, sizeof(Vertex), vk::VertexInputRate::eVertex}; - } - - static std::array getAttributeDescriptions() - { - return { - vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos)), - vk::VertexInputAttributeDescription(1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)), - vk::VertexInputAttributeDescription(2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord))}; - } - - bool operator==(const Vertex &other) const - { - return pos == other.pos && color == other.color && texCoord == other.texCoord; - } -}; +#include +#include +#include -template <> -struct std::hash -{ - size_t operator()(Vertex const &vertex) const noexcept - { - return ((hash()(vertex.pos) ^ (hash()(vertex.color) << 1)) >> 1) ^ (hash()(vertex.texCoord) << 1); - } -}; +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; -struct UniformBufferObject -{ - alignas(16) glm::mat4 model; - alignas(16) glm::mat4 view; - alignas(16) glm::mat4 proj; -}; - -class VulkanApplication +class HelloTriangleApplication { public: void run() @@ -101,103 +25,20 @@ class VulkanApplication } private: - GLFWwindow *window = nullptr; - vk::raii::Context context; - vk::raii::Instance instance = nullptr; - vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; - vk::raii::SurfaceKHR surface = nullptr; - vk::raii::PhysicalDevice physicalDevice = nullptr; - vk::raii::Device device = nullptr; - uint32_t queueIndex = ~0; - vk::raii::Queue queue = nullptr; - vk::raii::SwapchainKHR swapChain = nullptr; - std::vector swapChainImages; - vk::SurfaceFormatKHR swapChainSurfaceFormat; - vk::Extent2D swapChainExtent; - std::vector swapChainImageViews; - - vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; - vk::raii::PipelineLayout pipelineLayout = nullptr; - vk::raii::Pipeline graphicsPipeline = nullptr; - - vk::raii::Image depthImage = nullptr; - vk::raii::DeviceMemory depthImageMemory = nullptr; - vk::raii::ImageView depthImageView = nullptr; - - vk::raii::Image textureImage = nullptr; - vk::raii::DeviceMemory textureImageMemory = nullptr; - vk::raii::ImageView textureImageView = nullptr; - vk::raii::Sampler textureSampler = nullptr; - - std::vector vertices; - std::vector indices; - vk::raii::Buffer vertexBuffer = nullptr; - vk::raii::DeviceMemory vertexBufferMemory = nullptr; - vk::raii::Buffer indexBuffer = nullptr; - vk::raii::DeviceMemory indexBufferMemory = nullptr; - - std::vector uniformBuffers; - std::vector uniformBuffersMemory; - std::vector uniformBuffersMapped; - - vk::raii::DescriptorPool descriptorPool = nullptr; - std::vector descriptorSets; - - vk::raii::CommandPool commandPool = nullptr; - std::vector commandBuffers; - - std::vector presentCompleteSemaphores; - std::vector renderFinishedSemaphores; - std::vector inFlightFences; - uint32_t frameIndex = 0; - - bool framebufferResized = false; - - std::vector requiredDeviceExtension = { - vk::KHRSwapchainExtensionName}; + GLFWwindow *window = nullptr; void initWindow() { glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); - glfwSetWindowUserPointer(window, this); - glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); - } - - static void framebufferResizeCallback(GLFWwindow *window, int width, int height) - { - auto app = static_cast(glfwGetWindowUserPointer(window)); - app->framebufferResized = true; } void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createDescriptorSetLayout(); - createGraphicsPipeline(); - createCommandPool(); - createDepthResources(); - createTextureImage(); - createTextureImageView(); - createTextureSampler(); - loadModel(); - createVertexBuffer(); - createIndexBuffer(); - createUniformBuffers(); - createDescriptorPool(); - createDescriptorSets(); - createCommandBuffers(); - createSyncObjects(); } void mainLoop() @@ -205,1024 +46,22 @@ class VulkanApplication while (!glfwWindowShouldClose(window)) { glfwPollEvents(); - drawFrame(); } - - device.waitIdle(); } - void cleanupSwapChain() - { - swapChainImageViews.clear(); - swapChain = nullptr; - } - - void cleanup() const + void cleanup() { glfwDestroyWindow(window); glfwTerminate(); } - - void recreateSwapChain() - { - int width = 0, height = 0; - glfwGetFramebufferSize(window, &width, &height); - while (width == 0 || height == 0) - { - glfwGetFramebufferSize(window, &width, &height); - glfwWaitEvents(); - } - - device.waitIdle(); - - cleanupSwapChain(); - createSwapChain(); - createImageViews(); - createDepthResources(); - } - - void createInstance() - { - constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Hello Triangle", - .applicationVersion = VK_MAKE_VERSION(1, 0, 0), - .pEngineName = "No Engine", - .engineVersion = VK_MAKE_VERSION(1, 0, 0), - .apiVersion = vk::ApiVersion14}; - - // Get the required layers - std::vector requiredLayers; - if (enableValidationLayers) - { - requiredLayers.assign(validationLayers.begin(), validationLayers.end()); - } - - // Check if the required layers are supported by the Vulkan implementation. - auto layerProperties = context.enumerateInstanceLayerProperties(); - auto unsupportedLayerIt = std::ranges::find_if(requiredLayers, - [&layerProperties](auto const &requiredLayer) { - return std::ranges::none_of(layerProperties, - [requiredLayer](auto const &layerProperty) { return strcmp(layerProperty.layerName, requiredLayer) == 0; }); - }); - if (unsupportedLayerIt != requiredLayers.end()) - { - throw std::runtime_error("Required layer not supported: " + std::string(*unsupportedLayerIt)); - } - - // Get the required extensions. - auto requiredExtensions = getRequiredInstanceExtensions(); - - // Check if the required extensions are supported by the Vulkan implementation. - auto extensionProperties = context.enumerateInstanceExtensionProperties(); - auto unsupportedPropertyIt = - std::ranges::find_if(requiredExtensions, - [&extensionProperties](auto const &requiredExtension) { - return std::ranges::none_of(extensionProperties, - [requiredExtension](auto const &extensionProperty) { return strcmp(extensionProperty.extensionName, requiredExtension) == 0; }); - }); - if (unsupportedPropertyIt != requiredExtensions.end()) - { - throw std::runtime_error("Required extension not supported: " + std::string(*unsupportedPropertyIt)); - } - - vk::InstanceCreateInfo createInfo{.pApplicationInfo = &appInfo, - .enabledLayerCount = static_cast(requiredLayers.size()), - .ppEnabledLayerNames = requiredLayers.data(), - .enabledExtensionCount = static_cast(requiredExtensions.size()), - .ppEnabledExtensionNames = requiredExtensions.data()}; - instance = vk::raii::Instance(context, createInfo); - } - - void setupDebugMessenger() - { - if (!enableValidationLayers) - return; - - vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | - vk::DebugUtilsMessageSeverityFlagBitsEXT::eError); - vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( - vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation); - vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{.messageSeverity = severityFlags, - .messageType = messageTypeFlags, - .pfnUserCallback = &debugCallback}; - debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); - } - - void createSurface() - { - VkSurfaceKHR _surface; - if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) - { - throw std::runtime_error("failed to create window surface!"); - } - surface = vk::raii::SurfaceKHR(instance, _surface); - } - - bool isDeviceSuitable(vk::raii::PhysicalDevice const &physicalDevice) - { - // Check if the physicalDevice supports the Vulkan 1.3 API version - bool supportsVulkan1_3 = physicalDevice.getProperties().apiVersion >= VK_API_VERSION_1_3; - - // Check if any of the queue families support graphics operations - auto queueFamilies = physicalDevice.getQueueFamilyProperties(); - bool supportsGraphics = std::ranges::any_of(queueFamilies, [](auto const &qfp) { return !!(qfp.queueFlags & vk::QueueFlagBits::eGraphics); }); - - // Check if all required physicalDevice extensions are available - auto availableDeviceExtensions = physicalDevice.enumerateDeviceExtensionProperties(); - bool supportsAllRequiredExtensions = - std::ranges::all_of(requiredDeviceExtension, - [&availableDeviceExtensions](auto const &requiredDeviceExtension) { - return std::ranges::any_of(availableDeviceExtensions, - [requiredDeviceExtension](auto const &availableDeviceExtension) { return strcmp(availableDeviceExtension.extensionName, requiredDeviceExtension) == 0; }); - }); - - // Check if the physicalDevice supports the required features - auto features = physicalDevice.template getFeatures2(); - bool supportsRequiredFeatures = features.template get().features.samplerAnisotropy && - features.template get().dynamicRendering && - features.template get().extendedDynamicState; - - // Return true if the physicalDevice meets all the criteria - return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures; - } - - void pickPhysicalDevice() - { - std::vector physicalDevices = instance.enumeratePhysicalDevices(); - auto const devIter = std::ranges::find_if(physicalDevices, [&](auto const &physicalDevice) { return isDeviceSuitable(physicalDevice); }); - if (devIter == physicalDevices.end()) - { - throw std::runtime_error("failed to find a suitable GPU!"); - } - physicalDevice = *devIter; - } - - void createLogicalDevice() - { - std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); - - // get the first index into queueFamilyProperties which supports both graphics and present - for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) - { - if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && - physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface)) - { - // found a queue family that supports both graphics and present - queueIndex = qfpIndex; - break; - } - } - if (queueIndex == ~0) - { - throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); - } - - // query for Vulkan 1.3 features - vk::StructureChain featureChain = { - {.features = {.samplerAnisotropy = true}}, // vk::PhysicalDeviceFeatures2 - {.synchronization2 = true, .dynamicRendering = true}, // vk::PhysicalDeviceVulkan13Features - {.extendedDynamicState = true} // vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT - }; - - // create a Device - float queuePriority = 0.5f; - vk::DeviceQueueCreateInfo deviceQueueCreateInfo{.queueFamilyIndex = queueIndex, .queueCount = 1, .pQueuePriorities = &queuePriority}; - vk::DeviceCreateInfo deviceCreateInfo{.pNext = &featureChain.get(), - .queueCreateInfoCount = 1, - .pQueueCreateInfos = &deviceQueueCreateInfo, - .enabledExtensionCount = static_cast(requiredDeviceExtension.size()), - .ppEnabledExtensionNames = requiredDeviceExtension.data()}; - - device = vk::raii::Device(physicalDevice, deviceCreateInfo); - queue = vk::raii::Queue(device, queueIndex, 0); - } - - void createSwapChain() - { - vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface); - swapChainExtent = chooseSwapExtent(surfaceCapabilities); - uint32_t minImageCount = chooseSwapMinImageCount(surfaceCapabilities); - - std::vector availableFormats = physicalDevice.getSurfaceFormatsKHR(*surface); - swapChainSurfaceFormat = chooseSwapSurfaceFormat(availableFormats); - - std::vector availablePresentModes = physicalDevice.getSurfacePresentModesKHR(*surface); - vk::PresentModeKHR presentMode = chooseSwapPresentMode(availablePresentModes); - - vk::SwapchainCreateInfoKHR swapChainCreateInfo{.surface = *surface, - .minImageCount = minImageCount, - .imageFormat = swapChainSurfaceFormat.format, - .imageColorSpace = swapChainSurfaceFormat.colorSpace, - .imageExtent = swapChainExtent, - .imageArrayLayers = 1, - .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, - .imageSharingMode = vk::SharingMode::eExclusive, - .preTransform = surfaceCapabilities.currentTransform, - .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, - .presentMode = presentMode, - .clipped = true}; - - swapChain = vk::raii::SwapchainKHR(device, swapChainCreateInfo); - swapChainImages = swapChain.getImages(); - } - - void createImageViews() - { - assert(swapChainImageViews.empty()); - - vk::ImageViewCreateInfo imageViewCreateInfo{.viewType = vk::ImageViewType::e2D, - .format = swapChainSurfaceFormat.format, - .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; - for (auto &image : swapChainImages) - { - imageViewCreateInfo.image = image; - swapChainImageViews.emplace_back(device, imageViewCreateInfo); - } - } - - void createDescriptorSetLayout() - { - std::array bindings = { - vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex, nullptr), - vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, nullptr)}; - - vk::DescriptorSetLayoutCreateInfo layoutInfo{.bindingCount = static_cast(bindings.size()), .pBindings = bindings.data()}; - descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); - } - - void createGraphicsPipeline() - { - vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv")); - - vk::PipelineShaderStageCreateInfo vertShaderStageInfo{.stage = vk::ShaderStageFlagBits::eVertex, .module = shaderModule, .pName = "vertMain"}; - vk::PipelineShaderStageCreateInfo fragShaderStageInfo{.stage = vk::ShaderStageFlagBits::eFragment, .module = shaderModule, .pName = "fragMain"}; - vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; - - auto bindingDescription = Vertex::getBindingDescription(); - auto 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 = vk::SampleCountFlagBits::e1, - .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}; - 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 = &*descriptorSetLayout, .pushConstantRangeCount = 0}; - - pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); - - vk::Format depthFormat = findDepthFormat(); - - vk::StructureChain pipelineCreateInfoChain = { - {.stageCount = 2, - .pStages = shaderStages, - .pVertexInputState = &vertexInputInfo, - .pInputAssemblyState = &inputAssembly, - .pViewportState = &viewportState, - .pRasterizationState = &rasterizer, - .pMultisampleState = &multisampling, - .pDepthStencilState = &depthStencil, - .pColorBlendState = &colorBlending, - .pDynamicState = &dynamicState, - .layout = pipelineLayout, - .renderPass = nullptr}, - {.colorAttachmentCount = 1, .pColorAttachmentFormats = &swapChainSurfaceFormat.format, .depthAttachmentFormat = depthFormat}}; - - graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineCreateInfoChain.get()); - } - - void createCommandPool() - { - vk::CommandPoolCreateInfo poolInfo{ - .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, - .queueFamilyIndex = queueIndex}; - commandPool = vk::raii::CommandPool(device, poolInfo); - } - - void createDepthResources() - { - vk::Format depthFormat = findDepthFormat(); - - createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eDepthStencilAttachment, vk::MemoryPropertyFlagBits::eDeviceLocal, depthImage, depthImageMemory); - depthImageView = createImageView(depthImage, depthFormat, vk::ImageAspectFlagBits::eDepth); - } - - vk::Format findSupportedFormat(const std::vector &candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) const - { - for (const auto format : candidates) - { - vk::FormatProperties props = physicalDevice.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!"); - } - - [[nodiscard]] vk::Format findDepthFormat() const - { - return findSupportedFormat( - {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, - vk::ImageTiling::eOptimal, - vk::FormatFeatureFlagBits::eDepthStencilAttachment); - } - - static bool hasStencilComponent(vk::Format format) - { - return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; - } - - void createTextureImage() - { - int texWidth, texHeight, texChannels; - stbi_uc *pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); - vk::DeviceSize imageSize = texWidth * texHeight * 4; - - if (!pixels) - { - throw std::runtime_error("failed to load texture image!"); - } - - vk::raii::Buffer stagingBuffer({}); - vk::raii::DeviceMemory stagingBufferMemory({}); - createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); - - void *data = stagingBufferMemory.mapMemory(0, imageSize); - memcpy(data, pixels, imageSize); - stagingBufferMemory.unmapMemory(); - - stbi_image_free(pixels); - - createImage(texWidth, texHeight, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal, textureImage, textureImageMemory); - - transitionImageLayout(textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); - copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); - transitionImageLayout(textureImage, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); - } - - void createTextureImageView() - { - textureImageView = createImageView(textureImage, vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor); - } - - void createTextureSampler() - { - vk::PhysicalDeviceProperties properties = physicalDevice.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::True, - .maxAnisotropy = properties.limits.maxSamplerAnisotropy, - .compareEnable = vk::False, - .compareOp = vk::CompareOp::eAlways}; - textureSampler = vk::raii::Sampler(device, samplerInfo); - } - - vk::raii::ImageView createImageView(vk::raii::Image &image, vk::Format format, vk::ImageAspectFlags aspectFlags) - { - vk::ImageViewCreateInfo viewInfo{ - .image = image, - .viewType = vk::ImageViewType::e2D, - .format = format, - .subresourceRange = {aspectFlags, 0, 1, 0, 1}}; - return vk::raii::ImageView(device, viewInfo); - } - - void createImage(uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties, vk::raii::Image &image, vk::raii::DeviceMemory &imageMemory) - { - vk::ImageCreateInfo imageInfo{ - .imageType = vk::ImageType::e2D, - .format = format, - .extent = {width, height, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .samples = vk::SampleCountFlagBits::e1, - .tiling = tiling, - .usage = usage, - .sharingMode = vk::SharingMode::eExclusive, - .initialLayout = vk::ImageLayout::eUndefined}; - image = vk::raii::Image(device, imageInfo); - - vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); - vk::MemoryAllocateInfo allocInfo{ - .allocationSize = memRequirements.size, - .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties)}; - imageMemory = vk::raii::DeviceMemory(device, allocInfo); - image.bindMemory(imageMemory, 0); - } - - void transitionImageLayout(const vk::raii::Image &image, vk::ImageLayout oldLayout, vk::ImageLayout newLayout) - { - auto commandBuffer = beginSingleTimeCommands(); - - vk::ImageMemoryBarrier barrier{ - .oldLayout = oldLayout, - .newLayout = newLayout, - .image = image, - .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}}; - - 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, {}, {}, nullptr, barrier); - endSingleTimeCommands(*commandBuffer); - } - - void copyBufferToImage(const vk::raii::Buffer &buffer, vk::raii::Image &image, uint32_t width, uint32_t height) - { - std::unique_ptr commandBuffer = beginSingleTimeCommands(); - vk::BufferImageCopy region{ - .bufferOffset = 0, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, - .imageOffset = {0, 0, 0}, - .imageExtent = {width, height, 1}}; - commandBuffer->copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, {region}); - endSingleTimeCommands(*commandBuffer); - } - - void loadModel() - { - tinyobj::attrib_t attrib; - std::vector shapes; - std::vector materials; - std::string warn, err; - - if (!LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) - { - throw std::runtime_error(warn + err); - } - - std::unordered_map uniqueVertices{}; - - for (const auto &shape : shapes) - { - for (const auto &index : shape.mesh.indices) - { - Vertex vertex{}; - - vertex.pos = { - attrib.vertices[3 * index.vertex_index + 0], - attrib.vertices[3 * index.vertex_index + 1], - attrib.vertices[3 * index.vertex_index + 2]}; - - vertex.texCoord = { - attrib.texcoords[2 * index.texcoord_index + 0], - 1.0f - attrib.texcoords[2 * index.texcoord_index + 1]}; - - vertex.color = {1.0f, 1.0f, 1.0f}; - - if (!uniqueVertices.contains(vertex)) - { - uniqueVertices[vertex] = static_cast(vertices.size()); - vertices.push_back(vertex); - } - - indices.push_back(uniqueVertices[vertex]); - } - } - } - - void createVertexBuffer() - { - vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); - vk::raii::Buffer stagingBuffer({}); - vk::raii::DeviceMemory stagingBufferMemory({}); - createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); - - void *dataStaging = stagingBufferMemory.mapMemory(0, bufferSize); - memcpy(dataStaging, vertices.data(), bufferSize); - stagingBufferMemory.unmapMemory(); - - createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, vertexBuffer, vertexBufferMemory); - - copyBuffer(stagingBuffer, vertexBuffer, bufferSize); - } - - void createIndexBuffer() - { - vk::DeviceSize bufferSize = sizeof(indices[0]) * indices.size(); - - vk::raii::Buffer stagingBuffer({}); - vk::raii::DeviceMemory stagingBufferMemory({}); - createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory); - - void *data = stagingBufferMemory.mapMemory(0, bufferSize); - memcpy(data, indices.data(), bufferSize); - stagingBufferMemory.unmapMemory(); - - createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal, indexBuffer, indexBufferMemory); - - copyBuffer(stagingBuffer, indexBuffer, bufferSize); - } - - void createUniformBuffers() - { - uniformBuffers.clear(); - uniformBuffersMemory.clear(); - uniformBuffersMapped.clear(); - - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) - { - vk::DeviceSize bufferSize = sizeof(UniformBufferObject); - vk::raii::Buffer buffer({}); - vk::raii::DeviceMemory bufferMem({}); - createBuffer(bufferSize, vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, buffer, bufferMem); - uniformBuffers.emplace_back(std::move(buffer)); - uniformBuffersMemory.emplace_back(std::move(bufferMem)); - uniformBuffersMapped.emplace_back(uniformBuffersMemory[i].mapMemory(0, bufferSize)); - } - } - - void createDescriptorPool() - { - std::array poolSize{ - vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, MAX_FRAMES_IN_FLIGHT), - vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, MAX_FRAMES_IN_FLIGHT)}; - vk::DescriptorPoolCreateInfo poolInfo{ - .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, - .maxSets = MAX_FRAMES_IN_FLIGHT, - .poolSizeCount = static_cast(poolSize.size()), - .pPoolSizes = poolSize.data()}; - descriptorPool = vk::raii::DescriptorPool(device, poolInfo); - } - - void createDescriptorSets() - { - std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); - vk::DescriptorSetAllocateInfo allocInfo{ - .descriptorPool = descriptorPool, - .descriptorSetCount = static_cast(layouts.size()), - .pSetLayouts = layouts.data()}; - - descriptorSets.clear(); - descriptorSets = device.allocateDescriptorSets(allocInfo); - - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) - { - vk::DescriptorBufferInfo bufferInfo{ - .buffer = uniformBuffers[i], - .offset = 0, - .range = sizeof(UniformBufferObject)}; - vk::DescriptorImageInfo imageInfo{ - .sampler = textureSampler, - .imageView = textureImageView, - .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; - std::array descriptorWrites{ - vk::WriteDescriptorSet{ - .dstSet = descriptorSets[i], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eUniformBuffer, - .pBufferInfo = &bufferInfo}, - vk::WriteDescriptorSet{ - .dstSet = descriptorSets[i], - .dstBinding = 1, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = vk::DescriptorType::eCombinedImageSampler, - .pImageInfo = &imageInfo}}; - device.updateDescriptorSets(descriptorWrites, {}); - } - } - - void createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties, vk::raii::Buffer &buffer, vk::raii::DeviceMemory &bufferMemory) - { - vk::BufferCreateInfo bufferInfo{ - .size = size, - .usage = usage, - .sharingMode = vk::SharingMode::eExclusive}; - buffer = vk::raii::Buffer(device, bufferInfo); - vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); - vk::MemoryAllocateInfo allocInfo{ - .allocationSize = memRequirements.size, - .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties)}; - bufferMemory = vk::raii::DeviceMemory(device, allocInfo); - buffer.bindMemory(bufferMemory, 0); - } - - std::unique_ptr beginSingleTimeCommands() - { - vk::CommandBufferAllocateInfo allocInfo{ - .commandPool = commandPool, - .level = vk::CommandBufferLevel::ePrimary, - .commandBufferCount = 1}; - std::unique_ptr commandBuffer = std::make_unique(std::move(vk::raii::CommandBuffers(device, allocInfo).front())); - - vk::CommandBufferBeginInfo beginInfo{ - .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}; - commandBuffer->begin(beginInfo); - - return commandBuffer; - } - - void endSingleTimeCommands(const vk::raii::CommandBuffer &commandBuffer) const - { - commandBuffer.end(); - - vk::SubmitInfo submitInfo{.commandBufferCount = 1, .pCommandBuffers = &*commandBuffer}; - queue.submit(submitInfo, nullptr); - queue.waitIdle(); - } - - void copyBuffer(vk::raii::Buffer &srcBuffer, vk::raii::Buffer &dstBuffer, vk::DeviceSize size) - { - vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; - vk::raii::CommandBuffer commandCopyBuffer = std::move(device.allocateCommandBuffers(allocInfo).front()); - commandCopyBuffer.begin(vk::CommandBufferBeginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); - commandCopyBuffer.copyBuffer(*srcBuffer, *dstBuffer, vk::BufferCopy{.size = size}); - commandCopyBuffer.end(); - queue.submit(vk::SubmitInfo{.commandBufferCount = 1, .pCommandBuffers = &*commandCopyBuffer}, nullptr); - queue.waitIdle(); - } - - uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) - { - vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); - - for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) - { - if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) - { - return i; - } - } - - throw std::runtime_error("failed to find suitable memory type!"); - } - - void createCommandBuffers() - { - commandBuffers.clear(); - vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = MAX_FRAMES_IN_FLIGHT}; - commandBuffers = vk::raii::CommandBuffers(device, allocInfo); - } - - void recordCommandBuffer(uint32_t imageIndex) - { - auto &commandBuffer = commandBuffers[frameIndex]; - commandBuffer.begin({}); - // Before starting rendering, transition the swapchain image to COLOR_ATTACHMENT_OPTIMAL - transition_image_layout( - swapChainImages[imageIndex], - vk::ImageLayout::eUndefined, - vk::ImageLayout::eColorAttachmentOptimal, - {}, // srcAccessMask (no need to wait for previous operations) - vk::AccessFlagBits2::eColorAttachmentWrite, // dstAccessMask - vk::PipelineStageFlagBits2::eColorAttachmentOutput, // srcStage - vk::PipelineStageFlagBits2::eColorAttachmentOutput, // dstStage - vk::ImageAspectFlagBits::eColor); - // Transition depth image to depth attachment optimal layout - transition_image_layout( - *depthImage, - vk::ImageLayout::eUndefined, - vk::ImageLayout::eDepthAttachmentOptimal, - vk::AccessFlagBits2::eDepthStencilAttachmentWrite, - vk::AccessFlagBits2::eDepthStencilAttachmentWrite, - vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, - vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, - vk::ImageAspectFlagBits::eDepth); - - vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); - vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); - - vk::RenderingAttachmentInfo colorAttachmentInfo = { - .imageView = swapChainImageViews[imageIndex], - .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, - .loadOp = vk::AttachmentLoadOp::eClear, - .storeOp = vk::AttachmentStoreOp::eStore, - .clearValue = clearColor}; - - vk::RenderingAttachmentInfo depthAttachmentInfo = { - .imageView = depthImageView, - .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, - .loadOp = vk::AttachmentLoadOp::eClear, - .storeOp = vk::AttachmentStoreOp::eDontCare, - .clearValue = clearDepth}; - - vk::RenderingInfo renderingInfo = { - .renderArea = {.offset = {0, 0}, .extent = swapChainExtent}, - .layerCount = 1, - .colorAttachmentCount = 1, - .pColorAttachments = &colorAttachmentInfo, - .pDepthAttachment = &depthAttachmentInfo}; - commandBuffer.beginRendering(renderingInfo); - commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *graphicsPipeline); - commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); - commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); - commandBuffer.bindVertexBuffers(0, *vertexBuffer, {0}); - commandBuffer.bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint32); - commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, *descriptorSets[frameIndex], nullptr); - commandBuffer.drawIndexed(indices.size(), 1, 0, 0, 0); - commandBuffer.endRendering(); - // After rendering, transition the swapchain image to PRESENT_SRC - transition_image_layout( - swapChainImages[imageIndex], - vk::ImageLayout::eColorAttachmentOptimal, - vk::ImageLayout::ePresentSrcKHR, - vk::AccessFlagBits2::eColorAttachmentWrite, // srcAccessMask - {}, // dstAccessMask - vk::PipelineStageFlagBits2::eColorAttachmentOutput, // srcStage - vk::PipelineStageFlagBits2::eBottomOfPipe, // dstStage - vk::ImageAspectFlagBits::eColor); - commandBuffer.end(); - } - - void transition_image_layout( - vk::Image image, - vk::ImageLayout old_layout, - vk::ImageLayout new_layout, - vk::AccessFlags2 src_access_mask, - vk::AccessFlags2 dst_access_mask, - vk::PipelineStageFlags2 src_stage_mask, - vk::PipelineStageFlags2 dst_stage_mask, - vk::ImageAspectFlags image_aspect_flags) - { - vk::ImageMemoryBarrier2 barrier = { - .srcStageMask = src_stage_mask, - .srcAccessMask = src_access_mask, - .dstStageMask = dst_stage_mask, - .dstAccessMask = dst_access_mask, - .oldLayout = old_layout, - .newLayout = new_layout, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = image, - .subresourceRange = { - .aspectMask = image_aspect_flags, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1}}; - vk::DependencyInfo dependency_info = { - .dependencyFlags = {}, - .imageMemoryBarrierCount = 1, - .pImageMemoryBarriers = &barrier}; - commandBuffers[frameIndex].pipelineBarrier2(dependency_info); - } - - void createSyncObjects() - { - assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty()); - - for (size_t i = 0; i < swapChainImages.size(); i++) - { - renderFinishedSemaphores.emplace_back(device, vk::SemaphoreCreateInfo()); - } - - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) - { - presentCompleteSemaphores.emplace_back(device, vk::SemaphoreCreateInfo()); - inFlightFences.emplace_back(device, vk::FenceCreateInfo{.flags = vk::FenceCreateFlagBits::eSignaled}); - } - } - - void updateUniformBuffer(uint32_t currentImage) const - { - static auto startTime = std::chrono::high_resolution_clock::now(); - - auto currentTime = std::chrono::high_resolution_clock::now(); - float time = std::chrono::duration(currentTime - startTime).count(); - - UniformBufferObject ubo{}; - ubo.model = rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); - ubo.view = 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)); - ubo.proj = glm::perspective(glm::radians(45.0f), static_cast(swapChainExtent.width) / static_cast(swapChainExtent.height), 0.1f, 10.0f); - ubo.proj[1][1] *= -1; - - memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); - } - - void drawFrame() - { - // Note: inFlightFences, presentCompleteSemaphores, and commandBuffers are indexed by frameIndex, - // while renderFinishedSemaphores is indexed by imageIndex - auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX); - if (fenceResult != vk::Result::eSuccess) - { - throw std::runtime_error("failed to wait for fence!"); - } - - auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr); - - // Due to VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS being defined, eErrorOutOfDateKHR can be checked as a result - // here and does not need to be caught by an exception. - if (result == vk::Result::eErrorOutOfDateKHR) - { - recreateSwapChain(); - return; - } - // On other success codes than eSuccess and eSuboptimalKHR we just throw an exception. - // On any error code, aquireNextImage already threw an exception. - if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) - { - assert(result == vk::Result::eTimeout || result == vk::Result::eNotReady); - throw std::runtime_error("failed to acquire swap chain image!"); - } - updateUniformBuffer(frameIndex); - - // Only reset the fence if we are submitting work - device.resetFences(*inFlightFences[frameIndex]); - - commandBuffers[frameIndex].reset(); - recordCommandBuffer(imageIndex); - - vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); - const vk::SubmitInfo submitInfo{.waitSemaphoreCount = 1, - .pWaitSemaphores = &*presentCompleteSemaphores[frameIndex], - .pWaitDstStageMask = &waitDestinationStageMask, - .commandBufferCount = 1, - .pCommandBuffers = &*commandBuffers[frameIndex], - .signalSemaphoreCount = 1, - .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex]}; - queue.submit(submitInfo, *inFlightFences[frameIndex]); - - const vk::PresentInfoKHR presentInfoKHR{.waitSemaphoreCount = 1, - .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], - .swapchainCount = 1, - .pSwapchains = &*swapChain, - .pImageIndices = &imageIndex}; - result = queue.presentKHR(presentInfoKHR); - // Due to VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS being defined, eErrorOutOfDateKHR can be checked as a result - // here and does not need to be caught by an exception. - if ((result == vk::Result::eSuboptimalKHR) || (result == vk::Result::eErrorOutOfDateKHR) || framebufferResized) - { - framebufferResized = false; - recreateSwapChain(); - } - else - { - // There are no other success codes than eSuccess; on any error code, presentKHR already threw an exception. - assert(result == vk::Result::eSuccess); - } - frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT; - } - - [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector &code) const - { - vk::ShaderModuleCreateInfo createInfo{.codeSize = code.size(), .pCode = reinterpret_cast(code.data())}; - vk::raii::ShaderModule shaderModule{device, createInfo}; - - return shaderModule; - } - - static uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) - { - auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount); - if ((0 < surfaceCapabilities.maxImageCount) && (surfaceCapabilities.maxImageCount < minImageCount)) - { - minImageCount = surfaceCapabilities.maxImageCount; - } - return minImageCount; - } - - static vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector &availableFormats) - { - assert(!availableFormats.empty()); - const auto formatIt = std::ranges::find_if( - availableFormats, - [](const auto &format) { return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; }); - return formatIt != availableFormats.end() ? *formatIt : availableFormats[0]; - } - - static vk::PresentModeKHR chooseSwapPresentMode(std::vector const &availablePresentModes) - { - assert(std::ranges::any_of(availablePresentModes, [](auto presentMode) { return presentMode == vk::PresentModeKHR::eFifo; })); - return std::ranges::any_of(availablePresentModes, - [](const vk::PresentModeKHR value) { return vk::PresentModeKHR::eMailbox == value; }) ? - vk::PresentModeKHR::eMailbox : - vk::PresentModeKHR::eFifo; - } - - vk::Extent2D chooseSwapExtent(vk::SurfaceCapabilitiesKHR const &capabilities) - { - if (capabilities.currentExtent.width != std::numeric_limits::max()) - { - return capabilities.currentExtent; - } - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - return { - std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), - std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)}; - } - - [[nodiscard]] std::vector getRequiredInstanceExtensions() - { - uint32_t glfwExtensionCount = 0; - auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); - - std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); - if (enableValidationLayers) - { - extensions.push_back(vk::EXTDebugUtilsExtensionName); - } - - return extensions; - } - - static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity, vk::DebugUtilsMessageTypeFlagsEXT type, const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData, void *) - { - if (severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError || severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) - { - std::cerr << "validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl; - } - - return vk::False; - } - - static std::vector readFile(const std::string &filename) - { - std::ifstream file(filename, std::ios::ate | std::ios::binary); - if (!file.is_open()) - { - throw std::runtime_error("failed to open file!"); - } - std::vector buffer(file.tellg()); - file.seekg(0, std::ios::beg); - file.read(buffer.data(), static_cast(buffer.size())); - file.close(); - return buffer; - } }; int main() { try { - VulkanApplication app; + HelloTriangleApplication app; app.run(); } catch (const std::exception &e) From a390aa6f3815a7fa1d14c5cb7ad490fc6dec1772 Mon Sep 17 00:00:00 2001 From: Sascha Willems Date: Tue, 24 Mar 2026 07:06:36 +0100 Subject: [PATCH 3/3] Expand CMake setup with actual examples Add template setup Move CMake section --- en/02_Development_environment.adoc | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/en/02_Development_environment.adoc b/en/02_Development_environment.adoc index 75dc9eb2..ef632fb6 100644 --- a/en/02_Development_environment.adoc +++ b/en/02_Development_environment.adoc @@ -278,21 +278,6 @@ image::/images/library_directory.png[] Tinyobjloader can be installed with vcpkg like so: vcpkg install tinyobjloader -=== Setting up Visual Studio - -==== Setting up a CMake project - -Now that you have installed all the dependencies, we can set up a basic -CMake project for Vulkan and write a little bit of code to make sure that -everything works. - -We will assume that you already have some basic experience with CMake, like -how variables and rules work. If not, you can get up to speed very quickly with https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/tutorial/[this tutorial]. - -You can now use the code from any of the following chapters found in the `attachment` folder as a template for your Vulkan projects. Make a copy, rename it to something like `HelloTriangle` and remove all the code in `main.cpp`. - -Congratulations, you're all set for xref:03_Drawing_a_triangle/00_Setup/00_Base_code.adoc[playing with Vulkan]! - == Linux These instructions will be aimed at Ubuntu, Fedora and Arch Linux users, but @@ -515,3 +500,28 @@ You are now all set for xref:03_Drawing_a_triangle/00_Setup/00_Base_code.adoc[th == Android Vulkan is a first-class API on Android and widely supported. But using it differs in several key areas from window management to build systems. So while the basic chapters focus on desktop platforms, the tutorial also has a xref:14_Android.adoc[dedicated chapter] that walks you through setting up your development environment and getting the tutorial code up-and-running on Android. + +== Setting up a CMake project + +Now that you have installed all the dependencies, we can set up a basic +CMake project for Vulkan and write a little bit of code to make sure that +everything works. + +We will assume that you already have some basic experience with CMake, like +how variables and rules work. If not, you can get up to speed very quickly with https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/tutorial/[this tutorial]. + +The `attachment\template` folder contains a CMake template that you can use to create a project file in the `build` folder for your IDE of choice. Use this as a starting point to work along the tutorial: + +---- +cd attachments\template +cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path\to\vcpkg]\scripts\buildsystems\vcpkg.cmake +---- + +Alternatively you can do the same in the `attachments` folder, which will create a project with all chapters from the tutorial: + +---- +cd attachments +cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path\to\vcpkg]\scripts\buildsystems\vcpkg.cmake +---- + +Congratulations, you're all set for xref:03_Drawing_a_triangle/00_Setup/00_Base_code.adoc[playing with Vulkan]!