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
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:
[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 submittedSon 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
inFlightlistesine 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_COHERENTtercih et — olmadan her yazıştavkFlushMappedMemoryRangesgerekir; 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â
Transfersüresi tutar.
Derleme ve çalıştırma
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app --headless out.ppm && python3 check.py out.ppm#!/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
// 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.