diff --git a/12_graphics_pipeline_complete.cpp b/12_graphics_pipeline_complete.cpp new file mode 100644 index 0000000..be7cfaa --- /dev/null +++ b/12_graphics_pipeline_complete.cpp @@ -0,0 +1,462 @@ +#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES) +#include "vulkan/vulkan.hpp" +#include +#include +#else +import vulkan_hpp; +#endif + +#define GLFW_INCLUDE_VULKAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr uint32_t WIDTH = 800; +constexpr uint32_t HEIGHT = 600; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +#ifdef NDEBUG +constexpr bool enableValidationLayers = false; +#else +constexpr bool enableValidationLayers = true; +#endif + +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; +} + +class HelloTriangleApplication { + public: + void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); + } + private: + void initWindow() { + glfwInit(); + // Don't create an OpenGL context + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); + } + void initVulkan() { + createInstance(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createGraphicsPipeline(); + } + void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + } + void cleanup() { + glfwDestroyWindow(window); + glfwTerminate(); + } + 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(); + if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) { + return std::ranges::none_of(layerProperties, + [requiredLayer](auto const& layerProperty) + { return strcmp(layerProperty.layerName, requiredLayer) == 0; }); + })) + { + throw std::runtime_error("One or more required layers are not supported!"); + } + + // Get the required instance extensions from GLFW. + uint32_t glfwExtensionCount = 0; + auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + // Check if the required GLFW extensions are supported by the Vulkan implementation. + auto extensionProperties = context.enumerateInstanceExtensionProperties(); + for (uint32_t i = 0; i < glfwExtensionCount; ++i) + { + if (std::ranges::none_of(extensionProperties, + [glfwExtension = glfwExtensions[i]](auto const& extensionProperty) + { return strcmp(extensionProperty.extensionName, glfwExtension) == 0; })) + { + throw std::runtime_error("Required GLFW extension not supported: " + std::string(glfwExtensions[i])); + } + } + vk::InstanceCreateInfo createInfo { + .pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(requiredLayers.size()), + .ppEnabledLayerNames = requiredLayers.data(), + .enabledExtensionCount = glfwExtensionCount, + .ppEnabledExtensionNames = glfwExtensions, + }; + + instance = vk::raii::Instance(context, createInfo); + } + 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); + } + void pickPhysicalDevice() { + std::vector deviceExtensions = { + vk::KHRSwapchainExtensionName, + vk::KHRSpirv14ExtensionName, + vk::KHRSynchronization2ExtensionName, + vk::KHRCreateRenderpass2ExtensionName, + }; + + auto devices = instance.enumeratePhysicalDevices(); + if (devices.empty()) { + throw std::runtime_error("failed to find GPUs with Vulkan support!"); + } + + for (const auto &device : devices) { + auto deviceProperties = device.getProperties(); + auto deviceFeatures = device.getFeatures(); + auto queueFamilies = device.getQueueFamilyProperties(); + auto extensions = device.enumerateDeviceExtensionProperties(); + bool isSuitable = deviceProperties.apiVersion >= VK_API_VERSION_1_3; + bool extensionFound = true; + + const vk::QueueFamilyProperties *qf = nullptr; + for (const auto &qfp : queueFamilies) { + if ((qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0)) { + qf = &qfp; + break; + } + } + + isSuitable = isSuitable && (qf != nullptr); + + for (const auto &extension : deviceExtensions) { + auto extensionIter = std::ranges::find_if(extensions, [extension](auto const & ext) {return strcmp(ext.extensionName, extension) == 0;}); + extensionFound = extensionFound && extensionIter != extensions.end(); + } + + isSuitable = isSuitable && extensionFound; + + if (isSuitable) { + physicalDevice = device; + return; + } + + throw std::runtime_error("failed to find a suitable GPU"); + } + } + void createLogicalDevice() { + std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + uint32_t graphicsIndex = findQueueFamilies(physicalDevice); + float queuePriority = 0.5f; + vk::DeviceQueueCreateInfo deviceQueueCreateInfo { + .queueFamilyIndex = graphicsIndex, + .queueCount = 1, + .pQueuePriorities = &queuePriority, + }; + + // Create a chain of feature structures + vk::StructureChain featureChain = { + {}, // vk::PhysicalDeviceFeatures2 (empty for now) + {.dynamicRendering = true }, // Enable dynamic rendering from Vulkan 1.3 + {.shaderDrawParameters = true }, // Enable shader draw parameters from Vulkan 1.2 + {.extendedDynamicState = true } // Enable extended dynamic state from the extension + }; + + std::vector deviceExtensions = { + vk::KHRSwapchainExtensionName, + vk::KHRSpirv14ExtensionName, + vk::KHRSynchronization2ExtensionName, + vk::KHRCreateRenderpass2ExtensionName + }; + + vk::DeviceCreateInfo deviceCreateInfo { + .pNext = &featureChain.get(), + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &deviceQueueCreateInfo, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + }; + + device = vk::raii::Device(physicalDevice, deviceCreateInfo); + graphicsQueue = vk::raii::Queue(device, graphicsIndex, 0); + } + void createSwapChain() { + auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface); + swapChainSurfaceFormat = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR(surface)); + swapChainExtent = chooseSwapExtent(surfaceCapabilities); + auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount); + minImageCount = (surfaceCapabilities.maxImageCount > 0 && + minImageCount > surfaceCapabilities.maxImageCount) ? + surfaceCapabilities.maxImageCount : + minImageCount; + + vk::SwapchainCreateInfoKHR swapChainCreateInfo { + .flags = vk::SwapchainCreateFlagsKHR(), + .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 = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR(surface)), + .clipped = true, + .oldSwapchain = nullptr, + }; + + swapChain = vk::raii::SwapchainKHR(device, swapChainCreateInfo); + swapChainImages = swapChain.getImages(); + swapChainImageFormat = swapChainSurfaceFormat.format; + } + void createImageViews() { + swapChainImageViews.clear(); + + vk::ImageViewCreateInfo imageViewCreateInfo{ + .viewType = vk::ImageViewType::e2D, + .format = swapChainImageFormat, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + + for (auto image : swapChainImages) { + imageViewCreateInfo.image = image; + swapChainImageViews.emplace_back(vk::raii::ImageView(device, imageViewCreateInfo)); + } + } + void createGraphicsPipeline() { + vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/09_shader_base.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}; + + // Vertex input + vk::PipelineVertexInputStateCreateInfo vertexInputInfo; + + // Input assembly + vk::PipelineInputAssemblyStateCreateInfo inputAssembly = { + .topology = vk::PrimitiveTopology::eTriangleList, + }; + + // Dynamic state + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor, + }; + vk::PipelineDynamicStateCreateInfo dynamicState = { + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data(), + }; + // No need to specify viewport and scissor because they will be specified dynamically + vk::PipelineViewportStateCreateInfo viewportState = { + .viewportCount = 1, + .scissorCount = 1, + }; + + // Rasterisation + vk::PipelineRasterizationStateCreateInfo rasterizer = { + .depthClampEnable = vk::False, + .rasterizerDiscardEnable = vk::False, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eClockwise, + .depthBiasEnable = vk::False, + .depthBiasSlopeFactor = 1.0f, + .lineWidth = 1.0f + }; + + // Multisampling + vk::PipelineMultisampleStateCreateInfo multisampling = { + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = vk::False, + }; + + // Color blending + 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, + }; + + // Pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo = { + .setLayoutCount = 0, + .pushConstantRangeCount = 0, + }; + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // Dynamic rendering pipeline + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo = { + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + }; + vk::GraphicsPipelineCreateInfo pipelineInfo = { + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = nullptr, + }; + + // Create pipeline + graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + } + [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector &code) const { + vk::ShaderModuleCreateInfo createInfo { + .codeSize = code.size() * sizeof(char), + .pCode = reinterpret_cast(code.data()), + }; + + return vk::raii::ShaderModule{device, createInfo}; + } + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { + return availableFormat; + } + } + + return availableFormats[0]; + } + vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) { + return availablePresentMode; + } + } + + return vk::PresentModeKHR::eFifo; + } + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& 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), + }; + } + uint32_t findQueueFamilies(vk::raii::PhysicalDevice physicalDevice) { + // find the index of the first queue family that supports graphics + std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + + // get the first index into queueFamilyProperties which both supports graphics and present + uint32_t queueIndex = ~0; + for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); ++qfpIndex) { + if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && + physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface)) { + queueIndex = qfpIndex; + break; + } + } + + if (queueIndex == ~0) { + throw std::runtime_error("Could not find a queue for graphics and present -> terminating"); + } + + return queueIndex; + } + + GLFWwindow *window; + vk::raii::Context context; + vk::raii::Instance instance = nullptr; + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + vk::raii::Queue graphicsQueue = nullptr; + vk::raii::SurfaceKHR surface = nullptr; + vk::raii::SwapchainKHR swapChain = nullptr; + vk::SurfaceFormatKHR swapChainSurfaceFormat; + vk::Extent2D swapChainExtent; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + std::vector swapChainImages; + std::vector swapChainImageViews; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; +}; + +int main() { + HelloTriangleApplication app; + + try { + app.run(); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 882de40..6ba867d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,9 +198,9 @@ add_chapter (09_shader_modules add_chapter (10_fixed_functions SHADER 09_shader_base) -# add_chapter (12_graphics_pipeline_complete -# SHADER 09_shader_base) -# +add_chapter (12_graphics_pipeline_complete + SHADER 09_shader_base) + # add_chapter (14_command_buffers # SHADER 09_shader_base) #