Created
September 15, 2025 19:39
-
-
Save BalintCsala/40f9bae0cbb405701d55a0a0a339b519 to your computer and use it in GitHub Desktop.
The minimum amount of code you need to create a vulkan triangle while not hurting readability
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #define GLFW_INCLUDE_VULKAN | |
| #include "GLFW/glfw3.h" | |
| #include <vulkan/vulkan.hpp> | |
| #include <fstream> | |
| #include <iostream> | |
| #include <vector> | |
| const std::vector<const char *> DEVICE_EXTENSIONS = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; | |
| const uint32_t FRAMES_IN_FLIGHT = 3; | |
| template<typename T, typename F> | |
| T findWithDefault(const std::vector<T> &values, const F &predicate, const T &defaultValue) { | |
| auto it = std::ranges::find_if(values, predicate); | |
| return it == values.end() ? defaultValue : *it; | |
| } | |
| std::vector<uint32_t> readShaderFile(const std::string &path) { | |
| std::ifstream file(path, std::ios::ate | std::ios::binary); | |
| std::streampos size = file.tellg(); | |
| file.seekg(0); | |
| std::vector<uint32_t> result(size / sizeof(uint32_t)); | |
| file.read(reinterpret_cast<char *>(result.data()), size); | |
| return result; | |
| } | |
| vk::ImageSubresourceRange fullSubresource(vk::ImageAspectFlags aspect) { | |
| return vk::ImageSubresourceRange() | |
| .setAspectMask(aspect) | |
| .setBaseMipLevel(0) | |
| .setLevelCount(VK_REMAINING_MIP_LEVELS) | |
| .setBaseArrayLayer(0) | |
| .setLayerCount(VK_REMAINING_ARRAY_LAYERS); | |
| } | |
| void transition( | |
| const vk::CommandBuffer &commandBuffer, const vk::Image &image, | |
| const vk::AccessFlags2 srcAccess, const vk::AccessFlags2 dstAccess, | |
| const vk::PipelineStageFlags2 srcStageMask, const vk::PipelineStageFlags2 dstStageMask, | |
| const vk::ImageLayout &srcLayout, const vk::ImageLayout &dstLayout | |
| ) { | |
| auto barrier = vk::ImageMemoryBarrier2() | |
| .setImage(image) | |
| .setSubresourceRange(fullSubresource(vk::ImageAspectFlagBits::eColor)) | |
| .setSrcAccessMask(srcAccess) | |
| .setDstAccessMask(dstAccess) | |
| .setSrcStageMask(srcStageMask) | |
| .setDstStageMask(dstStageMask) | |
| .setOldLayout(srcLayout) | |
| .setNewLayout(dstLayout); | |
| auto dependencyInfo = vk::DependencyInfo() | |
| .setImageMemoryBarriers(barrier); | |
| commandBuffer.pipelineBarrier2(dependencyInfo); | |
| } | |
| void createSwapchain( | |
| const vk::Device &device, | |
| vk::UniqueSwapchainKHR &newSwapchain, | |
| const vk::SwapchainKHR &oldSwapchain, | |
| const vk::SurfaceKHR &surface, | |
| const vk::PresentModeKHR &presentMode, | |
| const vk::SurfaceFormatKHR &surfaceFormat, | |
| const uint32_t minImageCount, | |
| uint32_t width, | |
| uint32_t height, | |
| std::vector<vk::Image> &images, | |
| std::vector<vk::UniqueImageView> &imageViews, | |
| std::vector<vk::UniqueSemaphore> &renderingFinishedSemaphores | |
| ) { | |
| device.waitIdle(); | |
| imageViews.clear(); | |
| renderingFinishedSemaphores.clear(); | |
| auto swapchainCreateInfo = vk::SwapchainCreateInfoKHR() | |
| .setOldSwapchain(oldSwapchain) | |
| .setImageArrayLayers(1) | |
| .setImageColorSpace(surfaceFormat.colorSpace) | |
| .setImageExtent(vk::Extent2D(width, height)) | |
| .setImageFormat(surfaceFormat.format) | |
| .setImageSharingMode(vk::SharingMode::eExclusive) | |
| .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) | |
| .setMinImageCount(minImageCount) | |
| .setPreTransform(vk::SurfaceTransformFlagBitsKHR::eIdentity) | |
| .setPresentMode(presentMode) | |
| .setSurface(surface); | |
| newSwapchain = device.createSwapchainKHRUnique(swapchainCreateInfo); | |
| images = device.getSwapchainImagesKHR(*newSwapchain); | |
| auto imageViewCreateInfo = vk::ImageViewCreateInfo() | |
| .setComponents(vk::ComponentMapping()) | |
| .setFormat(surfaceFormat.format) | |
| .setSubresourceRange(fullSubresource(vk::ImageAspectFlagBits::eColor)) | |
| .setViewType(vk::ImageViewType::e2D); | |
| for (const auto &image: images) { | |
| imageViewCreateInfo.setImage(image); | |
| imageViews.push_back(device.createImageViewUnique(imageViewCreateInfo)); | |
| renderingFinishedSemaphores.push_back(device.createSemaphoreUnique(vk::SemaphoreCreateInfo())); | |
| } | |
| } | |
| int main() { | |
| int32_t width = 1920; | |
| int32_t height = 1080; | |
| glfwInit(); | |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); | |
| GLFWwindow *window = glfwCreateWindow(width, height, "Minimal vulkan", nullptr, nullptr); | |
| glfwShowWindow(window); | |
| uint32_t count; | |
| const char **requiredExtensions = glfwGetRequiredInstanceExtensions(&count); | |
| auto applicationInfo = vk::ApplicationInfo() | |
| .setApiVersion(VK_API_VERSION_1_3); | |
| auto instanceCreateInfo = vk::InstanceCreateInfo() | |
| .setPApplicationInfo(&applicationInfo) | |
| .setPpEnabledExtensionNames(requiredExtensions) | |
| .setEnabledExtensionCount(count); | |
| vk::UniqueInstance instance = vk::createInstanceUnique(instanceCreateInfo); | |
| std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices(); | |
| vk::PhysicalDevice physicalDevice = findWithDefault( | |
| physicalDevices, | |
| [](const vk::PhysicalDevice physicalDevice) { | |
| return physicalDevice.getProperties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu; | |
| }, | |
| physicalDevices[0] | |
| ); | |
| float priorities = 1.0f; | |
| auto queueCreateInfo = | |
| vk::DeviceQueueCreateInfo() | |
| .setQueueFamilyIndex(0) | |
| .setQueuePriorities(priorities) | |
| .setQueueCount(1); | |
| auto deviceCreateInfo = vk::StructureChain<vk::DeviceCreateInfo, vk::PhysicalDeviceVulkan13Features>( | |
| vk::DeviceCreateInfo() | |
| .setPEnabledExtensionNames(DEVICE_EXTENSIONS) | |
| .setQueueCreateInfos(queueCreateInfo), | |
| vk::PhysicalDeviceVulkan13Features() | |
| .setDynamicRendering(true) | |
| .setSynchronization2(true)) | |
| .get(); | |
| vk::UniqueDevice device = physicalDevice.createDeviceUnique(deviceCreateInfo); | |
| auto queue = device->getQueue(0, 0); | |
| VkSurfaceKHR rawSurface; | |
| glfwCreateWindowSurface(*instance, window, nullptr, &rawSurface); | |
| vk::UniqueSurfaceKHR surface(rawSurface, *instance); | |
| auto presentModes = physicalDevice.getSurfacePresentModesKHR(*surface); | |
| auto presentMode = findWithDefault( | |
| presentModes, | |
| [&](const vk::PresentModeKHR &presentModeKhr) { | |
| return presentModeKhr == vk::PresentModeKHR::eMailbox; | |
| }, | |
| vk::PresentModeKHR::eFifo | |
| ); | |
| auto surfaceFormats = physicalDevice.getSurfaceFormatsKHR(*surface); | |
| auto surfaceFormat = findWithDefault( | |
| surfaceFormats, | |
| [](const vk::SurfaceFormatKHR &surfaceFormat) { | |
| return surfaceFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear && | |
| (surfaceFormat.format == vk::Format::eB8G8R8A8Srgb || surfaceFormat.format == vk::Format::eR8G8B8A8Srgb); | |
| }, | |
| surfaceFormats[0] | |
| ); | |
| auto minImageCount = physicalDevice.getSurfaceCapabilitiesKHR(*surface).minImageCount; | |
| std::vector<uint32_t> code = readShaderFile("../shaders/test.spv"); | |
| auto moduleCreateInfo = vk::ShaderModuleCreateInfo() | |
| .setCode(code); | |
| vk::UniqueShaderModule module = device->createShaderModuleUnique(moduleCreateInfo); | |
| auto pipelineLayoutCreateInfo = vk::PipelineLayoutCreateInfo(); | |
| vk::UniquePipelineLayout pipelineLayout = device->createPipelineLayoutUnique(pipelineLayoutCreateInfo); | |
| auto attachment = vk::PipelineColorBlendAttachmentState() | |
| .setBlendEnable(false) | |
| .setColorWriteMask( | |
| vk::ColorComponentFlagBits::eR | | |
| vk::ColorComponentFlagBits::eG | | |
| vk::ColorComponentFlagBits::eB | | |
| vk::ColorComponentFlagBits::eA | |
| ); | |
| auto colorBlendState = vk::PipelineColorBlendStateCreateInfo() | |
| .setAttachments(attachment); | |
| auto depthStencilState = vk::PipelineDepthStencilStateCreateInfo(); | |
| std::vector<vk::DynamicState> dynamicStates = { | |
| vk::DynamicState::eViewport, | |
| vk::DynamicState::eScissor, | |
| }; | |
| auto dynamicState = vk::PipelineDynamicStateCreateInfo() | |
| .setDynamicStates(dynamicStates); | |
| auto inputAssemblyState = vk::PipelineInputAssemblyStateCreateInfo() | |
| .setTopology(vk::PrimitiveTopology::eTriangleList); | |
| auto multisampleState = vk::PipelineMultisampleStateCreateInfo(); | |
| auto rasterizationState = vk::PipelineRasterizationStateCreateInfo() | |
| .setLineWidth(1.0f); | |
| std::vector<vk::PipelineShaderStageCreateInfo> stages = { | |
| vk::PipelineShaderStageCreateInfo().setModule(*module) | |
| .setPName("vs") | |
| .setStage(vk::ShaderStageFlagBits::eVertex), | |
| vk::PipelineShaderStageCreateInfo().setModule(*module) | |
| .setPName("fs") | |
| .setStage(vk::ShaderStageFlagBits::eFragment), | |
| }; | |
| auto vertexInputState = vk::PipelineVertexInputStateCreateInfo(); | |
| auto viewportState = vk::PipelineViewportStateCreateInfo() | |
| .setViewportCount(1) | |
| .setScissorCount(1); | |
| std::vector<vk::Format> attachmentFormats{ | |
| surfaceFormat.format, | |
| }; | |
| auto pipelineCreateInfo = vk::StructureChain<vk::GraphicsPipelineCreateInfo, vk::PipelineRenderingCreateInfo>( | |
| vk::GraphicsPipelineCreateInfo() | |
| .setLayout(*pipelineLayout) | |
| .setPColorBlendState(&colorBlendState) | |
| .setPDepthStencilState(&depthStencilState) | |
| .setPDynamicState(&dynamicState) | |
| .setPInputAssemblyState(&inputAssemblyState) | |
| .setPMultisampleState(&multisampleState) | |
| .setPRasterizationState(&rasterizationState) | |
| .setStages(stages) | |
| .setPVertexInputState(&vertexInputState) | |
| .setPViewportState(&viewportState), | |
| vk::PipelineRenderingCreateInfo() | |
| .setColorAttachmentFormats(attachmentFormats) | |
| .setDepthAttachmentFormat(vk::Format::eD24UnormS8Uint) | |
| ).get(); | |
| vk::UniquePipeline pipeline = device->createGraphicsPipelineUnique(VK_NULL_HANDLE, pipelineCreateInfo).value; | |
| vk::UniqueSwapchainKHR swapchain; | |
| std::vector<vk::Image> images; | |
| std::vector<vk::UniqueImageView> imageViews; | |
| std::vector<vk::UniqueSemaphore> renderingFinishedSemaphores; | |
| createSwapchain( | |
| *device, | |
| swapchain, | |
| VK_NULL_HANDLE, | |
| *surface, | |
| presentMode, | |
| surfaceFormat, | |
| minImageCount + 1, | |
| width, | |
| height, | |
| images, | |
| imageViews, | |
| renderingFinishedSemaphores | |
| ); | |
| std::vector<vk::UniqueFence> frameFences; | |
| std::vector<vk::UniqueSemaphore> imageAcquiredSemaphores; | |
| std::vector<vk::UniqueCommandPool> commandPools; | |
| std::vector<std::vector<vk::CommandBuffer>> commandBuffers; | |
| auto fenceCreateInfo = vk::FenceCreateInfo() | |
| .setFlags(vk::FenceCreateFlagBits::eSignaled); | |
| auto commandPoolCreateInfo = vk::CommandPoolCreateInfo(); | |
| auto commandBufferAllocInfo = vk::CommandBufferAllocateInfo() | |
| .setCommandBufferCount(1) | |
| .setLevel(vk::CommandBufferLevel::ePrimary); | |
| for (int i = 0; i < FRAMES_IN_FLIGHT; i++) { | |
| frameFences.push_back(device->createFenceUnique(fenceCreateInfo)); | |
| imageAcquiredSemaphores.push_back(device->createSemaphoreUnique(vk::SemaphoreCreateInfo())); | |
| commandPools.push_back(device->createCommandPoolUnique(commandPoolCreateInfo)); | |
| commandBufferAllocInfo.setCommandPool(*commandPools[i]); | |
| commandBuffers.push_back(device->allocateCommandBuffers(commandBufferAllocInfo)); | |
| } | |
| uint32_t frameIndex = 0; | |
| while (!glfwWindowShouldClose(window)) { | |
| glfwPollEvents(); | |
| auto _ = device->waitForFences(*frameFences[frameIndex], true, std::numeric_limits<uint64_t>::max()); | |
| int32_t newWidth, newHeight; | |
| glfwGetWindowSize(window, &newWidth, &newHeight); | |
| if (newWidth == 0 || newHeight == 0) { | |
| continue; | |
| } else if (newWidth != width || newHeight != height) { | |
| auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface); | |
| width = std::clamp( | |
| newWidth, | |
| static_cast<int32_t>(surfaceCapabilities.minImageExtent.width), | |
| static_cast<int32_t>(surfaceCapabilities.maxImageExtent.width) | |
| ); | |
| height = std::clamp( | |
| newHeight, | |
| static_cast<int32_t>(surfaceCapabilities.minImageExtent.height), | |
| static_cast<int32_t>(surfaceCapabilities.maxImageExtent.height) | |
| ); | |
| vk::UniqueSwapchainKHR newSwapchain; | |
| createSwapchain( | |
| *device, | |
| newSwapchain, | |
| *swapchain, | |
| *surface, | |
| presentMode, | |
| surfaceFormat, | |
| minImageCount, | |
| width, | |
| height, | |
| images, | |
| imageViews, | |
| renderingFinishedSemaphores | |
| ); | |
| swapchain = std::move(newSwapchain); | |
| continue; | |
| } | |
| uint32_t imageIndex; | |
| try { | |
| imageIndex = device->acquireNextImageKHR( | |
| *swapchain, | |
| std::numeric_limits<uint64_t>::max(), | |
| *imageAcquiredSemaphores[frameIndex] | |
| ).value; | |
| } catch (vk::OutOfDateKHRError &ex) { | |
| glfwWaitEvents(); | |
| continue; | |
| } | |
| device->resetFences(*frameFences[frameIndex]); | |
| device->resetCommandPool(*commandPools[frameIndex]); | |
| auto commandBuffer = commandBuffers[frameIndex][0]; | |
| auto beginInfo = vk::CommandBufferBeginInfo() | |
| .setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); | |
| commandBuffer.begin(beginInfo); | |
| transition( | |
| commandBuffer, images[imageIndex], | |
| vk::AccessFlagBits2::eNone, vk::AccessFlagBits2::eColorAttachmentWrite, | |
| vk::PipelineStageFlagBits2::eTopOfPipe, vk::PipelineStageFlagBits2::eColorAttachmentOutput, | |
| vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal | |
| ); | |
| auto renderArea = vk::Rect2D(vk::Offset2D(0, 0), vk::Extent2D(width, height)); | |
| auto attachmentInfo = vk::RenderingAttachmentInfo() | |
| .setClearValue(vk::ClearValue{}.setColor(vk::ClearColorValue{}.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}))) | |
| .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) | |
| .setImageView(*imageViews[imageIndex]) | |
| .setLoadOp(vk::AttachmentLoadOp::eClear); | |
| auto renderingInfo = vk::RenderingInfo() | |
| .setLayerCount(1) | |
| .setColorAttachments(attachmentInfo) | |
| .setRenderArea(renderArea); | |
| commandBuffer.beginRendering(renderingInfo); | |
| auto viewport = vk::Viewport(0, 0, static_cast<float>(width), static_cast<float>(height)); | |
| commandBuffer.setViewport(0, viewport); | |
| commandBuffer.setScissor(0, renderArea); | |
| commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pipeline); | |
| commandBuffer.draw(3, 1, 0, 0); | |
| commandBuffer.endRendering(); | |
| transition( | |
| commandBuffer, images[imageIndex], | |
| vk::AccessFlagBits2::eColorAttachmentWrite, vk::AccessFlagBits2::eNone, | |
| vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::PipelineStageFlagBits2::eBottomOfPipe, | |
| vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::ePresentSrcKHR | |
| ); | |
| commandBuffer.end(); | |
| std::vector<vk::PipelineStageFlags> stageMask{vk::PipelineStageFlagBits::eColorAttachmentOutput}; | |
| auto submitInfo = vk::SubmitInfo() | |
| .setCommandBuffers(commandBuffer) | |
| .setSignalSemaphores(*renderingFinishedSemaphores[imageIndex]) | |
| .setWaitSemaphores(*imageAcquiredSemaphores[frameIndex]) | |
| .setWaitDstStageMask(stageMask); | |
| queue.submit(submitInfo, *frameFences[frameIndex]); | |
| try { | |
| auto presentInfo = vk::PresentInfoKHR() | |
| .setWaitSemaphores(*renderingFinishedSemaphores[imageIndex]) | |
| .setImageIndices(imageIndex) | |
| .setSwapchains(*swapchain); | |
| auto _ = queue.presentKHR(presentInfo); | |
| } catch (vk::OutOfDateKHRError &ex) { | |
| glfwWaitEvents(); | |
| continue; | |
| } | |
| frameIndex = (frameIndex + 1) % FRAMES_IN_FLIGHT; | |
| } | |
| device->waitIdle(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment