“With great power comes great responsibility”
Giriş
Bu yazı, yazmayı planladığım Vulkan serisinin giriş kısmı niteliğinde olacak. Öğreneceğimiz konuların fazla olmasından dolayı serinin de bir hayli uzun olmasını bekliyorum. Hedefim ben bu konuyu öğrenirken; “Keşke bu konuyu böyle anlatan bir yazı olsaydı, böylece anlamam daha kolay olurdu” dediğim yazıları, bir öğretici seri halinde başka insanlara sunmak. Ayrıca oldukça yeni olan bu API hakkında ciddi ve üzerine düşünülmüş bir Türkçe kaynak olmasını da istiyorum. Malesef ülkemizde bu konuda hem ciddi araştırma yapan insanların sayısı az, hem de ilgilenen insanlar uygun kaynak bulamadıkları için başka alanlara yönelmek zorunda kalıyorlar. Bu seri ile bu algıyı kırmayı hedefliyorum.
Bu yazıyı yazarken hala mühendislik öğrencisi olduğumu hatırlatmak isterim. Her ne kadar öğrenmek için gecemi gündüzüme katsam da, ben de bir insanım ve hata yapabilirim. Bu yüzden yazıların altına yorum yazarak eksik gördüğünüz kısımları belirtirseniz çok mutlu olurum.
Vulkan’ın bakış açısı
Konulara derinlemesine girmeden önce, niye Vulkan’ın var olduğunu ve çözdüğü problemlere değinirsek, anlamamızın daha kolay olacağını düşünüyorum. Daha detaylı bir inceleme isterseniz, yazdığım “Vulkan API nedir ?” yazısına bakabilirsiniz.
Eskiden GPU’lar çok katı ve dışa kapanık cihazlardı. Yaptıkları iş belli idi ve oluşturulan donanım da bu işleve özel olarak hazırlanırdı. Aslında bir bakıma, günümüzde kripto madenciliği için oluşturulan özel cihazlara benzetebiliriz. Oyunlarda oluşturmak istediğimiz görüntülerin işlemden geçtiği belirli bir sıra bulunmakta ve buna “Grafik Boru Hattı/ Graphics Pipeline” diyoruz. Eski GPU’lar da bu boru hattını aslında bir nevi donanım halinde sunuyorlardı. 3 boyutlu nesnelerin 2 boyutta temsil edecek bir bölüm, nesneleri ışıklandırmaya göre renklendirecek bir bölüm, ekranda her bir pikselin göstereceği rengi hesaplayan bir bölüm vs. Böylece CPU’ların, mimarisi gereği, işleyemediği veya çok yavaş işlediği işlem yükleri, belirli bir yol veya “Boru Hattı/Pipeline” ile çok daha hızlı işleniyordu. İleride daha çok üzerinde duracağımız graphics pipeline’ın ismi de bir nevi buradan gelmektedir.
Daha sonra geliştiriciler kendi geliştirdikleri ışıklandırma sistemleri, görsel efektler gibi şeyleri oyunlarına eklemek istediler. Fakat bu var olan GPU’ların yapamadığı bir şey idi. Donanım bunun için tasarlanmıştı ve dışına çıkılamazdı. Bu sorunu çözmek için GPU geliştiricileri, grafik boru hattının belirli kısımlarını programlanabilir hale getirmeye çalıştı, ve (kullanıma hazır bir ürün olarak) ilk kez 2001 yılında Nvidia Geforce 3 serisi piyasaya sunuldu. Böylece boru hattında önceden programlanamayan vertex shader, fragment/pixel shader gibi kısımlar programlanabilir oldu, geliştiriciler de kendi kodlarını GPU üzerinde çalıştırma imkanı buldular. Bu değişim ise hızlanarak devam etti. İlerleyen yıllarda GPU’lar çok daha “genel” bir yapıya büründü. Eskiden sadece belirli işlemlerden sorumlu olan birimler, artık genel amaçlı “Unified shader” halini aldı. Böylece yapabilecekleri işlemler çeşitlendi. Fakat bu genel amaçlı olarak üretilen birimlerin, günümüzdeki işlemciler kadar bir özgürlük sunmadığını da belirtmek isterim. GPU’ların en başta bu devasa hesaplama yeteneğine sahip olmasının temel sebebi, donanımın belirli bir amaca yönelik tasarlanmasıydı. Bu yüzden belirli işlemler için ise yeni donanım bölümleri oluşturuldu. Örneğin ışın izleme (Ray tracing) işlemi, çok pahalı bir işlem olduğu için, özel bir donanımı bulunmaktadır. Aynı şekilde belirli matematik işlemleri için belirli bölümler, hafıza ve ön bellek işlemleri için ayrı bölümler vardır. Bu özellik kümesi büyürken, var olan API’lar ise bu özelliklerin altında ezilmeye başladı. En başta bu API’lar eski GPU mimarileri için tasarlanmıştı. Yeni eklenen özellikleri desteklemek bir hayli pahalıya mal olurken, modern işlem birimlerinin çoklu çekirdek işleme kapasitesini de kullanamıyorlardı.
Bu problemi Vulkan, GPU sürücülerinin sorumluluğunu geliştiriciye vererek çözüyor. Yapacağımız işlemleri önceden detaylı şekilde belirterek hem GPU’daki hangi özellikleri ve donanım birimlerini kullanmak istediğimizi belirtiyoruz, hem de senkronizasyonu geliştiricinin sorumluluğuna vererek, çoklu çekirdekte programlarımızı geliştirmemizi çok daha kolay hale getiriyoruz. Ayrıca CPU-GPU iletişimini de azaltarak, GPU’nun sürekli meşgul tutulmasını sağlıyor ve CPU üzerindeki yükü hafifletiyoruz. Bu olay özellikle mobil cihazlar için hayati bir önem taşıyor.
Problemimizi ve çözümünü özetlediğimize göre, bizi bekleyen geliştirme sürecini de bir gözden geçirmekte fayda var. Vulkan, bu problemleri çözerken belirli bir bakış açısıyla ilerliyor ve geliştirici olarak bunu anlamak, ilerleyen zamanlarda işimizi çok daha kolay hale getirecektir. Fakat ilerlemeden önce, yeni başlayan kişiler için çok önemli bir konuya değinmek istiyorum.
Mental Model
Vulkan’ı kullanırken çok fazla iş yükümüz olduğunu söylememe gerek yok sanırım. Bu yüzden belirli bir altyapı oluşturana kadar, verdiğiniz emeğin karşılığını göremeyebilirsiniz. Bunun sizin azminizi kırmasına izin vermeyin. Bu durumun önüne geçmek için, öğrenme maceranız boyunca; “Bugün şöyle güzel görünen bir nesneyi ekranda çizdirdim” yerine “Bugün kullanacağım GPU’yu seçmeyi öğrendim” veya “Bugün command buffer oluşturmayı öğrendim” gibi kendinizi değerlendirmeniz, öğrenme hevesinizi canlı tutmak için çok faydalı olacaktır. Ayrıca adil olanda budur. Çünkü ekranda bir şey görmüyor oluşunuz, sizin bir şey öğrenmediğinizi ifade etmez. Vulkan’ın bu detaycı yaklaşımından dolayı öğrenme süreci sancılıdır, fakat sonuçta alacağınız ödül ise muazzamdır. İlerleyen zamanlarda öğrenme süreciniz kar topu efekti gibi, çok hızlı ve kendiliğinden devam eden bir hal alacak. Sadece bir oyun yapmayı değil, oyunun yapıldığı oyun motorunu nasıl yapacağınızı öğreneceksiniz. Dahası, elinizde bulunan ekran kartının, muazzam boyutta işlem kabiliyetine sahip bir canavar olduğunu fark edeceksiniz. Önerim, bir an önce sonuca ulaşmak yerine, sindire sindire ilerlemeniz. Okuyacağınız yazıların sonunda anlamadığınız noktaların olması doğaldır ve kendinizi anlamadım diye sakın strese sokmayın. Anlamadığınız yerleri tekrar tekrar okuyun. Eğer yine anlamaz iseniz yazıların sonuna yorum yazarak yardım isteyin. Yardım istemekten korkmayın; soru sormak sizi küçük düşürmez, aksine öğrenme sürecinde ilerlediğinizi gösterir.
Bir Üçgenin Hikayesi
Yeni bir programlama diline başladığınızda yapacağınız ilk şey ne olur ? Tabii ki de ekrana “Hello World” yazdırırsınız. Grafik programlamada ise durum benzerdir. Ekranda hello world yazdırmak yerine, yapılabilecek en temel işlemi yaparız; ekranda bir üçgen oluştururuz. Bilgisayar grafiklerinin yapı taşı üçgenlerdir, gerçek hayatta bunu atomlara benzetebilirsiniz. Oluşturduğunuz her şekil üçgenden oluşur. Bunun sebebi üçgenin en az kenara sahip olan (böylece en basit yapıya sahip) ve 3B düzlemde gösterilebilen şekil olmasıdır. Eğer ekranda üçgen gösterebiliyorsak, her türlü karmaşık şekli de gösterebiliriz. Bu yüzden yapacağımız ilk işlem, ekranda basit bir üçgen göstermek olacak.
Fakat Vulkan’da yapacağımız her işlemi önceden belirtmemiz gerektiği için, yapmamız gereken belirli işlemler bulunmakta. Örneğin en basitinden ekranın çözünürlüğünü ve ekranda gösterilecek piksellerin formatını belirtmekten, grafik boru hattının aşamalarını teker teker belirtmeye kadar ilerleyen bir süreç bizi beklemekte. Bu işlemlerin çoğu tek bir seferlik işlemler olmakla beraber, ilerleyen aşamalarda tekrar bazı kısımlara döneceğimizden her bir aşamayı anlamak önemli. Bu yüzden, basit bir üçgen oluşturmak için yapacağımız işlemleri özet geçerek anlatmak istiyorum. Belirli aşamaları anlamamanız normaldir, detaylı olarak her aşamayı bölüm bölüm ilerleyen derslerimizde inceleyeceğiz.
Adım 1: Instance oluşturma
Gerçek hayatta nasıl her bir şirketin bir temsilcisi, her bir devletin bir temsilcisi hatta her bir ortaokul sınıfının bir temsilci öğrencisi varsa, Vulkan’da da yapacağımız işlemlerde kullanacağımız bir temsilci oluşturmamız gerekli. Bu temsilci, türkçe çevirisini bulamadığımdan dolayı bu kısımdan sonra instance demeye devam edeceğim, bizim kullanacağımız özellikleri ve kullanacağımız fiziksel cihazları temsil etmekte. Bir nevi, ekran kartı sürücüsünün temsilcisi gibi düşünebilirsiniz. Belirli bir aşamaya kadar bize yardımcı olacak, GPU’yu sorgulamak ve onu kullanmak için gereken fonksiyonları bize sağlacayacak bir nesneden ibaret. Ayrıca daha sonra göreceğimiz “Validation Layers” gibi hata ayıklama mekanizmalarını bu aşamada, instance oluştururken belirteceğiz.
Adım 2: Kullanılacak fiziksel cihazı seçme
Instance nesnemizi oluşturduktan sonra sıra, kullanacağımız fiziksel cihazı seçmeye geliyor. Bu aşamada kullanmak istediğimiz cihaz veya “cihazları”, özelliklerine ve işlevlerine bağlı olarak seçeriz. Örneğin çok yorucu bir oyun çalıştırmak istiyorsak harici ekran kartını, arayüz gibi basit işlemler gerçekleştirecek isek dahili ekran kartını kullanabiliriz. Dahası, bizim ihtiyacımız olan özellikler ve eklentileri sorgulayarak cihazlar arasında bir tercih sıralaması yapabiliriz; ne kadar VRAM kapasitesi var ? Ray tracing destekliyor mu ? İleride kullanacağımız descriptor indexing özelliğini destekliyor mu ? gibi sorular ile elimizdeki cihazlara skor vererek en yüksek skora sahip olan cihazı seçebiliriz. Ayrıca tek bir cihaz ile sınırlı kalmayabilir, birden fazla GPU’yu aynı anda kullanabiliriz. Fakat biz bu seri boyunca tek bir GPU üzerinden gideceğiz.
Adım 3: Device nesnesi oluşturma
Kullanacağımız cihazı seçtikten sonra, yapacağımız işlemlerde kullanacağımız “Device” nesnesini oluşturuyoruz. Bu nesne, bizim cihaz ile ilgili yapacağımız her bir işlem için gerekli olacak. Bir nevi, kullandığımız cihazı işlevleriyle birlikte temsil eden bir nesnedir. Yalnız nesne kavramını, Nesne Tabanlı Programlamadaki nesne ile karıştırmayın. Bu nesneler aslında sürücünün kullanılan fiziksel kaynakları takip edebilmesi için kullanılan basit veri yapılarından ibaret. Hatta büyük çoğunluğu işaretsiz tamsayıdır. Bu nesneleri, kullanacağımız fonksiyonlara parametre vererek işlemlerimizi gerçekleştireceğiz. Örneğin ekran kartı hafızasında kendimize bir alan ayırmak istersek, kullanacağımız fonksiyonda vereceğimiz ilk parametre device nesnesi olacaktır. Aynı şekilde, ekran kartına komut gönderirken, hafıza transferi işlemlerini gerçekleştirirken, kısacası fiziksel cihazımızı kullandığımız her an, device nesnesini de parametre olarak vereceğiz. Ayrıca daha önce fiziksel cihazımızı seçtiğimiz aşamada sorguladığımız özellikleri de device nesnesini oluştururken belirtiyoruz. Özetle; device nesnesi hangi ekran kartını, hangi özelliklerini etkinleştirerek, hangi şekilde kullanacağımızı temsil eden bir nesnedir.
Hadi yapacağımız işlemleri daha iyi anlamanız için küçük bir araştırma yapalım. Hangi ekran kartlarının hangi özellikleri desteklediğini gösteren https://vulkan.gpuinfo.org/ adresinde bir site var. Bu siteye sahip olduğunuz ekran kartının modelini yazarak, ekran kartınızın sahip olduğu özellik ve eklentileri inceleyin. Öncelikle arama kısmına ekran kartının modelini yazın. Örneğin benim ekran kartım Nvidia RTX 2080 Super olduğu için arama kısmına “2080 Super” yazmam gerekiyor. Daha sonra çıkan sonuçlardan sahip olduğunuz ekran kartını seçin. Sonraki aşamada ise hangi platform ve sürücü sürümü için sorgu yapmak istediğinizi seçin. Bu seçimin sebebi eski sürücülerde veya işletim sistemlerinde kullanılabilecek özellikler değişeceği içindir. Daha sonra çıkan ekranda, ekran kartınıza ait olan özellikleri görebildiğiniz kısım gelecektir. Properties kısmı cihazınızın limitlerini gösterir; örneğin ne kadar VRAM’e sahip gibi. Features kısmı yaygın olarak kullanılan özelliklerin desteklenip desteklenmediğini gösterir. Örneğin geometry shaders, grafik boru hattında yer alan özel bir aşamadır ve her cihazda desteklenmeyebilir. Extensions kısmı ekran kartının sahip olduğu ek özellikleri gösterir. Genelde bunlar özel eklentilerdir ve her koşulda etkinleştirmeniz gerekmediği gibi, üreticiden üreticiye değişen özelliklerdirler. Örneğin “VK_NV_cuda_kernel_launch” eklentisi sadece Nvidia cihazlarında bulunan ve Vulkan’ın CUDA kütüphanesi ile uyumlu çalışmasını sağlayan bir eklentidir. Aynı şekilde AMD’nin de kendine özel eklentileri var ise burada gözükür. Formats kısmı ekran kartının hangi işlemlerde hangi görüntü veya hafıza formatlarını kullanabileceğini gösterir. Queue Families kısmı cihaza gönderebileceğimiz komutların işlendiği kuyruk tiplerini gösterir, açıklamasını aşağıda yapacağım. Kullanacağımız bu queue family’leri de device nesnesini oluştururken belirtmemiz gerektiğini de hatırlatırım. Memory kısmı, ekran kartında kullanılan hafıza tiplerini belirtir. Surface kısmı, ekran kartının görüntü çıktısı için kullanacağımız formatları, çıkış modlarını (V-Sync, Triple Buffering vs.) ve özelliklerini belirtir. Instance ise instance nesnemizi oluşturduğumuz aşamada etkinleştirebileceğimiz özellikleri gösterir. Ve son olarak Profiles kısmı ise, ekran kartının belirli standartları karşılayıp karşılamadığını belirtir. Bu kısmı ilerleyen yazılarda açıklayacağım.
Örneğin, ben ekran kartımın ray tracing desteği olup olmadığını öğrenmek istiyorsam, extensions kısmına gelip arama yerine “ray tracing” yazmam yeterlidir. Ray tracing ile ilgili desteklenen eklentiler, varsa, ekrana sıralanacaktır.

Instance oluştururken, fiziksel cihazımızı seçerken ve device nesnesi oluştururken burada bulunan özellikleri kodumuzda sorgulayacağız. Eğer merak ediyorsanız kendi cihazınızın özelliklerini kontrol edin.
Adım 4: Kullanacağımız Queue nesnelerini oluşturma
Yazının başında, ekran kartlarının bir çok özel işlem bölümlerine sahip olduğuna değinmiştik. Ve device nesnemizi oluştururken ise bu fiziksel cihazımızda kullanacağımız özellikleri seçtiğimizi de belirttik. Device nesnemizi oluştururken, komutlarımızı göndereceğimiz işlem kuyruğu tiplerini yani “Queue family”lerimizi de belirttik (Yani yukarıda “nasıl kullanacağımız” derken bunu kastediyordum). Bu queue family’leri, fabrikanın işlem bantları gibi düşünebilirsiniz. Araba fabrikasını örnek verelim; belirli bantlar arabanın kaputunu üretirken, belirli bantlar üretilen kaputları boyar, belirli bantlar ise boyanan kaputları arabaya monte eder. Her bant, belirli bir işlev üzerine şekillendirilmiştir ve sadece belirli bir amaca hizmet eder. Aynı şekilde, GPU’da da belirli amaca hizmet eden donanımlar bulunmakta. Komutları gönderirken kullanacağımız queue family’ler, donanımda hangi bölümü nasıl kullanacağımız belirtir ve aslında GPU’da bir karşılığı vardır. Örneğin ekranda görüntü oluştururken yapacağımız “render” işlemlerini, grafik işlemlerini destekleyen bir queue family’den gönderebiliriz, fakat bu işlemi desteklemeyen bir queue family’e render işlemlerini gönderemeyiz.
Kullanacağımız Queue family’leri seçtikten sonra, device nesnesini oluşturup, seçtiğimiz queue family’lerden queue nesneleri oluşturacağız. Komutları gönderirken kullanacağımız nesneler bu queue’ler olacak. Eğer yine fabrika örneğinden devam edersek; queue family’ler fabrikadaki bantların bölümleri iken (Kaput oluşturma, boyama, montaj), queue’ler bu bantları temsil eder.

Örneğin yukarıdaki resimde kırmızı daire içine aldığım kısım queue family’leri (Fabrikadaki üretim bantının tipi), yeşil içine aldığım kısım bu queue family’lerden kaç tane queue oluşturabileceğimizi (Bant sayısı), mavi içine aldığım kısım ise bu queue’ların hangi komutları desteklediğini gösteriyor. Gördüğünüz üzere her queue her işlemi desteklemiyor. Geriye dönük uyumluluktan ve genel amaçlı kullanım için Vulkan, standart olarak en az bir tane presentation, graphics ve compute işlemlerini gerçekleştirebilen bir queue olacağını garanti eder, fakat bu queue size her durumda en uygun performansı sağlamayabilir. Detaylarını ilerideki derslerimizde açıklayacağım.
Adım 5: Windows surface ve Swapchain oluşturma
Bu adımda oluşturduğumuz kareleri ekranda çizdirmek için kullanacağımız kaynakları oluşturuyoruz. Surface nesnemiz işletim sisteminin bize sunduğu ve görüntü oluşturmamızı sağlayan kaynaktır. Böylece işletim sistemi bizim çizdiğimiz kareleri diğer uygulamalar ile birlikte uyum içinde gösterebilir. Bu kısım, işletim sistemine bağımlı olduğu için her platforma özel olarak yazılması gerekir. Fakat biz bunun yerine, çoğu kişinin kullandığı yoldan giderek yardımcı bir kütüphane olan GLFW kullanacağız. Bu kütüphanenin amacı pencere yönetim sistemlerini platform bağımsız olarak geliştirmemizi sağlamaktır. Böylece Windows, Linux, Mac gibi sistemlerde aynı kod aynı şekilde çalışır.
Ardından Swapchain nesnemizi oluşturmamız gerekiyor. Swapchain dediğimiz şey ise, işletim sisteminin bize sunduğu ve ekranda çizilecek karelerin yazılacağı bellek alanımız, daha doğrusu “framebuffer”ımız(basitçe bellek alanı demek yanlış olur). Bildiğiniz gibi videolar, yani akan görüntüler temelde arka arkaya gösterilen statik karelerden oluşur. Zamanında mutlaka bir defterin her sayfasına küçük küçük resimler çizip, arka arkaya hızlıca sayfayı çevirerek bir animasyon oluşturmuşsunuzdur. Buradaki mantık da aynı. Swapchain’imizi oluştururken, aslında işletim sisteminden, çizmek istediğimiz sayfaları alıyoruz. Biz bir sayfayı çiziyoruz ve işletim sistemine, “Bu sayfayı ekranda gösterebilirsin” diyoruz. Daha sonra sonraki sayfaya geçiyoruz. Bunu saniyede 24 kere yaptığımızda ise bir film sahnesi, 60 kere yaptığımızda standart bir oyun, 144 ve daha fazlası ise çok daha akıcı bir oyun görüntüsü elde etmemizi sağlıyor. Bu sayfalara ilerleyen zamanlarda “framebuffer” diyeceğiz, ve sadece bir bellek alanını temsil etmekten ziyade, çizilen görüntünün formatı gibi özellikleri de barındıracak. İşletim sisteminin bize kaç tane framebuffer vereceğini biz kendimiz seçeriz, fakat garanti olarak istediğimiz kadar framebuffer elde edemeyiz(Donanımdan donanıma veya işletim sistemine fark ediyor). Örneğin yaygın olarak; 3 tane framebuffer aldığımızı düşünelim. Biz tüm işlemlerimizi bitirip görüntünün çizildiği framebuffer’ı işletim sistemine göndeririz. Ardından işletim sistemi görüntüyü ekranda çizerken, biz hiç beklemeden 2 kareyi çizmeye geçeriz. 2. framebuffer’ı, yani çizdiğimiz kareyi işletim sistemine gönderir ve 3.’ye geçeriz. Ardından tekrar ilk sayfaya döneriz ve bu döngü devam eder. Özetle; bu aşamada işletim sisteminden, görüntüyü ekranda çizdirmek için kullanacağımız kaynakları temin ederiz.
Adım 6: Renderpass oluşturma
Bu aşamada “Renderpass” dediğimiz ve Vulkan’a özel olan bir nesne oluşturacağız. Renderpass’in amacı, grafik boru hattımızda yapacağımız işlemlerde kullanacağımız resimlerin hangi formatta olduğunu, nasıl işlemlerden geçeceğini, yapılan işlemlerden sonra bu resimlerin formatlarının başka bir formata dönüşüp dönüşmeyeceğini ve hatta çizim öncesinde ekranı belirli bir renk ile temizlenip temizlenmeyeceğini (Kağıda çizilen resimleri silgi ile silerek temizlemek gibi düşünün) seçeriz. Ayrıca Renderpass nesneleri “Subpass” denilen aşamalardan oluşur, birden çok subpass, bir araya gelerek renderpass’i oluşturur. Bu subpass’ler arasında da bir senkronizasyon yapabiliriz. Örneğin, bir görsel efekt işlemini gerçekleştirebilmemiz için, öncelikle ham görüntünün (işlemden geçecek görüntünün) çizim işleminin bitmiş olması gerekir. Bir subpass, bizim ham görüntümüzü oluştururken, diğeri görsel efektimizi oluşturduğunu farz edersek, ilk subpass’de çizim işlemi bittikten sonra 2. subpass’in hesaplama işlemi başlasın gibi bağımlılıklar tanımlayabiliriz. Bu bağımlılıklara ise “Subpass Dependencies” denmektedir.
Renderpass kavramı, açıkçası benim anlamakta en zorlandığım kısımdı. Çünkü sadece Vulkan’a özel olan bu kavramı, daha önceden tecrübe ettiğim OpenGL’de görmemiştim. Fakat uzun araştırmalar sonucu renderpass kavramının sadece ne olduğunu değil, ne zaman kullanılıp kullanılmadığı, hangi probleme karşılık olarak ortaya çıktığı gibi soruların cevaplarını kafamda oturtmaya çalıştım. İlerleyen yazılarımızda bu konuya geldiğimizde, çok detaylı bir açıklama yaparak nasıl bu kavramın, özellikle mobil çiplerde mükemmel performans geliştirmelerine yol açtığını anlatacak ve renderpass’i ne zaman ve ne şekilde kullanmamız gerektiğini detaylıca açıklayacağım.
Adım 7: Graphics Pipeline/Grafik Boru Hattı oluşturma
Renderpass’imizi oluşturduktan sonra ise sıra, tüm parçaların bir araya geldiği Graphics Pipeline nesnemizi oluşturmaya geliyor. Grafik Pipeline, türkçesi ile grafik boru hattımız, bizim render işlemi sırasında gerçekleştireceğimiz işlemleri açıkladığımız nesnedir. Daha önceden GPU’ların bu boru hattını önceden belirli bir şekilde çalıştırdığını ve müdahelenin çok kısıtlı olduğunu söylemiştik. Günümüzde ise grafik boru hatları, sabit, ayarlanabilir ve programlanabilir olarak 3 farklı türde aşamalardan oluşur. Biz boru hattımızı oluştururken, bu ayarlanabilir ve programlanabilir kısımları açıkça belirtiriz. Örneğin viewport gibi ekranda çizilecek görüntünün boyutunu belirttiğimiz kısım ayarlanabilir bir kısımdır ve burada parametre verir gibi bu kısımları ayarlarız. Vertex shader ve fragment shader gibi kısımlar ise programlanabilir kısımlardır ve bu kısımlarda çalışacak kodumuzu, önceden derlediğimiz SPIR-V formatında sunarız. Bu derleme kısmına, konuya ait yazımızda değineceğim.
Vulkan’ın doğası gereği, izleyeceğimiz her adım, grafik boru hattı oluşumunda belirtilir, fakat oluşturulduktan sonra değiştirilemez. Bunu dezavantaj olarak görmeyin, bu yöntem bize GPU’da yapılacak işlemleri önceden derleyerek program çalışırken derleme yapmak yerine başlatma zamanında derleme yapma imkanı sunar. Böylece çalışma zamanında derleme yapılmadığı için program daha akıcı olur ve takılmaz. Kısacası “Frame drop” dediğimiz olayı minimuma indiririz. Fakat boru hattında bir şey değiştirmek için, tüm boru hattını tekrardan oluşturmamız gerekir. Sadece belirli dinamik aşamalar boru hattı üzerinde değiştirilebilir (Viewport ve scissor gibi). Geri kalan aşamalar için ise tamamen yeni bir boru hattı oluşturmalıyız. Yaygın olarak kullanılan teknik ise, programda kullanılabilecek tüm boru hatlarını önceden oluşturmaktır. Bunun için bulunan belirli teknikleri ve çözümleri, boru hattını anlatacağımız yazımızda detaylıca inceleyeceğiz.
Adım 8: Command Pool ve Command Buffer nesnelerini oluşturma
Vulkan’da GPU’ya göndereceğimiz komutlar “Command Buffer” denilen bir belleğe kaydedilerek gönderilir. Bu bellek alanlarını oluşturmak için ise “Command Pool” denilen nesnemizi de oluşturmalıyız. Command Buffer terimi, Vulkan’a özel olmamakla beraber yeni nesil API’ların hemen hemen hepsinde vardır. Amacı ise hem komutları bir bütün halinde göndererek CPU-GPU iletişimini azaltmak, yani tek bir CPU çağrısı ile birden fazla komut göndererek GPU’yu meşgul tutmaya yardım etmek, hem de komutları belleğe kaydederek tekrar kullanılabilir hale getirmektir. Böylece CPU aynı komutları tekrar tekrar göndermekten kurtulur. Command Pool ise bu komut belleklerinin, yani command buffer’ların, bellek alanlarını ayırmak için kullanılır. Tekrar tekrar hatırlatmak gibi olacak ama; Vulkan’da yapacağımız her şeyin hafıza yönetimini de biz yaparız. Buna komutları tutacağımız bellekler de dahildir.
Adım 9: Programın işleyişi başlar (Main loop)
Bu noktada ise ihtiyacımız olan çoğu kaynağı oluşturmuş oluruz. Artık, render işlemimizi yapmak için tek gerekli olan işlem, oluşturduğumuz bu sistemi çalıştırmaktır. Eğer bizim için gerekli veriler varsa belleğe yükler, ardından işlemlerimize başlarız. Hemen hemen her zaman bu kısım bir döngü şeklinde olur. Render işlemlerimizi yapar, görüntüyü işletim sistemine sunar, pencerede yapılan giriş çıkış işlemlerini işler, tekrar başa döneriz.
Vulkan’da kullanacağımız bir konsept
Bu yazıyı burada noktalamadan önce, Vulkan’da kullanacağımız bir yöntemden bahsedeceğim. Vulkan’da parametre olarak göndereceğimiz verileri, veri yapıları halinde sunarız. Örnek olarak;
Vk....CreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_...._CREATE_INFO;
createInfo.foo = bar;
createInfo.pNext = nullptr;
Vk... object;
if (vkCreate...(..., &createInfo, nullptr, &object) != VK_SUCCESS) {
throw std::runtime_error("failed to create object!");
}
Yukarıda verdiğim kod parçası, aşağı yukarı yapacağımız çoğu işlemi özetlemektedir. Bir nesne oluşturmadan önce bir oluşturma yapısı, ya da create info, dediğimiz bir veri yapısı oluştururuz. İlk önce bu veri yapısında “sType” denilen kısma bu veri yapısının ne için kullanılacağını belirtiriz. Daha sonra veri yapısındaki değişkenleri doldururuz. Bu değişkenler bizim parametrelerimizdir. En sonunda “pNext” olan kısım ise bizim veri yapılarını birbirine bağlayarak birden fazla veri yapısını parametre olarak gönderebilmemizi sağlar. Genelde nullptr olarak kullanırız, fakat zaman zaman yeni çıkan özellikleri kullanırken bu kısma başka veri yapıları da eklememiz gerekebilir. En sonunda ise nesnemizi oluşturmak için kullanacağımız fonksiyona gösterildiği gibi parametrelerimizi veririz. İlk parametre, eğer fiziksel cihazımız ile alakalı bir fonksiyonsa device nesnemiz, instance ile ilgili ise instance nesnemizi veririz. Kısacası hangi aşama veya kısım ile ilgili ise onun nesnesini veririz. Daha sonra oluşturduğumuz create info yapısınının bellek adresini göndeririz. Sonraki parametreler yapacağımız işleme özel olarak değişebilir. En sondaki parametrede ise oluşacak nesnenin kaydedileceği bellek adresini veririz. Fonksiyonun dönüş tipini de kontrol ederiz. Eğer başarılı bir dönüş yapmamış ise gerekli aksiyonları alırız. Geliştirme aşaması boyunca bu işlemi tekrar tekrar yapacağımız için önceden bilgilendirmek istedim.
Gelecek yazımızda geliştirme ortamını kuracağız ve instance nesnemizi oluşturarak bu yolculukta ilk adımımızı atacağız. Tavsiyem, dersler boyunca yavaş yavaş ilerlemeniz, hangi aşamaların ne yaptığını not almanız ve birbirleri ile ilişkilerini kafanızda oturtmanızdır. Eğer herhangi bir sorunuz olursa yorumlara yazabilirsiniz. Elimden geldiğince hızlı cevap vermeye çalışacağım.
One response to “Vulkan – Part 1: Genel bakış”
-
Launch into the breathtaking sandbox of EVE Online. Become a legend today. Fight alongside millions of explorers worldwide. Join now
RELATED POSTS
View all
Leave a Reply