VK_KHR_sampler_ycbcr_conversion ile YUV video karelerini doğrudan RGB olarak örnekleme

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

main.cpp (excerpt)
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:

main.cpp (excerpt)
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):

main.cpp (excerpt)
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:

main.cpp (excerpt)
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:

main.cpp (excerpt)
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

s.frag
#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

check.py
# 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")
example.sh
$ 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’taki pImmutableSamplers üzerinden almalı; descriptor başına yazmak VUID-VkDescriptorImageInfo-sampler-01564 / VUID-vkCmdDraw-None-08114‘tür (bu demonun ta kendisini VK_LAYER_KHRONOS_validation altı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_MPEG vb.).
  • Her formatın desteklendiğini varsaymayın — conversion-sampler ile manuel-shader fallback arasında seçim yapmadan önce vkGetPhysicalDeviceFormatProperties2 + VkSamplerYcbcrConversionImageFormatProperties ile araştırın.

YCbCr conversion sampler tarafından RGB’ye decode edilen dört kadran

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.


Check out similar posts by category: C++ Vulkan