Skip to content

Instantly share code, notes, and snippets.

@harrisonturton
Created September 5, 2025 06:45
Show Gist options
  • Save harrisonturton/adbedabd2568d9fda2a3026c1e6701cf to your computer and use it in GitHub Desktop.
Save harrisonturton/adbedabd2568d9fda2a3026c1e6701cf to your computer and use it in GitHub Desktop.
Vulkan triangle
#include "vulkan.hpp"
#include <fcntl.h>
#include <cassert>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <stdexcept>
// Must come before other vulkan includes
#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS 1
#include "vulkan/vulkan_raii.hpp"
// Separate from raii import
#include "vulkan/vulkan.hpp"
namespace vulkan {
// This assumption holds for both iOS and MacOS apps, and is coupled to the
// additional_contents attribute on the ios_application and macos_application
// Bazel rules.
// TODO: Figure out cross-platform mechanism for resource resolution
// const std::string kShaderPath = "/Contents/Shaders/simple.spv";
const std::string kShaderPath = "/simple.spv";
std::vector<uint8_t> readBinaryFile(const std::string &path);
vk::raii::ShaderModule createShaderModule(
vk::raii::Device &device, const std::vector<uint8_t> &spv);
Vulkan::Vulkan(std::string resourceRoot, void *layer) {
resourceRoot_ = resourceRoot;
layer_ = layer;
createInstance();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapchain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
createFramebuffers();
createCommandPool();
createCommandBuffer();
createSyncObjects();
}
void Vulkan::createInstance() {
vk::raii::Context context;
auto appInfo = vk::ApplicationInfo{
.pApplicationName = "Triangle",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = vk::ApiVersion14,
};
// TODO: Validate that required extensions exist before requesting them
std::vector<const char *> extensions = {
vk::KHRSurfaceExtensionName,
vk::EXTMetalSurfaceExtensionName,
};
// TODO: Build //third_party/vulkan_validaton_layers and use them here
std::vector<const char *> layers = {};
auto createInfo = vk::InstanceCreateInfo{
.pApplicationInfo = &appInfo,
.enabledExtensionCount = uint32_t(extensions.size()),
.ppEnabledExtensionNames = extensions.data(),
.ppEnabledLayerNames = layers.data(),
.enabledLayerCount = uint32_t(layers.size()),
};
instance_ = vk::raii::Instance(context, createInfo);
}
void Vulkan::createSurface() {
auto createInfo = vk::MetalSurfaceCreateInfoEXT{
.pLayer = layer_,
};
surface_ = instance_.createMetalSurfaceEXT(createInfo);
}
void Vulkan::pickPhysicalDevice() {
physicalDevice_ = instance_.enumeratePhysicalDevices().front();
}
void Vulkan::createLogicalDevice() {
// Locate the graphics queue
auto queueFamilyProperties = physicalDevice_.getQueueFamilyProperties();
auto graphicsQueueFamilyIndex = -1;
for (auto i = 0; i < queueFamilyProperties.size(); i++) {
auto queue = queueFamilyProperties[i];
if (queue.queueFlags & vk::QueueFlagBits::eGraphics) {
graphicsQueueFamilyIndex = i;
break;
}
}
if (graphicsQueueFamilyIndex < 0) {
std::cerr << "failed to find graphics queue\n";
return;
}
graphicsQueueFamilyIndex_ = graphicsQueueFamilyIndex;
// Making a simplifying assumption that the graphics queue family also
// supports presentation. This is often true, but not always.
// TODO: Support selecting distinct graphics and presentation queues
auto presentSupport = physicalDevice_.getSurfaceSupportKHR(
uint32_t(graphicsQueueFamilyIndex), *surface_);
if (!presentSupport) {
std::cerr
<< "error: presentation not supported by the graphics queue family\n";
return;
}
auto queuePriorities = std::vector<float>{
1.0,
};
auto queueCreateInfos = std::vector<vk::DeviceQueueCreateInfo>{
vk::DeviceQueueCreateInfo{
.queueFamilyIndex = uint32_t(graphicsQueueFamilyIndex),
.pQueuePriorities = queuePriorities.data(),
.queueCount = 1,
},
};
auto featureChain = vk::StructureChain<
vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features,
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>{
{},
{.dynamicRendering = true},
{.extendedDynamicState = true},
};
auto extensions = std::vector<const char *>{
vk::KHRSwapchainExtensionName,
vk::KHRSpirv14ExtensionName,
vk::KHRSynchronization2ExtensionName,
vk::KHRCreateRenderpass2ExtensionName,
};
auto deviceCreateInfo = vk::DeviceCreateInfo{
.pQueueCreateInfos = queueCreateInfos.data(),
.queueCreateInfoCount = uint32_t(queueCreateInfos.size()),
.ppEnabledExtensionNames = extensions.data(),
.enabledExtensionCount = uint32_t(extensions.size()),
.pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
};
device_ = physicalDevice_.createDevice(deviceCreateInfo);
graphicsQueue_ = device_.getQueue(uint32_t(graphicsQueueFamilyIndex), 0);
presentQueue_ = device_.getQueue(uint32_t(graphicsQueueFamilyIndex), 0);
}
void Vulkan::createSwapchain() {
vk::SurfaceCapabilitiesKHR surfaceCapabilities
= physicalDevice_.getSurfaceCapabilitiesKHR(*surface_);
std::vector<vk::SurfaceFormatKHR> availableFormats
= physicalDevice_.getSurfaceFormatsKHR(*surface_);
std::vector<vk::PresentModeKHR> availablePresentModes
= physicalDevice_.getSurfacePresentModesKHR(*surface_);
// TODO: Make this selection smarter
vk::SurfaceFormatKHR surfaceFormat = availableFormats[0];
vk::PresentModeKHR presentMode = vk::PresentModeKHR::eFifo;
vk::Extent2D swapchainExtent = {
.height = uint32_t(height_),
.width = uint32_t(width_),
};
swapchainImageFormat_ = surfaceFormat.format;
swapchainExtent_ = swapchainExtent;
auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount);
minImageCount = (surfaceCapabilities.maxImageCount > 0
&& minImageCount > surfaceCapabilities.maxImageCount)
? surfaceCapabilities.maxImageCount
: minImageCount;
uint32_t imageCount = surfaceCapabilities.minImageCount + 1;
if (surfaceCapabilities.maxImageCount > 0
&& imageCount > surfaceCapabilities.maxImageCount) {
imageCount = surfaceCapabilities.maxImageCount;
}
auto swapchainCreateInfo = vk::SwapchainCreateInfoKHR{
.flags = vk::SwapchainCreateFlagsKHR(),
.surface = *surface_,
.minImageCount = minImageCount,
.imageFormat = surfaceFormat.format,
.imageColorSpace = surfaceFormat.colorSpace,
.imageExtent = swapchainExtent,
.imageArrayLayers = 1,
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
.imageSharingMode = vk::SharingMode::eExclusive,
.preTransform = surfaceCapabilities.currentTransform,
.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
.presentMode = presentMode,
.clipped = true,
.oldSwapchain = nullptr,
// Assuming graphics and present queues are the same
.queueFamilyIndexCount = 0,
.pQueueFamilyIndices = nullptr,
};
swapchain_ = device_.createSwapchainKHR(swapchainCreateInfo);
swapchainImages_ = swapchain_.getImages();
}
void Vulkan::createImageViews() {
swapchainImageViews_.clear();
auto imageViewCreateInfo = vk::ImageViewCreateInfo{
.viewType = vk::ImageViewType::e2D,
.format = swapchainImageFormat_,
.subresourceRange = vk::ImageSubresourceRange{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
for (auto &image : swapchainImages_) {
imageViewCreateInfo.image = image;
swapchainImageViews_.emplace_back(
device_.createImageView(imageViewCreateInfo));
}
}
void Vulkan::createRenderPass() {
auto colorAttachment = vk::AttachmentDescription{
.format = swapchainImageFormat_,
.samples = vk::SampleCountFlagBits::e1,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::ePresentSrcKHR,
};
auto colorAttachmentRef = vk::AttachmentReference{
.attachment = 0,
.layout = vk::ImageLayout::eColorAttachmentOptimal,
};
auto subpass = vk::SubpassDescription{
.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = 1,
.pColorAttachments = &colorAttachmentRef,
};
auto renderPassInfo = vk::RenderPassCreateInfo{
.attachmentCount = 1,
.pAttachments = &colorAttachment,
.subpassCount = 1,
.pSubpasses = &subpass,
};
renderPass_ = device_.createRenderPass(renderPassInfo);
}
void Vulkan::createGraphicsPipeline() {
auto root = std::filesystem::path(resourceRoot_);
auto shaderPath = root.concat(kShaderPath);
auto shaderBytecode = readBinaryFile(shaderPath);
vk::raii::ShaderModule shaderModule
= createShaderModule(device_, shaderBytecode);
auto vertShaderStageInfo = vk::PipelineShaderStageCreateInfo{
.stage = vk::ShaderStageFlagBits::eVertex,
.module = shaderModule,
.pName = "vertMain",
};
auto fragShaderStageInfo = vk::PipelineShaderStageCreateInfo{
.stage = vk::ShaderStageFlagBits::eFragment,
.module = shaderModule,
.pName = "fragMain",
};
vk::PipelineShaderStageCreateInfo shaderStages[] = {
vertShaderStageInfo,
fragShaderStageInfo,
};
auto vertexInputInfo = vk::PipelineVertexInputStateCreateInfo{};
auto inputAssemblyInfo = vk::PipelineInputAssemblyStateCreateInfo{
.topology = vk::PrimitiveTopology::eTriangleList,
};
std::vector dynamicStates
= {vk::DynamicState::eViewport, vk::DynamicState::eScissor};
auto dynamicState = vk::PipelineDynamicStateCreateInfo{
.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size()),
.pDynamicStates = dynamicStates.data(),
};
auto viewportCreateInfo = vk::PipelineViewportStateCreateInfo{
.viewportCount = 1,
// Dynamic state, set at draw time
.pScissors = nullptr,
// Dynamic state, set at draw time
.pViewports = nullptr,
};
auto rasteriser = vk::PipelineRasterizationStateCreateInfo{
.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,
};
auto multisampling = vk::PipelineMultisampleStateCreateInfo{
.rasterizationSamples = vk::SampleCountFlagBits::e1,
.sampleShadingEnable = vk::False,
};
auto rgbaWriteMask
= vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG
| vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
auto colorBlendAttachment = vk::PipelineColorBlendAttachmentState{
.colorWriteMask = rgbaWriteMask,
.blendEnable = vk::False,
};
auto colorBlending = vk::PipelineColorBlendStateCreateInfo{
.logicOpEnable = vk::False,
.logicOp = vk::LogicOp::eCopy,
.attachmentCount = 1,
.pAttachments = &colorBlendAttachment,
};
auto pipelineLayoutCreateInfo = vk::PipelineLayoutCreateInfo{
.setLayoutCount = 0,
.pushConstantRangeCount = 0,
};
pipelineLayout_ = device_.createPipelineLayout(pipelineLayoutCreateInfo);
// TODO: Check whether we need to specify the
// vk::PipelineRenderingCreateInfo here, or whether that's only when dynamic
// rendering is enabled.
auto pipelineInfo = vk::GraphicsPipelineCreateInfo{
.stageCount = 2,
.pStages = shaderStages,
.pVertexInputState = &vertexInputInfo,
.pInputAssemblyState = &inputAssemblyInfo,
.pViewportState = &viewportCreateInfo,
.pRasterizationState = &rasteriser,
.pMultisampleState = &multisampling,
.pColorBlendState = &colorBlending,
.layout = *pipelineLayout_,
// MoltenVK does not support the dynamic state extension
.pDynamicState = &dynamicState,
.renderPass = *renderPass_,
};
graphicsPipeline_ = device_.createGraphicsPipeline(nullptr, pipelineInfo);
}
void Vulkan::createFramebuffers() {
for (auto i = 0; i < swapchainImageViews_.size(); i++) {
vk::ImageView attachments[] = {
swapchainImageViews_[i],
};
auto framebufferInfo = vk::FramebufferCreateInfo{
.renderPass = *renderPass_,
.attachmentCount = 1,
.pAttachments = attachments,
.width = swapchainExtent_.width,
.height = swapchainExtent_.height,
.layers = 1,
};
swapchainFramebuffers_.emplace_back(
device_.createFramebuffer(framebufferInfo));
}
}
void Vulkan::createCommandPool() {
auto poolInfo = vk::CommandPoolCreateInfo{
.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
.queueFamilyIndex = uint32_t(graphicsQueueFamilyIndex_),
};
commandPool_ = device_.createCommandPool(poolInfo);
}
void Vulkan::createCommandBuffer() {
auto allocInfo = vk::CommandBufferAllocateInfo{
.commandPool = *commandPool_,
.level = vk::CommandBufferLevel::ePrimary,
.commandBufferCount = 1,
};
auto buffers = device_.allocateCommandBuffers(allocInfo);
commandBuffer_ = std::move(buffers[0]);
}
void Vulkan::recordCommandBuffer(uint32_t imageIndex) {
commandBuffer_.begin({});
// Begin render pass
transitionImageLayout(
imageIndex, vk::ImageLayout::eUndefined,
vk::ImageLayout::eColorAttachmentOptimal, {}, // srcAccessMask
vk::AccessFlagBits2::eColorAttachmentWrite,
vk::PipelineStageFlagBits2::eTopOfPipe,
vk::PipelineStageFlagBits2::eColorAttachmentOutput);
vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f);
auto renderPassInfo = vk::RenderPassBeginInfo{
.renderPass = *renderPass_,
.framebuffer = swapchainFramebuffers_[imageIndex],
.renderArea = vk::Rect2D{
.offset = vk::Offset2D{0, 0},
.extent = swapchainExtent_,
},
.clearValueCount = 1,
.pClearValues = &clearColor,
};
commandBuffer_.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
// Draw with pipeline
commandBuffer_.bindPipeline(
vk::PipelineBindPoint::eGraphics, *graphicsPipeline_);
vk::Viewport viewport = vk::Viewport{
.x = 0.0,
.y = 0.0,
.width = static_cast<float>(swapchainExtent_.width),
.height = static_cast<float>(swapchainExtent_.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
commandBuffer_.setViewport(0, viewport);
auto scissor = vk::Rect2D{
.extent = swapchainExtent_,
.offset = vk::Offset2D{0, 0},
};
commandBuffer_.setScissor(0, scissor);
commandBuffer_.draw(
3, // Vertex count
1, // Instance count
0, // First vertex
0 // First instance
);
commandBuffer_.endRenderPass();
transitionImageLayout(
imageIndex, vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::ePresentSrcKHR,
vk::AccessFlagBits2::eColorAttachmentWrite, {},
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::PipelineStageFlagBits2::eBottomOfPipe);
commandBuffer_.end();
}
void Vulkan::transitionImageLayout(
uint32_t imageIndex, vk::ImageLayout oldLayout, vk::ImageLayout newLayout,
vk::AccessFlags2 srcAccessMask, vk::AccessFlags2 dstAccessMask,
vk::PipelineStageFlags2 srcStageMask,
vk::PipelineStageFlags2 dstStageMask) {
auto barrier = vk::ImageMemoryBarrier2{
.srcStageMask = srcStageMask,
.srcAccessMask = srcAccessMask,
.dstStageMask = dstStageMask,
.dstAccessMask = dstAccessMask,
.oldLayout = oldLayout,
.newLayout = newLayout,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.image = swapchainImages_[imageIndex],
.subresourceRange = {
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
auto dependencyInfo = vk::DependencyInfo{
.dependencyFlags = {},
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &barrier,
};
commandBuffer_.pipelineBarrier2(dependencyInfo);
}
void Vulkan::createSyncObjects() {
presentCompleteSemaphore_ = device_.createSemaphore({});
renderFinishedSemaphore_ = device_.createSemaphore({});
auto drawFenceInfo = vk::FenceCreateInfo{
// Create as already signalled to unblock rendering of the first frame
.flags = vk::FenceCreateFlagBits::eSignaled,
};
drawFence_ = device_.createFence(drawFenceInfo);
}
void Vulkan::drawFrame() {
if (framebufferResized_) {
framebufferResized_ = false;
recreateSwapchain();
return;
}
while (vk::Result::eTimeout
== device_.waitForFences(*drawFence_, vk::True, UINT64_MAX)) {
}
auto [result, imageIndex] = swapchain_.acquireNextImage(
UINT64_MAX, *presentCompleteSemaphore_, nullptr);
if (result == vk::Result::eErrorOutOfDateKHR) {
recreateSwapchain();
return;
} else if (
result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) {
std::cerr << "failed to acquire swapchain image\n";
throw std::runtime_error("failed to acquire swapchain image");
}
device_.resetFences(*drawFence_);
commandBuffer_.reset();
recordCommandBuffer(imageIndex);
vk::PipelineStageFlags waitDestinationStageMask
= vk::PipelineStageFlagBits::eColorAttachmentOutput;
auto submitInfo = vk::SubmitInfo{
.waitSemaphoreCount = 1,
.pWaitSemaphores = &*presentCompleteSemaphore_,
.pWaitDstStageMask = &waitDestinationStageMask,
.commandBufferCount = 1,
.pCommandBuffers = &*commandBuffer_,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &*renderFinishedSemaphore_,
};
try {
graphicsQueue_.submit(submitInfo, *drawFence_);
} catch (const std::exception &ex) {
std::cerr << "failed to submit: " << ex.what() << std::endl;
throw ex;
}
auto presentInfo = vk::PresentInfoKHR{
.pWaitSemaphores = &*renderFinishedSemaphore_,
.waitSemaphoreCount = 1,
.swapchainCount = 1,
.pSwapchains = &*swapchain_,
.pImageIndices = &imageIndex,
.pResults = nullptr,
};
auto presentResult = presentQueue_.presentKHR(presentInfo);
if (presentResult == vk::Result::eErrorOutOfDateKHR
|| presentResult == vk::Result::eSuboptimalKHR || framebufferResized_) {
framebufferResized_ = false;
recreateSwapchain();
} else if (presentResult != vk::Result::eSuccess) {
std::cerr << "failed to present swapchain image: " << presentResult
<< std::endl;
throw new std::runtime_error("failed to present swapchain image");
}
}
void Vulkan::resize(int width, int height) {
width_ = width;
height_ = height;
framebufferResized_ = true;
}
void Vulkan::recreateSwapchain() {
device_.waitIdle();
// Cleanup
swapchainFramebuffers_.clear();
swapchainImageViews_.clear();
swapchain_ = nullptr;
// Recreate
createSwapchain();
createImageViews();
createFramebuffers();
}
vk::raii::ShaderModule createShaderModule(
vk::raii::Device &device, const std::vector<uint8_t> &spv) {
auto createInfo = vk::ShaderModuleCreateInfo{
.codeSize = spv.size(),
.pCode = reinterpret_cast<const uint32_t *>(spv.data()),
};
return device.createShaderModule(createInfo);
}
std::vector<std::uint8_t> readBinaryFile(const std::string &path) {
std::ifstream file = std::ifstream(path, std::ios::binary | std::ios::ate);
assert(!file.fail() && "failed to open file");
std::ifstream::pos_type size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buf(size);
file.read(reinterpret_cast<char *>(buf.data()), size);
return buf;
}
} // namespace vulkan
#pragma once
#include <cstddef>
#include <memory>
// Must come before other vulkan includes
#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS 1
#include "vulkan/vulkan_raii.hpp"
namespace vulkan {
class Vulkan {
public:
/**
* Create a Vulkan instance from a CAMetalLayer.
*/
explicit Vulkan(std::string resourceRoot, void* layer);
void drawFrame();
void resize(int width, int height);
private:
void createInstance();
void createSurface();
void pickPhysicalDevice();
void createLogicalDevice();
void createSwapchain();
void createImageViews();
void createRenderPass();
void createGraphicsPipeline();
void createFramebuffers();
void createCommandPool();
void createCommandBuffer();
void createSyncObjects();
void recreateSwapchain();
void recordCommandBuffer(uint32_t imageIndex);
void transitionImageLayout(
uint32_t imageIndex, vk::ImageLayout oldLayout, vk::ImageLayout newLayout,
vk::AccessFlags2 srcAccessMask, vk::AccessFlags2 dstAccessMask,
vk::PipelineStageFlags2 srcStageMask,
vk::PipelineStageFlags2 dstStageMask);
void* layer_;
std::string resourceRoot_;
// Account for device pixel ratio
int width_ = 1200 * 2;
int height_ = 800 * 2;
bool framebufferResized_ = false;
vk::raii::Instance instance_ = nullptr;
vk::raii::SurfaceKHR surface_ = nullptr;
vk::raii::PhysicalDevice physicalDevice_ = nullptr;
vk::raii::Device device_ = nullptr;
vk::raii::Queue graphicsQueue_ = nullptr;
vk::raii::Queue presentQueue_ = nullptr;
vk::raii::SwapchainKHR swapchain_ = nullptr;
std::vector<vk::Image> swapchainImages_;
std::vector<vk::raii::ImageView> swapchainImageViews_;
vk::raii::RenderPass renderPass_ = nullptr;
vk::raii::PipelineLayout pipelineLayout_ = nullptr;
vk::raii::Pipeline graphicsPipeline_ = nullptr;
std::vector<vk::raii::Framebuffer> swapchainFramebuffers_;
vk::raii::CommandPool commandPool_ = nullptr;
vk::raii::CommandBuffer commandBuffer_ = nullptr;
vk::raii::Semaphore presentCompleteSemaphore_ = nullptr;
vk::raii::Semaphore renderFinishedSemaphore_ = nullptr;
vk::raii::Fence drawFence_ = nullptr;
vk::Format swapchainImageFormat_;
vk::Extent2D swapchainExtent_;
int graphicsQueueFamilyIndex_ = -1;
};
} // namespace vulkan
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment