Example: VulkanExamples/p38_ycbcr
Serideki post 38 (post 5: texture upload).
Şimdiye kadar bir texture örneklemek RGB veri gir, RGB veri çık demekti. Video farklı: H.264/HEVC decoder’lar YCbCr plane’leri üretir (NV12, P010, I420 …) ve olağan yaklaşım — luma ile chroma’yı ayrı ayrı çekip matrisi kendisi çalıştıran bir fragment shader — ekstra fetch’ler ve el yapımı bir chroma upsampler’a mal olur.
VK_KHR_sampler_ycbcr_conversion (Vulkan 1.1’den beri core) bunların
hepsini sampler‘a taşır: bir multiplanar image oluşturursunuz,
bir conversion tanımını (BT.601/709/2020, narrow/full range, chroma
siting) sampler’a eklersiniz ve texture() hazır RGB döndürür.
Bu post 64×64 NV12 tarzı bir image oluşturur
(VK_FORMAT_G8_B8R8_2PLANE_420_UNORM — tam çözünürlüklü bir Y
plane’i artı yarım çözünürlüklü interleaved CbCr plane’i), dört
kadranı bilinen BT.601 studio-swing renkleriyle doldurur ve
örnekler.
Feature’ı etkinleştirme
vk::PhysicalDeviceVulkan11Features f11{};
f11.setSamplerYcbcrConversion(VK_TRUE);
vk.createDevice({}, &f11);Conversion nesnesi
VkSamplerYcbcrConversion renk uzayı dönüşümünü tanımlayan bağımsız
bir nesnedir:
vk::SamplerYcbcrConversionCreateInfo ci{};
ci.setFormat(fmt)
.setYcbcrModel(vk::SamplerYcbcrModelConversion::eYcbcr601)
.setYcbcrRange(vk::SamplerYcbcrRange::eItuNarrow)
.setComponents(vk::ComponentMapping{})
.setXChromaOffset(vk::ChromaLocation::eCositedEven)
.setYChromaOffset(vk::ChromaLocation::eCositedEven)
.setChromaFilter(vk::Filter::eNearest);
auto conv = vk.device->createSamplerYcbcrConversionUnique(ci);Kamera metadata’sının önemli olduğu yer burası: model (601/709/
2020) ve range (eItuNarrow = “studio swing”, luma 16–235 /
chroma 16–240, çoğu video için varsayılan; eItuFull = JPEG tarzı
0–255) encoder’ın ürettiğiyle eşleşmeli, yoksa her şey hafifçe soluk
ya da aşırı doygun çıkar. Chroma siting (eCositedEven vs
eMidpoint) yarım çözünürlüklü chroma örneğinin luma ızgarasına
göre nereye oturduğunu belirler — MPEG-2 tarzı akışlar midpoint’tir,
H.264 tipik olarak cosited’dir.
Multiplanar image
Bir _2PLANE_/_3PLANE_ formatı hâlâ TEK bir VkImage‘dir —
plane’ler tek allocation’da yaşar (eDisjoint‘i seçmediğiniz
sürece):
vk::ImageCreateInfo ii{};
ii.setFormat(vk::Format::eG8B8R82Plane420Unorm)
.setExtent({W, H, 1})
.setFlags(vk::ImageCreateFlagBits::eMutableFormat) // gerekli!
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled)
/* ... */;eMutableFormat zorunludur — spec, multiplanar bir image’ın bir
conversion sampler üzerinden örneklenmeden önce bunu gerektirir
(sampler dahili olarak her plane’i verinin kendi R8/R8G8
“view"u olarak görür).
Plane başına upload
vkCmdCopyBufferToImage hâlâ çalışır — imageSubresource.aspectMask
plane’i adresler (ePlane0 = Y, ePlane1 = CbCr, 3-plane’li
formatlarda ePlane2 = Cr). Extent’ler o plane’in texel’leri
cinsindendir: 64×64 4:2:0 bir image’ın CbCr plane’i her biri
2 bayt olan 32×32 texel’dir:
vk::BufferImageCopy cy{};
cy.setImageSubresource({vk::ImageAspectFlagBits::ePlane0, 0, 0, 1})
.setImageExtent({W, H, 1});
vk::BufferImageCopy cc{};
cc.setImageSubresource({vk::ImageAspectFlagBits::ePlane1, 0, 0, 1})
.setImageExtent({W / 2, H / 2, 1});
c.copyBufferToImage(sty.buf.get(), img, eTransferDstOptimal, cy);
c.copyBufferToImage(stc.buf.get(), img, eTransferDstOptimal, cc);Layout barrier’ları aspectMask = eColor kullanır, bu tüm
plane’leri bir kerede kapsar — luma plane’ini ayrı olarak geçiremezsiniz
(image eDisjoint olmadıkça, ki o başka bir hikâye).
Immutable-sampler kuralı
Atladığınızda validation’ın yakaladığı kısım budur: bir conversion taşıyan sampler immutable sampler olmalıdır — descriptor set layout’una gömülür — ve descriptor yazımı sonra yalnızca image view içerir, hiç sampler yoktur:
vk::SamplerYcbcrConversionInfo convInfo{};
convInfo.setConversion(conv.get());
vk::SamplerCreateInfo si{};
si.setMagFilter(vk::Filter::eNearest) /* ... */
.setPNext(&convInfo); // conversion sampler'ın İÇİNDE yaşar
auto sampler = vk.device->createSamplerUnique(si);
vk::Sampler imSamp = sampler.get();
vk::DescriptorSetLayoutBinding binding{
0, vk::DescriptorType::eCombinedImageSampler, 1,
vk::ShaderStageFlagBits::eFragment, &imSamp}; // pImmutableSamplers
// descriptor yazımı: sampler alanı YOK — layout zaten sahip
vk::DescriptorImageInfo ii2{nullptr, view.get(),
vk::ImageLayout::eShaderReadOnlyOptimal};Image view da convInfo‘yu zincirler (vi.setPNext(&convInfo)) —
view ile sampler conversion üzerinde aynı fikirde olmalı. Bu
katılığın nedeni: conversion hızlı yollara derlenir; descriptor
başına değiştirmek “fetch başına sıfır ek iş” fikrinin tamamını
bozardı.
Pratik sonuç: renk uzayı başına bir immutable binding. BT.601 ve BT.709 kameralarını karıştıran bir video wall küçük bir conversion nesnesi seti tutar ve eşleşeni bind eder.
Shader tam bir hayal kırıklığı — kasıtlı olarak
#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); // luma+chroma fetch, upsample, matrix
}Tek bir texture() çağrısı. Chroma upsampling (chromaFilter uyarınca
nearest veya filtered) ve YCbCr→RGB matrisi texture unit’in içinde
çalışır.
Plane’leri doldurma
Demo bilinen BT.601-narrow kodlamalarını — kırmızı (82,90,240),
yeşil (146,54,34), mavi (41,240,110), beyaz (235,128,128) —
tam çözünürlükte luma plane’ine ve yarım çözünürlükte paylaşılan
CbCr plane’ine yazar, böylece her çıktı kadranı sRGB ana rengi
olarak geri dönmelidir.
Kontrol
# SPDX-License-Identifier: CC0-1.0
import re, subprocess
subprocess.run(["./app", "--headless", "out.ppm"], check=True,
capture_output=True)
d = open("out.ppm", "rb").read()
m = re.match(rb"P6\s+(\d+)\s+(\d+)\s+(\d+)\s", d)
w, h = int(m.group(1)), int(m.group(2))
px = d[m.end():m.end() + w * h * 3]
def P(x, y):
i = (y * w + x) * 3
return px[i], px[i + 1], px[i + 2]
def near(a, b, tol=6):
return all(abs(x - y) <= tol for x, y in zip(a, b))
# NV12 image'ının kadranları: SA=kırmızı SÜ=yeşil AA=mavi AB=beyaz
assert near(P(w // 4, h // 4), (255, 0, 0)), P(w // 4, h // 4)
assert near(P(3 * w // 4, h // 4), (0, 255, 0)), P(3 * w // 4, h // 4)
assert near(P(w // 4, 3 * h // 4), (0, 0, 255)), P(w // 4, 3 * h // 4)
assert near(P(3 * w // 4, 3 * h // 4), (255, 255, 255)), \
P(3 * w // 4, 3 * h // 4)
print("OK: NV12 YCbCr converted to RGB inside the sampler")$ python3 check.py
OK: NV12 YCbCr converted to RGB inside the sampler
±6 tolerans conversion matrisindeki tamsayı yuvarlamayı absorbe eder — renkler YUV olarak kodlandı, yani bu Python’da yansıtılmış bir formülden ziyade tüm donanım yolunu doğrular.
Nasıl KULLANILMAMALI
- Conversion yapan sampler’ı
pImageInfo‘ya koymayın — descriptor onu layout’takipImmutableSamplersüzerinden almalı; descriptor başına yazmakVUID-VkDescriptorImageInfo-sampler-01564/VUID-vkCmdDraw-None-08114‘tür (bu demonun ta kendisiniVK_LAYER_KHRONOS_validationaltında çalıştırarak bulundu). - Image’daki
eMutableFormat‘ı unutmayın — conversion üzerinden örneklenen multiplanar image’lar için gerekli. - Range/model’i tahmin etmeyin — narrow-range bir akışın
full-range olarak decode edilmesi siyahları ve beyazları ezer;
konteynerin ne söylediğini loglayın (FFmpeg’de
AVCOL_RANGE_MPEGvb.). - Her formatın desteklendiğini varsaymayın — conversion-sampler
ile manuel-shader fallback arasında seçim yapmadan önce
vkGetPhysicalDeviceFormatProperties2+VkSamplerYcbcrConversionImageFormatPropertiesile araştırın.
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.
