Vulkan texture'ına image yükleme ve fragment shader'da örnekleme

Example: VulkanExamples/p05-texture

Serideki post 5 (1: surface, 2: ilk üçgen, 3: vertex buffer’lar, 4: push constant’lar). Texturing dört kavramı birden içeri çektiği için şimdiye kadarki en büyük tek adım:

  1. image upload — staging buffer → vkCmdCopyBufferToImage,
  2. image layout transition’ları — UNDEFINED → TRANSFER_DST → SHADER_READ,
  3. descriptor’lar — bir VkImageView+VkSampler‘ı shader binding’ine bağlayan mekanizma,
  4. sampler’lar — filtre/adres-modu durumu.

CPU’da üretilmiş 8×8 dama tahtası bir fullscreen üçgene çizilir — bu sefer vertex buffer yok, konumlar gl_VertexIndex‘ten gelir.

Tam kareye örneklenmiş dama tahtası texture’ı

Nasıl çalışır

Fullscreen üçgen numarası

fs.vert (excerpt)
vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0);

(-1,-1) (3,-1) (-1,3)‘te üç vertex — viewport’un iki katı bir üçgen, tam onu kaplayacak şekilde kırpılmış, UV’ler ekran boyunca 0→1 ara değerlenir. Fullscreen geçişler için standart.

Image upload: vertex buffer’larla aynı staging deseni

example.txt
staging (HOST_VISIBLE) --vkCmdCopyBufferToImage--> image (DEVICE_LOCAL)

Buffer’lardan farkı: image’lar GPU’nun kullanıma göre farklı dahili tiling’lerde sakladığı için ek layout transition’ları gerektirir. Kopya için eTransferDstOptimal, örnekleme için eShaderReadOnlyOptimal — ve her transition bir erişim maskesini bir pipeline aşamasıyla eşleştirir (burada eTransfer→eFragmentShader).

Descriptor’lar: shader’ın kaynaklara bakışı

GLSL layout(binding = 0) uniform sampler2D tex; dört nesne gerektirir:

nesnerol
VkDescriptorSetLayoutarayüz: “binding 0 = combined image sampler, fragment aşaması”
VkDescriptorPoolset’ler için allocator (1 descriptor için bile gerekli)
VkDescriptorSetörnek: {bizim sampler, bizim imageView, eShaderReadOnlyOptimal}
VkPipelineLayoutset layout’tan oluşturulur — pipeline’ın tam kaynak arayüzü

vkUpdateDescriptorSets işaretçi tuple’ını set’e yazar; vkCmdBindDescriptorSets(set=0) onu shader’a görünür yapar. Bir “combined image sampler” VkImageView ve VkSampler‘ı tek binding’de birleştirir — image view layout‘u sabitler (eShaderReadOnlyOptimal), bu da draw zamanında image’ın içinde olduğu layout’la eşleşmelidir.

Sampler

eNearest filtreleme texel okumalarını kesin tutar — dama tahtası hücrelerinin kontrolün == ile karşılaştırabileceği sert 80×60 px bloklar olarak düşmesi için bilinçli seçildi. eClampToEdge kenar-rengi sürprizlerini önler.

Derle ve çalıştır

build.sh
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app                    # windowed: textured fullscreen
./app --headless out.ppm # offscreen -> PPM
python3 check.py out.ppm

Kontrol

256×256 texture (32 px hücreler) 640×480 hedef boyunca gerilir, yani her texture hücresi 80×60 px’lik bir blok kaplar. Betik 64 ekran hücresinin her birinin merkezini örnekler ve kesin dama tahtası rengini assert eder — tek bir yanlış layout transition’ı veya swizzle’lı upload yüksek sesle başarısız olur.

check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — post 5'in texture'lı fullscreen render'ını doğrula.
# 256x256 texture (8x8 dama tahtası, 32px hücreler) NEAREST ile
# tüm 640x480 hedefe örneklenir -> her texture hücresi bir
# 80x60 px blok kaplar. (i+j) çiftse hücre (i,j) renk A, değilse B.
import sys

A = (230, 40, 60)
B = (40, 230, 60)


def read_ppm(path):
    d = open(path, "rb").read()
    hdr, data = d.split(b"255\n", 1)
    toks = hdr.split()
    assert toks[0] == b"P6"
    w, h = int(toks[1]), int(toks[2])
    assert len(data) == w * h * 3
    return w, h, data


def px(d, w, x, y):
    i = (y * w + x) * 3
    return tuple(d[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)

    # her ekran hücresinin merkezini örnekle (hücreler 80x60 px)
    for j in range(8):
        for i in range(8):
            x, y = i * 80 + 40, j * 60 + 30
            want = A if (i + j) % 2 == 0 else B
            got = px(d, w, x, y)
            assert got == want, \
                f"cell({i},{j}) px({x},{y})={got} want {want}"
    print("OK: 8x8 checkerboard sampled exactly")


if __name__ == "__main__":
    main()
output.txt
$ ./app --headless out.ppm && python3 check.py out.ppm
device: AMD Radeon RX 7800 XT (RADV NAVI32)
wrote out.ppm
OK: 8x8 checkerboard sampled exactly

Tam kaynak

main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// Post 5: texture'lar. Fullscreen bir üçgen (vertex buffer yok),
// CPU'da üretilmiş ve staging buffer -> vkCmdCopyBufferToImage ->
// layout transition'ları ile yüklenmiş bir dama tahtası texture'ını
// örnekler. Ayrıca serinin ilk descriptor set'i: bir combined
// image sampler binding'i.
//
//   ./app                 windowed: textured fullscreen triangle
//   ./app --headless o.ppm     one frame -> PPM for check.py
//
// Derleme:  g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc

#include "vkmini.hpp"

#include <GLFW/glfw3.h>

// ---------------- texture verisi (CPU tarafı) ----------------------
// 256x256 RGBA, iki doygun rengin 8x8 dama tahtası. Nearest
// örnekleme texel okumalarını kesin tutar ki kontrol == ile
// karşılaştırabilsin.
constexpr uint32_t kTexW = 256, kTexH = 256, kCell = 32;
static std::vector<uint8_t> makeCheckerboard() {
    std::vector<uint8_t> px(kTexW * kTexH * 4);
    for (uint32_t y = 0; y < kTexH; ++y)
        for (uint32_t x = 0; x < kTexW; ++x) {
            bool a = ((x / kCell) + (y / kCell)) % 2 == 0;
            uint8_t* p = &px[(y * kTexW + x) * 4];
            p[0] = a ? 230 : 40;  // R
            p[1] = a ? 40 : 230;  // G
            p[2] = 60;            // B
            p[3] = 255;
        }
    return px;
}

static const char* kVert = R"GLSL(
#version 460
// Fullscreen üçgen: 3 vertex tüm viewport'u kaplar.
// positions: (-1,-1) (3,-1) (-1,3); uv: (0,0) (2,0) (0,2)
layout(location = 0) out vec2 vUV;
void main() {
    vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
    gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0);
}
)GLSL";

static const char* kFrag = R"GLSL(
#version 460
layout(location = 0) in vec2 vUV;
layout(location = 0) out vec4 outColor;
// Combined image sampler = tek binding'de VkImageView + VkSampler.
layout(binding = 0) uniform sampler2D tex;
void main() { outColor = texture(tex, vUV); }
)GLSL";

/// Tek bir combined-image-sampler için descriptor kaynakları.
struct TexBinding {
    vk::UniqueDescriptorSetLayout setLayout;
    vk::UniqueDescriptorPool pool;
    vk::DescriptorSet set;
    vk::UniqueSampler sampler;
};

/// Bu postun yeni yönü: bir VkImage'ı fragment shader'ın
/// `texture()` yapabildiği bir şeye dönüştürmek.
static TexBinding makeTextureBinding(vkmini::Vk& vk,
                                     vk::ImageView texView) {
    TexBinding o;

    // 1. Sampler: dama tahtası net kalsın ve kontrolün texel
    //    değerleri kesin olsun diye nearest filtreleme.
    vk::SamplerCreateInfo si{};
    si.setMagFilter(vk::Filter::eNearest)
        .setMinFilter(vk::Filter::eNearest)
        .setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
        .setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
        .setAddressModeW(vk::SamplerAddressMode::eClampToEdge);
    o.sampler = vk.device->createSamplerUnique(si);

    // 2. Descriptor set layout: "binding 0 bir combined image
    //    sampler, fragment aşamasına görünür".
    vk::DescriptorSetLayoutBinding lb{};
    lb.setBinding(0)
        .setDescriptorType(
            vk::DescriptorType::eCombinedImageSampler)
        .setDescriptorCount(1)
        .setStageFlags(vk::ShaderStageFlagBits::eFragment);
    vk::DescriptorSetLayoutCreateInfo li{};
    li.setBindings(lb);
    o.setLayout = vk.device->createDescriptorSetLayoutUnique(li);

    // 3. Pool + set (tek descriptor için bile pool gerekli).
    vk::DescriptorPoolSize ps{vk::DescriptorType::eCombinedImageSampler,
                              1};
    vk::DescriptorPoolCreateInfo pi{};
    pi.setMaxSets(1).setPoolSizes(ps);
    o.pool = vk.device->createDescriptorPoolUnique(pi);
    vk::DescriptorSetAllocateInfo ai{};
    ai.setDescriptorPool(o.pool.get())
        .setSetLayouts(o.setLayout.get());
    o.set = vk.device->allocateDescriptorSets(ai).front();

    // 4. Binding 0'ı {sampler, imageView, shaderRead layout}'a işaret ettir.
    vk::DescriptorImageInfo di{};
    di.setSampler(o.sampler.get())
        .setImageView(texView)
        .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
    vk::WriteDescriptorSet w{};
    w.setDstSet(o.set)
        .setDstBinding(0)
        .setDescriptorType(
            vk::DescriptorType::eCombinedImageSampler)
        .setImageInfo(di);
    vk.device->updateDescriptorSets(w, {});
    return o;
}

/// Staging + barrier'larla sampled bir image'a piksel verisi yükle.
/// Sırayla layout dansı:
///   UNDEFINED -> TRANSFER_DST  (staging verisini içeri yaz)
///   TRANSFER_DST -> SHADER_READ (sampler onu okur)
/// İkisi de pipeline aşamalarıyla eşleşen erişim-maskesi çiftleri
/// gerektirir.
static vkmini::Vk::Image uploadTexture(vkmini::Vk& vk, uint32_t w,
                                       uint32_t h,
                                       const void* pixels,
                                       size_t size) {
    auto staging = vk.createBuffer(
        size, vk::BufferUsageFlagBits::eTransferSrc,
        vk::MemoryPropertyFlagBits::eHostVisible |
            vk::MemoryPropertyFlagBits::eHostCoherent,
        true);
    memcpy(staging.mapped, pixels, size);

    auto img = vk.createImage(w, h, vk::Format::eR8G8B8A8Unorm,
                              vk::ImageUsageFlagBits::eSampled |
                                  vk::ImageUsageFlagBits::eTransferDst);
    vk::ImageSubresourceRange sr{vk::ImageAspectFlagBits::eColor, 0,
                                 1, 0, 1};
    vk.oneTime([&](vk::CommandBuffer c) {
        vk::ImageMemoryBarrier toDst{};
        toDst.setOldLayout(vk::ImageLayout::eUndefined)
            .setNewLayout(vk::ImageLayout::eTransferDstOptimal)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img.img.get())
            .setSubresourceRange(sr)
            .setSrcAccessMask({})
            .setDstAccessMask(vk::AccessFlagBits::eTransferWrite);
        c.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
                          vk::PipelineStageFlagBits::eTransfer, {},
                          {}, {}, toDst);

        vk::BufferImageCopy r{};
        r.setImageSubresource(
             {vk::ImageAspectFlagBits::eColor, 0, 0, 1})
            .setImageExtent({w, h, 1});
        c.copyBufferToImage(staging.buf.get(), img.img.get(),
                            vk::ImageLayout::eTransferDstOptimal, r);

        vk::ImageMemoryBarrier toRead{};
        toRead.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
            .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img.img.get())
            .setSubresourceRange(sr)
            .setSrcAccessMask(vk::AccessFlagBits::eTransferWrite)
            .setDstAccessMask(vk::AccessFlagBits::eShaderRead);
        c.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                          vk::PipelineStageFlagBits::eFragmentShader,
                          {}, {}, {}, toRead);
    });
    return img;
}

struct Pipe {
    vk::UniquePipelineLayout layout;
    vk::UniquePipeline pipeline;
};

/// Pipeline artık descriptor set layout alıyor — VkPipelineLayout'un
/// parçası olur ("shader arayüzü").
static Pipe makePipeline(vkmini::Vk& vk, vk::Format colorFormat,
                         vk::DescriptorSetLayout setLayout) {
    Pipe o;
    auto vs = vk.shader(kVert, shaderc_vertex_shader, "fs.vert");
    auto fs = vk.shader(kFrag, shaderc_fragment_shader, "fs.frag");
    vk::PipelineShaderStageCreateInfo stages[2];
    stages[0].setStage(vk::ShaderStageFlagBits::eVertex)
        .setModule(vs.get())
        .setPName("main");
    stages[1].setStage(vk::ShaderStageFlagBits::eFragment)
        .setModule(fs.get())
        .setPName("main");
    vk::PipelineVertexInputStateCreateInfo vin{}; // shader-üretimli
    vk::PipelineInputAssemblyStateCreateInfo ia{};
    ia.setTopology(vk::PrimitiveTopology::eTriangleList);
    vk::PipelineViewportStateCreateInfo vp{};
    vp.setViewportCount(1).setScissorCount(1);
    std::array dynStates{vk::DynamicState::eViewport,
                         vk::DynamicState::eScissor};
    vk::PipelineDynamicStateCreateInfo dyn{};
    dyn.setDynamicStates(dynStates);
    vk::PipelineRasterizationStateCreateInfo rs{};
    rs.setPolygonMode(vk::PolygonMode::eFill)
        .setCullMode(vk::CullModeFlagBits::eNone)
        .setFrontFace(vk::FrontFace::eCounterClockwise)
        .setLineWidth(1.0f);
    vk::PipelineMultisampleStateCreateInfo ms{};
    ms.setRasterizationSamples(vk::SampleCountFlagBits::e1);
    vk::PipelineColorBlendAttachmentState cb{};
    cb.setColorWriteMask(
        vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
        vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA);
    vk::PipelineColorBlendStateCreateInfo blend{};
    blend.setAttachments(cb);
    vk::PipelineLayoutCreateInfo li{};
    li.setSetLayouts(setLayout); // <- shader arayüzü
    o.layout = vk.device->createPipelineLayoutUnique(li);
    vk::PipelineRenderingCreateInfo rendering{};
    rendering.setColorAttachmentFormats(colorFormat);
    vk::GraphicsPipelineCreateInfo gi{};
    gi.setStages(stages)
        .setPVertexInputState(&vin)
        .setPInputAssemblyState(&ia)
        .setPViewportState(&vp)
        .setPDynamicState(&dyn)
        .setPRasterizationState(&rs)
        .setPMultisampleState(&ms)
        .setPColorBlendState(&blend)
        .setLayout(o.layout.get())
        .setPNext(&rendering);
    auto res = vk.device->createGraphicsPipelineUnique({}, gi);
    if (res.result != vk::Result::eSuccess)
        throw std::runtime_error("pipeline creation failed");
    o.pipeline = std::move(res.value);
    return o;
}

static void drawTextured(vk::CommandBuffer c, vk::ImageView target,
                         vk::Extent2D extent, const Pipe& p,
                         vk::DescriptorSet set) {
    vk::RenderingAttachmentInfo att{};
    att.setImageView(target)
        .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
        .setLoadOp(vk::AttachmentLoadOp::eClear)
        .setStoreOp(vk::AttachmentStoreOp::eStore)
        .setClearValue(vk::ClearValue{
            vk::ClearColorValue{std::array{0.f, 0.f, 0.f, 1.f}}});
    vk::RenderingInfo ri{};
    ri.setRenderArea({{0, 0}, extent})
        .setLayerCount(1)
        .setColorAttachments(att);
    c.beginRendering(ri);
    c.bindPipeline(vk::PipelineBindPoint::eGraphics, p.pipeline.get());
    // bindDescriptorSets `set`'i set=0 olarak görünür yapar — bu
    // yüzden shader sadece `layout(binding = 0)` der
    // (set varsayılanı 0'dır).
    c.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
                         p.layout.get(), 0, set, {});
    vk::Viewport vp{0, 0, float(extent.width), float(extent.height),
                    0.f, 1.f};
    c.setViewport(0, vp);
    vk::Rect2D sc{{0, 0}, extent};
    c.setScissor(0, sc);
    c.draw(3, 1, 0, 0);
    c.endRendering();
}

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

    vkmini::Vk vk;
    GLFWwindow* win = nullptr;
    vk::UniqueSurfaceKHR surface;
    if (!headless) {
        glfwInit();
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
        win = glfwCreateWindow(640, 480, "vkmini p05", nullptr, nullptr);
    }
    std::vector<const char*> instExts;
    if (!headless) {
        uint32_t n = 0;
        const char** e = glfwGetRequiredInstanceExtensions(&n);
        instExts.assign(e, e + n);
    }
    vk.createInstance(instExts);
    if (!headless) {
        VkSurfaceKHR raw;
        glfwCreateWindowSurface(vk.instance.get(), win, nullptr, &raw);
        surface = vk::UniqueSurfaceKHR(raw, vk.instance.get());
    }
    vk.pickPhysicalDevice();
    vk.createDevice(headless ? std::vector<const char*>{}
                             : std::vector<const char*>{
                                   VK_KHR_SWAPCHAIN_EXTENSION_NAME});

    auto pixels = makeCheckerboard();
    auto tex = uploadTexture(vk, kTexW, kTexH, pixels.data(),
                             pixels.size());
    auto binding = makeTextureBinding(vk, tex.view.get());

    if (headless) {
        vkmini::Headless hl;
        hl.init(vk, 640, 480);
        Pipe p = makePipeline(vk, vk::Format::eR8G8B8A8Unorm,
                              binding.setLayout.get());
        hl.render(vk::ImageLayout::eColorAttachmentOptimal,
                  [&](vk::CommandBuffer c) {
                      drawTextured(c, hl.color.view.get(), hl.extent,
                                   p, binding.set);
                  });
        hl.savePpm(ppmOut);
        printf("wrote %s\n", ppmOut);
        return 0;
    }

    vkmini::Swapchain sc;
    sc.create(vk, vk.phys, surface.get(), 640, 480);
    Pipe p = makePipeline(vk, sc.format, binding.setLayout.get());
    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 f = 0; f < 240 && !glfwWindowShouldClose(win); ++f) {
        glfwPollEvents();
        vk.device->resetFences(fence.get());
        auto acq = vk.device->acquireNextImageKHR(
            sc.sc.get(), UINT64_MAX, acqSem.get(), {});
        vk::Image img = sc.images[acq.value];
        cmd[0]->reset();
        cmd[0]->begin(
            {vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
        vk::ImageSubresourceRange sr{vk::ImageAspectFlagBits::eColor,
                                     0, 1, 0, 1};
        vk::ImageMemoryBarrier toAtt{};
        toAtt.setOldLayout(vk::ImageLayout::eUndefined)
            .setNewLayout(vk::ImageLayout::eColorAttachmentOptimal)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img)
            .setSubresourceRange(sr)
            .setSrcAccessMask({})
            .setDstAccessMask(
                vk::AccessFlagBits::eColorAttachmentWrite);
        cmd[0]->pipelineBarrier(
            vk::PipelineStageFlagBits::eTopOfPipe,
            vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {},
            {}, toAtt);
        drawTextured(cmd[0].get(), sc.views[acq.value].get(),
                     sc.extent, p, binding.set);
        vk::ImageMemoryBarrier toPresent{};
        toPresent.setOldLayout(
                     vk::ImageLayout::eColorAttachmentOptimal)
            .setNewLayout(vk::ImageLayout::ePresentSrcKHR)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(img)
            .setSubresourceRange(sr)
            .setSrcAccessMask(
                vk::AccessFlagBits::eColorAttachmentWrite)
            .setDstAccessMask({});
        cmd[0]->pipelineBarrier(
            vk::PipelineStageFlagBits::eColorAttachmentOutput,
            vk::PipelineStageFlagBits::eBottomOfPipe, {}, {}, {},
            toPresent);
        cmd[0]->end();
        vk::PipelineStageFlags waitStage =
            vk::PipelineStageFlagBits::eColorAttachmentOutput;
        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(sc.sc.get())
            .setImageIndices(acq.value);
        (void)vk.queue.presentKHR(pi);
        (void)vk.device->waitForFences(fence.get(), VK_TRUE,
                                     UINT64_MAX);
    }
    vk.queue.waitIdle();
    sc.views.clear();
    sc.sc.reset();
    surface.reset();
    glfwDestroyWindow(win);
    glfwTerminate();
    return 0;
}

Bu serideki tüm kaynak kod CC0 1.0 Universal (CC0-1.0) altında yayımlanmıştır — kamu malı tahsisi, atıf gerekmez.

Seride sıradaki: compute shader’lar — bir gl_GlobalInvocationID programı tarafından doldurulan ve CPU’ya geri okunan bir storage buffer, GPU’nun ilk graphics-dışı kullanımı.


Check out similar posts by category: C++ Vulkan