Vulkan'da kalıcı bir staging ring üzerinden upload'ları akışa alma

Example: VulkanExamples/p31-stagingring

Vertex-buffer yazısı (staging buffer oluştur, map, memcpy, copy, waitIdle, destroy — tek seferlik upload için uygun) bir kez upload etti. Bunu frame başı texture başı yaparsan her şeyi seri hale getirirsin — her upload tüm kuyruğu bekler. Staging ring bunu çözer: N adet kalıcı-mapped HOST_VISIBLE slot, her biri fence’li ve sadece yeniden kullanmak üzere olduğun slot‘u beklersin.

Desen

example.cpp
struct Slot { Buffer buf; UniqueFence fence; };
std::array<Slot, RING> ring;           // tipik olarak 2–4 slot

for (upload& u : uploads) {
    Slot& s = ring[i % RING];
    device->waitForFences(s.fence);    // sadece bu slot'un kopyası
    device->resetFences(s.fence);
    memcpy(s.buf.mapped, u.data, u.size);   // sonsuza kadar mapped kalır
    /* copyBufferToImage/buffer kaydet */
    queue.submit(si, s.fence);         // fence = slot'un in-flight bayrağı
}

Upload başı allocation ile karşılaştırma: vkMapMemory yok, vkAllocateMemory yok, waitIdle yok — upload’ın durağan hal maliyeti memcpy + record + submit ve beklemeler cerrahi.

Demo wraparound’u zorlar

2 slot, 5 upload → 3 bekleme. Log hangi slotların blokladığını kanıtlar:

output.txt
[ring] upload 0 -> slot 0 submitted
[ring] upload 1 -> slot 1 submitted
[ring] slot 0 in flight, waiting
[ring] upload 2 -> slot 0 submitted
[ring] slot 1 in flight, waiting
[ring] upload 3 -> slot 1 submitted
[ring] slot 0 in flight, waiting
[ring] upload 4 -> slot 0 submitted

Son sample edilen image = upload 4’ün deseni — kontrol hem logu hem pikselleri doğrular.

Ring’i boyutlandırma

Çok küçük → wraparound beklemeleri darboğaz olur (serileşmeye geri dön). Çok büyük → HOST_VISIBLE bellek israfı (discrete GPU’larda sınırlı BAR/PCIe-window alanı — capability-query yazısındaki heap’leri kontrol et — raporu hangi heap’lerin HOST_VISIBLE olduğunu ve ne kadar büyük olduklarını listeler). Kural: frame-sync’li veri için framesInFlight + 1 slot (uniform buffer’lar, frame başı texture’lar), wraparound beklemesinin norm değil istisna olacağı şekilde boyutlandır.

Nasıl KULLANILMAMALI

  • Fence’ini beklemeden slot’a yazma — kopyası hâlâ uçuşta olan bir buffer’a memcpy kopyayı bozar.
  • Kaydedilmiş command buffer’ı erken free etme — fence retire olana kadar yaşamalı (demo inFlight listesine erteler; üretim ring’leri slot’un sonraki beklemesinde free eder).
  • Upload başına değil slot başına bir fence — fence “bu slot yeniden kullanım için güvenli” der, ring’in ihtiyacı olan şey bu.
  • HOST_COHERENT tercih et — olmadan her yazışta vkFlushMappedMemoryRanges gerekir; coherent bu kayıt işini küçük bir bant genişliği maliyetiyle kaldırır.
  • Ring bir GPU-tarafı kuyruk değildir — CPU→GPU bant genişliğini yönetir; GPU-tarafı kopya hâlâ Transfer süresi tutar.

Derleme ve çalıştırma

build.sh
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app --headless out.ppm && python3 check.py out.ppm
check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — staging ring'i doğrula: 2 slot üzerinden 5 upload
# (yani 3 wraparound beklemesi olmalı) ve son sample image
# upload #4'ün deseni.
import re
import subprocess
import sys


def read_ppm(path):
    d = open(path, "rb").read()
    hdr, data = d.split(b"255\n", 1)
    toks = hdr.split()
    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"
    out = subprocess.run(["./app", "--headless", path],
                         capture_output=True, text=True)
    assert out.returncode == 0, out.stderr
    print(out.stdout, end="")

    submits = re.findall(r"upload (\d+) -> slot (\d+) submitted",
                         out.stdout)
    waits = re.findall(r"slot (\d+) in flight, waiting", out.stdout)
    assert len(submits) == 5, submits
    # slot deseni: 0,1,0,1,0 -> 3 wraparound beklemesi
    assert [int(s) for _, s in submits] == [0, 1, 0, 1, 0]
    assert len(waits) == 3, waits

    w, h, d = read_ppm(path)
    # son image = upload 4. Çift hücrelere yazılan RGBA baytları
    # (40+4*30, 40+4*30, 220) -> RGB (160,160,220); tek hücreler
    # (20,20,160). NEAREST tam ekran -> hücre (0,0) ekranı
    # ~[0..48]px kaplar.
    e = px(d, w, 72, 21)            # çift hücre
    o = px(d, w, 21, 21)            # tek hücre
    print("even/odd cell:", e, o)
    assert abs(e[0] - 160) < 40 and abs(e[2] - 220) < 40, e
    assert abs(o[0] - 20) < 40 and abs(o[2] - 160) < 40, o
    print("OK: 5 uploads streamed through 2-slot ring, final "
          "image = upload 4")


if __name__ == "__main__":
    main()

Tam kaynak

main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// Kalıcı staging ring: upload başına yeni staging buffer
// allocate etmek (alloc + map + submit + wait + destroy —
// her frame'i serileştir) yerine kalıcı mapped HOST_VISIBLE
// buffer'ların küçük bir ring'ini tut, her biri fence'li.
// Upload = SONRAKİ ring slot'una memcpy; slot'un kopyası
// hâlâ uçuştaysa sadece *onun* fence'ini bekle — tüm kuyruğu
// değil.
//
// Demo: 2 slot'lu ring, 5 farklı image upload et ->
// wraparound beklemelerini zorlar; son image geri okunur ve
// doğrulanır. Log hangi slotların beklediğini gösterir.
//
//   ./app --headless o.ppm     tek kare -> check.py için PPM
//   ./app                      pencereli
//
// Build:  g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc

#include "vkmini.hpp"

#include <GLFW/glfw3.h>

static const char* kVert = R"GLSL(
#version 460
layout(location = 0) out vec2 vUV;
void main() {
    vec2 p = vec2((gl_VertexIndex << 1 & 2), gl_VertexIndex & 2);
    vUV = p;
    gl_Position = vec4(p * 2.0 - 1.0, 0, 1);
}
)GLSL";

static const char* kFrag = R"GLSL(
#version 460
layout(binding = 0) uniform sampler2D tex;
layout(location = 0) in vec2 vUV;
layout(location = 0) out vec4 outColor;
void main() { outColor = texture(tex, vUV); }
)GLSL";

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

    vkmini::Vk vk;
    vk.createInstance({});
    vk.pickPhysicalDevice();
    vk.createDevice({});

    const uint32_t IMG = 64, N_UPLOADS = 5, RING = 2;

    // ---- ring -----------------------------------------------------
    struct Slot {
        vkmini::Vk::Buffer buf;
        vk::UniqueFence fence;
    };
    std::array<Slot, RING> ring;
    for (auto& s : ring) {
        s.buf = vk.createBuffer(
            IMG * IMG * 4, vk::BufferUsageFlagBits::eTransferSrc,
            vk::MemoryPropertyFlagBits::eHostVisible |
                vk::MemoryPropertyFlagBits::eHostCoherent,
            true);   // kalıcı mapped — sonsuza kadar mapped kalır
        s.fence = vk.device->createFenceUnique(
            {vk::FenceCreateFlagBits::eSignaled});  // başta idle
    }

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

    // ---- 2-slot ring üzerinden 5 upload akışa al -------------------
    std::vector<vk::UniqueCommandBuffer> inFlight;
    for (uint32_t up = 0; up < N_UPLOADS; ++up) {
        auto& slot = ring[up % RING];
        // sadece BU slot'un önceki kopyasını bekle — kuyruğu değil
        if (up >= RING)
            printf("[ring] slot %u in flight, waiting\n",
                   up % RING);
        (void)vk.device->waitForFences(slot.fence.get(), true,
                                       UINT64_MAX);
        vk.device->resetFences(slot.fence.get());

        // bu upload'un desenini doldur: i-inci image = hue-rotated grid
        auto* px = static_cast<uint8_t*>(slot.buf.mapped);
        for (uint32_t y = 0; y < IMG; ++y)
            for (uint32_t x = 0; x < IMG; ++x) {
                uint8_t* p = px + (y * IMG + x) * 4;
                bool even = ((x / 8) ^ (y / 8)) & 1;
                p[0] = even ? uint8_t(40 + up * 30) : 20;   // B
                p[1] = even ? uint8_t(40 + up * 30) : 20;   // G
                p[2] = even ? 220 : uint8_t(40 + up * 30);  // R
                p[3] = 255;
            }

        auto cbuf = std::move(
            vk.device->allocateCommandBuffersUnique(
                {vk.pool.get(), vk::CommandBufferLevel::ePrimary,
                 1})
                .front());
        vk::CommandBuffer c = cbuf.get();
        c.begin({vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
        vk::BufferImageCopy cp{};
        cp.setImageSubresource(
              {vk::ImageAspectFlagBits::eColor, 0, 0, 1})
            .setImageExtent(vk::Extent3D{IMG, IMG, 1});
        c.copyBufferToImage(slot.buf.buf.get(), dst.img.get(),
                            vk::ImageLayout::eTransferDstOptimal,
                            cp);
        c.end();
        vk::SubmitInfo si{};
        si.setCommandBuffers(c);
        vk.queue.submit(si, slot.fence.get());  // fence slot'a SAHİP
        // command buffer'ı fence retire olana kadar canlı tut —
        // gerçek ring'de aynı slot'un sonraki beklemesinde free edersin
        inFlight.push_back(std::move(cbuf));
        printf("[ring] upload %u -> slot %u submitted\n", up,
               up % RING);
    }
    vk.queue.waitIdle();

    // ---- sampled'a transition + çizim ------------------------------
    vk.oneTime([&](vk::CommandBuffer c) {
        vk::ImageMemoryBarrier b{};
        b.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
            .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
            .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setImage(dst.img.get())
            .setSubresourceRange(sr)
            .setSrcAccessMask(vk::AccessFlagBits::eTransferWrite)
            .setDstAccessMask(vk::AccessFlagBits::eShaderRead);
        c.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                          vk::PipelineStageFlagBits::eFragmentShader,
                          {}, {}, {}, b);
    });

    // pipeline + descriptor (kısa form — texture yazısına bak)
    auto vs = vk.shader(kVert, shaderc_vertex_shader, "s.vert");
    auto fs = vk.shader(kFrag, shaderc_fragment_shader, "s.frag");
    vk::DescriptorSetLayoutBinding b0{
        0, vk::DescriptorType::eCombinedImageSampler, 1,
        vk::ShaderStageFlagBits::eFragment};
    vk::DescriptorSetLayoutCreateInfo dslci{};
    dslci.setBindings(b0);
    auto dsl = vk.device->createDescriptorSetLayoutUnique(dslci);
    vk::DescriptorPoolSize ps{vk::DescriptorType::
                                  eCombinedImageSampler,
                              1};
    auto dpool = vk.device->createDescriptorPoolUnique(
        {{vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet}, 1,
         ps});
    vk::DescriptorSetLayout dslH = dsl.get();
    vk::DescriptorSetAllocateInfo dsai{};
    dsai.setDescriptorPool(dpool.get()).setSetLayouts(dslH);
    auto dsets = vk.device->allocateDescriptorSetsUnique(dsai);
    auto sampler = vk.device->createSamplerUnique(
        {{},
         vk::Filter::eNearest,
         vk::Filter::eNearest,
         vk::SamplerMipmapMode::eNearest});
    vk::DescriptorImageInfo ii{sampler.get(), dst.view.get(),
                               vk::ImageLayout::eShaderReadOnlyOptimal};
    vk::WriteDescriptorSet w{};
    w.setDstSet(dsets[0].get())
        .setDstBinding(0)
        .setDescriptorType(vk::DescriptorType::eCombinedImageSampler)
        .setImageInfo(ii);
    vk.device->updateDescriptorSets(w, {});

    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{};
    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 plci{};
    plci.setSetLayouts(dslH);
    auto layout = vk.device->createPipelineLayoutUnique(plci);
    vk::Format cfmt = vk::Format::eR8G8B8A8Unorm;
    vk::PipelineRenderingCreateInfo rendering{};
    rendering.setColorAttachmentFormats(cfmt);
    vk::GraphicsPipelineCreateInfo gi{};
    gi.setStages(stages)
        .setPVertexInputState(&vin)
        .setPInputAssemblyState(&ia)
        .setPViewportState(&vp)
        .setPDynamicState(&dyn)
        .setPRasterizationState(&rs)
        .setPMultisampleState(&ms)
        .setPColorBlendState(&blend)
        .setLayout(layout.get())
        .setPNext(&rendering);
    auto pres = vk.device->createGraphicsPipelineUnique({}, gi);
    if (pres.result != vk::Result::eSuccess)
        throw std::runtime_error("pipeline failed");
    auto pipe = std::move(pres.value);

    vkmini::Headless hl;
    hl.init(vk, 384, 384);
    hl.render(vk::ImageLayout::eColorAttachmentOptimal,
              [&](vk::CommandBuffer c) {
                  vk::RenderingAttachmentInfo att{};
                  att.setImageView(hl.color.view.get())
                      .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}, hl.extent})
                      .setLayerCount(1)
                      .setColorAttachments(att);
                  c.beginRendering(ri);
                  vk::Viewport v{0, 0, 384, 384, 0.f, 1.f};
                  c.setViewport(0, v);
                  vk::Rect2D sc{{0, 0}, hl.extent};
                  c.setScissor(0, sc);
                  c.bindPipeline(vk::PipelineBindPoint::eGraphics,
                                 pipe.get());
                  c.bindDescriptorSets(
                      vk::PipelineBindPoint::eGraphics, layout.get(),
                      0, dsets[0].get(), {});
                  c.draw(3, 1, 0, 0);
                  c.endRendering();
              });
    hl.savePpm(ppmOut);
    printf("wrote %s\n", ppmOut);
    return 0;
}

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


Check out similar posts by category: C++ Vulkan