Vulkan subpass'ları ve input attachment'ları açıklaması: doğru yapılan tile tabanlı render

Mobil GPU’lar DRAM’e render etmez — küçük bir on-chip tile buffer’ına (örn. 128×128 piksel) render eder, resolve eder, sonra sonraki tile’a geçer. Bir render target’ı sample etmek için geri okumak bir tile flush’a zorlar — tam olarak tile GPU’ların var olma nedeninin önlediği şey. Subpass’ler + input attachment’lar çözümdür: ikinci subpass, birinci subpass’in çıktısını hâlâ tile üzerindeyken okur, belleğe hiç dokunmaz.

Zihinsel model

tile.txt
subpass 0: albedo/normal/depth'i color attachment'lara render et
           (tile belleğinde kalır — henüz resolve yok)
subpass 1: lighting pass — albedo/normal'i input_attachment ile okur
           son rengi yazar
           → sadece BİR DRAM resolve'u, sonda

Subpass’siz: pass 1 DRAM’e yazar, pass 2 geri okur — iki tam framebuffer gidiş-dönüşü. Subpass’li: sıfır.

Kurulum

subpass.cpp
// iki subpass, bir render pass:
vk::AttachmentReference gbufferRefs[] = {albedoRef, normalRef};
vk::SubpassDescription sp0{};
sp0.colorAttachmentCount = 2;
sp0.pColorAttachments    = gbufferRefs;
sp0.pDepthStencilAttachment = &depthRef;

vk::AttachmentReference inputRefs[] = {albedoIn, normalIn, depthIn};
vk::SubpassDescription sp1{};
sp1.inputAttachmentCount   = 3;
sp1.pInputAttachments      = inputRefs;   // subpass 0'ın çıktısını OKU
sp1.colorAttachmentCount   = 1;
sp1.pColorAttachments      = &finalRef;

// bağımlılık — subpass 1, subpass 0'ın yazmalarını bekler:
vk::SubpassDependency dep{};
dep.srcSubpass    = 0;
dep.dstSubpass    = 1;
dep.srcStageMask  = vk::PipelineStageFlagBits::eColorAttachmentOutput;
dep.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
dep.dstStageMask  = vk::PipelineStageFlagBits::eFragmentShader;
dep.dstAccessMask = vk::AccessFlagBits::eInputAttachmentRead;

Shader tarafı

light.frag
// input_attachment_index N, set/binding — AYNI pikseli okur:
layout(input_attachment_index = 0, set = 0, binding = 0)
    subpassInput gAlbedo;
layout(input_attachment_index = 1, set = 0, binding = 1)
    subpassInput gNormal;

void main() {
    vec4 albedo = subpassLoad(gAlbedo);   // UV yok — aynı-piksel okuması
    vec3 n      = subpassLoad(gNormal).xyz;
    // aydınlatma…
}

subpassLoad‘un koordinatı yoktur — geçerli fragment’in pikselini okur. Onu tile-local yapan kısıt budur.

Hâlâ önemli olduğu yerler

  • Mobil/tile GPU’lar (Mali, Adreno, PowerVR) — tüm amaç bu; DRAM gidiş-dönüşleriyle deferred rendering bir performans uçurumudur.
  • Masaüstü immediate renderer’lar — orada subpass’ler etkisizdir (masaüstü okumaları zaten cache’ten geçer); dynamic rendering + ayrı pass’ler daha basittir.
  • Deferred shading — klasik kullanım; forward+ daha az faydalanır.

Pratik kurallar

  • gbuffer attachment’larında loadOp = eClear/eDontCare ve storeOp = eDontCare (pass içinde tüketilir, resolve edilmez) — verinin tile ile öldüğünü sürücüye söylemek perf kazancıdır.
  • Yalnızca subpassLoad — texture() sampling yok, offset’li texelFetch yok; input attachment’lar kesinlikle aynı-pikseldir.
  • gbuffer image’larında VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT + MEMORY LazilyAllocated — tile GPU’larda “bellek” sadece tile’dır; DRAM’de hiç allocate edilmeyebilir.
  • VK_KHR_dynamic_rendering + vkCmdNextSubpass… dynamic rendering için vkCmdNextSubpass yoktur — subpass’ler bir render-pass-object özelliğidir; dynamic rendering bunun yerine VK_KHR_dynamic_rendering_local_read kullanır (aynı fikir, farklı tesisat).
  • Bir input attachment = bir piksel okuması; bir komşuluk örneklemek için hâlâ gerçek texture + ayrı pass gerekir — subpass’ler blur yapamaz.

Check out similar posts by category: Vulkan C++