やっていくVulkan入門

4-3. スワップチェーンとレンダリング(準備編)

この節および次節では描画(レンダリング)を行います。3章まででレンダリングの基本については学んだと思うので、その復習も少ししましょう。

3章では自分で描画先となるイメージを準備してそこに描画しましたが、今回は少し違います。まずはじめに、スワップチェーンは自分でイメージを保持しています。我々はイメージを手づから作成する必要はなく、スワップチェーンが元から保持しているイメージを取得してそこに描画することになります。

スワップチェーンからイメージを取得した後は基本的に同じです。イメージからイメージビューを作成、レンダーパス・パイプラインなどの作成、フレームバッファを作成してイメージビューと紐づけ、コマンドバッファにコマンドを積んでキューに送信…あまりよく思い出せないという人は3章までを見返しましょう。


イメージとイメージビュー

まずはスワップチェーンのイメージを取得しましょう。これはgetSwapchainImagesKHRメソッドで行えます。スワップチェーン作成後の部分に以下のコードを追加してください。

std::vector<vk::Image> swapchainImages = device->getSwapchainImagesKHR(swapchain.get());

スワップチェーンは複数のイメージを持っているので、配列の形で取得することになります。Vulkan-Hppの場合はstd::vectorの形で出てきます。

こいつをもとにイメージビューを作成しましょう。3章では1枚だけでしたが、今回はイメージが複数あるのでそれぞれについて作成しなければなりません。

std::vector<vk::UniqueImageView> swapchainImageViews(swapchainImages.size());

for (size_t i = 0; i < swapchainImages.size(); i++) {
    vk::ImageViewCreateInfo imgViewCreateInfo;
    imgViewCreateInfo.image = swapchainImages[i];
    imgViewCreateInfo.viewType = vk::ImageViewType::e2D;
    imgViewCreateInfo.format = swapchainFormat.format;
    imgViewCreateInfo.components.r = vk::ComponentSwizzle::eIdentity;
    imgViewCreateInfo.components.g = vk::ComponentSwizzle::eIdentity;
    imgViewCreateInfo.components.b = vk::ComponentSwizzle::eIdentity;
    imgViewCreateInfo.components.a = vk::ComponentSwizzle::eIdentity;
    imgViewCreateInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    imgViewCreateInfo.subresourceRange.baseMipLevel = 0;
    imgViewCreateInfo.subresourceRange.levelCount = 1;
    imgViewCreateInfo.subresourceRange.baseArrayLayer = 0;
    imgViewCreateInfo.subresourceRange.layerCount = 1;

    swapchainImageViews[i] = device->createImageViewUnique(imgViewCreateInfo);
}

3章のときと大体同じですが、imgViewCreateInfo.format の部分に注目してください。

以前も説明したように、ここに指定するフォーマットは元となるイメージに合わせる必要があります。スワップチェーンのイメージの場合、イメージのフォーマットはスワップチェーンを作成する時に指定したフォーマットになるため、その値を指定しています。

swapchainFormatという変数を使って情報を保持していたのはこのためでした。


レンダーパスとパイプライン

イメージビューが作れたらフレームバッファを作りたいところですが、こちらはレンダーパスがないと作れないので、先にレンダーパスを作ってしまいましょう。ついでにパイプラインも作ります。

まずはレンダーパスから。

vk::AttachmentDescription attachments[1];
attachments[0].format = swapchainFormat.format;
attachments[0].samples = vk::SampleCountFlagBits::e1;
attachments[0].loadOp = vk::AttachmentLoadOp::eClear;
attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
attachments[0].initialLayout = vk::ImageLayout::eUndefined;
attachments[0].finalLayout = vk::ImageLayout::ePresentSrcKHR;

vk::AttachmentReference subpass0_attachmentRefs[1];
subpass0_attachmentRefs[0].attachment = 0;
subpass0_attachmentRefs[0].layout = vk::ImageLayout::eColorAttachmentOptimal;

vk::SubpassDescription subpasses[1];
subpasses[0].pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
subpasses[0].colorAttachmentCount = 1;
subpasses[0].pColorAttachments = subpass0_attachmentRefs;

vk::RenderPassCreateInfo renderpassCreateInfo;
renderpassCreateInfo.attachmentCount = 1;
renderpassCreateInfo.pAttachments = attachments;
renderpassCreateInfo.subpassCount = 1;
renderpassCreateInfo.pSubpasses = subpasses;
renderpassCreateInfo.dependencyCount = 0;
renderpassCreateInfo.pDependencies = nullptr;

vk::UniqueRenderPass renderpass = device->createRenderPassUnique(renderpassCreateInfo);

これも以前と大体同じですが、やはりformatにはイメージのフォーマット情報を指定する必要があります。

そしてもう一つ、今回重要なのがfinalLayoutです。これについては以前は特に説明せずvk::ImageLayout::eGeneralを指定していましたが、今回は vk::ImageLayout::ePresentSrcKHR を指定しています。

これは一体何なのかというと、イメージのメモリ上における配置方法・取扱い方に関する指定です。イメージレイアウトと言います。実に色々な設定があり、必要に応じて最適なものを指定しなければなりません。公式ドキュメントなどの資料を参考に正しいものを探すしかない面倒なところです。間違ったものを指定するとエラーが出る場合があります。

レンダーパスの設定によって、レンダリング処理が終わった後でどのようなイメージレイアウトにするかを決めることができます。今回はレンダリングが終わった後で表示(プレゼン)しなければならず、その場合はvk::ImageLayout::ePresentSrcKHRでなければならないという決まりなので今回これを指定しています。

ちなみにeGeneral形式のイメージは(最適ではないものの)一応どんな扱いを受けても基本的にはOKらしく、eGeneralのままでも動くには動きます。でもやめておいた方が無難です。


パイプラインも作成します。ここは特に変更点はありません。シェーダの準備も忘れないように。

vk::Viewport viewports[1];
viewports[0].x = 0.0;
viewports[0].y = 0.0;
viewports[0].minDepth = 0.0;
viewports[0].maxDepth = 1.0;
viewports[0].width = screenWidth;
viewports[0].height = screenHeight;

vk::Rect2D scissors[1];
scissors[0].offset = vk::Offset2D{ 0, 0 };
scissors[0].extent = vk::Extent2D{ screenWidth, screenHeight };

vk::PipelineViewportStateCreateInfo viewportState;
viewportState.viewportCount = 1;
viewportState.pViewports = viewports;
viewportState.scissorCount = 1;
viewportState.pScissors = scissors;

vk::PipelineVertexInputStateCreateInfo vertexInputInfo;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr;

vk::PipelineInputAssemblyStateCreateInfo inputAssembly;
inputAssembly.topology = vk::PrimitiveTopology::eTriangleList;
inputAssembly.primitiveRestartEnable = false;

vk::PipelineRasterizationStateCreateInfo rasterizer;
rasterizer.depthClampEnable = false;
rasterizer.rasterizerDiscardEnable = false;
rasterizer.polygonMode = vk::PolygonMode::eFill;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = vk::CullModeFlagBits::eBack;
rasterizer.frontFace = vk::FrontFace::eClockwise;
rasterizer.depthBiasEnable = false;

vk::PipelineMultisampleStateCreateInfo multisample;
multisample.sampleShadingEnable = false;
multisample.rasterizationSamples = vk::SampleCountFlagBits::e1;

vk::PipelineColorBlendAttachmentState blendattachment[1];
blendattachment[0].colorWriteMask =
    vk::ColorComponentFlagBits::eA |
    vk::ColorComponentFlagBits::eR |
    vk::ColorComponentFlagBits::eG |
    vk::ColorComponentFlagBits::eB;
blendattachment[0].blendEnable = false;

vk::PipelineColorBlendStateCreateInfo blend;
blend.logicOpEnable = false;
blend.attachmentCount = 1;
blend.pAttachments = blendattachment;

vk::PipelineLayoutCreateInfo layoutCreateInfo;
layoutCreateInfo.setLayoutCount = 0;
layoutCreateInfo.pSetLayouts = nullptr;

vk::UniquePipelineLayout pipelineLayout = device->createPipelineLayoutUnique(layoutCreateInfo);

size_t vertSpvFileSz = std::filesystem::file_size("shader.vert.spv");

std::ifstream vertSpvFile("shader.vert.spv", std::ios_base::binary);

std::vector<char> vertSpvFileData(vertSpvFileSz);
vertSpvFile.read(vertSpvFileData.data(), vertSpvFileSz);

vk::ShaderModuleCreateInfo vertShaderCreateInfo;
vertShaderCreateInfo.codeSize = vertSpvFileSz;
vertShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(vertSpvFileData.data());

vk::UniqueShaderModule vertShader = device->createShaderModuleUnique(vertShaderCreateInfo);

size_t fragSpvFileSz = std::filesystem::file_size("shader.frag.spv");

std::ifstream fragSpvFile("shader.frag.spv", std::ios_base::binary);

std::vector<char> fragSpvFileData(fragSpvFileSz);
fragSpvFile.read(fragSpvFileData.data(), fragSpvFileSz);

vk::ShaderModuleCreateInfo fragShaderCreateInfo;
fragShaderCreateInfo.codeSize = fragSpvFileSz;
fragShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(fragSpvFileData.data());

vk::UniqueShaderModule fragShader = device->createShaderModuleUnique(fragShaderCreateInfo);

vk::PipelineShaderStageCreateInfo shaderStage[2];
shaderStage[0].stage = vk::ShaderStageFlagBits::eVertex;
shaderStage[0].module = vertShader.get();
shaderStage[0].pName = "main";
shaderStage[1].stage = vk::ShaderStageFlagBits::eFragment;
shaderStage[1].module = fragShader.get();
shaderStage[1].pName = "main";

vk::GraphicsPipelineCreateInfo pipelineCreateInfo;
pipelineCreateInfo.pViewportState = &viewportState;
pipelineCreateInfo.pVertexInputState = &vertexInputInfo;
pipelineCreateInfo.pInputAssemblyState = &inputAssembly;
pipelineCreateInfo.pRasterizationState = &rasterizer;
pipelineCreateInfo.pMultisampleState = &multisample;
pipelineCreateInfo.pColorBlendState = &blend;
pipelineCreateInfo.layout = pipelineLayout.get();
pipelineCreateInfo.renderPass = renderpass.get();
pipelineCreateInfo.subpass = 0;
pipelineCreateInfo.stageCount = 2;
pipelineCreateInfo.pStages = shaderStage;

vk::UniquePipeline pipeline = device->createGraphicsPipelineUnique(nullptr, pipelineCreateInfo).value;

フレームバッファ

さてフレームバッファを作成しましょう。

フレームバッファはレンダーパスのアタッチメントとイメージビューを対応付けるものでした。今回は画面へ表示するにあたり、スワップチェーンの各イメージビューに描画していきます。

ということは、フレームバッファは1つだけではなくイメージビューごとに作成する必要があります。面倒ですが枚数分ちゃんと作りましょう。

std::vector<vk::UniqueFramebuffer> swapchainFramebufs(swapchainImages.size());

for (size_t i = 0; i < swapchainImages.size(); i++) {
    vk::ImageView frameBufAttachments[1];
    frameBufAttachments[0] = swapchainImageViews[i].get();

    vk::FramebufferCreateInfo frameBufCreateInfo;
    frameBufCreateInfo.width = surfaceCapabilities.currentExtent.width;
    frameBufCreateInfo.height = surfaceCapabilities.currentExtent.height;
    frameBufCreateInfo.layers = 1;
    frameBufCreateInfo.renderPass = renderpass.get();
    frameBufCreateInfo.attachmentCount = 1;
    frameBufCreateInfo.pAttachments = frameBufAttachments;

    swapchainFramebufs[i] = device->createFramebufferUnique(frameBufCreateInfo);
}

widthheightcurrentExtentから取ってきている部分にだけ少し注意しましょう。後々画面のリサイズ処理に対応するときにこうしないと問題になります。


コマンドバッファ

さて次は、描画コマンドを記録するためのコマンドバッファ(およびそのためのコマンドプール)を作成します。ほとんど2章と変わらないのですが、ひとつ問題があります。

私たちはこれから毎フレーム、スワップチェーンの各イメージに代わる代わる描画処理を行っていくことになります。しかし3章のコードを精読すればわかるように、コマンドバッファに記録するコマンドには「どのフレームバッファを使って描画するか」という情報が内包されています

vk::CommandBufferBeginInfo cmdBeginInfo;
cmdBufs[0]->begin(cmdBeginInfo);

// (中略)

vk::RenderPassBeginInfo renderpassBeginInfo;
renderpassBeginInfo.renderPass = renderpass.get();
renderpassBeginInfo.framebuffer = frameBuf.get();   // フレームバッファを指定している
renderpassBeginInfo.renderArea = vk::Rect2D({ 0,0 }, { screenWidth, screenHeight });
renderpassBeginInfo.clearValueCount = 1;
renderpassBeginInfo.pClearValues = clearVal;

// (中略)

cmdBufs[0]->end();

もしも固定の内容のコマンドバッファを使ってしまうと、毎回同じフレームバッファが使われることになります。毎回同じフレームバッファということは、毎回同じイメージに向けて描画することになってしまいます。これではスワップチェーンによる表示処理がうまくいきません。

そこで毎フレームコマンドバッファをリセットして、コマンドを記録し直すことにします。 こうすることで毎回別のイメージに向けて描画できます。

リセット出来るようにするためには少し追加の設定が必要です。

vk::CommandPoolCreateInfo cmdPoolCreateInfo;
cmdPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
cmdPoolCreateInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;   // 追加部分
vk::UniqueCommandPool cmdPool = device->createCommandPoolUnique(cmdPoolCreateInfo);

コマンドプール作成時、vk::CommandPoolCreateInfo::flagsvk::CommandPoolCreateFlagBits::eResetCommandBufferを指定すると、そのコマンドプールから作成したコマンドバッファはリセット可能になります。

なお、リセットの方法はコマンドバッファのreset()メソッドを呼び出すだけです。

cmdBufs[0]->reset();

以上を踏まえながら、コマンドプールの作成~コマンドバッファの作成までの処理を追加しましょう。

// コマンドプールの作成
vk::CommandPoolCreateInfo cmdPoolCreateInfo;
cmdPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
cmdPoolCreateInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
vk::UniqueCommandPool cmdPool = device->createCommandPoolUnique(cmdPoolCreateInfo);

// コマンドバッファの作成
vk::CommandBufferAllocateInfo cmdBufAllocInfo;
cmdBufAllocInfo.commandPool = cmdPool.get();
cmdBufAllocInfo.commandBufferCount = 1;
cmdBufAllocInfo.level = vk::CommandBufferLevel::ePrimary;
std::vector<vk::UniqueCommandBuffer> cmdBufs =
    device->allocateCommandBuffersUnique(cmdBufAllocInfo);

今はまだ追加しませんが、描画ループの部分は大枠としてこんな感じになります。

while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();

    // コマンドバッファのリセット
    cmdBufs[0]->reset();

    // コマンドの記録
    vk::CommandBufferBeginInfo cmdBeginInfo;
    cmdBufs[0]->begin();

        // 描画処理(後で説明)

    cmdBufs[0]->end();

    // コマンドバッファの送信
    vk::CommandBuffer submitCmdBuf[1] = { cmdBufs[0].get() };
    vk::SubmitInfo submitInfo;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = submitCmdBuf;
    graphicsQueue.submit({ submitInfo }, nullptr);
    graphicsQueue.waitIdle();

    // 表示処理(後で説明)
}

コマンドバッファを毎フレーム記録しなおせるようにするのは、描画対象のイメージを切り替えるという以外にも意味があります。今は単なる三角形の表示ですが、動くアニメーションになれば毎フレーム描画内容が変わります。そこでフレーム毎にコマンド(描画命令)を変えるのは自然なことでしょう。


これでようやく準備が整いました。描画処理を書いていきましょう...と言いたいところですが、ここで一つ問題があります。

イメージは複数枚あり、スワップチェーンがそれをフレーム毎に切り替えていきます。私たちはどのタイミングで、どのイメージにレンダリングすれば良いのでしょうか。

これについて次節で解説します。


この節ではスワップチェーンのイメージの取得、およびイメージへのレンダリングの準備を行いました。

次回は実際にレンダリングを行います。(表示までは行きません。)

この節のコード
#include <iostream>
#include <fstream>
#include <filesystem>
#include <vulkan/vulkan.hpp>
#include <GLFW/glfw3.h>

const uint32_t screenWidth = 640;
const uint32_t screenHeight = 480;

int main() {
    if (!glfwInit())
        return -1;

    uint32_t requiredExtensionsCount;
    const char** requiredExtensions = glfwGetRequiredInstanceExtensions(&requiredExtensionsCount);

    vk::InstanceCreateInfo createInfo;
    createInfo.enabledExtensionCount = requiredExtensionsCount;
    createInfo.ppEnabledExtensionNames = requiredExtensions;

    vk::UniqueInstance instance;
    instance = vk::createInstanceUnique(createInfo);

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    GLFWwindow* window;
    window = glfwCreateWindow(screenWidth, screenHeight, "GLFW Test Window", NULL, NULL);
    if (!window) {
        const char* err;
        glfwGetError(&err);
        std::cout << err << std::endl;
        glfwTerminate();
        return -1;
    }

    VkSurfaceKHR c_surface;
    auto result = glfwCreateWindowSurface(instance.get(), window, nullptr, &c_surface);
    if (result != VK_SUCCESS) {
        const char* err;
        glfwGetError(&err);
        std::cout << err << std::endl;
        glfwTerminate();
        return -1;
    }
    vk::UniqueSurfaceKHR surface{ c_surface, instance.get() };

    std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices();

    vk::PhysicalDevice physicalDevice;
    bool existsSuitablePhysicalDevice = false;
    uint32_t graphicsQueueFamilyIndex;

    for (size_t i = 0; i < physicalDevices.size(); i++) {
        std::vector<vk::QueueFamilyProperties> queueProps = physicalDevices[i].getQueueFamilyProperties();
        bool existsGraphicsQueue = false;

        for (size_t j = 0; j < queueProps.size(); j++) {
            if (queueProps[j].queueFlags & vk::QueueFlagBits::eGraphics) {
                existsGraphicsQueue = true;
                graphicsQueueFamilyIndex = j;
                break;
            }
        }

        std::vector<vk::ExtensionProperties> extProps = physicalDevices[i].enumerateDeviceExtensionProperties();
        bool supportsSwapchainExtension = false;

        for (size_t j = 0; j < extProps.size(); j++) {
            if (std::string_view(extProps[j].extensionName.data()) == VK_KHR_SWAPCHAIN_EXTENSION_NAME) {
                supportsSwapchainExtension = true;
                break;
            }
        }

        if (existsGraphicsQueue && supportsSwapchainExtension) {
            physicalDevice = physicalDevices[i];
            existsSuitablePhysicalDevice = true;
            break;
        }
    }

    if (!existsSuitablePhysicalDevice) {
        std::cerr << "使用可能な物理デバイスがありません。" << std::endl;
        return -1;
    }

    vk::DeviceCreateInfo devCreateInfo;

    auto devRequiredExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

    devCreateInfo.enabledExtensionCount = devRequiredExtensions.size();
    devCreateInfo.ppEnabledExtensionNames = devRequiredExtensions.begin();

    vk::DeviceQueueCreateInfo queueCreateInfo[1];
    queueCreateInfo[0].queueFamilyIndex = graphicsQueueFamilyIndex;
    queueCreateInfo[0].queueCount = 1;

    float queuePriorities[1] = { 1.0 };

    queueCreateInfo[0].pQueuePriorities = queuePriorities;

    devCreateInfo.pQueueCreateInfos = queueCreateInfo;
    devCreateInfo.queueCreateInfoCount = 1;

    vk::UniqueDevice device = physicalDevice.createDeviceUnique(devCreateInfo);

    vk::Queue graphicsQueue = device->getQueue(graphicsQueueFamilyIndex, 0);

    vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface.get());
    std::vector<vk::SurfaceFormatKHR> surfaceFormats = physicalDevice.getSurfaceFormatsKHR(surface.get());
    std::vector<vk::PresentModeKHR> surfacePresentModes = physicalDevice.getSurfacePresentModesKHR(surface.get());

    vk::SurfaceFormatKHR swapchainFormat = surfaceFormats[0];
    vk::PresentModeKHR swapchainPresentMode = surfacePresentModes[0];

    vk::SwapchainCreateInfoKHR swapchainCreateInfo;
    swapchainCreateInfo.surface = surface.get();
    swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + 1;
    swapchainCreateInfo.imageFormat = swapchainFormat.format;
    swapchainCreateInfo.imageColorSpace = swapchainFormat.colorSpace;
    swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;
    swapchainCreateInfo.imageArrayLayers = 1;
    swapchainCreateInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment;
    swapchainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive;
    swapchainCreateInfo.preTransform = surfaceCapabilities.currentTransform;
    swapchainCreateInfo.presentMode = swapchainPresentMode;
    swapchainCreateInfo.clipped = VK_TRUE;

    vk::UniqueSwapchainKHR swapchain = device->createSwapchainKHRUnique(swapchainCreateInfo);

    std::vector<vk::Image> swapchainImages = device->getSwapchainImagesKHR(swapchain.get());

    vk::AttachmentDescription attachments[1];
    attachments[0].format = swapchainFormat.format;
    attachments[0].samples = vk::SampleCountFlagBits::e1;
    attachments[0].loadOp = vk::AttachmentLoadOp::eClear;
    attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
    attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
    attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
    attachments[0].initialLayout = vk::ImageLayout::eUndefined;
    attachments[0].finalLayout = vk::ImageLayout::ePresentSrcKHR;

    vk::AttachmentReference subpass0_attachmentRefs[1];
    subpass0_attachmentRefs[0].attachment = 0;
    subpass0_attachmentRefs[0].layout = vk::ImageLayout::eColorAttachmentOptimal;

    vk::SubpassDescription subpasses[1];
    subpasses[0].pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
    subpasses[0].colorAttachmentCount = 1;
    subpasses[0].pColorAttachments = subpass0_attachmentRefs;

    vk::RenderPassCreateInfo renderpassCreateInfo;
    renderpassCreateInfo.attachmentCount = 1;
    renderpassCreateInfo.pAttachments = attachments;
    renderpassCreateInfo.subpassCount = 1;
    renderpassCreateInfo.pSubpasses = subpasses;
    renderpassCreateInfo.dependencyCount = 0;
    renderpassCreateInfo.pDependencies = nullptr;

    vk::UniqueRenderPass renderpass = device->createRenderPassUnique(renderpassCreateInfo);

    vk::Viewport viewports[1];
    viewports[0].x = 0.0;
    viewports[0].y = 0.0;
    viewports[0].minDepth = 0.0;
    viewports[0].maxDepth = 1.0;
    viewports[0].width = screenWidth;
    viewports[0].height = screenHeight;

    vk::Rect2D scissors[1];
    scissors[0].offset = vk::Offset2D{ 0, 0 };
    scissors[0].extent = vk::Extent2D{ screenWidth, screenHeight };

    vk::PipelineViewportStateCreateInfo viewportState;
    viewportState.viewportCount = 1;
    viewportState.pViewports = viewports;
    viewportState.scissorCount = 1;
    viewportState.pScissors = scissors;

    vk::PipelineVertexInputStateCreateInfo vertexInputInfo;
    vertexInputInfo.vertexAttributeDescriptionCount = 0;
    vertexInputInfo.pVertexAttributeDescriptions = nullptr;
    vertexInputInfo.vertexBindingDescriptionCount = 0;
    vertexInputInfo.pVertexBindingDescriptions = nullptr;

    vk::PipelineInputAssemblyStateCreateInfo inputAssembly;
    inputAssembly.topology = vk::PrimitiveTopology::eTriangleList;
    inputAssembly.primitiveRestartEnable = false;

    vk::PipelineRasterizationStateCreateInfo rasterizer;
    rasterizer.depthClampEnable = false;
    rasterizer.rasterizerDiscardEnable = false;
    rasterizer.polygonMode = vk::PolygonMode::eFill;
    rasterizer.lineWidth = 1.0f;
    rasterizer.cullMode = vk::CullModeFlagBits::eBack;
    rasterizer.frontFace = vk::FrontFace::eClockwise;
    rasterizer.depthBiasEnable = false;

    vk::PipelineMultisampleStateCreateInfo multisample;
    multisample.sampleShadingEnable = false;
    multisample.rasterizationSamples = vk::SampleCountFlagBits::e1;

    vk::PipelineColorBlendAttachmentState blendattachment[1];
    blendattachment[0].colorWriteMask =
        vk::ColorComponentFlagBits::eA |
        vk::ColorComponentFlagBits::eR |
        vk::ColorComponentFlagBits::eG |
        vk::ColorComponentFlagBits::eB;
    blendattachment[0].blendEnable = false;

    vk::PipelineColorBlendStateCreateInfo blend;
    blend.logicOpEnable = false;
    blend.attachmentCount = 1;
    blend.pAttachments = blendattachment;

    vk::PipelineLayoutCreateInfo layoutCreateInfo;
    layoutCreateInfo.setLayoutCount = 0;
    layoutCreateInfo.pSetLayouts = nullptr;

    vk::UniquePipelineLayout pipelineLayout = device->createPipelineLayoutUnique(layoutCreateInfo);

    size_t vertSpvFileSz = std::filesystem::file_size("shader.vert.spv");

    std::ifstream vertSpvFile("shader.vert.spv", std::ios_base::binary);

    std::vector<char> vertSpvFileData(vertSpvFileSz);
    vertSpvFile.read(vertSpvFileData.data(), vertSpvFileSz);

    vk::ShaderModuleCreateInfo vertShaderCreateInfo;
    vertShaderCreateInfo.codeSize = vertSpvFileSz;
    vertShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(vertSpvFileData.data());

    vk::UniqueShaderModule vertShader = device->createShaderModuleUnique(vertShaderCreateInfo);

    size_t fragSpvFileSz = std::filesystem::file_size("shader.frag.spv");

    std::ifstream fragSpvFile("shader.frag.spv", std::ios_base::binary);

    std::vector<char> fragSpvFileData(fragSpvFileSz);
    fragSpvFile.read(fragSpvFileData.data(), fragSpvFileSz);

    vk::ShaderModuleCreateInfo fragShaderCreateInfo;
    fragShaderCreateInfo.codeSize = fragSpvFileSz;
    fragShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(fragSpvFileData.data());

    vk::UniqueShaderModule fragShader = device->createShaderModuleUnique(fragShaderCreateInfo);

    vk::PipelineShaderStageCreateInfo shaderStage[2];
    shaderStage[0].stage = vk::ShaderStageFlagBits::eVertex;
    shaderStage[0].module = vertShader.get();
    shaderStage[0].pName = "main";
    shaderStage[1].stage = vk::ShaderStageFlagBits::eFragment;
    shaderStage[1].module = fragShader.get();
    shaderStage[1].pName = "main";

    vk::GraphicsPipelineCreateInfo pipelineCreateInfo;
    pipelineCreateInfo.pViewportState = &viewportState;
    pipelineCreateInfo.pVertexInputState = &vertexInputInfo;
    pipelineCreateInfo.pInputAssemblyState = &inputAssembly;
    pipelineCreateInfo.pRasterizationState = &rasterizer;
    pipelineCreateInfo.pMultisampleState = &multisample;
    pipelineCreateInfo.pColorBlendState = &blend;
    pipelineCreateInfo.layout = pipelineLayout.get();
    pipelineCreateInfo.renderPass = renderpass.get();
    pipelineCreateInfo.subpass = 0;
    pipelineCreateInfo.stageCount = 2;
    pipelineCreateInfo.pStages = shaderStage;

    vk::UniquePipeline pipeline = device->createGraphicsPipelineUnique(nullptr, pipelineCreateInfo).value;

    std::vector<vk::UniqueImageView> swapchainImageViews(swapchainImages.size());

    for (size_t i = 0; i < swapchainImages.size(); i++) {
        vk::ImageViewCreateInfo imgViewCreateInfo;
        imgViewCreateInfo.image = swapchainImages[i];
        imgViewCreateInfo.viewType = vk::ImageViewType::e2D;
        imgViewCreateInfo.format = swapchainFormat.format;
        imgViewCreateInfo.components.r = vk::ComponentSwizzle::eIdentity;
        imgViewCreateInfo.components.g = vk::ComponentSwizzle::eIdentity;
        imgViewCreateInfo.components.b = vk::ComponentSwizzle::eIdentity;
        imgViewCreateInfo.components.a = vk::ComponentSwizzle::eIdentity;
        imgViewCreateInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
        imgViewCreateInfo.subresourceRange.baseMipLevel = 0;
        imgViewCreateInfo.subresourceRange.levelCount = 1;
        imgViewCreateInfo.subresourceRange.baseArrayLayer = 0;
        imgViewCreateInfo.subresourceRange.layerCount = 1;

        swapchainImageViews[i] = device->createImageViewUnique(imgViewCreateInfo);
    }

    std::vector<vk::UniqueFramebuffer> swapchainFramebufs(swapchainImages.size());

    for (size_t i = 0; i < swapchainImages.size(); i++) {
        vk::ImageView frameBufAttachments[1];
        frameBufAttachments[0] = swapchainImageViews[i].get();

        vk::FramebufferCreateInfo frameBufCreateInfo;
        frameBufCreateInfo.width = surfaceCapabilities.currentExtent.width;
        frameBufCreateInfo.height = surfaceCapabilities.currentExtent.height;
        frameBufCreateInfo.layers = 1;
        frameBufCreateInfo.renderPass = renderpass.get();
        frameBufCreateInfo.attachmentCount = 1;
        frameBufCreateInfo.pAttachments = frameBufAttachments;

        swapchainFramebufs[i] = device->createFramebufferUnique(frameBufCreateInfo);
    }

    vk::CommandPoolCreateInfo cmdPoolCreateInfo;
    cmdPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
    cmdPoolCreateInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
    vk::UniqueCommandPool cmdPool = device->createCommandPoolUnique(cmdPoolCreateInfo);

    vk::CommandBufferAllocateInfo cmdBufAllocInfo;
    cmdBufAllocInfo.commandPool = cmdPool.get();
    cmdBufAllocInfo.commandBufferCount = 1;
    cmdBufAllocInfo.level = vk::CommandBufferLevel::ePrimary;
    std::vector<vk::UniqueCommandBuffer> cmdBufs =
        device->allocateCommandBuffersUnique(cmdBufAllocInfo);

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

#version 450
#extension GL_ARB_separate_shader_objects : enable

void main() {
    if(gl_VertexIndex == 0) {
        gl_Position = vec4(0.0, -0.5, 0.0, 1.0);
    } else if(gl_VertexIndex == 1) {
        gl_Position = vec4(0.5, 0.5, 0.0, 1.0);
    } else if(gl_VertexIndex == 2) {
        gl_Position = vec4(-0.5, 0.5, 0.0, 1.0);
    }
}
#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
cmake_minimum_required(VERSION 3.22)

project(vulkan-test)

set(CMAKE_CXX_STANDARD 17)

add_executable(app main.cpp)

find_package(Vulkan REQUIRED)
target_include_directories(app PRIVATE ${Vulkan_INCLUDE_DIRS})
target_link_libraries(app PRIVATE ${Vulkan_LIBRARIES})

find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(app PRIVATE glfw)