Skip to content

Instantly share code, notes, and snippets.

@BalintCsala
Created September 15, 2025 19:39
Show Gist options
  • Select an option

  • Save BalintCsala/40f9bae0cbb405701d55a0a0a339b519 to your computer and use it in GitHub Desktop.

Select an option

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
#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