GLFW kullanarak Vulkan surface ve swapchain oluşturma

Example: VulkanExamples/p01-surface

Bu, küçük ve ayrı ayrı doğrulanabilir adımlarla karmaşıklık zincirinde yükselen bir Vulkan serisinin ilk postu. Aşağıdaki her örnek tam, derlenebilir bir program — ve her biri tek bir kareyi ekran dışında bir PPM dosyasına render eden --headless modu ile gerçek piksel değerlerini assert eden küçük bir Python kontrolü içerir.

Burada hâlâ piksel üreten mutlak minimumu yapıyoruz: bir GLFW penceresi, ona bağlı bir VkSurfaceKHR, bir swapchain ve kare başına bir vkCmdClearColorImage — bilinçli olarak graphics pipeline yok, böylece odaktaki tek şey surface/swapchain mekanizması.

Headless kontrol tarafından render edilen düz mavi kare

Nasıl çalışır

Bootstrap zincirinin katı bir sırası var ve her adım bir sonrakinin ihtiyaç duyduğu soruyu yanıtlar:

  1. API context’siz GLFW penceresi. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API) çıplak bir pencere oluşturur — GL/GLES context yok. Vulkan yalnızca platform pencere handle’ına ihtiyaç duyar.
  2. GLFW’nin gerekli uzantılarıyla instance. Vulkan instance’ı pencere sisteminden bağımsızdır; WSI tutkalı uzantılarda yaşar. Hangileri (örn. VK_KHR_surface + VK_KHR_xcb_surface) platforma bağlı — glfwGetRequiredInstanceExtensions() vk::createInstance‘a geçilecek listeyi tam olarak döndürür.
  3. glfwCreateWindowSurface → VkSurfaceKHR. Postun konusu bu adım. Surface, “o pencerenin çizilebilir bölgesi” için opak bir handle. Surface’e asla doğrudan render edilmez — bir swapchain’in ona bağlanabilmesi için var olur.
  4. VK_KHR_swapchain‘li device. Present etmek device-seviyesinde bir özellik, bu yüzden logical device swapchain uzantısını etkinleştirmeli. Ayrıca getSurfaceSupportKHR(queueFamily, surface)‘i doğrula — her queue family her surface’e present edemez.
  5. Swapchain müzakeresi. getSurfaceCapabilitiesKHR + getSurfaceFormatsKHR neyin yasal olduğunu söyler. Aşağıdaki koda gömülü iki gerçek-dünya tuzağı:
    • currentExtent == 0xFFFFFFFF “surface bir boyut dikte etmiyor” demektir — min/maxImageExtent içine kısıtlanmış GLFW framebuffer boyutunu sağlamalısın. Sentinel’i olduğu gibi geçmek ~4 milyar piksellik bir swapchain yaratır → anında VK_ERROR_OUT_OF_HOST_MEMORY.
    • maxImageCount == 0 “sınırsız” demektir, “sıfır” değil.
  6. Pipeline’sız render. vkCmdClearColorImage bir image’ı doğrudan doldurur — yalnızca eTransferDst usage gerekir, bu yüzden swapchain eColorAttachment | eTransferDst ile oluşturulur. Kare başına: image acquire → eTransferDstOptimal‘a geçiş → clear → ePresentSrcKHR‘ye geçiş → present. Tek bir shader olmadan ekranda gerçek bir mavi/kırmızı değişen kare titremesi elde edersin.

Örnekler küçük bir helper header’ı paylaşır, vkmini.hpp (instance/ device/bellek/buffer/image/tek-seferlik-komutlar/shaderc tutkalı artı kontrollerin kullandığı Headless readback hedefi). Altta tam olarak dahil; serinin sonraki postları onu değiştirmeden yeniden kullanır.

Derle ve çalıştır

build.sh
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app                    # windowed: alternating blue/red for ~2s
./app --headless out.ppm # offscreen: one blue frame -> out.ppm
python3 check.py out.ppm # assert the pixels are exactly #3366CC

vkmini.hpp helper’ı (aşağıda dahil) sonraki postlar GLSL’i runtime’da SPIR-V’ye derlediği için shaderc’yi bağlar — bu post henüz shader kullanmıyor. Debian/Ubuntu paketleri: apt install libvulkan-dev libglfw3-dev libshaderc-dev.

Kontrol

check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# Headless render'ın düz (51,102,204) mavi bir 640x480 image
# ürettiğini assert eder — 4x4 grid artı tam merkez satırı örnekler.
import sys


def read_ppm(path):
    d = open(path, "rb").read()
    # "255\n" ile böl: whitespace-tokenize etmek \r/\n/boşluk olan
    # önde gelen piksel baytlarını yerdi.
    hdr, data = d.split(b"255\n", 1)
    toks = hdr.split()
    assert toks[0] == b"P6", "not a binary PPM"
    w, h = int(toks[1]), int(toks[2])
    assert len(data) == w * h * 3, "truncated pixel data"
    return w, h, data


def px(data, w, x, y):
    i = (y * w + x) * 3
    return tuple(data[i:i + 3])


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "out.ppm"
    w, h, d = read_ppm(path)
    assert (w, h) == (640, 480), f"unexpected size {w}x{h}"
    expected = (51, 102, 204)  # main.cpp'de temizlenen mavi
    for gy in range(4):
        for gx in range(4):
            p = px(d, w, 80 + gx * 160, 60 + gy * 120)
            assert p == expected, f"pixel({gx},{gy})={p}"
    row = {px(d, w, x, h // 2) for x in range(w)}
    assert row == {expected}, f"centre row not uniform: {row}"
    print(f"OK: {w}x{h} uniform {expected}")


if __name__ == "__main__":
    main()

Çalıştırma:

output.txt
$ ./app --headless out.ppm && python3 check.py out.ppm
device: AMD Radeon RX 7800 XT (RADV NAVI32)
wrote out.ppm
OK: 640x480 uniform (51, 102, 204)

Tam kaynak

main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// Post 1: GLFW üzerinden Vulkan surface, üzerinde swapchain oluştur
// ve her kareyi düz renge temizleyerek çalıştığını göster.
//
//   ./app              windowed: clears the window, alternating color
//   ./app --headless o.ppm   offscreen: same clear, dumped to a PPM
//
// Derle:  g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc

#include "vkmini.hpp"

#include <GLFW/glfw3.h>

int main(int argc, char** argv) {
    bool headless = argc > 1 && std::string(argv[1]) == "--headless";
    const char* ppmOut = argc > 2 ? argv[2] : "out.ppm";

    // ---- 1. GLFW penceresi ----------------------------------------------
    // GLFW_NO_API: GLFW bir OpenGL context'i oluşturmamalı — pencere
    // Vulkan'a verdiğimiz sadece bir platform handle'ı.
    if (!headless) {
        if (!glfwInit()) throw std::runtime_error("glfwInit failed");
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    }

    // ---- 2. Instance -----------------------------------------------------
    // Surface uzantısı (VK_KHR_surface + VK_KHR_xcb_surface gibi bir
    // platform WSI uzantısı) pencere sistemi tarafından dikte edilir —
    // GLFW hangi instance uzantılarına ihtiyaç duyduğunu tam olarak
    // söyler.
    vkmini::Vk vk;
    std::vector<const char*> instExts;
    if (!headless) {
        uint32_t n = 0;
        const char** glfwExt = glfwGetRequiredInstanceExtensions(&n);
        instExts.assign(glfwExt, glfwExt + n);
    }
    vk.createInstance(instExts);

    // ---- 3. Surface'in kendisi -------------------------------------------
    // glfwCreateWindowSurface arka planda doğru VK_KHR_*_surface
    // uzantısını seçer ve bir VkSurfaceKHR döndürür — "bu pencerenin
    // çizilebilir alanı" için opak bir handle. Surface'e doğrudan hiçbir
    // şey render edilmez; yalnızca bir *swapchain* ona bağlanabilsin
    // diye var olur.
    GLFWwindow* win = nullptr;
    vk::UniqueSurfaceKHR surface;
    if (!headless) {
        win = glfwCreateWindow(640, 480, "vkmini p01", nullptr, nullptr);
        VkSurfaceKHR raw;
        if (glfwCreateWindowSurface(vk.instance.get(), win, nullptr,
                                    &raw) != VK_SUCCESS)
            throw std::runtime_error("surface creation failed");
        surface = vk::UniqueSurfaceKHR(raw, vk.instance.get());
    }

    // ---- 4. Physical + logical device ------------------------------------
    vk.pickPhysicalDevice();
    // *Present eden* bir device swapchain uzantısına ihtiyaç duyar.
    vk.createDevice(headless
                        ? std::vector<const char*>{}
                        : std::vector<const char*>{VK_KHR_SWAPCHAIN_EXTENSION_NAME});

    // ---- 5. Present-desteği kontrolü -------------------------------------
    // Her queue family her surface'e present edemez. Vulkan'a sor.
    if (!headless) {
        vk::Bool32 canPresent =
            vk.phys.getSurfaceSupportKHR(vk.qfam, surface.get());
        if (!canPresent)
            throw std::runtime_error("queue family cannot present");
    }

    // ---- 6. Swapchain -----------------------------------------------------
    // Swapchain, compositor'un arasında geçiş yaptığı bir image halkasına
    // sahip. Çift tamponlu, FIFO (vsync), BGRA8 istiyoruz.
    vk::UniqueSwapchainKHR swapchain;
    std::vector<vk::Image> scImages;
    vk::Format scFormat = vk::Format::eB8G8R8A8Unorm;
    vk::Extent2D scExtent{640, 480};
    if (!headless) {
        auto caps = vk.phys.getSurfaceCapabilitiesKHR(surface.get());
        // currentExtent == 0xFFFFFFFF "surface boyut dikte etmiyor"
        // demektir — surface'in izin verilen extent aralığına kısıtlanmış
        // GLFW framebuffer boyutunu seç. Sentinel'i olduğu gibi kullanmak
        // ~4G x ~4G swapchain ister (anında OUT_OF_HOST_MEMORY —
        // klasik tuzak).
        if (caps.currentExtent.width == 0xFFFFFFFFu) {
            int fw = 0, fh = 0;
            glfwGetFramebufferSize(win, &fw, &fh);
            scExtent.width = std::clamp<uint32_t>(
                fw, caps.minImageExtent.width,
                caps.maxImageExtent.width);
            scExtent.height = std::clamp<uint32_t>(
                fh, caps.minImageExtent.height,
                caps.maxImageExtent.height);
        } else {
            scExtent = caps.currentExtent;
        }
        // BGRA ummak yerine surface-destekli bir format seç
        auto fmts = vk.phys.getSurfaceFormatsKHR(surface.get());
        scFormat = fmts.front().format;
        uint32_t nImages = caps.minImageCount + 1;
        if (caps.maxImageCount) // 0 "sınırsız" demektir
            nImages = std::min(nImages, caps.maxImageCount);
        vk::SwapchainCreateInfoKHR si{};
        si.setSurface(surface.get())
            .setMinImageCount(nImages)
            .setImageFormat(scFormat)
            .setImageColorSpace(vk::ColorSpaceKHR::eSrgbNonlinear)
            .setImageExtent(scExtent)
            .setImageArrayLayers(1)
            .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment |
                           vk::ImageUsageFlagBits::eTransferDst)
            .setImageSharingMode(vk::SharingMode::eExclusive)
            .setPreTransform(caps.currentTransform)
            .setCompositeAlpha(vk::CompositeAlphaFlagBitsKHR::eOpaque)
            .setPresentMode(vk::PresentModeKHR::eFifo)
            .setClipped(VK_TRUE);
        swapchain = vk.device->createSwapchainKHRUnique(si);
        scImages = vk.device->getSwapchainImagesKHR(swapchain.get());
    }

    // ---- 7. Render --------------------------------------------------------
    // Henüz pipeline yok: vkCmdClearColorImage bir image'ı doğrudan
    // doldurur. eTransferDst usage gerektirir (yukarıda swapchain'de
    // ayarlı) ve tüm zincirin çalıştığını kanıtlamanın en ucuz yolu.
    const vk::ClearColorValue colors[] = {
        std::array{0.2f, 0.4f, 0.8f, 1.0f}, // mavi
        std::array{0.8f, 0.3f, 0.2f, 1.0f}, // kırmızı
    };

    if (headless) {
        vkmini::Headless hl;
        hl.init(vk, 640, 480);
        // headless.render() komutlarımızı clear'ın ihtiyaç duyduğu
        // layout geçişleriyle sarar.
        hl.render(vk::ImageLayout::eTransferDstOptimal,
                  [&](vk::CommandBuffer c) {
                      vk::ImageSubresourceRange sr{
                          vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
                      c.clearColorImage(hl.color.img.get(),
                                        vk::ImageLayout::eTransferDstOptimal,
                                        colors[0], sr);
                  });
        hl.savePpm(ppmOut);
        printf("wrote %s\n", ppmOut);
        return 0;
    }

    // Pencereli: acquire → clear → present, ~2sn değişen renk.
    auto cmd = vk.device->allocateCommandBuffersUnique(
        {vk.pool.get(), vk::CommandBufferLevel::ePrimary, 1});
    auto acqSem = vk.device->createSemaphoreUnique({});
    auto doneSem = vk.device->createSemaphoreUnique({});
    auto fence = vk.device->createFenceUnique({});

    for (int frame = 0; frame < 120 && !glfwWindowShouldClose(win);
         ++frame) {
        glfwPollEvents();
        vk.device->resetFences(fence.get());
        auto acq = vk.device->acquireNextImageKHR(
            swapchain.get(), UINT64_MAX, acqSem.get(), {});
        vk::Image img = scImages[acq.value];

        cmd[0]->reset();
        cmd[0]->begin(
            {vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
        // swapchain image'ları UNDEFINED-benzeri durumda gelir:
        // TRANSFER_DST'ye geçiş, clear, PRESENT'e geçiş.
        vk::ImageSubresourceRange sr{
            vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
        vk::ImageMemoryBarrier toDst{};
        toDst.setOldLayout(vk::ImageLayout::eUndefined)
            .setNewLayout(vk::ImageLayout::eTransferDstOptimal)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img)
            .setSubresourceRange(sr)
            .setSrcAccessMask({})
            .setDstAccessMask(vk::AccessFlagBits::eTransferWrite);
        cmd[0]->pipelineBarrier(
            vk::PipelineStageFlagBits::eTopOfPipe,
            vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, toDst);
        cmd[0]->clearColorImage(
            img, vk::ImageLayout::eTransferDstOptimal,
            colors[frame & 1], sr);
        vk::ImageMemoryBarrier toPresent{};
        toPresent.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
            .setNewLayout(vk::ImageLayout::ePresentSrcKHR)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img)
            .setSubresourceRange(sr)
            .setSrcAccessMask(vk::AccessFlagBits::eTransferWrite)
            .setDstAccessMask({});
        cmd[0]->pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                                vk::PipelineStageFlagBits::eBottomOfPipe,
                                {}, {}, {}, toPresent);
        cmd[0]->end();

        vk::PipelineStageFlags waitStage =
            vk::PipelineStageFlagBits::eTransfer;
        vk::SubmitInfo si{};
        si.setWaitSemaphores(acqSem.get())
            .setWaitDstStageMask(waitStage)
            .setCommandBuffers(cmd[0].get())
            .setSignalSemaphores(doneSem.get());
        vk.queue.submit(si, fence.get());
        vk::PresentInfoKHR pi{};
        pi.setWaitSemaphores(doneSem.get())
            .setSwapchains(swapchain.get())
            .setImageIndices(acq.value);
        (void)vk.queue.presentKHR(pi);
        (void)vk.device->waitForFences(fence.get(), VK_TRUE, UINT64_MAX);
    }
    // Yok etme sırası önemli: swapchain ve surface platform penceresini
    // referans alır — glfwDestroyWindow'dan önce serbest bırak, yoksa
    // sürücünün WSI yıkımı ölü bir wl_surface/xcb penceresine dokunur.
    vk.queue.waitIdle();
    swapchain.reset();
    surface.reset();
    glfwDestroyWindow(win);
    glfwTerminate();
    return 0;
}
vkmini.hpp
// SPDX-License-Identifier: CC0-1.0
//
// vkmini.hpp — TechOverflow Vulkan öğretici serisi için paylaşılan
// yardımcılar. Kasıtlı olarak küçük: örneklerin paylaştığı her şey
// (instance, device, bellek, buffer'lar, image'lar, tek-seferlik
// komutlar, shaderc, PPM readback'li headless render hedefi) burada
// yaşar, böylece her post tam olarak bir yeni unsura odaklanabilir.
// vulkan-hpp C++ API'sini kullanır.
//
// Every example supports two modes:
//   ./app            opens a GLFW window and renders live (where the
//                    aspect applies — compute-only examples stay
//                    headless)
//   ./app --headless out.ppm
//                    renders exactly one frame offscreen and writes a
//                    binary PPM, so a small python script can assert on
//                    actual pixel values.
#pragma once

#include <vulkan/vulkan.hpp>
#include <shaderc/shaderc.hpp>

#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>

namespace vkmini {

/// Instance + physical device + logical device + tek queue.
/// Her şey için bir queue family (graphics, burada ilgili tüm
/// gerçek-dünya donanımında compute+transfer'i ima eder — ve öğretici
/// queue-transfer karmaşasından arı kalır).
struct Vk {
    vk::UniqueInstance instance;
    vk::PhysicalDevice phys;
    vk::UniqueDevice device;
    vk::Queue queue;
    uint32_t qfam = 0;
    vk::UniqueCommandPool pool;

    /// instance için layer+extension listesi; GLFW'nin gerektirdiği
    /// surface uzantıları gerektiğinde çağıran tarafından eklenir.
    void createInstance(std::vector<const char*> exts,
                        const char* appName = "vkmini") {
        vk::ApplicationInfo ai{};
        ai.setPApplicationName(appName)
            .setApiVersion(VK_API_VERSION_1_3);
        vk::InstanceCreateInfo ici{};
        ici.setPApplicationInfo(&ai)
            .setPEnabledExtensionNames(exts);
        instance = vk::createInstanceUnique(ici);
    }

    void pickPhysicalDevice() {
        auto devs = instance->enumeratePhysicalDevices();
        if (devs.empty())
            throw std::runtime_error("no Vulkan device");
        phys = devs.front();
        auto p = phys.getProperties();
        printf("device: %s\n", p.deviceName.data());
    }

    /// Dynamic rendering etkin logical device (tüm seri VkRenderPass
    /// nesneleri olmadan render eder).
    void createDevice(std::vector<const char*> exts = {}) {
        auto props = phys.getQueueFamilyProperties();
        for (uint32_t i = 0; i < props.size(); ++i) {
            if (props[i].queueFlags & vk::QueueFlagBits::eGraphics) {
                qfam = i;
                break;
            }
        }
        float prio = 1.0f;
        vk::DeviceQueueCreateInfo qi{};
        qi.setQueueFamilyIndex(qfam)
            .setQueueCount(1)
            .setPQueuePriorities(&prio);
        vk::PhysicalDeviceDynamicRenderingFeatures dyn{};
        dyn.setDynamicRendering(true);
        vk::DeviceCreateInfo di{};
        di.setQueueCreateInfos(qi)
            .setPEnabledExtensionNames(exts)
            .setPNext(&dyn);
        device = phys.createDeviceUnique(di);
        queue = device->getQueue(qfam, 0);
        pool = device->createCommandPoolUnique(
            {vk::CommandPoolCreateFlagBits::eResetCommandBuffer, qfam});
    }

    // ---------- bellek --------------------------------------------------
    uint32_t memoryType(uint32_t bits,
                        vk::MemoryPropertyFlags want) const {
        auto mem = phys.getMemoryProperties();
        for (uint32_t i = 0; i < mem.memoryTypeCount; ++i)
            if ((bits & (1u << i)) &&
                (mem.memoryTypes[i].propertyFlags & want) == want)
                return i;
        throw std::runtime_error("no suitable memory type");
    }

    struct Buffer {
        vk::UniqueBuffer buf;
        vk::UniqueDeviceMemory mem;
        void* mapped = nullptr;
        vk::DeviceSize size = 0;
    };

    Buffer createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
                        vk::MemoryPropertyFlags props,
                        bool map = false) const {
        Buffer o;
        o.size = size;
        o.buf = device->createBufferUnique(
            {{}, size, usage, vk::SharingMode::eExclusive});
        auto req = device->getBufferMemoryRequirements(o.buf.get());
        o.mem = device->allocateMemoryUnique(
            {req.size, memoryType(req.memoryTypeBits, props)});
        device->bindBufferMemory(o.buf.get(), o.mem.get(), 0);
        if (map)
            o.mapped = device->mapMemory(o.mem.get(), 0, size);
        return o;
    }

    struct Image {
        vk::UniqueImage img;
        vk::UniqueDeviceMemory mem;
        vk::UniqueImageView view;
    };

    Image createImage(uint32_t w, uint32_t h, vk::Format fmt,
                      vk::ImageUsageFlags usage,
                      vk::ImageTiling tiling =
                          vk::ImageTiling::eOptimal) const {
        Image o;
        vk::ImageCreateInfo ii{};
        ii.setImageType(vk::ImageType::e2D)
            .setFormat(fmt)
            .setExtent({w, h, 1})
            .setMipLevels(1)
            .setArrayLayers(1)
            .setSamples(vk::SampleCountFlagBits::e1)
            .setTiling(tiling)
            .setUsage(usage)
            .setSharingMode(vk::SharingMode::eExclusive)
            .setInitialLayout(vk::ImageLayout::eUndefined);
        o.img = device->createImageUnique(ii);
        auto req = device->getImageMemoryRequirements(o.img.get());
        o.mem = device->allocateMemoryUnique(
            {req.size,
             memoryType(req.memoryTypeBits,
                        vk::MemoryPropertyFlagBits::eDeviceLocal)});
        device->bindImageMemory(o.img.get(), o.mem.get(), 0);
        vk::ImageViewCreateInfo vi{};
        vi.setImage(o.img.get())
            .setViewType(vk::ImageViewType::e2D)
            .setFormat(fmt)
            .setSubresourceRange(
                {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});
        o.view = device->createImageViewUnique(vi);
        return o;
    }

    // ---------- komutlar ------------------------------------------------
    /// `fn(cmd)`'i yeni bir command buffer'a kaydet, submit et, idle bekle.
    template <typename F> void oneTime(F&& fn) const {
        auto cmds = device->allocateCommandBuffersUnique(
            {pool.get(), vk::CommandBufferLevel::ePrimary, 1});
        cmds[0]->begin(
            {vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
        fn(cmds[0].get());
        cmds[0]->end();
        vk::SubmitInfo si{};
        si.setCommandBuffers(cmds[0].get());
        queue.submit(si);
        queue.waitIdle();
    }

    // ---------- shader'lar ------------------------------------------------
    /// shaderc ile GLSL'i runtime'da SPIR-V'ye derle — her örneği tek
    /// komutla derlenebilir tutar (çevrimdışı glslc adımı yok).
    vk::UniqueShaderModule shader(const char* glsl,
                                  shaderc_shader_kind kind,
                                  const char* name) const {
        shaderc::Compiler cc;
        auto res = cc.CompileGlslToSpv(glsl, kind, name);
        if (res.GetCompilationStatus() !=
            shaderc_compilation_status_success)
            throw std::runtime_error(name + std::string(": ") +
                                     res.GetErrorMessage());
        std::vector<uint32_t> spv(res.cbegin(), res.cend());
        vk::ShaderModuleCreateInfo si{};
        si.setCode(spv);
        return device->createShaderModuleUnique(si);
    }
};

/// --headless modu için ekran dışı RGBA8 render hedefi + host-görünür
/// readback. Image kareler arasında eShaderReadOnlyOptimal'de durur;
/// yardımcılar onu örneğin ihtiyacına göre geçirir.
struct Headless {
    Vk* vm;
    Vk::Image color;      // eColorAttachment|eTransferSrc|eSampled|eStorage
    Vk::Buffer readback;
    vk::Extent2D extent;

    void init(Vk& v, uint32_t w, uint32_t h) {
        vm = &v;
        extent = vk::Extent2D{w, h};
        color = v.createImage(
            w, h, vk::Format::eR8G8B8A8Unorm,
            vk::ImageUsageFlagBits::eColorAttachment |
                vk::ImageUsageFlagBits::eTransferSrc |
                vk::ImageUsageFlagBits::eSampled |
                vk::ImageUsageFlagBits::eStorage);
        readback = v.createBuffer(
            vk::DeviceSize(w) * h * 4,
            vk::BufferUsageFlagBits::eTransferDst,
            vk::MemoryPropertyFlagBits::eHostVisible |
                vk::MemoryPropertyFlagBits::eHostCoherent,
            true);
    }

    /// `render(cmd)` UNDEFINED -> renderLayout -> TRANSFER_SRC
    /// geçişleriyle sarılmış tek-seferlik command buffer içinde çalışır,
    /// ardından image `readback`'e kopyalanır. Tek atış: headless mod
    /// piksel kontrolü için tam bir kare render eder.
    template <typename F>
    void render(vk::ImageLayout renderLayout, F&& render) {
        vm->oneTime([&](vk::CommandBuffer c) {
            barrier(c, vk::ImageLayout::eUndefined, renderLayout,
                    vk::AccessFlags{},
                    vk::AccessFlagBits::eColorAttachmentWrite |
                        vk::AccessFlagBits::eShaderWrite |
                        vk::AccessFlagBits::eTransferWrite);
            render(c);
            barrier(c, renderLayout,
                    vk::ImageLayout::eTransferSrcOptimal,
                    vk::AccessFlagBits::eColorAttachmentWrite |
                        vk::AccessFlagBits::eShaderWrite |
                        vk::AccessFlagBits::eTransferWrite,
                    vk::AccessFlagBits::eTransferRead);
            vk::BufferImageCopy r{};
            r.setImageSubresource(
                 {vk::ImageAspectFlagBits::eColor, 0, 0, 1})
                .setImageExtent({extent.width, extent.height, 1});
            c.copyImageToBuffer(color.img.get(),
                                vk::ImageLayout::eTransferSrcOptimal,
                                readback.buf.get(), r);
        });
    }

    void barrier(vk::CommandBuffer c, vk::ImageLayout ol,
                 vk::ImageLayout nl, vk::AccessFlags src,
                 vk::AccessFlags dst) {
        vk::ImageMemoryBarrier b{};
        b.setOldLayout(ol)
            .setNewLayout(nl)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(color.img.get())
            .setSubresourceRange(
                {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1})
            .setSrcAccessMask(src)
            .setDstAccessMask(dst);
        c.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
                          vk::PipelineStageFlagBits::eAllCommands, {},
                          {}, {}, b);
    }

    /// Readback buffer'ı ikili PPM (P6, alfa yok) olarak yaz.
    void savePpm(const char* path) const {
        std::ofstream f(path, std::ios::binary);
        f << "P6\n" << extent.width << " " << extent.height << "\n255\n";
        auto* px = static_cast<const uint8_t*>(readback.mapped);
        for (uint32_t y = 0; y < extent.height; ++y)
            for (uint32_t x = 0; x < extent.width; ++x) {
                const uint8_t* p = px + (y * extent.width + x) * 4;
                char rgb[3] = {char(p[0]), char(p[1]), char(p[2])};
                f.write(rgb, 3);
            }
    }
};

/// Surface capability'lerine karşı müzakere edilen swapchain + image
/// başına view'lar. Post 2'den itibaren pencereli modda kullanılır
/// (post 1 uzun formu gösterir).
struct Swapchain {
    vk::UniqueSwapchainKHR sc;
    std::vector<vk::Image> images;
    std::vector<vk::UniqueImageView> views;
    vk::Format format = vk::Format::eB8G8R8A8Unorm;
    vk::Extent2D extent{};

    void create(Vk& v, vk::PhysicalDevice phys, vk::SurfaceKHR surface,
                uint32_t wantW, uint32_t wantH) {
        auto caps = phys.getSurfaceCapabilitiesKHR(surface);
        extent = caps.currentExtent;
        if (extent.width == 0xFFFFFFFFu) { // "surface umursamıyor"
            extent.width = std::clamp(
                wantW, caps.minImageExtent.width,
                caps.maxImageExtent.width);
            extent.height = std::clamp(
                wantH, caps.minImageExtent.height,
                caps.maxImageExtent.height);
        }
        format = phys.getSurfaceFormatsKHR(surface).front().format;
        uint32_t n = caps.minImageCount + 1;
        if (caps.maxImageCount) n = std::min(n, caps.maxImageCount);
        vk::SwapchainCreateInfoKHR si{};
        si.setSurface(surface)
            .setMinImageCount(n)
            .setImageFormat(format)
            .setImageColorSpace(vk::ColorSpaceKHR::eSrgbNonlinear)
            .setImageExtent(extent)
            .setImageArrayLayers(1)
            .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment)
            .setImageSharingMode(vk::SharingMode::eExclusive)
            .setPreTransform(caps.currentTransform)
            .setCompositeAlpha(vk::CompositeAlphaFlagBitsKHR::eOpaque)
            .setPresentMode(vk::PresentModeKHR::eFifo)
            .setClipped(VK_TRUE);
        sc = v.device->createSwapchainKHRUnique(si);
        images = v.device->getSwapchainImagesKHR(sc.get());
        views.clear();
        for (auto img : images) {
            vk::ImageViewCreateInfo vi{};
            vi.setImage(img)
                .setViewType(vk::ImageViewType::e2D)
                .setFormat(format)
                .setSubresourceRange(
                    {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});
            views.push_back(v.device->createImageViewUnique(vi));
        }
    }
};

} // namespace vkmini

Bu serideki tüm kaynak kodu CC0 1.0 Universal (CC0-1.0) altında yayınlanır — kamu malı adaması, atıf gerekmez.

Seride sıradaki: gerçek bir üçgen çizmek — bir graphics pipeline, SPIR-V shader’ları ve VkRenderPass nesneleri yerine VK_KHR_dynamic_rendering.


Check out similar posts by category: C++ Vulkan