Vulkan'da instance başına vertex attribute'larıyla instanced rendering nasıl kullanılır

Example: VulkanExamples/p13-inst

Serideki 13. yazı (3: vertex buffer’lar, 4: push constant’lar, 9: index buffer’lar, 12: swapchain yeniden oluşturma). Aynı mesh’i 256 kez çizmek 256 değil tek draw call olmalı. Buna instancing denir: vkCmdDraw(6, 256, 0, 0) instance başına 6 vertex üretir, 256 kez. Instance başına veri, input rate’i eVertex yerine eInstance olan bir vertex buffer’dan gelir.

16x16 instanced grid — tek draw call

Nasıl çalışır

Input assembler’ın iki ilerleme modu vardır:

inputRatebuffer ilerlemesikullanım
eVertexüretilen vertex başına birpozisyonlar, normaller, UV’ler
eInstanceinstance başına biroffset’ler, renkler, transform’lar

Demoda quad’ın kendisi hiç vertex buffer istemez — gl_VertexIndex (0..5) shader’daki köşe tablosunu indeksler. Bind edilen tek buffer, binding 0’a eInstance ile bağlanan Instance{ox,oy,r,g,b}‘dir (20 B). Instance i (0..255) kayıt i‘yi okur; ham indeks gerekiyorsa shader’da gl_InstanceIndex mevcut.

example.cpp
binding.setBinding(0)
    .setStride(sizeof(Instance))
    .setInputRate(vk::VertexInputRate::eInstance);   // <- konu bu

Eşitlenmesi gereken iki alan:

  • vkCmdDraw/vkCmdDrawIndexed‘teki firstInstance, instance-rate buffer’lara offset verir — parçalı upload’lar için kullanışlı.
  • stride, vertex boyutu değil instance kayıt boyutuna (burada 20 B) eşit olmalı.

Build & çalıştırma

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

Kontrol

Deterministik instance verisi doğrulamayı kolaylaştırır: 16×16 grid’in (x,y) instance’ı (x/15, y/15, parity) rengini alır ve bilinen bir NDC offset’inde durur. check.py beklenen 256 rengin hepsini yeniden hesaplar, her hücre merkezini örnekler ve quad’lar arası boşlukların arka planı gösterdiğini assert eder — hem instance başına offset’lerin (yerleşim) hem instance başına attribute’ların (renk) doğru ulaştığını kanıtlar.

check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — 16x16 instanced grid'i doğrula.
#
# C++ tarafı TEK vkCmdDraw(6, 256, 0, 0) çağrısıyla 256 quad çizer.
# Instance (x,y), 512x512 image'da quad yarı boyutu 0.0275 NDC ile
# NDC offset (-0.9375 + 0.125*x, -0.9375 + 0.125*y)'de durur.
# Renk: r = x/15, g = y/15, b = (x^y)&1'e göre 1.0 veya 0.2.
#
# Her hücre merkezini örnekleyip rengin beklenen instance verisiyle
# (nicemlemeye kadar) tam eşleştiğini, ayrıca hücreler arası boşluğun
# arka planı gösterdiğini kontrol ediyoruz.
import sys
import struct


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


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


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "out.ppm"
    w, h, data = load_ppm(path)
    assert (w, h) == (512, 512), (w, h)
    BG = (13, 13, 20)

    def near(a, b, tol=2):
        return all(abs(x - y) <= tol for x, y in zip(a, b))

    bad = 0
    for gy in range(16):
        for gx in range(16):
            # NDC -> piksel: Vulkan viewport'u Y-AŞAĞI
            # (ndc -1 = satır 0)
            px_x = int(((-0.9375 + 0.125 * gx) + 1.0) * 0.5 * w)
            px_y = int(((-0.9375 + 0.125 * gy) + 1.0) * 0.5 * h)
            exp = (round(gx / 15 * 255),
                   round(gy / 15 * 255),
                   255 if (gx ^ gy) & 1 else 51)
            got = px(data, w, px_x, px_y)
            if not near(got, exp):
                bad += 1
                print(f"cell ({gx},{gy}) @({px_x},{px_y}): "
                      f"got {got}, expected ~{exp}")
    assert bad == 0, f"{bad} cells mismatched"

    # hücreler arası boşluk = arka plan. Hücre pitch = 0.125*256 =
    # 32px, quad = 0.055*256 ~ 14px => boşluk merkezlerin ortasında.
    gap = px(data, w, 16 + 0, 16 + 0 + 0)  # en köşe hücre merkezi x=16'da
    # image köşesi = arka plan
    assert near(px(data, w, 2, 2), BG), px(data, w, 2, 2)
    # iki hücre merkezi arası orta nokta (32px ayrık => 16px öte)
    cx = int(((-0.9375 + 0.125 * 0) + 1.0) * 0.5 * w)
    cy = int(((-0.9375 + 0.125 * 0) + 1.0) * 0.5 * h)
    assert near(px(data, w, cx + 16, cy), BG), \
        px(data, w, cx + 16, cy)
    print("OK: 256 instances in one draw, all 16x16 cell colors exact")


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: 256 instances in one draw, all 16x16 cell colors exact

Tam kaynak

main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// Post 13: instanced rendering. Tek draw call 256 quad render
// eder — quad köşeleri gl_VertexIndex'ten, instance başına
// offset+renk instance input rate'inde bind edilmiş vertex
// buffer'dan gelir (veri vertex başına değil instance başına
// ilerler).
//
//   ./app                 pencereli
//   ./app --headless o.ppm     tek frame -> check.py için PPM
//
// Build:  g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc

#include "vkmini.hpp"

#include <GLFW/glfw3.h>

/// Instance başına veri: grid offset + renk. Input assembler
/// açısından tüm struct tek bir "vertex" — üretilen vertex başına
/// değil instance başına ilerler.
struct Instance {
    float ox, oy;      // 8 B
    float r, g, b;     // 12 B
};

static const char* kVert = R"GLSL(
#version 460
layout(location = 0) in vec2 iOffset;   // INSTANCE başına
layout(location = 1) in vec3 iColor;    // INSTANCE başına
layout(location = 0) out vec3 vColor;
void main() {
    // quad köşeleri prosedürel üretilir — 6 vertex = 2 üçgen
    vec2 corner[6] = vec2[](vec2(-0.5,-0.5), vec2( 0.5,-0.5), vec2( 0.5, 0.5),
                            vec2(-0.5,-0.5), vec2( 0.5, 0.5), vec2(-0.5, 0.5));
    vec2 local = corner[gl_VertexIndex] * 0.055;  // NDC'de quad boyutu
    gl_Position = vec4(local + iOffset, 0.0, 1.0);
    vColor = iColor;
}
)GLSL";

static const char* kFrag = R"GLSL(
#version 460
layout(location = 0) in vec3 vColor;
layout(location = 0) out vec4 outColor;
void main() { outColor = vec4(vColor, 1.0); }
)GLSL";

static vkmini::Vk::Buffer makeInstanceBuffer(vkmini::Vk& vk) {
    // 16x16 instance grid'i: deterministik renkler —
    // r = sütun, g = satır, b = dama paritesi.
    std::array<Instance, 256> inst{};
    for (int y = 0; y < 16; ++y)
        for (int x = 0; x < 16; ++x) {
            auto& i = inst[y * 16 + x];
            i.ox = -0.9375f + x * 0.125f;
            i.oy = -0.9375f + y * 0.125f;
            i.r = x / 15.0f;
            i.g = y / 15.0f;
            i.b = (x ^ y) & 1 ? 1.0f : 0.2f;
        }
    vk::DeviceSize size = sizeof(inst);
    auto staging = vk.createBuffer(
        size, vk::BufferUsageFlagBits::eTransferSrc,
        vk::MemoryPropertyFlagBits::eHostVisible |
            vk::MemoryPropertyFlagBits::eHostCoherent,
        true);
    memcpy(staging.mapped, inst.data(), size);
    auto buf = vk.createBuffer(
        size,
        vk::BufferUsageFlagBits::eVertexBuffer |
            vk::BufferUsageFlagBits::eTransferDst,
        vk::MemoryPropertyFlagBits::eDeviceLocal);
    vk.oneTime([&](vk::CommandBuffer c) {
        vk::BufferCopy r{0, 0, size};
        c.copyBuffer(staging.buf.get(), buf.buf.get(), r);
        vk::BufferMemoryBarrier b{};
        b.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setSrcAccessMask(vk::AccessFlagBits::eTransferWrite)
            .setDstAccessMask(
                vk::AccessFlagBits::eVertexAttributeRead)
            .setBuffer(buf.buf.get())
            .setOffset(0)
            .setSize(size);
        c.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                          vk::PipelineStageFlagBits::eVertexInput,
                          {}, {}, b, {});
    });
    return buf;
}

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

/// Post 3 ile aynı pipeline — tek fark binding tanımının eInstance
/// demesi; her 20-baytlık Instance kaydı bir instance'ı besler.
static Pipe makePipeline(vkmini::Vk& vk, vk::Format fmt) {
    Pipe o;
    auto vs = vk.shader(kVert, shaderc_vertex_shader, "i.vert");
    auto fs = vk.shader(kFrag, shaderc_fragment_shader, "i.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::VertexInputBindingDescription binding{};
    binding.setBinding(0)
        .setStride(sizeof(Instance))
        // BÜTÜN MESELE: vertex başına değil instance başına ilerle
        .setInputRate(vk::VertexInputRate::eInstance);
    std::array attrs{
        vk::VertexInputAttributeDescription{}
            .setLocation(0)
            .setBinding(0)
            .setFormat(vk::Format::eR32G32Sfloat)
            .setOffset(offsetof(Instance, ox)),
        vk::VertexInputAttributeDescription{}
            .setLocation(1)
            .setBinding(0)
            .setFormat(vk::Format::eR32G32B32Sfloat)
            .setOffset(offsetof(Instance, r)),
    };
    vk::PipelineVertexInputStateCreateInfo vin{};
    vin.setVertexBindingDescriptions(binding)
        .setVertexAttributeDescriptions(attrs);
    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);
    o.layout = vk.device->createPipelineLayoutUnique({});
    vk::PipelineRenderingCreateInfo rendering{};
    rendering.setColorAttachmentFormats(fmt);
    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 failed");
    o.pipeline = std::move(res.value);
    return o;
}

static void record(vk::CommandBuffer c, vk::ImageView view,
                   vk::Extent2D extent, const Pipe& p,
                   vk::Buffer instBuf) {
    vk::RenderingAttachmentInfo att{};
    att.setImageView(view)
        .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
        .setLoadOp(vk::AttachmentLoadOp::eClear)
        .setStoreOp(vk::AttachmentStoreOp::eStore)
        .setClearValue(vk::ClearValue{
            vk::ClearColorValue{std::array{0.05f, 0.05f, 0.08f,
                                           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());
    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);
    vk::DeviceSize off = 0;
    c.bindVertexBuffers(0, instBuf, off);
    c.draw(6, 256, 0, 0); // 6 vertex x 256 instance, TEK çağrı
    c.endRendering();
}

static void imgBarrier(vk::CommandBuffer c, vk::Image img,
                       vk::ImageAspectFlags aspect,
                       vk::ImageLayout ol, vk::ImageLayout nl,
                       vk::AccessFlags src, vk::AccessFlags dst,
                       vk::PipelineStageFlags ss,
                       vk::PipelineStageFlags ds) {
    vk::ImageMemoryBarrier b{};
    b.setOldLayout(ol)
        .setNewLayout(nl)
        .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
        .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
        .setImage(img)
        .setSubresourceRange({aspect, 0, 1, 0, 1})
        .setSrcAccessMask(src)
        .setDstAccessMask(dst);
    c.pipelineBarrier(ss, ds, {}, {}, {}, b);
}

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(512, 512, "vkmini p13", 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 ibuf = makeInstanceBuffer(vk);

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

    vkmini::Swapchain sc;
    sc.create(vk, vk.phys, surface.get(), 512, 512);
    Pipe p = makePipeline(vk, sc.format);
    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});
        imgBarrier(cmd[0].get(), img, vk::ImageAspectFlagBits::eColor,
                   vk::ImageLayout::eUndefined,
                   vk::ImageLayout::eColorAttachmentOptimal,
                   vk::AccessFlags{},
                   vk::AccessFlagBits::eColorAttachmentWrite,
                   vk::PipelineStageFlagBits::eTopOfPipe,
                   vk::PipelineStageFlagBits::eColorAttachmentOutput);
        record(cmd[0].get(), sc.views[acq.value].get(), sc.extent, p,
               ibuf.buf.get());
        imgBarrier(cmd[0].get(), img, vk::ImageAspectFlagBits::eColor,
                   vk::ImageLayout::eColorAttachmentOptimal,
                   vk::ImageLayout::ePresentSrcKHR,
                   vk::AccessFlagBits::eColorAttachmentWrite,
                   vk::AccessFlags{},
                   vk::PipelineStageFlagBits::eColorAttachmentOutput,
                   vk::PipelineStageFlagBits::eBottomOfPipe);
        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 kodu CC0 1.0 Universal (CC0-1.0) altında yayınlandı — kamu malı adaması, atıf gerekmez.

Serinin devamı: MSAA — hâlâ VkRenderPass’sız, multisample edilmiş attachment’lar ve resolve adımı.


Check out similar posts by category: C++ Vulkan