Anda di halaman 1dari 286

Machine Translated by Google

tutorial gunung berapi

Alexander Overvoorde

April 2022
Machine Translated by Google

Isi

Pendahuluan
Tentang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
E-book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 .
Struktur tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.8

Ikhtisar
Asal Vulkan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 . 10
. . . . . . . . . . .
Apa yang diperlukan untuk menggambar segitiga . . . . . . . . . . 11
Langkah 1 - Pemilihan perangkat fisik dan instans . . . . . . . . . . . 11
Langkah 2 - Perangkat logis dan keluarga antrean. . . . . . . . . . . . . 11
Langkah 3 - Permukaan jendela dan rantai tukar. . . . . . . . . . . . . . 11
Langkah 4 - Tampilan gambar dan framebuffer. . . . . . . . . . . . . . . 12 .
Langkah 5 - Render pass . . . . . . . . . . . . . . . . . . . . . . . 12 .
Langkah 6 - Pipa grafis . . . . . . . . . . . . . . . . . . . . . 13 .
Langkah 7 - Kumpulan perintah dan buffer perintah . . . . . . . . . 13 .
Langkah 8 - Putaran utama. . . . . . . . . . . . . . . . . . . . . . . . . 13 .
Ringkasan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 .
konsep API. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 .
Konvensi pengkodean. . . . . . . . . . . . . . . . . . . . . . . . . 15 .
Lapisan validasi . . . . . . . . . . . . . . . . . . . . . . . . . . 15

Lingkungan pengembangan
Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 .
Vulkan SDK. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 .
GLFW. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 .
GLM. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 .
Menyiapkan Visual Studio. . . . . . . . . . . . . . . . . . . . . . 19 .
Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 .
Paket Vulkan . . . . . . . . . . . . . . . . . . . . . . . . . . 27 .
GLFW. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 .
GLM. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 .
Penyusun Shader. . . . . . . . . . . . . . . . . . . . . . . . . . 29 . 29
Menyiapkan proyek makefile . . . . . . . . . . . . . . . . . . . . 29

1
Machine Translated by Google

MacOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Vulkan SDK. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 .
GLFW. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 .
GLM. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 .
Menyiapkan Xcode . . . . . . . . . . . . . . . . . . . . . . . . . . 34

Kode dasar
Struktur umum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 .
Pengelolaan sumber daya . . . . . . . . . . . . . . . . . . . . . . . . . . 40 .
Mengintegrasikan GLFW . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 . 42

Instance
Membuat instance . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 .
Memeriksa dukungan ekstensi . . . . . . . . . . . . . . . . . . . . . 45 .
Membersihkan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 . 48

Lapisan validasi Apa


itu lapisan validasi? . . . . . . . . . . . . . . . . . . . . . . . 49 .
Menggunakan lapisan validasi . . . . . . . . . . . . . . . . . . . . . . . . . . 49 .
Panggilan balik pesan. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 .
Men-debug pembuatan dan penghancuran instance . . . . . . . . . . . . . 52 .
Pengujian . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 .
Konfigurasi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 . 59

Perangkat fisik dan kelompok antrean Memilih 61


perangkat fisik . . . . . . . . . . . . . . . . . . . . . . . . . 61 .
Pemeriksaan kesesuaian perangkat dasar . . . . . . . . . . . . . . . . . . . . . . 62 .
Keluarga antrian . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

Perangkat logis dan antrian


Pendahuluan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 .
Menentukan antrian yang akan dibuat. . . . . . . . . . . . . . . . . . . 69 .
Menentukan fitur perangkat yang digunakan . . . . . . . . . . . . . . . . . . . . . 69 .
Membuat perangkat logis. . . . . . . . . . . . . . . . . . . . . . . . 70 .
Mengambil pegangan antrian . . . . . . . . . . . . . . . . . . . . . . . . 70 . 72

Permukaan jendela
Penciptaan permukaan jendela . . . . . . . . . . . . . . . . . . . . . . . . . 73 .
Meminta dukungan presentasi . . . . . . . . . . . . . . . . . . . 73 .
Membuat antrian presentasi. . . . . . . . . . . . . . . . . . . . 75 . 76

Rantai swap
Memeriksa dukungan rantai swap. . . . . . . . . . . . . . . . . . . . 78 .
Mengaktifkan ekstensi perangkat . . . . . . . . . . . . . . . . . . . . . . . 78 .
Menanyakan detail dukungan rantai pertukaran . . . . . . . . . . . . . . . . . 80 .
Memilih pengaturan yang tepat untuk rantai pertukaran . . . . . . . . . . . . . 80 . 82
bentuk permukaan. . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

2
Machine Translated by Google

Modus presentasi. . . . . . . . . . . . . . . . . . . . . . . . . . 83
Swap sejauh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 .
Membuat rantai pertukaran . . . . . . . . . . . . . . . . . . . . . . . . . 86 .
Mengambil gambar rantai swap . . . . . . . . . . . . . . . . . . . . 90

Tampilan gambar 91

pengantar 94

Modul shader Vertex


shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 .
Pengubah fragmen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 .
Warna per-simpul . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 .
Mengkompilasi shader. . . . . . . . . . . . . . . . . . . . . . . . . . 101 .
Memuat shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 .
Membuat modul shader. . . . . . . . . . . . . . . . . . . . . . . . . 104 .
Penciptaan panggung shader . . . . . . . . . . . . . . . . . . . . . . . . . . 105 . 107

Fungsi tetap
Masukan simpul. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 .
Majelis masukan. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 .
Area pandang dan gunting . . . . . . . . . . . . . . . . . . . . . . . . . . 110 . 110
Rasterisasi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
Multisampling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Pengujian kedalaman dan stensil . . . . . . . . . . . . . . . . . . . . . . . . . 113 .
Pencampuran warna. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 .
Keadaan dinamis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 .
Tata letak saluran pipa. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 .
Kesimpulan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

Render melewati
Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 .
Deskripsi lampiran. . . . . . . . . . . . . . . . . . . . . . . . . 118 .
Subpasses dan referensi lampiran . . . . . . . . . . . . . . . . . . 118 .
Render lulus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 . 121

Kesimpulan 123

Framebuffer 126

Buffer perintah
Kolam komando. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 .
Alokasi buffer perintah. . . . . . . . . . . . . . . . . . . . . . . 129 .
Perekaman buffer perintah. . . . . . . . . . . . . . . . . . . . . . . 131 .
Memulai render pass . . . . . . . . . . . . . . . . . . . . . . . . . . 132 .
Perintah menggambar dasar. . . . . . . . . . . . . . . . . . . . . . . . 133 . 134
Menyelesaikan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

3
Machine Translated by Google

Rendering dan presentasi Outline 136


dari sebuah frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 .
Sinkronisasi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 .
Semafor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 .
Pagar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 .
Apa yang harus dipilih? . . . . . . . . . . . . . . . . . . . . . . . . . . 139 .
Membuat objek sinkronisasi . . . . . . . . . . . . . . . . . . 139 .
Menunggu bingkai sebelumnya. . . . . . . . . . . . . . . . . . . . . 141 .
Memperoleh gambar dari rantai swap . . . . . . . . . . . . . . . . 142 .
Merekam buffer perintah. . . . . . . . . . . . . . . . . . . . . 142 .
Mengirimkan buffer perintah . . . . . . . . . . . . . . . . . . . . . 143 .
Subpass dependensi. . . . . . . . . . . . . . . . . . . . . . . . . . 144 .
Presentasi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 .
Kesimpulan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

Bingkai dalam penerbangan

Bingkai dalam penerbangan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 . 148

Pendahuluan rekreasi rantai


tukar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 .
Membuat ulang rantai pertukaran . . . . . . . . . . . . . . . . . . . . . . . . 152 .
Rantai pertukaran yang kurang optimal atau kedaluwarsa . . . . . . . . . . . . . . . . . 152 .
Memperbaiki kebuntuan. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 . 155
Menangani pengubahan ukuran secara eksplisit . . . . . . . . . . . . . . . . . . . . . . . . . 156
Penanganan minimalisasi . . . . . . . . . . . . . . . . . . . . . . . . . . . 158

Deskripsi input vertex


Pendahuluan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 .
Shader verteks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 .
Data simpul. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 .
Deskripsi yang mengikat. . . . . . . . . . . . . . . . . . . . . . . . . . . 160 .
Deskripsi atribut. . . . . . . . . . . . . . . . . . . . . . . . . . 160 .
Input titik pipa . . . . . . . . . . . . . . . . . . . . . . . . . . . 161 . 163

Pendahuluan pembuatan buffer


vertex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 .
Pembuatan penyangga. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 .
Persyaratan memori. . . . . . . . . . . . . . . . . . . . . . . . . . 164 .
Alokasi memori . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 .
Mengisi buffer vertex . . . . . . . . . . . . . . . . . . . . . . . . . 168 .
Mengikat buffer vertex . . . . . . . . . . . . . . . . . . . . . . . . 169 . 170

Penyangga
. . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 .
pementasan Pendahuluan
Antrean transfer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 .
Mengabstraksi pembuatan buffer . . . . . . . . . . . . . . . . . . . . . . . 172 . 173

4
Machine Translated by Google

. . . . . . . . . . . . . . . . . . . . . . . . . . 174 .
Menggunakan penyangga pementasan.
Kesimpulan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177

Pendahuluan 178 .
penyangga indeks. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 .
Pembuatan buffer indeks. . . . . . . . . . . . . . . . . . . . . . . . . . . 179 .
Menggunakan buffer indeks . . . . . . . . . . . . . . . . . . . . . . . . . . 181

Tata letak deskriptor dan Pendahuluan 183 .


buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 .
Shader verteks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 .
Deskriptor mengatur tata letak . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 .
penyangga seragam. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 .
Memperbarui data seragam . . . . . . . . . . . . . . . . . . . . . . . . . . 189

Kumpulan deskriptor dan


. . . .. . . .
kumpulan Pendahuluan . . . . . . . . . . . . . . . . . . . . . . . . 192 .
Kumpulan deskriptor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192 .
Set deskriptor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192 .
Menggunakan set deskriptor. . . . . . . . . . . . . . . . . . . . . . . . . . . 193 .
Persyaratan keselarasan. . . . . . . . . . . . . . . . . . . . . . . . . 196 .
Beberapa set deskriptor . . . . . . . . . . . . . . . . . . . . . . . . . 197 . 200

201
Pengenalan Gambar .. . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . 201 .
Pustaka gambar. . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . 202 .
Memuat gambar . . . . .. . . . . . . . . . . . . . . . . . . . . . . . 203 .
Penyangga pementasan. . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . 205 .
Gambar Tekstur . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . 205 .
Transisi tata letak. . . .. . . . . . . . . . . . . . . . . . . . . . . . 210 .
Menyalin buffer ke image . . . . . . . . . . . . . . . . . . . . . . . . . 213 .
Mempersiapkan gambar tekstur. . . . . . . . . . . . . . . . . . . . . . . 214 .
Topeng penghalang transisi . . . . . . . . . . . . . . . . . . . . . . . . . 215 .
Pembersihan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217

Tampilan gambar dan sampler 218 .


Tampilan gambar tekstur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 .
Sampler. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 .
Fitur perangkat anisotropi . . . . . . . . . . . . . . . . . . . . . . . . 225

Pendahuluan sampler gambar 227 .


gabungan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 .
Memperbarui deskriptor. . . . . . . . . . . . . . . . . . . . . . . . . 227 .
Koordinat tekstur. . . . . . . . . . . . . . . . . . . . . . . . . . . 230 .
Shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231

Buffer kedalaman 236

5
Machine Translated by Google

Pengantar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
geometri 3D. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 .
Kedalaman gambar dan tampilan . . . . . . . . . . . . . . . . . . . . . . . . . . 239 .
Secara eksplisit mentransisikan gambar kedalaman . . . . . . . . . . . . . 243 .
Render lulus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244 .
Framebuffer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 .
Nilai yang jelas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 .
Kedalaman dan keadaan stensil . . . . . . . . . . . . . . . . . . . . . . . . . 247 .
Menangani pengubahan ukuran jendela . . . . . . . . . . . . . . . . . . . . . . . . . 248

Memuat model
Pendahuluan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 .
Perpustakaan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 .
Jaring sampel. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 .
Memuat simpul dan indeks . . . . . . . . . . . . . . . . . . . . . . . 251 .
Deduplikasi simpul . . . . . . . . . . . . . . . . . . . . . . . . . . . 252 . 256

Membuat Pengenalan
Mipmaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 .
Pembuatan gambar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 .
Menghasilkan Mipmap . . . . . . . . . . . . . . . . . . . . . . . . . . . 260 .
Dukungan penyaringan linier. . . . . . . . . . . . . . . . . . . . . . . . . 261 .
Sampel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 . 267

Pengantar
Multisampling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 .
Mendapatkan jumlah sampel yang tersedia. . . . . . . . . . . . . . . . . . . . . 271 .
Menyiapkan target render . . . . . . . . . . . . . . . . . . . . . . . . 273 .
Menambahkan lampiran baru. . . . . . . . . . . . . . . . . . . . . . . . . 274 .
Peningkatan kualitas. . . . . . . . . . . . . . . . . . . . . . . . . . 276 .
Kesimpulan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 . 280

FAQ 282

Kebijakan Privasi
Umum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 .
Analitik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 .
Iklan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 .
Komentar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 . 284

6
Machine Translated by Google

pengantar

Tentang

Tutorial ini akan mengajari Anda dasar-dasar penggunaan grafis Vulkan dan menghitung API.
Vulkan adalah API baru dari grup Khronos (dikenal dengan OpenGL) yang menyediakan
abstraksi kartu grafis modern yang jauh lebih baik. Antarmuka baru ini memungkinkan Anda
untuk menjelaskan dengan lebih baik apa yang ingin dilakukan aplikasi Anda, yang dapat
menghasilkan kinerja yang lebih baik dan perilaku driver yang tidak terlalu mengejutkan
dibandingkan dengan API yang sudah ada seperti OpenGL dan Direct3D. Ide di balik Vulkan
mirip dengan Direct3D 12 dan Metal, tetapi Vulkan memiliki keuntungan karena sepenuhnya
lintas platform dan memungkinkan Anda mengembangkan untuk Windows, Linux, dan Android
pada saat yang bersamaan.

Namun, harga yang Anda bayar untuk manfaat ini adalah Anda harus bekerja dengan API yang
jauh lebih verbose. Setiap detail yang terkait dengan API grafis perlu disiapkan dari awal oleh
aplikasi Anda, termasuk pembuatan buffer bingkai awal dan pengelolaan memori untuk objek
seperti buffer dan gambar tekstur.
Driver grafis akan melakukan lebih sedikit pegangan tangan, yang berarti Anda harus melakukan
lebih banyak pekerjaan dalam aplikasi Anda untuk memastikan perilaku yang benar.

Pesan takeaway di sini adalah bahwa Vulkan bukan untuk semua orang. Ini ditargetkan untuk
pemrogram yang antusias dengan grafik komputer berkinerja tinggi, dan bersedia untuk bekerja.
Jika Anda lebih tertarik pada pengembangan game, daripada grafik komputer, maka Anda
mungkin ingin tetap menggunakan OpenGL atau Direct3D, yang mana tidak akan ditinggalkan
demi Vulkan dalam waktu dekat. Alternatif lain adalah dengan menggunakan mesin seperti
Unreal Engine atau Unity, yang akan dapat menggunakan Vulkan sambil memaparkan API
tingkat yang jauh lebih tinggi kepada Anda.

Dengan itu, mari kita bahas beberapa prasyarat untuk mengikuti tutorial ini: • Kartu grafis dan

driver yang kompatibel dengan Vulkan (NVIDIA, AMD, Intel,


Apple Silicon (Atau Apple M1))
• Pengalaman dengan C++ (akrab dengan RAII, daftar penginisialisasi) •
Kompiler dengan dukungan fitur C++17 yang layak (Visual Studio 2017+,
GCC 7+, Atau Dentang 5+) •
Beberapa pengalaman yang ada dengan grafik komputer 3D

7
Machine Translated by Google

Tutorial ini tidak akan mengasumsikan pengetahuan tentang konsep OpenGL atau Direct3D, tetapi
ini mengharuskan Anda untuk mengetahui dasar-dasar grafik komputer 3D. Itu tidak akan
menjelaskan matematika di balik proyeksi perspektif, misalnya. Lihat buku online ini untuk pengenalan
yang bagus tentang konsep grafis komputer. Beberapa sumber grafik komputer hebat lainnya adalah:

• Ray tracing dalam satu akhir pekan


• Buku Rendering Berbasis Fisik • Vulkan
digunakan dalam mesin nyata di Quake dan DOOM sumber terbuka
3

Anda dapat menggunakan C alih-alih C++ jika Anda mau, tetapi Anda harus menggunakan
perpustakaan aljabar linier yang berbeda dan Anda akan sendirian dalam hal penataan kode.
Kami akan menggunakan fitur C++ seperti kelas dan RAII untuk mengatur logika dan masa pakai
sumber daya. Ada juga versi alternatif dari tutorial ini yang tersedia untuk pengembang Rust.

Untuk membuatnya lebih mudah diikuti oleh pengembang yang menggunakan bahasa pemrograman
lain, dan untuk mendapatkan pengalaman dengan API dasar, kami akan menggunakan API C asli
untuk bekerja dengan Vulkan. Namun, jika Anda menggunakan C++, Anda mungkin lebih suka
menggunakan binding Vulkan-Hpp yang lebih baru yang memisahkan beberapa pekerjaan kotor dan
membantu mencegah kelas kesalahan tertentu.

E-book
Jika Anda lebih suka membaca tutorial ini sebagai e-book, Anda dapat mengunduh versi EPUB atau
PDF di sini:

• EPUB
• PDF

Struktur tutorial
Kita akan mulai dengan ikhtisar tentang cara kerja Vulkan dan pekerjaan yang harus kita lakukan
untuk menampilkan segitiga pertama di layar. Tujuan dari semua langkah yang lebih kecil akan lebih
masuk akal setelah Anda memahami peran dasarnya dalam gambaran keseluruhan. Selanjutnya,
kita akan menyiapkan lingkungan pengembangan dengan Vulkan SDK, pustaka GLM untuk operasi
aljabar linier, dan GLFW untuk pembuatan jendela.
Tutorial akan mencakup cara mengaturnya di Windows dengan Visual Studio, dan di Ubuntu Linux
dengan GCC.

Setelah itu, kami akan mengimplementasikan semua komponen dasar program Vulkan yang
diperlukan untuk merender segitiga pertama Anda. Setiap bab akan mengikuti kira-kira struktur
berikut:

• Perkenalkan konsep baru dan tujuannya • Gunakan


semua panggilan API yang relevan untuk mengintegrasikannya ke dalam program Anda

8
Machine Translated by Google

• Abstrak bagian-bagian itu menjadi fungsi pembantu

Meskipun setiap bab ditulis sebagai tindak lanjut dari bab sebelumnya, Anda juga dapat
membaca bab-bab tersebut sebagai artikel mandiri yang memperkenalkan fitur Vulkan tertentu.
Artinya, situs tersebut juga berguna sebagai referensi. Semua fungsi dan jenis Vulkan ditautkan
ke spesifikasi, sehingga Anda dapat mengekliknya untuk mempelajari lebih lanjut. Vulkan adalah
API yang sangat baru, jadi mungkin ada beberapa kekurangan dalam spesifikasi itu sendiri.
Anda dianjurkan untuk mengirimkan umpan balik ke repositori Khronos ini.

Seperti disebutkan sebelumnya, Vulkan API memiliki API yang agak bertele-tele dengan banyak
parameter untuk memberi Anda kontrol maksimal atas perangkat keras grafis. Ini menyebabkan
operasi dasar seperti membuat tekstur mengambil banyak langkah yang harus diulang setiap
saat. Oleh karena itu, kami akan membuat koleksi fungsi pembantu kami sendiri di sepanjang
tutorial.

Setiap bab juga akan diakhiri dengan tautan ke daftar kode lengkap hingga saat itu. Anda dapat
merujuknya jika Anda ragu tentang struktur kode, atau jika Anda berurusan dengan bug dan
ingin membandingkan. Semua file kode telah diuji pada kartu grafis dari berbagai vendor untuk
memverifikasi kebenarannya. Setiap bab juga memiliki bagian komentar di bagian akhir di mana
Anda dapat mengajukan pertanyaan apa pun yang relevan dengan materi pelajaran tertentu.
Harap tentukan platform Anda, versi driver, kode sumber, perilaku yang diharapkan, dan perilaku
aktual untuk membantu kami membantu Anda.

Tutorial ini dimaksudkan sebagai upaya komunitas. Vulkan masih merupakan API yang sangat
baru dan praktik terbaiknya belum benar-benar ditetapkan. Jika Anda memiliki umpan balik
tentang tutorial dan situs itu sendiri, jangan ragu untuk mengirimkan masalah atau menarik
permintaan ke repositori GitHub. Anda dapat menonton repositori untuk diberi tahu tentang
pembaruan tutorial.

Setelah Anda menjalani ritual menggambar segitiga bertenaga Vulkan pertama Anda di layar,
kami akan mulai memperluas program untuk menyertakan transformasi linier, tekstur, dan model
3D.

Jika Anda pernah bermain dengan API grafis sebelumnya, Anda akan tahu bahwa ada banyak
langkah hingga geometri pertama muncul di layar. Ada banyak langkah awal di Vulkan, tetapi
Anda akan melihat bahwa setiap langkah mudah dipahami dan tidak terasa berlebihan. Penting
juga untuk diingat bahwa setelah Anda memiliki segitiga yang terlihat membosankan,
menggambar model 3D yang sepenuhnya bertekstur tidak membutuhkan banyak pekerjaan
ekstra, dan setiap langkah di luar titik itu jauh lebih bermanfaat.

Jika Anda mengalami masalah saat mengikuti tutorial, periksa terlebih dahulu FAQ untuk melihat
apakah masalah Anda dan solusinya sudah tercantum di sana. Jika Anda masih buntu setelah
itu, jangan ragu untuk meminta bantuan di bagian komentar bab terkait terdekat.

Siap terjun ke masa depan API grafis performa tinggi? Ayo pergi!

9
Machine Translated by Google

Ringkasan

Bab ini akan dimulai dengan pengenalan Vulkan dan masalah yang ditanganinya. Setelah itu
kita akan melihat bahan-bahan yang dibutuhkan untuk segitiga pertama. Ini akan memberi
Anda gambaran besar untuk menempatkan setiap bab berikutnya. Kami akan menyimpulkan
dengan membahas struktur API Vulkan dan pola penggunaan umum.

Asal Vulkan
Sama seperti API grafis sebelumnya, Vulkan dirancang sebagai abstraksi lintas platform
melalui GPU. Masalah dengan sebagian besar API ini adalah bahwa era di mana mereka
dirancang menampilkan perangkat keras grafis yang sebagian besar terbatas pada
fungsionalitas tetap yang dapat dikonfigurasi. Pemrogram harus menyediakan data vertex
dalam format standar dan bergantung pada produsen GPU sehubungan dengan opsi
pencahayaan dan bayangan.

Saat arsitektur kartu grafis semakin matang, mereka mulai menawarkan lebih banyak
fungsionalitas yang dapat diprogram. Semua fungsi baru ini entah bagaimana harus
diintegrasikan dengan API yang ada. Hal ini menghasilkan abstraksi yang kurang ideal dan
banyak dugaan di sisi driver grafis untuk memetakan tenda programmer ke arsitektur grafis
modern. Itu sebabnya ada begitu banyak pembaruan driver untuk meningkatkan kinerja dalam
game, terkadang dengan margin yang signifikan. Karena kompleksitas dari driver ini,
pengembang aplikasi juga harus berurusan dengan inkonsistensi antar vendor, seperti sintaks
yang diterima untuk shader. Selain fitur-fitur baru ini, dekade terakhir juga melihat masuknya
perangkat seluler dengan perangkat keras grafis yang kuat. GPU seluler ini memiliki arsitektur
berbeda berdasarkan kebutuhan energi dan ruangnya. Salah satu contohnya adalah rendering
ubin, yang akan mendapat manfaat dari peningkatan kinerja dengan menawarkan kontrol
lebih besar kepada pemrogram atas fungsi ini. Keterbatasan lain yang berasal dari usia API
ini adalah dukungan multi-threading yang terbatas, yang dapat mengakibatkan kemacetan di
sisi CPU.

Vulkan memecahkan masalah ini dengan mendesain dari nol untuk arsitektur grafis modern.
Ini mengurangi overhead driver dengan memungkinkan pemrogram untuk secara jelas
menentukan maksud mereka menggunakan API yang lebih verbose, dan memungkinkan banyak utas

10
Machine Translated by Google

membuat dan mengirimkan perintah secara paralel. Ini mengurangi ketidakkonsistenan dalam
kompilasi shader dengan beralih ke format kode byte standar dengan satu penyusun. Terakhir,
ini mengakui kemampuan pemrosesan tujuan umum dari kartu grafis modern dengan
menyatukan fungsionalitas grafis dan komputasi ke dalam satu API.

Apa yang diperlukan untuk menggambar segitiga

Sekarang kita akan melihat ikhtisar dari semua langkah yang diperlukan untuk merender
segitiga dalam program Vulkan yang berperilaku baik. Semua konsep yang diperkenalkan di
sini akan diuraikan dalam bab-bab selanjutnya. Ini hanya untuk memberi Anda gambaran
besar untuk menghubungkan semua komponen individual.

Langkah 1 - Pemilihan instans dan perangkat fisik


Aplikasi Vulkan dimulai dengan menyiapkan API Vulkan melalui VkInstance.
Instance dibuat dengan mendeskripsikan aplikasi Anda dan ekstensi API apa pun yang akan
Anda gunakan. Setelah membuat instans, Anda dapat meminta perangkat keras yang
didukung Vulkan dan memilih satu atau beberapa VkPhysicalDevices untuk digunakan dalam
operasi. Anda dapat meminta properti seperti ukuran VRAM dan kemampuan perangkat untuk
memilih perangkat yang diinginkan, misalnya untuk memilih menggunakan kartu grafis khusus.

Langkah 2 - Perangkat logis dan keluarga antrean


Setelah memilih perangkat keras yang tepat untuk digunakan, Anda perlu membuat VkDevice
(perangkat logis), tempat Anda menjelaskan secara lebih spesifik Fitur VkPhysicalDevice
mana yang akan Anda gunakan, seperti rendering multi viewport dan pelampung 64 bit. Anda
juga perlu menentukan kelompok antrean mana yang ingin Anda gunakan. Sebagian besar
operasi yang dilakukan dengan Vulkan, seperti perintah menggambar dan operasi memori,
dieksekusi secara asinkron dengan mengirimkannya ke VkQueue. Antrean dialokasikan dari
kelompok antrean, di mana setiap kelompok antrean mendukung rangkaian operasi tertentu
dalam antreannya. Misalnya, mungkin ada kelompok antrean terpisah untuk operasi grafis,
komputasi, dan transfer memori. Ketersediaan keluarga antrian juga dapat digunakan sebagai
faktor pembeda dalam pemilihan perangkat fisik.
Perangkat dengan dukungan Vulkan mungkin tidak menawarkan fungsi grafis apa pun,
namun semua kartu grafis dengan dukungan Vulkan saat ini umumnya akan mendukung
semua operasi antrean yang kami minati.

Langkah 3 - Permukaan jendela dan rantai tukar


Kecuali jika Anda hanya tertarik pada perenderan di luar layar, Anda harus membuat jendela
untuk menampilkan gambar yang dirender. Windows dapat dibuat dengan API atau pustaka
platform asli seperti GLFW dan SDL. Kami akan menggunakan GLFW dalam tutorial ini, tetapi
lebih lanjut tentang itu di bab berikutnya.

11
Machine Translated by Google

Kami membutuhkan dua komponen lagi untuk benar-benar merender jendela: permukaan jendela
(VkSurfaceKHR) dan rantai swap (VkSwapchainKHR). Perhatikan postfix KHR, yang berarti objek
ini adalah bagian dari ekstensi Vulkan. Vulkan API itu sendiri sepenuhnya agnostik platform, oleh
karena itu kita perlu menggunakan ekstensi standar WSI (Window System Interface) untuk
berinteraksi dengan pengelola jendela. Permukaannya adalah abstraksi lintas platform di atas
jendela untuk dirender dan umumnya dibuat dengan memberikan referensi ke pegangan jendela
asli, misalnya HWND di Windows. Untungnya, pustaka GLFW memiliki fungsi bawaan untuk
menangani perincian khusus platform ini.

Rantai swap adalah kumpulan target render. Tujuan dasarnya adalah untuk memastikan bahwa
gambar yang sedang kita render berbeda dari gambar yang saat ini ada di layar. Ini penting untuk
memastikan bahwa hanya gambar lengkap yang ditampilkan. Setiap kali kami ingin menggambar
bingkai, kami harus meminta rantai pertukaran untuk memberi kami gambar untuk dirender. Saat
kita selesai menggambar bingkai, gambar dikembalikan ke rantai pertukaran untuk ditampilkan ke
layar di beberapa titik. Jumlah target render dan kondisi untuk menyajikan gambar jadi ke layar
bergantung pada mode saat ini. Mode umum saat ini adalah buffering ganda (vsync) dan buffering
tiga kali lipat. Kami akan melihat ini di bab pembuatan rantai pertukaran.

Beberapa platform memungkinkan Anda merender langsung ke tampilan tanpa berinteraksi


dengan pengelola jendela apa pun melalui ekstensi VK_KHR_display dan
VK_KHR_display_swapchain. Ini memungkinkan Anda membuat permukaan yang mewakili
seluruh layar dan dapat digunakan untuk mengimplementasikan pengelola jendela Anda sendiri,
misalnya.

Langkah 4 - Tampilan gambar dan framebuffer


Untuk menggambar ke gambar yang diperoleh dari rantai swap, kita harus membungkusnya
menjadi VkImageView dan VkFramebuffer. Tampilan gambar mereferensikan bagian tertentu dari
gambar yang akan digunakan, dan framebuffer mereferensikan tampilan gambar yang akan
digunakan untuk target warna, kedalaman, dan stensil. Karena mungkin ada banyak gambar
berbeda dalam rantai pertukaran, kami akan terlebih dahulu membuat tampilan gambar dan buffer
bingkai untuk masing-masing gambar dan memilih gambar yang tepat pada waktu menggambar.

Langkah 5 - Render pass


Render pass di Vulkan menjelaskan jenis gambar yang digunakan selama operasi rendering,
bagaimana gambar tersebut akan digunakan, dan bagaimana kontennya harus diperlakukan.
Dalam aplikasi rendering segitiga awal kami, kami akan memberi tahu Vulkan bahwa kami akan
menggunakan satu gambar sebagai target warna dan kami ingin itu dibersihkan menjadi warna
solid tepat sebelum operasi menggambar. Sedangkan render pass hanya mendeskripsikan jenis
gambar, VkFramebuffer sebenarnya mengikat gambar tertentu ke slot ini.

12
Machine Translated by Google

Langkah 6 - Pipa grafis


Pipa grafis di Vulkan disiapkan dengan membuat objek VkPipeline. Ini menjelaskan
status kartu grafis yang dapat dikonfigurasi, seperti ukuran viewport dan operasi
penyangga kedalaman dan status yang dapat diprogram menggunakan objek
VkShaderModule. Objek VkShaderModule dibuat dari kode byte shader. Pengemudi
juga perlu mengetahui target render mana yang akan digunakan dalam pipeline, yang
kami tentukan dengan mereferensikan pass render.

Salah satu fitur Vulkan yang paling khas dibandingkan dengan API yang ada adalah
bahwa hampir semua konfigurasi pipa grafis perlu diatur terlebih dahulu.
Itu berarti bahwa jika Anda ingin beralih ke shader yang berbeda atau sedikit mengubah
tata letak vertex Anda, maka Anda perlu membuat ulang pipa grafis sepenuhnya.
Itu berarti Anda harus membuat banyak objek VkPipeline terlebih dahulu untuk semua
kombinasi berbeda yang Anda perlukan untuk operasi rendering. Hanya beberapa
konfigurasi dasar, seperti ukuran area pandang dan warna bening, yang dapat diubah
secara dinamis. Semua keadaan juga perlu dijelaskan secara eksplisit, misalnya tidak
ada keadaan campuran warna default.

Kabar baiknya adalah karena Anda melakukan kompilasi sebelumnya yang setara
versus kompilasi tepat waktu, ada lebih banyak peluang pengoptimalan untuk driver
dan kinerja runtime lebih dapat diprediksi, karena perubahan status yang besar seperti
beralih ke pipa grafis yang berbeda dibuat sangat eksplisit.

Langkah 7 - Kumpulan perintah dan buffer perintah


Seperti disebutkan sebelumnya, banyak operasi di Vulkan yang ingin kita jalankan,
seperti operasi menggambar, harus diserahkan ke antrean. Operasi ini pertama-tama
harus direkam ke dalam VkCommandBuffer sebelum dapat dikirimkan.
Buffer perintah ini dialokasikan dari VkCommandPool yang terkait dengan kelompok
antrian tertentu. Untuk menggambar segitiga sederhana, kita perlu merekam buffer
perintah dengan operasi berikut:

• Mulai pass render • Ikat


pipa grafis • Gambar 3 simpul •
Akhiri pass render

Karena image dalam framebuffer bergantung pada image spesifik mana yang akan
diberikan rantai swap kepada kita, kita perlu merekam buffer perintah untuk setiap
kemungkinan image dan memilih yang tepat pada waktu pengundian. Alternatifnya
adalah merekam buffer perintah lagi setiap frame, yang tidak seefisien itu.

Langkah 8 - Putaran utama

Sekarang perintah menggambar telah dibungkus ke dalam buffer perintah, loop utama
cukup mudah. Kami pertama kali memperoleh gambar dari

13
Machine Translated by Google

tukar rantai dengan vkAcquireNextImageKHR. Kami kemudian dapat memilih buffer perintah yang
sesuai untuk gambar itu dan menjalankannya dengan vkQueueSubmit. Terakhir, kami mengembalikan
gambar ke rantai swap untuk dipresentasikan ke layar dengan vkQueuePresentKHR.

Operasi yang dikirimkan ke antrian dieksekusi secara asinkron. Oleh karena itu kita harus
menggunakan objek sinkronisasi seperti semafor untuk memastikan urutan eksekusi yang benar.
Eksekusi buffer perintah draw harus diatur untuk menunggu akuisisi gambar selesai, jika tidak, kita
mungkin mulai merender gambar yang masih dibaca untuk ditampilkan di layar. Panggilan
vkQueuePresentKHR pada gilirannya harus menunggu rendering selesai, yang mana kita akan
menggunakan semaphore kedua yang ditandai setelah rendering selesai.

Ringkasan
Tur angin puyuh ini akan memberi Anda pemahaman dasar tentang pekerjaan di depan untuk
menggambar segitiga pertama. Program dunia nyata berisi lebih banyak langkah, seperti
mengalokasikan buffer vertex, membuat buffer seragam, dan mengunggah gambar tekstur yang akan
dibahas di bab selanjutnya, tetapi kita akan memulai dengan sederhana karena Vulkan memiliki kurva
pembelajaran yang cukup curam. Perhatikan bahwa kita akan sedikit curang dengan terlebih dahulu
menyematkan koordinat vertex di shader vertex alih-alih menggunakan buffer vertex. Itu karena
mengelola buffer vertex memerlukan beberapa keakraban dengan buffer perintah terlebih dahulu.

Singkatnya, untuk menggambar segitiga pertama kita perlu:

• Buat VkInstance • Pilih kartu


grafis yang didukung (VkPhysicalDevice) • Buat VkDevice dan VkQueue
untuk menggambar dan presentasi • Buat jendela, permukaan jendela, dan rantai
tukar • Bungkus gambar rantai tukar ke dalam VkImageView • Buat pass render
yang menentukan render target dan penggunaan • Membuat framebuffer untuk pass
render • Menyiapkan pipeline grafis • Mengalokasikan dan merekam buffer perintah
dengan perintah draw untuk setiap

kemungkinan gambar rantai swap


• Draw frame dengan memperoleh gambar, mengirimkan buffer perintah draw yang tepat dan
mengembalikan gambar kembali ke rantai swap

Ada banyak langkah, tetapi tujuan dari setiap langkah akan dibuat sangat sederhana dan jelas di bab-
bab selanjutnya. Jika Anda bingung tentang hubungan satu langkah dibandingkan dengan keseluruhan
program, Anda harus merujuk kembali ke bab ini.

14
Machine Translated by Google

konsep API
Bab ini akan diakhiri dengan ikhtisar singkat tentang bagaimana API Vulkan disusun pada tingkat
yang lebih rendah.

Konvensi pengkodean
Semua fungsi, enumerasi, dan struct Vulkan didefinisikan dalam header vulkan.h, yang termasuk
dalam SDK Vulkan yang dikembangkan oleh LunarG.
Kami akan melihat cara menginstal SDK ini di bab berikutnya.

Fungsi memiliki awalan vk huruf kecil, tipe seperti enumerasi dan struct memiliki awalan Vk dan
nilai enumerasi memiliki awalan VK_. API banyak menggunakan struct untuk menyediakan
parameter ke fungsi. Misalnya, pembuatan objek umumnya mengikuti pola ini:

1 VkXXXCreateInfo buatInfo{}; 2
buatInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO; 3
createInfo.pBerikutnya = nullptr; 4 buatInfo.foo = ...; 5 buatInfo.bar = ...;

6
7 objek VkXXX; 8 if
(vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) { std::cerr << "gagal membuat
objek" << std::endl; 9 kembali salah;
10
11 }

Banyak struktur di Vulkan mengharuskan Anda menentukan secara eksplisit jenis struktur di
anggota sType. Anggota pNext dapat menunjuk ke struktur ekstensi dan akan selalu menjadi
nullptr dalam tutorial ini. Fungsi yang membuat atau menghancurkan objek akan memiliki parameter
VkAllocationCallbacks yang memungkinkan Anda menggunakan pengalokasi khusus untuk memori
driver, yang juga akan dibiarkan nullptr dalam tutorial ini.

Hampir semua fungsi mengembalikan VkResult yang berupa VK_SUCCESS atau kode kesalahan.
Spesifikasi menjelaskan kode kesalahan mana yang dapat dikembalikan oleh setiap fungsi dan
apa artinya.

Lapisan validasi
Seperti disebutkan sebelumnya, Vulkan dirancang untuk performa tinggi dan overhead driver yang
rendah. Oleh karena itu akan mencakup pemeriksaan kesalahan dan kemampuan debugging yang
sangat terbatas secara default. Pengemudi akan sering macet alih-alih mengembalikan kode
kesalahan jika Anda melakukan kesalahan, atau lebih buruk lagi, itu akan tampak berfungsi pada
kartu grafis Anda dan gagal total pada yang lain.

15
Machine Translated by Google

Vulkan memungkinkan Anda mengaktifkan pemeriksaan ekstensif melalui fitur yang


dikenal sebagai lapisan validasi. Lapisan validasi adalah potongan kode yang dapat
disisipkan antara API dan driver grafis untuk melakukan hal-hal seperti menjalankan
pemeriksaan tambahan pada parameter fungsi dan melacak masalah manajemen memori.
Hal yang menyenangkan adalah Anda dapat mengaktifkannya selama pengembangan
dan kemudian menonaktifkannya sepenuhnya saat merilis aplikasi Anda tanpa biaya
tambahan. Siapa pun dapat menulis lapisan validasinya sendiri, tetapi Vulkan SDK oleh
LunarG menyediakan seperangkat lapisan validasi standar yang akan kita gunakan dalam
tutorial ini. Anda juga perlu mendaftarkan fungsi callback untuk menerima pesan debug dari layer.

Karena Vulkan sangat eksplisit tentang setiap operasi dan lapisan validasinya sangat luas,
sebenarnya jauh lebih mudah untuk mengetahui mengapa layar Anda hitam dibandingkan
dengan OpenGL dan Direct3D!

Hanya ada satu langkah lagi sebelum kita mulai menulis kode dan itu adalah menyiapkan
lingkungan pengembangan.

16
Machine Translated by Google

Pengembangan lingkungan

Dalam bab ini, kami akan menyiapkan lingkungan Anda untuk mengembangkan aplikasi Vulkan
dan memasang beberapa pustaka yang bermanfaat. Semua alat yang akan kami gunakan,
kecuali kompiler, kompatibel dengan Windows, Linux, dan MacOS, tetapi langkah-langkah untuk
menginstalnya sedikit berbeda, oleh karena itu dijelaskan secara terpisah di sini.

Windows
Jika Anda mengembangkan untuk Windows, saya akan berasumsi bahwa Anda menggunakan
Visual Studio untuk mengkompilasi kode Anda. Untuk dukungan C++17 lengkap, Anda perlu
menggunakan Visual Studio 2017 atau 2019. Langkah-langkah yang diuraikan di bawah ini ditulis
untuk VS 2017.

Vulkan SDK
Komponen terpenting yang Anda perlukan untuk mengembangkan aplikasi Vulkan adalah SDK.
Ini mencakup header, lapisan validasi standar, alat debug, dan pemuat untuk fungsi Vulkan.
Pemuat mencari fungsi di driver saat runtime, mirip dengan GLEW untuk OpenGL - jika Anda
terbiasa dengan itu.

SDK dapat diunduh dari situs web LunarG menggunakan tombol di bagian bawah halaman. Anda
tidak perlu membuat akun, tetapi ini akan memberi Anda akses ke beberapa dokumentasi
tambahan yang mungkin berguna bagi Anda.

Lanjutkan penginstalan dan perhatikan lokasi penginstalan SDK. Hal pertama yang akan kami
lakukan adalah memverifikasi bahwa kartu grafis dan driver Anda mendukung Vulkan dengan
benar. Buka direktori tempat Anda menginstal SDK,

17
Machine Translated by Google

buka direktori Bin dan jalankan demo vkcube.exe. Anda harus melihat yang berikut ini:

Jika Anda menerima pesan kesalahan, pastikan driver Anda mutakhir, sertakan runtime
Vulkan dan kartu grafis Anda didukung. Lihat bab pengantar untuk link ke driver dari
vendor besar.

Ada program lain di direktori ini yang akan berguna untuk pengembangan.
Program glslangValidator.exe dan glslc.exe akan digunakan untuk mengkompilasi
shader dari GLSL yang dapat dibaca manusia menjadi bytecode. Kami akan membahas
ini secara mendalam di bab modul shader. Direktori Bin juga berisi binari pemuat
Vulkan dan lapisan validasi, sedangkan direktori Lib berisi pustaka.

Terakhir, ada direktori Sertakan yang berisi header Vulkan. Silakan menjelajahi file lain,
tetapi kami tidak membutuhkannya untuk tutorial ini.

18
Machine Translated by Google

GLFW
Seperti disebutkan sebelumnya, Vulkan dengan sendirinya adalah API platform agnostik
dan tidak menyertakan alat untuk membuat jendela untuk menampilkan hasil yang
dirender. Untuk memanfaatkan keunggulan lintas platform Vulkan dan untuk menghindari
kengerian Win32, kami akan menggunakan pustaka GLFW untuk membuat jendela, yang
mendukung Windows, Linux, dan MacOS. Ada perpustakaan lain yang tersedia untuk
tujuan ini, seperti SDL, tetapi keuntungan dari GLFW adalah ia juga mengabstraksi
beberapa hal khusus platform lain di Vulkan selain hanya pembuatan jendela.
Anda dapat menemukan rilis terbaru GLFW di situs resminya. Dalam tutorial ini kita akan
menggunakan binari 64-bit, tetapi tentu saja Anda juga dapat memilih untuk membangun
dalam mode 32 bit. Dalam hal ini pastikan untuk menautkan dengan binari Vulkan SDK di
direktori Lib32, bukan Lib. Setelah mengunduhnya, ekstrak arsip ke lokasi yang nyaman.
Saya telah memilih untuk membuat direktori Perpustakaan di direktori Visual Studio di
bawah dokumen.

GLM
Tidak seperti DirectX 12, Vulkan tidak menyertakan pustaka untuk operasi aljabar linier,
jadi kita harus mengunduhnya. GLM adalah pustaka bagus yang dirancang untuk
digunakan dengan API grafis dan juga biasa digunakan dengan OpenGL.

GLM adalah pustaka khusus tajuk, jadi cukup unduh versi terbaru dan simpan di lokasi
yang nyaman. Anda harus memiliki struktur direktori yang mirip dengan yang berikut ini
sekarang:

19
Machine Translated by Google

Menyiapkan Visual Studio


Sekarang setelah Anda menginstal semua dependensi, kami dapat menyiapkan proyek
Visual Studio dasar untuk Vulkan dan menulis sedikit kode untuk memastikan semuanya
berfungsi.

Mulai Visual Studio dan buat proyek Windows Desktop Wizard baru dengan memasukkan
nama dan menekan OK.

20
Machine Translated by Google

Pastikan Aplikasi Konsol (.exe) dipilih sebagai jenis aplikasi sehingga kami
memiliki tempat untuk mencetak pesan debug, dan centang Proyek Kosong
untuk mencegah Visual Studio menambahkan kode boilerplate.

21
Machine Translated by Google

Tekan OK untuk membuat proyek dan menambahkan file sumber C++. Anda seharusnya sudah
tahu cara melakukannya, tetapi langkah-langkahnya disertakan di sini untuk kelengkapan.

Sekarang tambahkan kode berikut ke file. Jangan khawatir tentang mencoba memahaminya
sekarang; kami hanya memastikan bahwa Anda dapat mengompilasi dan menjalankan aplikasi
Vulkan. Kita akan mulai dari awal di bab berikutnya.

1 #define GLFW_INCLUDE_VULKAN 2
#termasuk <GLFW/glfw3.h> 3

4 #define GLM_FORCE_RADIANS 5
#define GLM_FORCE_DEPTH_ZERO_TO_ONE 6
#include <glm/vec4.hpp> 7 #include <glm/
mat4x4.hpp> 8

9 #termasuk <iostream>
10

22
Machine Translated by Google

11 int main()
12 { glfwInit();
13
14 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
15 GLFWwindow* jendela = glfwCreateWindow(800, 600, "Vulkan jendela",
nullptr, nullptr);
16
17 uint32_t extensionCount = 0;
18 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);
19
20 std::cout << extensionCount << "
ekstensi didukung\n";
21
22 glm::mat4 matriks;
23 glm::vec4 vec; tes
24 otomatis = matriks * vec;
25
26 while(!glfwWindowShouldClose(window))
27 { glfwPollEvents();
28 }
29
30 glfwDestroyWindow(jendela);
31
32 glfwHentikan();
33
34 kembali 0;
35 }

Mari sekarang konfigurasikan proyek untuk menghilangkan kesalahan. Buka dialog properti
proyek dan pastikan bahwa Semua Konfigurasi dipilih, karena sebagian besar pengaturan
berlaku untuk mode Debug dan Rilis.

23
Machine Translated by Google

Pergi ke C++ -> General -> Additional Sertakan Direktori dan tekan <Edit...> di kotak
dropdown.

Tambahkan direktori tajuk untuk Vulkan, GLFW, dan GLM:

24
Machine Translated by Google

Selanjutnya, buka editor untuk direktori perpustakaan di bawah Linker -> General:

Dan tambahkan lokasi file objek untuk Vulkan dan GLFW:

Buka Linker -> Input dan tekan <Edit...> di kotak tarik-turun Ketergantungan Tambahan.

25
Machine Translated by Google

Masukkan nama file objek Vulkan dan GLFW:

Dan akhirnya ubah kompiler untuk mendukung fitur C++ 17:

Anda sekarang dapat menutup dialog properti proyek. Jika Anda melakukan semuanya
dengan benar maka Anda tidak akan lagi melihat kesalahan yang disorot dalam kode.

Terakhir, pastikan Anda benar-benar mengkompilasi dalam mode 64 bit:

Tekan F5 untuk mengkompilasi dan menjalankan proyek dan Anda akan melihat prompt
perintah dan jendela munculan seperti ini:

26
Machine Translated by Google

Jumlah ekstensi harus bukan nol. Selamat, Anda siap bermain dengan Vulkan!

Linux
Instruksi ini akan ditujukan untuk pengguna Ubuntu, Fedora dan Arch Linux, tetapi Anda
mungkin dapat mengikutinya dengan mengubah perintah khusus manajer paket menjadi
perintah yang sesuai untuk Anda. Anda harus memiliki kompiler yang mendukung C+
+17 (GCC 7+ atau Dentang 5+). Anda juga membutuhkan make.

Paket Vulkan
Komponen terpenting yang Anda perlukan untuk mengembangkan aplikasi Vulkan di
Linux adalah pemuat Vulkan, lapisan validasi, dan beberapa utilitas baris perintah untuk
menguji apakah mesin Anda mendukung Vulkan:

• sudo apt install vulkan-tools atau sudo dnf install vulkan-tools: Utilitas baris perintah,
yang paling penting vulkaninfo dan vkcube. Jalankan ini untuk mengonfirmasi
mesin Anda mendukung Vulkan.
• sudo apt install libvulkan-dev atau sudo dnf install vulkan-loader-devel : Instal Vulkan loader.
Pemuat mencari fungsi di driver saat runtime, mirip dengan GLEW untuk OpenGL - jika
Anda terbiasa dengan itu. • sudo apt install vulkan-validationlayers-dev spirv-tools atau

sudo dnf install mesa-vulkan-devel vulkan-validation-layers-devel: Menginstal lapisan


validasi standar dan alat SPIR-V yang diperlukan. Ini sangat penting saat men-debug
aplikasi Vulkan, dan kami akan membahasnya di bab mendatang.

Di Arch Linux, Anda dapat menjalankan sudo pacman -S vulkan-devel untuk menginstal semua
alat yang diperlukan di atas.

Jika penginstalan berhasil, Anda harus siap dengan bagian Vulkan.


Ingatlah untuk menjalankan vkcube dan pastikan Anda melihat pop up berikut di jendela:

27
Machine Translated by Google

Jika Anda menerima pesan kesalahan, pastikan driver Anda mutakhir, sertakan runtime
Vulkan dan kartu grafis Anda didukung. Lihat bab pengantar untuk link ke driver dari
vendor besar.

GLFW
Seperti disebutkan sebelumnya, Vulkan dengan sendirinya adalah API platform agnostik
dan tidak menyertakan alat untuk membuat jendela untuk menampilkan hasil yang
dirender. Untuk memanfaatkan keunggulan lintas platform Vulkan dan untuk menghindari
kengerian X11, kami akan menggunakan pustaka GLFW untuk membuat jendela, yang
mendukung Windows, Linux, dan MacOS. Ada perpustakaan lain yang tersedia untuk
tujuan ini, seperti SDL, tetapi keuntungan dari GLFW adalah ia juga mengabstraksi
beberapa hal khusus platform lain di Vulkan selain hanya pembuatan jendela.

Kami akan menginstal GLFW dari perintah berikut:

1 sudo apt install libglfw3-dev

28
Machine Translated by Google

atau

1 sudo dnf instal glfw-devel

atau

1 sudo pacman -S glfw-wayland # glfw-x11 untuk pengguna X11

GLM
Tidak seperti DirectX 12, Vulkan tidak menyertakan pustaka untuk operasi aljabar linier, jadi kita harus
mengunduhnya. GLM adalah pustaka bagus yang dirancang untuk digunakan dengan API grafis dan juga biasa
digunakan dengan OpenGL.

Ini adalah pustaka khusus tajuk yang dapat diinstal dari paket libglm-dev atau glm-devel:

1 sudo apt install libglm-dev

atau

1 sudo dnf instal glm-devel

atau

1 sudo pacman -S glm

Kompiler Shader
Kami memiliki hampir semua yang kami butuhkan, kecuali kami menginginkan sebuah program untuk
mengkompilasi shader dari GLSL yang dapat dibaca manusia ke bytecode.

Dua kompiler shader yang populer adalah glslangValidator dari Khronos Group dan glslc dari Google. Yang
terakhir memiliki penggunaan GCC- dan Dentang yang sudah dikenal, jadi kita akan melakukannya: di Ubuntu,
unduh binari tidak resmi Google dan salin glslc ke /usr/local/bin Anda. Catatan Anda mungkin perlu sudo
tergantung pada per misi Anda. Di Fedora gunakan sudo dnf install glslc, sedangkan di Arch Linux jalankan sudo
pacman -S shaderc. Untuk menguji, jalankan glslc dan seharusnya mengeluh bahwa kami tidak memberikan
shader apa pun untuk dikompilasi:

glslc: error: tidak ada file input

Kami akan membahas glslc secara mendalam di bab modul shader.

Menyiapkan proyek makefile


Sekarang setelah Anda menginstal semua dependensi, kita dapat menyiapkan proyek makefile dasar untuk
Vulkan dan menulis sedikit kode untuk memastikan semuanya berfungsi.

29
Machine Translated by Google

Buat direktori baru di lokasi yang nyaman dengan nama seperti VulkanTest.
Buat file sumber bernama main.cpp dan masukkan kode berikut. Jangan khawatir tentang
mencoba memahaminya sekarang; kami hanya memastikan bahwa Anda dapat mengompilasi
dan menjalankan aplikasi Vulkan. Kita akan mulai dari awal di bab selanjutnya.

1 #define GLFW_INCLUDE_VULKAN
2 #termasuk <GLFW/glfw3.h>
3
4 #define GLM_FORCE_RADIANS
5 #define GLM_FORCE_DEPTH_ZERO_TO_ONE
6 #include <glm/vec4.hpp> 7 #include <glm/
mat4x4.hpp> 8

9 #termasuk <iostream>
10
11 int main()
12 { glfwInit();
13
14 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
15 GLFWwindow* jendela = glfwCreateWindow(800, 600, "jendela Vulkan", nullptr,
nullptr);
16
17 uint32_t extensionCount = 0;
18 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);
19
20 std::cout << extensionCount << " ekstensi didukung\n";
21
22 glm::mat4 matriks;
23 glm::vec4 vec; tes
24 otomatis = matriks * vec;
25
26 while(!glfwWindowShouldClose(window))
27 { glfwPollEvents();
28 }
29
30 glfwDestroyWindow(jendela);
31
32 glfwHentikan();
33
kembali 0;
34 35 }

Selanjutnya, kita akan menulis makefile untuk mengkompilasi dan menjalankan kode Vulkan dasar ini.
Buat file kosong baru bernama Makefile. Saya akan berasumsi bahwa Anda sudah memiliki beberapa

30
Machine Translated by Google

pengalaman dasar dengan makefile, seperti cara kerja variabel dan aturan. Jika tidak, Anda dapat
meningkatkan kecepatan dengan sangat cepat dengan tutorial ini.

Pertama-tama kita akan mendefinisikan beberapa variabel untuk menyederhanakan sisa file.
Tentukan variabel CFLAGS yang akan menentukan flag compiler dasar:

1 CFLAGS = -std=c++17 -O2

Kita akan menggunakan C++ modern (-std=c++17), dan kita akan menyetel tingkat pengoptimalan
ke O2. Kita dapat menghapus -O2 untuk mengkompilasi program lebih cepat, tetapi kita harus ingat
untuk mengembalikannya untuk rilis build.

Demikian pula, tentukan flag linker dalam variabel LDFLAGS:

1 LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr


-lXi

Bendera -lglfw adalah untuk tautan GLFW, -lvulkan dengan pemuat fungsi Vulkan dan bendera
lainnya adalah pustaka sistem tingkat rendah yang dibutuhkan GLFW. Bendera yang tersisa adalah
dependensi dari GLFW itu sendiri: manajemen threading dan jendela.

Ada kemungkinan pustaka Xxf68vm dan Xi belum terpasang di sistem Anda. Anda dapat
menemukannya di paket berikut:

1 sudo apt install libxxf86vm-dev libxi-dev

atau

1 sudo dnf instal libXi libXxf86vm

atau

1 sudo pacman -S libxi libxxf86vm

Menentukan aturan untuk mengompilasi VulkanTest sangatlah mudah sekarang. Pastikan untuk
menggunakan tab untuk lekukan, bukan spasi.

1 VulkanTest: main.cpp g++ $


(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS) 2

Verifikasi bahwa aturan ini berfungsi dengan menyimpan makefile dan menjalankan make di direktori
dengan main.cpp dan Makefile. Ini akan menghasilkan VulkanTest yang dapat dieksekusi.

Kami sekarang akan mendefinisikan dua aturan lagi, uji dan bersihkan, di mana yang pertama akan menjalankan yang
dapat dieksekusi dan yang terakhir akan menghapus yang dapat dieksekusi yang dibangun:

1 .PHONY: tes bersih


2
3 tes: VulkanTest

31
Machine Translated by Google

4 ./VulkanTest
5
6 bersih:
7 rm -f VulkanTest

Running make test harus menunjukkan program berjalan dengan sukses, dan menampilkan jumlah
ekstensi Vulkan. Aplikasi harus keluar dengan kode pengembalian sukses (0) saat Anda menutup
jendela kosong. Anda sekarang harus memiliki makefile lengkap yang menyerupai berikut ini:

1 CFLAGS = -std=c++17 -O2


2 LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr
-lXi
3
4 VulkanTest: main.cpp g++ $
(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS) 5
6
7 .PHONY: tes bersih
8
9 tes: VulkanTest ./
10 VulkanTest
11
12 bersih:
13 rm -f VulkanTest

Sekarang Anda dapat menggunakan direktori ini sebagai template untuk proyek Vulkan Anda. Buat
salinan, ganti namanya menjadi sesuatu seperti HelloTriangle dan hapus semua kode di main.cpp.

Anda sekarang siap untuk petualangan nyata.

MacOS
Instruksi ini menganggap Anda menggunakan Xcode dan manajer paket Homebrew . Selain itu,
perlu diingat bahwa Anda memerlukan setidaknya MacOS versi 10.11, dan perangkat Anda harus
mendukung Metal API.

Vulkan SDK
Komponen terpenting yang Anda perlukan untuk mengembangkan aplikasi Vulkan adalah SDK. Ini
mencakup header, lapisan validasi standar, alat debug, dan pemuat untuk fungsi Vulkan. Pemuat
mencari fungsi di driver saat runtime, mirip dengan GLEW untuk OpenGL - jika Anda terbiasa dengan
itu.

SDK dapat diunduh dari situs web LunarG menggunakan tombol di bagian bawah halaman. Anda
tidak perlu membuat akun, tetapi ini akan memberi Anda akses ke beberapa dokumentasi tambahan
yang mungkin berguna bagi Anda.

32
Machine Translated by Google

Versi SDK untuk MacOS secara internal menggunakan MoltenVK. Tidak ada dukungan
asli untuk Vulkan di MacOS, jadi yang dilakukan MoltenVK sebenarnya bertindak sebagai
lapisan yang menerjemahkan panggilan API Vulkan ke kerangka kerja grafis Logam
Apple. Dengan ini, Anda dapat memanfaatkan manfaat debug dan kinerja dari kerangka
Logam Apple.

Setelah mengunduhnya, cukup ekstrak isinya ke folder pilihan Anda (perlu diingat bahwa
Anda perlu merujuknya saat membuat proyek di Xcode). Di dalam folder yang diekstraksi,
di folder Aplikasi Anda harus memiliki beberapa file yang dapat dieksekusi yang akan
menjalankan beberapa demo menggunakan SDK. Jalankan vkcube yang dapat
dieksekusi dan Anda akan melihat yang berikut:

33
Machine Translated by Google

GLFW
Seperti disebutkan sebelumnya, Vulkan dengan sendirinya adalah API platform agnostik dan tidak
menyertakan alat untuk membuat jendela untuk menampilkan hasil yang dirender. Kami akan
menggunakan pustaka GLFW untuk membuat jendela, yang mendukung Windows, Linux, dan MacOS.
Ada perpustakaan lain yang tersedia untuk tujuan ini, seperti SDL, tetapi keuntungan dari GLFW
adalah ia juga mengabstraksi beberapa hal khusus platform lainnya di Vulkan selain hanya
pembuatan jendela.

Untuk menginstal GLFW di MacOS, kami akan menggunakan pengelola paket Homebrew untuk
mendapatkan paket glfw:

1 minuman instal glfw

GLM
Vulkan tidak menyertakan pustaka untuk operasi aljabar linier, jadi kita harus mengunduhnya. GLM
adalah pustaka bagus yang dirancang untuk digunakan dengan API grafis dan juga biasa digunakan
dengan OpenGL.

Ini adalah pustaka khusus tajuk yang dapat diinstal dari paket glm:

1 minuman instal glm

Menyiapkan Xcode
Sekarang setelah semua dependensi terinstal, kita dapat menyiapkan proyek Xcode dasar untuk
Vulkan. Sebagian besar instruksi di sini pada dasarnya banyak "pipa ledeng" sehingga kita bisa
mendapatkan semua dependensi yang ditautkan ke proyek. Juga, perlu diingat bahwa selama
instruksi berikut setiap kali kami menyebutkan folder vulkansdk, kami merujuk ke folder tempat
Anda mengekstrak Vulkan SDK.

Mulai Xcode dan buat proyek Xcode baru. Pada jendela yang akan terbuka pilih Application >
Command Line Tool.

34
Machine Translated by Google

Pilih Berikutnya, tulis nama proyek dan untuk Bahasa pilih C++.

Tekan Berikutnya dan proyek seharusnya sudah dibuat. Sekarang, mari ubah kode di file
main.cpp yang dihasilkan menjadi kode berikut:
1 #define GLFW_INCLUDE_VULKAN
2 #termasuk <GLFW/glfw3.h>
3
4 #menentukan GLM_FORCE_RADIANS

35
Machine Translated by Google

5 #define GLM_FORCE_DEPTH_ZERO_TO_ONE
6 #include <glm/vec4.hpp> 7 #include <glm/
mat4x4.hpp>
8
9 #termasuk <iostream>
10
11 int main()
12 { glfwInit();
13
14 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
15 GLFWwindow* jendela = glfwCreateWindow(800, 600, "Vulkan jendela",
nullptr, nullptr);
16
17 uint32_t extensionCount = 0;
18 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);
19
20 std::cout << extensionCount << " ekstensi didukung\n";
21
22 glm::mat4 matriks;
23 glm::vec4 vec; tes
24 otomatis = matriks * vec;
25
26 while(!glfwWindowShouldClose(window))
27 { glfwPollEvents();
28 }
29
30 glfwDestroyWindow(jendela);
31
32 glfwHentikan();
33
34 kembali 0;
35 }

Ingatlah bahwa Anda belum diharuskan untuk memahami semua kode ini, kami hanya
menyiapkan beberapa panggilan API untuk memastikan semuanya berfungsi.

Xcode seharusnya sudah menunjukkan beberapa kesalahan seperti pustaka yang tidak dapat ditemukan.
Kami sekarang akan mulai mengonfigurasi proyek untuk menghilangkan kesalahan itu.
Pada panel Project Navigator pilih proyek Anda. Buka tab Build Settings dan kemudian:

• Temukan bidang Header Search Paths dan tambahkan tautan ke /usr/local/include (di
sinilah Homebrew memasang tajuk, sehingga file tajuk glm dan glfw3 seharusnya ada di
sana) dan tautan ke vulkansdk/macOS/include untuk tajuk Vulkan . • Temukan field
Library Search Paths dan tambahkan link ke /usr/local/lib

36
Machine Translated by Google

(sekali lagi, di sinilah Homebrew menginstal pustaka, jadi file glm dan glfw3 lib
harus ada di sana) dan tautan ke vulkansdk/macOS/lib.
Seharusnya terlihat seperti itu (jelas, jalur akan berbeda tergantung di mana Anda
meletakkan file Anda):

Sekarang, di tab Build Phases , di Link Binary With Libraries kita akan menambahkan
framework glfw3 dan vulkan. Untuk mempermudah, kami akan menambahkan pustaka
dinamis dalam proyek (Anda dapat memeriksa dokumentasi pustaka ini jika ingin
menggunakan kerangka kerja statis).
• Untuk glfw buka folder /usr/local/lib dan di sana Anda akan menemukan nama
file seperti libglfw.3.x.dylib (“x” adalah nomor versi pustaka, mungkin berbeda
tergantung kapan Anda mengunduh paket dari minuman rumahan). Cukup seret
file itu ke tab Linked Frameworks and Libraries di Xcode.

• Untuk vulkan, buka vulkansdk/macOS/lib. Lakukan hal yang sama untuk file
kedua file libvulkan.1.dylib dan libvulkan.1.x.xx.dylib (di mana "x" akan menjadi
nomor versi SDK yang Anda unduh).
Setelah menambahkan pustaka tersebut, di tab yang sama pada Salin File , ubah
Tujuan menjadi "Kerangka Kerja", hapus subjalur dan batalkan pilihan "Salin hanya
saat memasang". Klik pada tanda “+” dan tambahkan ketiga kerangka tersebut di sini
juga.

Konfigurasi Xcode Anda akan terlihat seperti:

37
Machine Translated by Google

Hal terakhir yang perlu Anda atur adalah beberapa variabel lingkungan. Pada bilah alat
Xcode, buka Produk > Skema > Edit Skema..., dan di tab Argumen tambahkan dua
variabel lingkungan berikut:

• VK_ICD_FILENAMES = vulkansdk/macOS/share/vulkan/icd.d/MoltenVK_icd.json • VK_LAYER_PATH


= vulkansdk/macOS/share/vulkan/explicit_layer.d
Seharusnya terlihat seperti ini:

Akhirnya, Anda harus siap! Sekarang jika Anda menjalankan proyek (ingat untuk
menyetel konfigurasi build ke Debug atau Rilis tergantung pada konfigurasi

38
Machine Translated by Google

yang Anda pilih) Anda akan melihat yang berikut:

Jumlah ekstensi harus bukan nol. Log lainnya berasal dari perpustakaan, Anda mungkin
mendapatkan pesan yang berbeda dari yang bergantung pada konfigurasi Anda.

Anda sekarang siap untuk hal yang nyata.

39
Machine Translated by Google

Kode dasar

Struktur umum
Di bab sebelumnya, Anda telah membuat proyek Vulkan dengan semua konfigurasi yang
benar dan mengujinya dengan kode contoh. Dalam bab ini kita mulai dari awal dengan
kode berikut:
1 #sertakan <vulkan/vulkan.h>
2
3 #sertakan <iostream>
4 # sertakan <stdkecuali> 5
# sertakan <cstdlib>
6
7 kelas HelloTriangleApplication { 8 publik:
void run() {
9
10 initVulkan();
11 mainLoop();
12 membersihkan();
13 }
14
15 pribadi:
batal initVulkan() { 16
17
18 }
19
20 batal mainLoop() {
21
22 }
23
24 pembersihan batal () {
25
26 }
27 };
28

40
Machine Translated by Google

29 int main() {
30 Aplikasi HelloTriangleApplication;
31
32 coba
33 { app.run(); }
34 catch (const std::exception& e) {
35 std::cerr << e.apa() << std::endl; kembalikan
36 EXIT_FAILURE;
37 }
38
39 kembalikan EXIT_SUCCESS;
40 }

Kami pertama-tama menyertakan header Vulkan dari LunarG SDK, yang menyediakan fungsi,
struktur, dan pencacahan. Header stdkecuali dan iostream disertakan untuk melaporkan dan
menyebarkan kesalahan. Header cstdlib menyediakan makro EXIT_SUCCESS dan
EXIT_FAILURE.

Program itu sendiri dibungkus ke dalam kelas tempat kita akan menyimpan objek Vulkan
sebagai anggota kelas privat dan menambahkan fungsi untuk memulai masing-masing objek,
yang akan dipanggil dari fungsi initVulkan. Setelah semuanya siap, kita masuk ke loop utama
untuk mulai merender frame. Kami akan mengisi fungsi mainLoop untuk menyertakan loop
yang berulang hingga jendela ditutup sebentar lagi.
Setelah jendela ditutup dan mainLoop kembali, kami akan memastikan untuk membatalkan alokasi
sumber daya yang telah kami gunakan dalam fungsi pembersihan.

Jika terjadi kesalahan fatal apa pun selama eksekusi, maka kami akan melempar pengecualian
std::runtime_error dengan pesan deskriptif, yang akan mempropagasi kembali ke fungsi
utama dan dicetak ke prompt perintah. Untuk menangani berbagai jenis pengecualian standar
juga, kami menangkap std::exception yang lebih umum. Salah satu contoh kesalahan yang
akan segera kami tangani adalah mengetahui bahwa ekstensi tertentu yang diperlukan tidak
didukung.

Kira-kira setiap bab yang mengikuti setelah ini akan menambahkan satu fungsi baru yang
akan dipanggil dari initVulkan dan satu atau lebih objek Vulkan baru ke anggota kelas privat
yang perlu dibebaskan pada akhir pembersihan.

Pengelolaan sumber daya


Sama seperti setiap potongan memori yang dialokasikan dengan malloc memerlukan
panggilan untuk membebaskan, setiap objek Vulkan yang kita buat perlu dihancurkan secara
eksplisit saat kita tidak lagi membutuhkannya. Di C++ dimungkinkan untuk melakukan
manajemen sumber daya otomatis menggunakan RAII atau smart pointer yang disediakan di
header <memory>. Namun, saya telah memilih untuk secara eksplisit tentang alokasi dan
dealokasi objek Vulkan dalam tutorial ini. Lagi pula, ceruk Vulkan harus eksplisit tentang
setiap operasi untuk menghindari kesalahan, jadi sebaiknya eksplisit tentang masa pakai
objek untuk mempelajari cara kerja API.

41
Machine Translated by Google

Setelah mengikuti tutorial ini, Anda dapat mengimplementasikan manajemen sumber daya
otomatis dengan menulis kelas C++ yang memperoleh objek Vulkan di konstruktornya dan
melepaskannya di destruktornya, atau dengan menyediakan penghapus khusus ke
std::unique_ptr atau std::shared_ptr, tergantung pada kepemilikan Anda memerlukan ments.
RAII adalah model yang direkomendasikan untuk program Vulkan yang lebih besar, tetapi untuk
tujuan pembelajaran, selalu baik untuk mengetahui apa yang terjadi di balik layar.

Objek Vulkan dibuat langsung dengan fungsi seperti vkCreateXXX, atau dialokasikan melalui
objek lain dengan fungsi seperti vkAllocateXXX. Setelah memastikan bahwa suatu objek tidak
lagi digunakan di mana pun, Anda perlu menghancurkannya dengan vkDestroyXXX dan
vkFreeXXX. Parameter untuk fungsi-fungsi ini umumnya bervariasi untuk jenis objek yang
berbeda, tetapi ada satu parameter yang sama-sama digunakan: pAllocator. Ini adalah parameter
opsional yang memungkinkan Anda menentukan panggilan balik untuk pengalokasi memori
khusus. Kami akan mengabaikan parameter ini di tutorial dan selalu memberikan nullptr sebagai
argumen.

Mengintegrasikan GLFW
Vulkan berfungsi dengan sangat baik tanpa membuat jendela jika Anda ingin menggunakannya
untuk rendering di luar layar, tetapi jauh lebih menarik untuk benar-benar menampilkan sesuatu!
Pertama ganti baris #include <vulkan/vulkan.h> dengan

1 #define GLFW_INCLUDE_VULKAN
2 #termasuk <GLFW/glfw3.h>

Dengan begitu GLFW akan menyertakan definisinya sendiri dan secara otomatis memuat
header Vulkan dengannya. Tambahkan fungsi initWindow dan tambahkan panggilan dari fungsi
jalankan sebelum panggilan lainnya. Kami akan menggunakan fungsi itu untuk menginisialisasi
GLFW dan membuat jendela.

1 batal dijalankan() {
2 jendela init();
3 initVulkan();
4 mainLoop();
5 membersihkan();
6}
7
8 pribadi: 9
batal initWindow() {
10
11 }

Panggilan pertama di initWindow adalah glfwInit(), yang menginisialisasi pustaka GLFW. Karena
GLFW pada awalnya didesain untuk membuat konteks OpenGL, kita perlu memberitahukannya
untuk tidak membuat konteks OpenGL dengan panggilan selanjutnya:

42
Machine Translated by Google

1 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

Karena menangani jendela yang diubah ukurannya membutuhkan perhatian khusus yang akan kita lihat nanti,
nonaktifkan untuk saat ini dengan panggilan petunjuk jendela lainnya:

1 glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

Yang tersisa sekarang hanyalah membuat jendela yang sebenarnya. Tambahkan jendela
GLFWwindow*; anggota kelas pribadi untuk menyimpan referensi ke sana dan menginisialisasi jendela dengan:

1 jendela = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);

Tiga parameter pertama menentukan lebar, tinggi, dan judul jendela.


Parameter keempat memungkinkan Anda untuk secara opsional menentukan monitor untuk membuka
jendela dan parameter terakhir hanya relevan untuk OpenGL.

Merupakan ide bagus untuk menggunakan konstanta daripada angka lebar dan tinggi yang di-
hardcode karena kita akan mengacu pada nilai-nilai ini beberapa kali di masa mendatang.
Saya telah menambahkan baris berikut di atas definisi kelas HelloTriangleApplication:

1 const uint32_t LEBAR = 800; 2 const


uint32_t HEIGHT = 600;

dan mengganti panggilan pembuatan jendela dengan

1 jendela = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);

Anda sekarang harus memiliki fungsi initWindow yang terlihat seperti ini:

1 void initWindow() { glfwInit();


2
3
4 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
5 glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
6
7 jendela = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr,
nullptr);
8}

Agar aplikasi tetap berjalan hingga terjadi kesalahan atau jendela ditutup, kita perlu menambahkan
event loop ke fungsi mainLoop sebagai berikut:

1 void mainLoop() { while (!


2 glfwWindowShouldClose(window)) { glfwPollEvents();
3
4 }
5}

43
Machine Translated by Google

Kode ini harus cukup jelas. Itu berputar dan memeriksa acara seperti menekan tombol X
hingga jendela ditutup oleh pengguna. Ini juga merupakan loop di mana nanti kita akan
memanggil fungsi untuk merender satu frame.

Setelah jendela ditutup, kita perlu membersihkan sumber daya dengan menghancurkannya dan
mengakhiri GLFW itu sendiri. Ini akan menjadi kode pembersihan pertama kami:

1 pembersihan batal ()
2 { glfwDestroyWindow(jendela);
3
4 glfwHentikan();
5}

Saat Anda menjalankan program sekarang, Anda akan melihat jendela berjudul Vulkan
muncul hingga aplikasi dihentikan dengan menutup jendela. Sekarang kita memiliki
kerangka untuk aplikasi Vulkan, mari buat objek Vulkan pertama!
kode C++

44
Machine Translated by Google

Contoh

Membuat instance
Hal pertama yang perlu Anda lakukan adalah menginisialisasi pustaka Vulkan dengan
membuat instance. Instance adalah koneksi antara aplikasi Anda dan pustaka Vulkan dan
pembuatannya melibatkan penetapan beberapa detail tentang aplikasi Anda ke driver.

Mulailah dengan menambahkan fungsi createInstance dan menjalankannya di fungsi


initVulkan.

1 batal initVulkan()
2 { createInstance();
3}

Selain itu, tambahkan anggota data untuk memegang pegangan ke instance:

1 pribadi:
2 contoh VkInstance;

Sekarang, untuk membuat instance pertama-tama kita harus mengisi struct dengan beberapa
informasi tentang aplikasi kita. Data ini secara teknis bersifat opsional, tetapi mungkin
memberikan beberapa informasi yang berguna bagi pengemudi untuk mengoptimalkan aplikasi
khusus kami (misalnya karena menggunakan mesin grafis terkenal dengan perilaku khusus tertentu).
Struktur ini disebut VkApplicationInfo:
1 batal createInstance()
2 { VkApplicationInfo appInfo{};
3 appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
4 appInfo.pApplicationName = "Halo Segitiga";
5 appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
6 appInfo.pEngineName = "Tanpa Mesin"; appInfo.engineVersion =
7 VK_MAKE_VERSION(1, 0, 0); appInfo.apiVersion =
8 VK_API_VERSION_1_0;
9}

45
Machine Translated by Google

Seperti disebutkan sebelumnya, banyak struct di Vulkan mengharuskan Anda untuk secara
eksplisit menentukan jenis anggota sType. Ini juga salah satu dari banyak struct dengan
anggota pNext yang dapat menunjukkan informasi ekstensi di masa mendatang. Kami
menggunakan inisialisasi nilai di sini untuk membiarkannya sebagai nullptr.

Banyak informasi di Vulkan dilewatkan melalui struct alih-alih parameter fungsi dan kita harus
mengisi satu struct lagi untuk menyediakan formasi yang cukup untuk membuat instance.
Struktur berikutnya ini bukan opsional dan memberi tahu driver Vulkan ekstensi global dan
lapisan validasi mana yang ingin kita gunakan.
Global di sini berarti bahwa mereka berlaku untuk seluruh program dan bukan perangkat
tertentu, yang akan menjadi jelas dalam beberapa bab berikutnya.

1 VkInstanceCreateInfo createInfo{}; 2
buatInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; 3
createInfo.pApplicationInfo = &appInfo;

Dua parameter pertama sangat mudah. Dua lapisan berikutnya menentukan ekstensi global
yang diinginkan. Seperti disebutkan dalam bab ikhtisar, Vulkan adalah API agnostik platform,
yang berarti Anda memerlukan ekstensi untuk berinteraksi dengan sistem jendela. GLFW
memiliki fungsi bawaan yang berguna yang mengembalikan ekstensi yang diperlukan untuk
melakukan apa yang dapat kita berikan ke struct:

1 uint32_t glfwExtensionCount = 0; 2 const


char** glfwExtensions;
3
4 glfwExtensions =
glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
5
6 createInfo.enabledExtensionCount = glfwExtensionCount; 7
createInfo.ppEnabledExtensionNames = glfwExtensions;

Dua anggota terakhir dari struct menentukan lapisan validasi global untuk diaktifkan. Kita
akan membicarakan hal ini lebih mendalam di bab berikutnya, jadi kosongkan saja untuk saat
ini.

1 createInfo.enabledLayerCount = 0;

Kami sekarang telah menentukan semua yang diperlukan Vulkan untuk membuat instance dan akhirnya
kami dapat mengeluarkan panggilan vkCreateInstance:

1 Hasil VkResult = vkCreateInstance(&createInfo, nullptr, &instance);

Seperti yang akan Anda lihat, pola umum yang menjadi parameter fungsi pembuatan objek
Vulkan ikuti adalah:

• Pointer ke struktur dengan info pembuatan


• Pointer ke callback pengalokasi khusus, selalu nullptr dalam tutorial ini • Pointer ke
variabel yang menyimpan pegangan ke objek baru

46
Machine Translated by Google

Jika semuanya berjalan dengan baik maka pegangan ke instance disimpan di anggota kelas
VkInstance. Hampir semua fungsi Vulkan mengembalikan nilai bertipe VkResult baik
VK_SUCCESS atau kode kesalahan. Untuk memeriksa apakah instance berhasil dibuat,
kita tidak perlu menyimpan hasilnya dan cukup menggunakan pemeriksaan untuk nilai
sukses sebagai gantinya:

1 jika (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)

2 { throw std::runtime_error("gagal membuat instance!");


3}

Sekarang jalankan program untuk memastikan bahwa instance berhasil dibuat.

Memeriksa dukungan ekstensi


Jika Anda melihat dokumentasi vkCreateInstance maka Anda akan melihat bahwa salah
satu kemungkinan kode kesalahan adalah VK_ERROR_EXTENSION_NOT_PRESENT.
Kami cukup menentukan ekstensi yang kami butuhkan dan hentikan jika kode kesalahan itu
muncul kembali. Masuk akal untuk ekstensi penting seperti antarmuka sistem jendela, tetapi
bagaimana jika kita ingin memeriksa fungsionalitas opsional?

Untuk mengambil daftar ekstensi yang didukung sebelum membuat instance, ada fungsi
vkEnumerateInstanceExtensionProperties. Dibutuhkan penunjuk ke variabel yang menyimpan
jumlah ekstensi dan larik VkExtensionProperties untuk menyimpan detail ekstensi. Ini juga
membutuhkan parameter pertama opsional yang memungkinkan kita memfilter ekstensi
dengan lapisan validasi tertentu, yang akan kita abaikan untuk saat ini.

Untuk mengalokasikan array untuk menyimpan detail ekstensi, pertama-tama kita perlu
mengetahui berapa banyak yang ada. Anda dapat meminta jumlah ekstensi saja dengan
mengosongkan parameter terakhir:

1 uint32_t jumlah ekstensi = 0; 2


vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
nullptr);

Sekarang alokasikan array untuk menyimpan detail ekstensi (termasuk <vector>):

1 std::vector<VkExtensionProperties> ekstensi(extensionCount);

Akhirnya kami dapat menanyakan detail ekstensi:

1 vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());

Setiap struktur VkExtensionProperties berisi nama dan versi ekstensi. Kita dapat
mencantumkannya dengan perulangan for sederhana (\t adalah tab untuk lekukan):

47
Machine Translated by Google

1 std::cout << "ekstensi yang tersedia:\n";


2
3 for (const auto& extension : extensions) { std::cout <<
4 '\t' << extension.extensionName << '\n';
5}

Anda dapat menambahkan kode ini ke fungsi createInstance jika ingin memberikan beberapa
detail tentang dukungan Vulkan. Sebagai tantangan, coba buat fungsi yang memeriksa
apakah semua ekstensi yang dikembalikan oleh glfwGetRequiredInstanceExtensions
disertakan dalam daftar ekstensi yang didukung.

Membersihkan
VkInstance hanya boleh dihancurkan tepat sebelum program ditutup. Itu dapat dihancurkan
dalam pembersihan dengan fungsi vkDestroyInstance:

1 pembersihan batal ()
2 { vkDestroyInstance(instance, nullptr);
3
4 glfwDestroyWindow(jendela);
5
6 glfwHentikan();
7}

Parameter untuk fungsi vkDestroyInstance sangat mudah. Seperti disebutkan di bab


sebelumnya, fungsi alokasi dan deallokasi di Vulkan memiliki callback pengalokasi opsional
yang akan kita abaikan dengan meneruskan nullptr ke fungsi tersebut. Semua sumber daya
Vulkan lain yang akan kita buat di bab berikut harus dibersihkan sebelum instance
dihancurkan.

Sebelum melanjutkan dengan langkah yang lebih kompleks setelah pembuatan instance,
saatnya untuk mengevaluasi opsi debug kami dengan memeriksa lapisan validasi.

kode C++

48
Machine Translated by Google

Lapisan validasi

Apa itu lapisan validasi?


Vulkan API dirancang berdasarkan gagasan overhead driver minimal dan salah satu manifestasi
dari tujuan tersebut adalah bahwa ada pemeriksaan kesalahan yang sangat terbatas di API
secara default. Bahkan kesalahan sesederhana menyetel pencacahan ke nilai yang salah atau
meneruskan penunjuk nol ke parameter yang diperlukan umumnya tidak ditangani secara
eksplisit dan hanya akan mengakibatkan crash atau perilaku yang tidak terdefinisi. Karena
Vulkan mengharuskan Anda untuk sangat eksplisit tentang semua yang Anda lakukan, mudah
untuk membuat banyak kesalahan kecil seperti menggunakan fitur GPU baru dan lupa
memintanya pada waktu pembuatan perangkat logis.

Namun, bukan berarti pemeriksaan ini tidak dapat ditambahkan ke API. Vulkan memperkenalkan
sistem yang elegan untuk ini yang dikenal sebagai lapisan validasi. Lapisan validasi adalah
komponen opsional yang terhubung ke panggilan fungsi Vulkan untuk menerapkan operasi
tambahan. Operasi umum dalam lapisan validasi adalah:

• Memeriksa nilai parameter terhadap spesifikasi untuk mendeteksi mis


menggunakan

• Melacak pembuatan dan penghancuran objek untuk menemukan kebocoran


sumber daya • Memeriksa keamanan thread dengan melacak thread asal panggilan •
Mencatat setiap panggilan dan parameternya ke output standar • Melacak panggilan
Vulkan untuk pembuatan profil dan pemutaran ulang

Berikut adalah contoh penerapan fungsi dalam lapisan validasi diagnostik:

1 VkResult vkCreateInstance(
2 const VkInstanceCreateInfo* pCreateInfo, const
3 VkAllocationCallbacks* pAllocator,
4 Contoh VkInstance*) {
5
6 if (pCreateInfo == nullptr || instance == nullptr) {
7 log("Null pointer diteruskan ke parameter yang diperlukan!");
8 kembalikan VK_ERROR_INITIALIZATION_FAILED;
9 }

49
Machine Translated by Google

10
11 kembalikan real_vkCreateInstance(pCreateInfo, pAllocator, instance);
12 }

Lapisan validasi ini dapat ditumpuk secara bebas untuk menyertakan semua fungsi debugging yang
Anda minati. Anda cukup mengaktifkan lapisan validasi untuk build debug dan sepenuhnya
menonaktifkannya untuk build rilis, yang memberi Anda yang terbaik dari keduanya!

Vulkan tidak dilengkapi dengan lapisan validasi bawaan, tetapi LunarG Vulkan SDK menyediakan
satu set lapisan bagus yang memeriksa kesalahan umum. Mereka juga sepenuhnya open source,
sehingga Anda dapat memeriksa jenis kesalahan apa yang mereka periksa dan kontribusikan.
Menggunakan lapisan validasi adalah cara terbaik untuk menghindari kerusakan aplikasi pada driver
yang berbeda dengan secara tidak sengaja mengandalkan perilaku yang tidak terdefinisi.

Lapisan validasi hanya dapat digunakan jika telah diinstal ke dalam sistem.
Misalnya, lapisan validasi LunarG hanya tersedia di PC dengan Vulkan SDK terpasang.

Sebelumnya ada dua jenis lapisan validasi di Vulkan: khusus perangkat dan instans. Idenya adalah
bahwa lapisan instans hanya akan memeriksa panggilan yang terkait dengan objek Vulkan global
seperti instans, dan lapisan khusus perangkat hanya akan memeriksa panggilan yang terkait dengan
GPU tertentu. Lapisan khusus perangkat sekarang sudah tidak digunakan lagi, yang berarti lapisan
validasi instance berlaku untuk semua panggilan Vulkan.
Dokumen spesifikasi tetap menyarankan agar Anda mengaktifkan lapisan validasi pada tingkat
perangkat juga untuk kompatibilitas, yang diperlukan oleh beberapa implementasi.
Kami hanya akan menentukan lapisan yang sama dengan instance pada tingkat perangkat logis,
yang akan kita lihat nanti.

Menggunakan lapisan validasi


Di bagian ini, kita akan melihat cara mengaktifkan lapisan diagnostik standar yang disediakan oleh
Vulkan SDK. Sama seperti ekstensi, lapisan validasi perlu diaktifkan dengan menentukan namanya.
Semua validasi standar yang berguna digabungkan ke dalam lapisan yang termasuk dalam SDK
yang dikenal sebagai VK_LAYER_KHRONOS_validation.

Mari pertama-tama tambahkan dua variabel konfigurasi ke program untuk menentukan lapisan yang
akan diaktifkan dan apakah akan mengaktifkannya atau tidak. Saya telah memilih untuk mendasarkan
nilai tersebut pada apakah program sedang dikompilasi dalam mode debug atau tidak. Makro
NDEBUG adalah bagian dari standar C++ dan berarti "bukan debug".

1 const uint32_t LEBAR = 800; 2 const


uint32_t HEIGHT = 600;
3
4 const std::vector<const char*> validationLayers = {
5 "VK_LAYER_KHRONOS_validasi"
6 };

50
Machine Translated by Google

7
8 #ifdef NDEBUG
9 const bool enableValidationLayers = false;
10 #lain
11 const bool enableValidationLayers = true; 12 #endif

Kami akan menambahkan fungsi baru checkValidationLayerSupport yang memeriksa apakah


semua lapisan yang diminta tersedia. Daftar pertama semua layer yang tersedia menggunakan
fungsi vkEnumerateInstanceLayerProperties. Penggunaannya identik dengan
vkEnumerateInstanceExtensionProperties yang dibahas di bab pembuatan instance.

1 bool checkValidationLayerSupport() { uint32_t


vkEnumerateInstanceLayerProperties(&layerCount,
layerCount; 2
3 nullptr);
4
5 std::vector<VkLayerProperties> availableLayers(layerCount);
6 vkEnumerateInstanceLayerProperties(&layerCount,
availableLayers.data());
7
kembali salah;
89}

Selanjutnya, periksa apakah semua layer di validationLayers ada di daftar availableLayers.


Anda mungkin perlu menyertakan <cstring> untuk strcmp.

1 untuk (const char* layerName : validasiLayers) { bool layerFound


2 = false;
3
4 untuk (const auto& layerProperties : availableLayers) {
5 if (strcmp(layerName, layerProperties.layerName) == 0) {
6 layerFound = true;
7 merusak;
8 }
9 }
10
11 if (!layerFound) { return
12 false;
13 }
14 }
15
16 kembali benar;

Kita sekarang dapat menggunakan fungsi ini di createInstance:

1 batal createInstance() {

51
Machine Translated by Google

2 if (enableValidationLayers && !checkValidationLayerSupport()) { throw std::runtime_error("


3 lapisan validasi diminta, tetapi
tidak tersedia!");
4 }
5
6 ...
7}

Sekarang jalankan program dalam mode debug dan pastikan kesalahan tidak terjadi.
Jika ya, lihat FAQ.

Terakhir, modifikasi instance struct VkInstanceCreateInfo untuk menyertakan nama layer validasi jika
diaktifkan:

1 jika (enableValidationLayers) { 2
createInfo.enabledLayerCount =
static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validasiLayers.data(); 3 4 } lain {

createInfo.enabledLayerCount = 0;
56}

Jika pemeriksaan berhasil maka vkCreateInstance tidak boleh mengembalikan a


Kesalahan VK_ERROR_LAYER_NOT_PRESENT, tetapi Anda harus menjalankan program untuk membuatnya
Tentu.

Panggilan balik pesan


Lapisan validasi akan mencetak pesan debug ke keluaran standar karena kesalahan,
tetapi kami juga dapat menanganinya sendiri dengan memberikan panggilan balik
eksplisit dalam program kami. Ini juga akan memungkinkan Anda untuk memutuskan
jenis pesan yang ingin Anda lihat, karena tidak semua merupakan kesalahan (fatal). Jika
Anda tidak ingin melakukannya sekarang, Anda dapat melompat ke bagian terakhir di bab ini.

Untuk menyiapkan callback dalam program untuk menangani pesan dan detail terkait, kita harus
menyiapkan messenger debug dengan callback menggunakan ekstensi VK_EXT_debug_utils.

Pertama-tama kita akan membuat fungsi getRequiredExtensions yang akan mengembalikan


daftar ekstensi yang diperlukan berdasarkan apakah lapisan validasi diaktifkan atau tidak:

1 std::vector<const char*> getRequiredExtensions() { uint32_t


2 glfwExtensionCount = 0; const char** glfwExtensions; glfwExtensions
3 = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
4

52
Machine Translated by Google

6 std::vektor<const char*> ekstensi(glfwExtensions, glfwExtensions +


glfwExtensionCount);
7
8 if (enableValidationLayers) {
9 extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
10 }
11
12 ekstensi pengembalian ;
13 }

Ekstensi yang ditentukan oleh GLFW selalu diperlukan, tetapi ekstensi debug messenger
ditambahkan secara kondisional. Perhatikan bahwa saya telah menggunakan makro
VK_EXT_DEBUG_UTILS_EXTENSION_NAME di sini yang sama dengan string literal
“VK_EXT_debug_utils”. Menggunakan makro ini memungkinkan Anda menghindari kesalahan ketik.

Kita sekarang dapat menggunakan fungsi ini di createInstance:

1 ekstensi otomatis = getRequiredExtensions(); 2


createInfo.enabledExtensionCount =
static_cast<uint32_t>(extensions.size());
3 createInfo.ppEnabledExtensionNames = extensions.data();

Jalankan program untuk memastikan Anda tidak menerima kesalahan


VK_ERROR_EXTENSION_NOT_PRESENT. Kita sebenarnya tidak perlu mengecek keberadaan
ekstensi ini, karena seharusnya sudah tersirat dengan tersedianya lapisan validasi.

Sekarang mari kita lihat seperti apa fungsi panggilan balik debug. Tambahkan fungsi anggota
statis baru bernama debugCallback dengan prototipe PFN_vkDebugUtilsMessengerCallbackEXT.
VKAPI_ATTR dan VKAPI_CALL memastikan bahwa fungsi tersebut memiliki tanda tangan yang tepat
untuk Vulkan memanggilnya.

1 statis VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(


2 VkDebugUtilsMessageSeverityFlagBitsPesan SEXKeparahan,
3 VkDebugUtilsMessageTypeFlagsEXT messageType, const
4 VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
5
6

7
std::cerr << "lapisan validasi: std::endl; " << pCallbackData->pMessage <<

8
9 kembalikan VK_FALSE;
10 }

Parameter pertama menentukan tingkat keparahan pesan, yang merupakan salah satu dari tanda
berikut:

• VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: Diagnostik
pesan

53
Machine Translated by Google

• VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: Pesan informasi seperti


pembuatan sumber daya • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
Pesan tentang perilaku yang belum tentu merupakan kesalahan, tetapi kemungkinan besar bug
dalam aplikasi Anda

• VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: Pesan tentang perilaku yang


tidak valid dan dapat menyebabkan crash

Nilai pencacahan ini diatur sedemikian rupa sehingga Anda dapat menggunakan operasi perbandingan
untuk memeriksa apakah pesan sama atau lebih buruk dibandingkan dengan beberapa tingkat
keparahan, misalnya:

1 jika (Severitas pesan >=


VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
2 // Pesan cukup penting untuk ditampilkan
3}

Parameter messageType dapat memiliki nilai berikut:

• VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT: Beberapa peristiwa telah terjadi


pened yang tidak terkait dengan spesifikasi atau kinerja
• VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT: Telah terjadi sesuatu yang
melanggar spesifikasi atau menunjukkan kemungkinan kesalahan •
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT: Potensi penggunaan
Vulkan yang tidak optimal

Parameter pCallbackData mengacu pada struct VkDebugUtilsMessengerCallbackDataEXT yang berisi detail


pesan itu sendiri, dengan anggota yang paling penting adalah:

• pMessage: Pesan debug sebagai string yang diakhiri null • pObjects: Larik
pegangan objek Vulkan yang terkait dengan pesan • objectCount: Jumlah objek dalam
larik

Terakhir, parameter pUserData berisi pointer yang ditentukan selama penyiapan callback dan
memungkinkan Anda meneruskan data Anda sendiri ke sana.

Callback mengembalikan boolean yang menunjukkan jika panggilan Vulkan yang memicu pesan
lapisan validasi harus dibatalkan. Jika callback kembali benar, maka panggilan dibatalkan dengan
kesalahan VK_ERROR_VALIDATION_FAILED_EXT. Ini biasanya hanya digunakan untuk menguji
lapisan validasi itu sendiri, jadi Anda harus selalu mengembalikan VK_FALSE.

Yang tersisa sekarang hanyalah memberi tahu Vulkan tentang fungsi callback. Mungkin agak
mengejutkan, bahkan panggilan balik debug di Vulkan dikelola dengan pegangan yang perlu dibuat
dan dihancurkan secara eksplisit. Callback semacam itu adalah bagian dari debug messenger dan
Anda dapat memilikinya sebanyak yang Anda inginkan. Tambahkan anggota kelas untuk pegangan
ini tepat di bawah contoh:

1 VkDebugUtilsMessengerEXT debugMessenger;

54
Machine Translated by Google

Sekarang tambahkan fungsi setupDebugMessenger untuk dipanggil dari initVulkan tepat setelah
createInstance:

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger();
4}
5
6 batal setupDebugMessenger() { jika (!
enableValidationLayers) kembali; 7
8
9}

Kita harus mengisi struktur dengan detail tentang messenger dan panggilan baliknya:

1 VkDebugUtilsMessengerCreateInfoEXT createInfo{}; 2 buatInfo.sType


= VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;

3 createInfo.messageSeverity =
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; 4
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
5 createInfo.pfnUserCallback = debugCallback; 6
createInfo.pUserData = nullptr; // Opsional

Bidang messageSeverity memungkinkan Anda untuk menentukan semua jenis tingkat keparahan
yang Anda inginkan untuk panggilan balik Anda. Saya telah menentukan semua jenis kecuali
untuk VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT di sini untuk menerima
pemberitahuan tentang kemungkinan masalah sambil mengabaikan info debug umum yang lengkap.

Demikian pula bidang messageType memungkinkan Anda memfilter jenis pesan apa yang
diberitahukan oleh callback Anda. Saya cukup mengaktifkan semua jenis di sini. Anda selalu
dapat menonaktifkan beberapa jika tidak berguna bagi Anda.

Terakhir, bidang pfnUserCallback menentukan penunjuk ke fungsi callback.


Secara opsional, Anda dapat meneruskan penunjuk ke bidang pUserData yang akan diteruskan
ke fungsi callback melalui parameter pUserData. Anda bisa menggunakan ini untuk meneruskan
penunjuk ke kelas HelloTriangleApplication, misalnya.

Perhatikan bahwa ada lebih banyak cara untuk mengonfigurasi pesan lapisan validasi dan
panggilan balik debug, tetapi ini adalah penyiapan yang baik untuk memulai tutorial ini.
Lihat spesifikasi ekstensi untuk info lebih lanjut tentang kemungkinannya.

Struktur ini harus diteruskan ke fungsi vkCreateDebugUtilsMessengerEXT untuk membuat objek


VkDebugUtilsMessengerEXT. Sayangnya, karena fungsi ini merupakan fungsi ekstensi, maka
tidak dimuat secara otomatis. Kita punya

55
Machine Translated by Google

untuk mencari sendiri alamatnya menggunakan vkGetInstanceProcAddr. Kami akan


membuat fungsi proxy kami sendiri yang menangani ini di latar belakang. Saya telah
menambahkannya tepat di atas definisi kelas HelloTriangleApplication.

1 VkResult CreateDebugUtilsMessengerEXT(instance VkInstance, const


VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const
VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT*
pDebugMessenger) { auto func = (PFN_vkCreateDebugUtilsMessengerEXT)
2 vkGetInstanceProcAddr(instance, "vkCreateUssengerEXT); if (func != nullptr)
{ return func(instance, pCreateInfo, pAllocator, pDebugMessenger); } lain
{ kembalikan VK_ERROR_EXTENSION_NOT_PRESENT;
3
4

5
6
7 }
8}

Fungsi vkGetInstanceProcAddr akan mengembalikan nullptr jika fungsi tidak dapat dimuat.
Kita sekarang dapat memanggil fungsi ini untuk membuat objek ekstensi jika tersedia:

1 jika (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr,


&debugMessenger) != VK_SUCCESS)
2 { throw std::runtime_error("gagal mengatur debug messenger!");
3}

Parameter kedua hingga terakhir sekali lagi adalah panggilan balik pengalokasi opsional
yang kami atur ke nullptr, selain itu parameternya cukup mudah. Karena pembawa pesan
debug khusus untuk instance Vulkan dan lapisannya, ia perlu ditentukan secara eksplisit
sebagai argumen pertama. Anda juga akan melihat pola ini dengan objek anak lainnya
nanti.

Objek VkDebugUtilsMessengerEXT juga perlu dibersihkan dengan panggilan ke


vkDestroyDebugUtilsMessengerEXT. Sama halnya dengan vkCreateDebugUtilsMessengerEXT, fungsi
harus dimuat secara eksplisit.

Buat fungsi proxy lain tepat di bawah CreateDebugUtilsMessengerEXT:

1 batal DestroyDebugUtilsMessengerEXT(VkInstance instance,


VkDebugUtilsMessengerEXT debugMessenger, const
VkAllocationCallbacks* pAllocator) { auto func =
2 (PFN_vkDestroyDebugUtilsMessengerEXT)
vkGetInstanceProcAddr(instance,
"vkDestroyDebugUtilsMessengerEXT"); if (fungsi != nullptr) {
3
4 func(instance, debugMessenger, pAllocator);

56
Machine Translated by Google

5 }
6}

Pastikan bahwa fungsi ini adalah fungsi kelas statis atau fungsi di luar kelas. Kami
kemudian dapat memanggilnya dalam fungsi pembersihan:

1 pembersihan batal ()
2 { jika (enableValidationLayers) {
3 HancurkanDebugUtilsMessengerEXT(instance, debugMessenger,
nullptr);
4 }
5
6 vkDestroyInstance(contoh, nullptr);
7
8 glfwDestroyWindow(jendela);
9
10 glfwHentikan();
11 }

Men-debug pembuatan dan penghancuran instance


Meskipun kami sekarang telah menambahkan debug dengan lapisan validasi ke program,
kami belum mencakup semuanya. Panggilan vkCreateDebugUtilsMessengerEXT
memerlukan instance yang valid untuk dibuat dan vkDestroyDebugUtilsMessengerEXT harus dipanggil
sebelum instance dihancurkan. Hal ini membuat kami tidak dapat men-debug masalah apa pun dalam
panggilan vkCreateInstance dan vkDestroyInstance.

Namun, jika Anda membaca dokumentasi ekstensi dengan saksama, Anda akan melihat
bahwa ada cara untuk membuat messenger utils debug terpisah khusus untuk dua
pemanggilan fungsi tersebut. Anda hanya perlu meneruskan penunjuk ke struktur
VkDebugUtilsMessengerCreateInfoEXT di bidang ekstensi pNext VkInstanceCreateInfo.
Ekstrak pertama populasi messenger buat info menjadi fungsi terpisah:

1 kosong

populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo)
{ createInfo = {}; createInfo.sType =
2 VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
3

4 createInfo.messageSeverity =
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
5 createInfo.messageType =
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |

57
Machine Translated by Google

VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
6 createInfo.pfnUserCallback = debugCallback;
7}
8
9 ...
10
11 void setupDebugMessenger() { if (!
12 enableValidationLayers) return;
13
14 VkDebugUtilsMessengerCreateInfoEXT createInfo;
15 populateDebugMessengerCreateInfo(createInfo);
16
17 jika (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr,
&debugMessenger) != VK_SUCCESS) { throw
18 std::runtime_error("gagal menyiapkan debug
kurir!");
}
19 20 }

Kami sekarang dapat menggunakan kembali ini di fungsi createInstance:

1 batal createInstance() {
2 ...
3
4 VkInstanceCreateInfo createInfo{}; createInfo.sType
5 = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo
6 = &appInfo;
7
8 ...
9
10 VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; if
11 (enableValidationLayers) {
12 createInfo.enabledLayerCount =
static_cast<uint32_t>(validationLayers.size());
13 createInfo.ppEnabledLayerNames = validasiLayers.data();
14
15 populateDebugMessengerCreateInfo(debugCreateInfo); createInfo.pNext
16 = (VkDebugUtilsMessengerCreateInfoEXT*)
&debugBuatInfo; } else
17 { createInfo.enabledLayerCount = 0;
18
19
20 createInfo.pBerikutnya = nullptr;
21 }
22

58
Machine Translated by Google

23 if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS)


{ throw std::runtime_error("gagal membuat instance!");
24
25 }
26 }

Variabel debugCreateInfo ditempatkan di luar pernyataan if untuk memastikan bahwa variabel


tersebut tidak dihancurkan sebelum panggilan vkCreateInstance. Dengan membuat messenger
debug tambahan dengan cara ini, secara otomatis akan digunakan selama vkCreateInstance
dan vkDestroyInstance dan dibersihkan setelah itu.

Pengujian

Sekarang mari kita sengaja membuat kesalahan untuk melihat lapisan validasi beraksi.
Hapus sementara panggilan ke DestroyDebugUtilsMessengerEXT dalam fungsi pembersihan
dan jalankan program Anda. Setelah keluar, Anda akan melihat sesuatu seperti ini:

Jika Anda tidak melihat pesan apa pun, periksa instalasi Anda.

Jika Anda ingin melihat panggilan mana yang memicu pesan, Anda dapat menambahkan breakpoint
ke panggilan balik pesan dan melihat pelacakan tumpukan.

Konfigurasi
Ada lebih banyak pengaturan untuk perilaku layer validasi daripada hanya flag yang ditentukan
dalam struct VkDebugUtilsMessengerCreateInfoEXT. Jelajahi Vulkan SDK dan buka direktori
Config. Di sana Anda akan menemukan file vk_layer_settings.txt yang menjelaskan cara
mengonfigurasi layer.

Untuk mengonfigurasi setelan lapisan untuk aplikasi Anda sendiri, salin file ke direktori Debug
dan Rilis proyek Anda dan ikuti petunjuk untuk menyetel perilaku yang diinginkan. Namun,
untuk sisa tutorial ini saya akan berasumsi bahwa Anda menggunakan pengaturan default.

Sepanjang tutorial ini saya akan membuat beberapa kesalahan yang disengaja untuk
menunjukkan kepada Anda betapa membantu lapisan validasi dalam menangkapnya dan untuk
mengajari Anda betapa pentingnya mengetahui dengan tepat apa yang Anda lakukan dengan
Vulkan. Sekarang saatnya melihat perangkat Vulkan di sistem.

59
Machine Translated by Google

kode C++

60
Machine Translated by Google

Perangkat fisik dan keluarga


antrean

Memilih perangkat fisik


Setelah menginisialisasi pustaka Vulkan melalui VkInstance, kita perlu mencari dan memilih
kartu grafis di sistem yang mendukung fitur yang kita perlukan. Sebenarnya kita bisa memilih
sejumlah kartu grafis dan menggunakannya secara bersamaan, namun dalam tutorial ini kita
akan tetap pada kartu grafis pertama yang sesuai dengan kebutuhan kita.

Kami akan menambahkan fungsi pickPhysicalDevice dan menambahkan panggilan ke fungsi


initVulkan.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger();
4 pickPhysicalDevice();
5}
6
7 membatalkan pickPhysicalDevice()
{8
9}

Kartu grafis yang akan kita pilih akan disimpan di VkPhysicalDe vice handle yang ditambahkan
sebagai anggota kelas baru. Objek ini akan dihancurkan secara implisit saat VkInstance
dihancurkan, jadi kita tidak perlu melakukan sesuatu yang baru dalam fungsi pembersihan.

1 VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

Mencantumkan kartu grafis sangat mirip dengan mencantumkan ekstensi dan mulai dengan
menanyakan nomornya saja.

1 uint32_t DeviceCount = 0; 2
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

61
Machine Translated by Google

Jika ada 0 perangkat dengan dukungan Vulkan maka tidak ada gunanya melangkah lebih jauh.

1 if (deviceCount == 0) { throw
2 std::runtime_error("gagal menemukan GPU dengan dukungan Vulkan!");

3}

Kalau tidak, kita sekarang dapat mengalokasikan array untuk menampung semua pegangan
VkPhysicalDevice.

1 std::vector<VkPhysicalDevice> perangkat(deviceCount); 2
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

Sekarang kita perlu mengevaluasi masing-masing dan memeriksa apakah cocok untuk operasi yang
ingin kita lakukan, karena tidak semua kartu grafis dibuat sama.
Untuk itu kami akan memperkenalkan fungsi baru:

1 bool isDeviceSuitable(perangkat VkPhysicalDevice) { 2 3 }


kembali benar;

Dan kami akan memeriksa apakah ada perangkat fisik yang memenuhi persyaratan yang akan kami
tambahkan ke fungsi tersebut.

1 untuk (const auto& device : devices) { if


2 (isDeviceSuitable(device)) { physicalDevice =
3 device; merusak;
4
5 }
6}
7
8 if (physicalDevice == VK_NULL_HANDLE) { throw
9 std::runtime_error("gagal menemukan GPU yang cocok!");
10 }

Bagian selanjutnya akan memperkenalkan persyaratan pertama yang akan kita periksa dalam
fungsi isDeviceSuitable. Karena kami akan mulai menggunakan lebih banyak fitur Vulkan di bab
selanjutnya, kami juga akan memperluas fungsi ini untuk menyertakan lebih banyak pemeriksaan.

Pemeriksaan kesesuaian perangkat dasar

Untuk mengevaluasi kesesuaian suatu perangkat, kita dapat memulai dengan menanyakan beberapa detail.
Properti perangkat dasar seperti nama, jenis, dan versi Vulkan yang didukung dapat ditanyakan
menggunakan vkGetPhysicalDeviceProperties.

1 VkPhysicalDeviceProperties perangkatProperti; 2
vkGetPhysicalDeviceProperties(perangkat, &Properti perangkat);

62
Machine Translated by Google

Dukungan untuk fitur opsional seperti kompresi tekstur, float 64 bit, dan rendering multi viewport
(berguna untuk VR) dapat ditanyakan menggunakan vkGetPhysi calDeviceFeatures:

1 VkPhysicalDeviceFeatures deviceFitur; 2
vkGetFiturPerangkatFisik(perangkat, &Fiturperangkat);

Ada lebih banyak detail yang dapat ditanyakan dari perangkat yang akan kita diskusikan nanti
mengenai memori perangkat dan keluarga antrean (lihat bagian berikutnya).

Sebagai contoh, katakanlah kami menganggap aplikasi kami hanya dapat digunakan untuk kartu
grafis khusus yang mendukung shader geometri. Maka fungsi isDeviceSuitable akan terlihat seperti
ini:

1 bool isDeviceSuitable(perangkat VkPhysicalDevice) {


VkPhysicalDeviceProperties perangkatProperti; 2
3 VkPhysicalDeviceFeatures Fitur perangkat;
4 vkGetPhysicalDeviceProperties(perangkat, &Properti perangkat);
5 vkGetFiturPerangkatFisik(perangkat, &Fiturperangkat);
6
7 kembalikan deviceProperties.deviceType ==
VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
deviceFeatures.geometryShader;
89}

Alih-alih hanya memeriksa apakah suatu perangkat cocok atau tidak dan menggunakan yang
pertama, Anda juga dapat memberi skor pada setiap perangkat dan memilih yang tertinggi.
Dengan begitu Anda dapat memilih kartu grafis khusus dengan memberikan skor yang lebih
tinggi, tetapi kembali ke GPU terintegrasi jika itu satu-satunya yang tersedia. Anda dapat
menerapkan sesuatu seperti itu sebagai berikut:

1 #termasuk <peta>
2
3 ...
4
5 membatalkan pickPhysicalDevice()
{6 ...
7
8 // Gunakan peta terurut untuk mengurutkan kandidat secara otomatis
meningkatkan skor
9 kandidat std::multimap<int, VkPhysicalDevice>;
10
11 for (const auto& device : devices) { int score =
12 rateDeviceSuitability(device);
13 kandidat.sisipkan(std::make_pair(skor, perangkat));
14 }
15
16 // Periksa apakah kandidat terbaik cocok sama sekali

63
Machine Translated by Google

17 if (kandidat.rbegin()->pertama > 0) { Perangkat fisik


18 = kandidat.rbegin()->kedua; } else { throw
19 std::runtime_error("gagal menemukan GPU yang cocok!");
20
21 }
22 }
23
24 int rateDeviceSuitability(perangkat VkPhysicalDevice) {
25 ...
26
27 int skor = 0;
28
29 // GPU diskrit memiliki keunggulan kinerja yang signifikan jika
30 (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
{ skor += 1000;
31
32 }
33
34 // Ukuran tekstur maksimum yang mungkin memengaruhi skor kualitas grafis +=
35 deviceProperties.limits.maxImageDimension2D;
36
37 // Aplikasi tidak dapat berfungsi tanpa shader geometri if (!
38 deviceFeatures.geometryShader) { return 0;
39
40 }
41
42 skor pengembalian ;
43 }

Anda tidak perlu mengimplementasikan semua itu untuk tutorial ini, tetapi itu untuk memberi Anda
gambaran tentang bagaimana Anda bisa mendesain proses pemilihan perangkat Anda. Tentu saja
Anda juga dapat menampilkan nama pilihan dan mengizinkan pengguna untuk memilih.

Karena kami baru memulai, dukungan Vulkan adalah satu-satunya hal yang kami butuhkan dan oleh
karena itu kami akan puas dengan sembarang GPU:

1 bool isDeviceSuitable(perangkat VkPhysicalDevice) { 2


kembali benar;
3}

Di bagian selanjutnya kita akan membahas fitur pertama yang benar-benar diperlukan untuk diperiksa.

Keluarga antrian
Telah disinggung secara singkat sebelumnya bahwa hampir setiap operasi di Vulkan,
mulai dari menggambar hingga mengunggah tekstur, memerlukan perintah untuk dikirimkan

64
Machine Translated by Google

ke antrian. Ada berbagai jenis antrian yang berasal dari keluarga antrian yang berbeda dan setiap
keluarga antrian hanya mengizinkan sebagian dari perintah. Misalnya, mungkin ada kelompok
antrean yang hanya mengizinkan pemrosesan perintah komputasi atau yang hanya mengizinkan
perintah terkait transfer memori.

Kita perlu memeriksa keluarga antrean mana yang didukung oleh perangkat dan mana yang
mendukung perintah yang ingin kita gunakan. Untuk itu kita akan menambahkan fungsi baru
findQueueFamilies yang mencari semua keluarga antrian yang kita butuhkan.

Saat ini kita hanya akan mencari antrian yang mendukung perintah grafis, sehingga fungsinya terlihat
seperti ini:

1 uint32_t findQueueFamilies(perangkat VkPhysicalDevice) { 2


// Logika untuk menemukan keluarga antrian grafik
3}

Namun, di salah satu bab berikutnya kita sudah akan mencari antrean lain, jadi lebih baik
mempersiapkannya dan menggabungkan indeks ke dalam sebuah struct:

1 struct QueueFamilyIndices { uint32_t


2 graphicsFamily;
3 };
4
5 QueueFamilyIndices findQueueFamilies(perangkat VkPhysicalDevice) {
6 Indeks QueueFamilyIndices;
7 // Logika untuk menemukan indeks keluarga antrean untuk mengisi struct dengan
8 indeks pengembalian ;
9}

Tetapi bagaimana jika keluarga antrean tidak tersedia? Kami dapat memberikan pengecualian di
findQueueFamilies, tetapi fungsi ini sebenarnya bukan tempat yang tepat untuk membuat keputusan
tentang kesesuaian perangkat. Misalnya, kami mungkin lebih memilih perangkat dengan keluarga
antrean transfer khusus, tetapi tidak memerlukannya. Oleh karena itu, kami memerlukan beberapa
cara untuk menunjukkan apakah keluarga antrian tertentu telah ditemukan.

Tidak mungkin menggunakan nilai ajaib untuk menunjukkan tidak adanya keluarga antrian, karena
nilai uint32_t apa pun secara teori dapat menjadi indeks keluarga antrian yang valid termasuk 0.
Untungnya C++17 memperkenalkan struktur data untuk membedakan antara kasus nilai yang ada
atau tidak:

1 #termasuk <opsional> 2

3 ...
4
5 std::opsional<uint32_t> graphicsFamily;
6
7 std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // Salah

65
Machine Translated by Google

8
9 grafikKeluarga = 0;
10
11 std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; //
BENAR

std::opsional adalah pembungkus yang tidak mengandung nilai sampai Anda menetapkan
sesuatu padanya. Kapan saja Anda dapat menanyakan apakah itu berisi nilai atau tidak
dengan memanggil fungsi anggota has_value() . Itu berarti bahwa kita dapat mengubah logika menjadi:

1 #termasuk <opsional> 2

3 ...
4
5 struct QueueFamilyIndices
{ std::opsional<uint32_t> graphicsFamily; 6
7 };
8
9 QueueFamilyIndices findQueueFamilies(perangkat VkPhysicalDevice) {
10 Indeks QueueFamilyIndices;
11 // Tetapkan indeks ke kelompok antrean yang dapat ditemukan
12 kembali indeks;
13 }

Kami sekarang dapat mulai menerapkan findQueueFamilies:

1 QueueFamilyIndices findQueueFamilies(perangkat VkPhysicalDevice) {


2 Indeks QueueFamilyIndices;
3
4 ...
5
6 indeks pengembalian ;
7}

Proses mengambil daftar kelompok antrean persis seperti yang Anda harapkan dan
menggunakan vkGetPhysicalDeviceQueueFamilyProperties:

1 uint32_t queueFamilyCount = 0; 2
vkGetPhysicalDeviceQueueFamilyProperties(perangkat, &queueFamilyCount,
nullptr);
3
4 std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount); 5
vkGetPhysicalDeviceQueueFamilyProperties(perangkat, &queueFamilyCount, queueFamilies.data());

Struktur VkQueueFamilyProperties berisi beberapa detail tentang kelompok antrian,


termasuk jenis operasi yang didukung dan jumlah

66
Machine Translated by Google

antrian yang dapat dibuat berdasarkan keluarga itu. Kami perlu menemukan setidaknya satu
kelompok antrean yang mendukung VK_QUEUE_GRAPHICS_BIT.

1 int i = 0; 2
untuk (const auto& queueFamily : queueFamilies) {
3 if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
4 indeks.grafisKeluarga = i;
5 }
6
7 saya++;

8}

Sekarang setelah kita memiliki fungsi pencarian keluarga antrean mewah ini, kita dapat
menggunakannya sebagai tanda centang di fungsi isDeviceSuitable untuk memastikan bahwa
perangkat dapat memproses perintah yang ingin kita gunakan:

1 bool isDeviceSuitable(perangkat VkPhysicalDevice) { 2


indeks QueueFamilyIndices = findQueueFamilies(device);
3
4 return indexes.graphicsFamily.has_value();
5}

Untuk membuatnya sedikit lebih nyaman, kami juga akan menambahkan pemeriksaan umum ke
struct itu sendiri:

1 struct QueueFamilyIndices {
2 std::opsional<uint32_t> graphicsFamily;
3
4 bool isComplete()
5 { mengembalikan graphicsFamily.has_value();
6 }
7 };
8
9 ...
10
11 bool isDeviceSuitable(perangkat VkPhysicalDevice) { 12
indeks QueueFamilyIndices = findQueueFamilies(device);
13
14 return indexs.isComplete();
15 }

Kami sekarang juga dapat menggunakan ini untuk keluar lebih awal dari findQueueFamilies:

1 untuk (const auto& queueFamily : queueFamilies) {


2 ...
3
4 if (indices.isComplete()) { istirahat;
5

67
Machine Translated by Google

6 }
7
8 saya++;

9}

Bagus, hanya itu yang kami butuhkan saat ini untuk menemukan perangkat fisik yang tepat! Langkah
selanjutnya adalah membuat perangkat logis untuk berinteraksi dengannya.

kode C++

68
Machine Translated by Google

Perangkat logis dan antrian

pengantar

Setelah memilih perangkat fisik untuk digunakan, kita perlu menyiapkan perangkat logis untuk
berinteraksi dengannya. Proses pembuatan perangkat logis mirip dengan proses pembuatan
instance dan menjelaskan fitur yang ingin kita gunakan. Kami juga perlu menentukan antrean
mana yang akan dibuat setelah kami menanyakan kelompok antrean mana yang tersedia. Anda
bahkan dapat membuat beberapa perangkat logis dari perangkat fisik yang sama jika Anda
memiliki persyaratan yang berbeda-beda.

Mulailah dengan menambahkan anggota kelas baru untuk menyimpan pegangan perangkat logis.

1 perangkat VkDevice;

Selanjutnya, tambahkan fungsi createLogicalDevice yang dipanggil dari initVulkan.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger();
4 pickPhysicalDevice();
5 createLogicalDevice();
6}
7
8 membatalkan createLogicalDevice() { 9

10 }

Menentukan antrian yang akan dibuat


Pembuatan perangkat logis melibatkan penentuan banyak detail dalam struct lagi, yang pertama
adalah VkDeviceQueueCreateInfo. Struktur ini menjelaskan jumlah antrian yang kita inginkan
untuk satu keluarga antrian. Saat ini kami hanya tertarik pada antrean dengan kemampuan grafis.

1 indeks QueueFamilyIndices = findQueueFamilies(physicalDevice);

69
Machine Translated by Google

3 VkDeviceQueueCreateInfo queueCreateInfo{}; 4
antrianCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
5 queueCreateInfo.queueFamilyIndex = indexes.graphicsFamily.value(); 6
queueCreateInfo.queueCount = 1;

Driver yang tersedia saat ini hanya akan memungkinkan Anda membuat sejumlah kecil antrean
untuk setiap keluarga antrean dan Anda tidak benar-benar membutuhkan lebih dari satu. Itu
karena Anda dapat membuat semua buffer perintah di beberapa utas dan kemudian mengirimkan
semuanya sekaligus di utas utama dengan satu panggilan overhead rendah.

Vulkan memungkinkan Anda menetapkan prioritas ke antrean untuk memengaruhi penjadwalan


eksekusi buffer perintah menggunakan angka floating point antara 0,0 dan 1,0. Ini diperlukan
meskipun hanya ada satu antrean:

1 float antrianPrioritas = 1.0f; 2


queueCreateInfo.pQueuePriorities = &queuePriority;

Menentukan fitur perangkat yang digunakan


Informasi selanjutnya untuk ditentukan adalah sekumpulan fitur perangkat yang akan kita
gunakan. Ini adalah fitur yang kami minta dukungannya dengan vkGetPhysicalDeviceFeatures di
bab sebelumnya, seperti shader geometri. Saat ini kami tidak memerlukan sesuatu yang istimewa,
jadi kami cukup mendefinisikannya dan menyerahkan semuanya ke VK_FALSE. Kami akan
kembali ke struktur ini setelah kami mulai melakukan hal yang lebih menarik dengan Vulkan.

1 VkPhysicalDeviceFeatures deviceFeatures{};

Membuat perangkat logis


Dengan dua struktur sebelumnya, kita dapat mulai mengisi struktur utama VkDeviceCreateInfo.

1 VkDeviceCreateInfo createInfo{}; 2
buatInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

Pertama-tama tambahkan pointer ke info pembuatan antrean dan struktur fitur perangkat:

1 createInfo.pQueueCreateInfos = &queueCreateInfo; 2
createInfo.queueCreateInfoCount = 1;
3
4 createInfo.pEnabledFeatures = &deviceFeatures;

Informasi selebihnya memiliki kemiripan dengan struktur VkInstanceCreateInfo dan mengharuskan


Anda menentukan ekstensi dan lapisan validasi. Perbedaannya adalah bahwa ini adalah perangkat
khusus saat ini.

70
Machine Translated by Google

Contoh ekstensi khusus perangkat adalah VK_KHR_swapchain, yang memungkinkan Anda


menyajikan gambar yang dirender dari perangkat itu ke windows. Mungkin ada perangkat
Vulkan di sistem yang tidak memiliki kemampuan ini, misalnya karena hanya mendukung
operasi komputasi. Kami akan kembali ke ekstensi ini di bab rantai pertukaran.

Implementasi Vulkan sebelumnya membuat perbedaan antara instance dan lapisan validasi
khusus perangkat, tetapi hal ini tidak lagi terjadi. Itu berarti bahwa bidang EnableLayerCount
dan ppEnabledLayerNames dari VkDeviceCreateInfo diabaikan oleh implementasi terbaru.
Namun, masih merupakan ide bagus untuk mengaturnya agar kompatibel dengan
implementasi yang lebih lama:

1 createInfo.enabledExtensionCount = 0;
2
3 jika (enableValidationLayers) { 4
createInfo.enabledLayerCount =
static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validasiLayers.data(); 5 6 } lain {

createInfo.enabledLayerCount = 0;
78}

Kami tidak memerlukan ekstensi khusus perangkat apa pun untuk saat ini.

Itu saja, kami sekarang siap untuk membuat instance perangkat logis dengan panggilan ke
fungsi vkCreateDevice yang diberi nama dengan tepat.

1 if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { throw


std::runtime_error("gagal membuat perangkat logis!");
2
3}

Parameternya adalah perangkat fisik untuk berinteraksi, antrian dan info penggunaan yang
baru saja kita tentukan, penunjuk callback alokasi opsional dan penunjuk ke variabel untuk
menyimpan pegangan perangkat logis. Mirip dengan fungsi pembuatan instance, panggilan
ini dapat mengembalikan kesalahan berdasarkan pengaktifan ekstensi yang tidak ada atau
menentukan penggunaan fitur yang tidak didukung yang diinginkan.

Perangkat harus dihancurkan dalam pembersihan dengan fungsi vkDestroyDevice :

1 pembersihan batal ()
{ vkDestroyDevice(perangkat, nullptr); 2
...
34}

Perangkat logis tidak berinteraksi langsung dengan instance, oleh karena itu tidak disertakan
sebagai parameter.

71
Machine Translated by Google

Mengambil pegangan antrian


Antrean dibuat secara otomatis bersama dengan perangkat logis, tetapi kami belum memiliki pegangan
untuk berinteraksi dengannya. Pertama tambahkan anggota kelas untuk menyimpan pegangan ke
antrean grafik:

1 VkQueue graphicsQueue;

Antrean perangkat secara implisit dibersihkan saat perangkat dihancurkan, jadi kami tidak perlu
melakukan apa pun dalam pembersihan.

Kita dapat menggunakan fungsi vkGetDeviceQueue untuk mengambil penanganan antrean untuk
setiap keluarga antrean. Parameternya adalah perangkat logis, kelompok antrean, indeks antrean,
dan penunjuk ke variabel untuk menyimpan pegangan antrean. Karena kami hanya membuat satu
antrean dari kelompok ini, kami hanya akan menggunakan indeks 0.

1 vkGetDeviceQueue(perangkat, indeks.grafisKeluarga.nilai(), 0,
&grafisAntrean);

Dengan perangkat logis dan pegangan antrean, kini kita dapat benar-benar mulai menggunakan kartu
grafis untuk melakukan banyak hal! Dalam beberapa bab berikutnya kita akan menyiapkan sumber
daya untuk mempresentasikan hasil ke sistem jendela.

kode C++

72
Machine Translated by Google

Permukaan jendela

Karena Vulkan adalah API agnostik platform, Vulkan tidak dapat berinteraksi langsung dengan
sistem jendelanya sendiri. Untuk membuat koneksi antara Vulkan dan sistem jendela untuk
menampilkan hasil ke layar, kita perlu menggunakan ekstensi WSI (Window System Integration).
Pada bab ini kita akan membahas yang pertama yaitu VK_KHR_surface. Itu memaparkan objek
VkSurfaceKHR yang mewakili jenis permukaan abstrak untuk menyajikan gambar yang dirender.
Permukaan dalam program kita akan didukung oleh jendela yang telah kita buka dengan GLFW.

Ekstensi VK_KHR_surface adalah ekstensi tingkat instans dan kami sebenarnya telah
mengaktifkannya, karena ekstensi ini termasuk dalam daftar yang dikembalikan oleh
glfwGetRequiredInstanceExtensions. Daftar ini juga menyertakan beberapa ekstensi WSI lainnya
yang akan kita gunakan dalam beberapa bab berikutnya.

Permukaan jendela perlu dibuat segera setelah pembuatan instance, karena sebenarnya dapat
memengaruhi pemilihan perangkat fisik. Alasan kami menunda ini adalah karena permukaan
jendela adalah bagian dari topik yang lebih besar dari target render dan presentasi yang
penjelasannya akan mengacaukan penyiapan dasar. Perlu juga dicatat bahwa permukaan jendela
adalah komponen yang sepenuhnya opsional di Vulkan, jika Anda hanya memerlukan rendering
di luar layar. Vulkan memungkinkan Anda melakukannya tanpa peretasan seperti membuat
jendela tak terlihat (diperlukan untuk OpenGL).

Pembuatan permukaan jendela

Mulailah dengan menambahkan anggota kelas permukaan tepat di bawah callback debug.

1 permukaan VkSurfaceKHR;

Meskipun objek VkSurfaceKHR dan penggunaannya adalah agnostik platform, pembuatannya


bukan karena bergantung pada detail sistem jendela. Misalnya, diperlukan pegangan HWND dan
HMODULE di Windows. Oleh karena itu ada tambahan khusus platform untuk ekstensi, yang
pada Windows disebut VK_KHR_win32_surface dan juga secara otomatis disertakan dalam daftar
dari glfwGetRequiredInstanceExtensions.

73
Machine Translated by Google

Saya akan mendemonstrasikan bagaimana ekstensi khusus platform ini dapat digunakan untuk
membuat permukaan di Windows, tetapi kami tidak akan benar-benar menggunakannya dalam
tutorial ini. Tidak masuk akal untuk menggunakan perpustakaan seperti GLFW dan kemudian tetap
menggunakan kode khusus platform. GLFW sebenarnya memiliki glfwCreateWindowSurface yang
menangani perbedaan platform untuk kita. Tetap saja, bagus untuk melihat apa yang dilakukannya
di balik layar sebelum kita mulai mengandalkannya.

Untuk mengakses fungsi platform asli, Anda perlu memperbarui penyertaan di atas:

1 #define VK_USE_PLATFORM_WIN32_KHR 2
#define GLFW_INCLUDE_VULKAN 3 #include
<GLFW/glfw3.h> 4 #define
GLFW_EXPOSE_NATIVE_WIN32 5 #include
<GLFW/glfw3native.h>

Karena permukaan jendela adalah objek Vulkan, ia dilengkapi dengan struct VkWin32SurfaceCreateInfoKHR yang
perlu diisi. Ini memiliki dua parameter penting: hwnd dan hinstance. Ini adalah pegangan ke jendela dan prosesnya.

1 VkWin32SurfaceCreateInfoKHR createInfo{}; 2 buatInfo.sType


= VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; 3 createInfo.hwnd =
glfwGetWin32Window(jendela); 4 createInfo.hinstance = GetModuleHandle(nullptr);

Fungsi glfwGetWin32Window digunakan untuk mendapatkan HWND mentah dari objek jendela
GLFW. Panggilan GetModuleHandle mengembalikan pegangan HINSTANCE dari proses saat ini.

Setelah itu, permukaan dapat dibuat dengan vkCreateWin32SurfaceKHR, yang menyertakan


parameter untuk instance, detail pembuatan permukaan, pengalokasi khusus, dan variabel untuk
pegangan permukaan yang akan disimpan. Secara teknis, ini adalah fungsi ekstensi WSI, tetapi
sangat umum digunakan bahwa pemuat Vulkan standar menyertakannya, jadi tidak seperti ekstensi
lain, Anda tidak perlu memuatnya secara eksplisit.

1 jika (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr,


&permukaan) != VK_SUCCESS)
2 { throw std::runtime_error("gagal membuat permukaan jendela!");
3}

Prosesnya serupa untuk platform lain seperti Linux, di mana vkCreateXcbSurfaceKHR menggunakan
koneksi dan jendela XCB sebagai detail pembuatan dengan X11.

Fungsi glfwCreateWindowSurface melakukan operasi ini dengan implementasi yang berbeda untuk
setiap platform. Kami sekarang akan mengintegrasikannya ke dalam program kami. Tambahkan
fungsi createSurface untuk dipanggil dari initVulkan tepat setelah pembuatan instance dan
setupDebugMessenger.

1 batal initVulkan()
2 { createInstance();

74
Machine Translated by Google

3 setupDebugMessenger();
4 buat Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7}
8
9 membatalkan createSurface() {
10
11 }

Panggilan GLFW mengambil parameter sederhana alih-alih struct yang membuat


implementasi fungsi menjadi sangat mudah:
1 void createSurface() { if
2 (glfwCreateWindowSurface(instance, window, nullptr, &surface)
!= VK_SUCCESS)
3 { throw std::runtime_error("gagal membuat permukaan jendela!");
4 }
5}

Parameternya adalah VkInstance, penunjuk jendela GLFW, pengalokasi khusus, dan


penunjuk ke variabel VkSurfaceKHR. Itu hanya melewati VkResult dari panggilan platform
yang relevan. GLFW tidak menawarkan fungsi khusus untuk menghancurkan permukaan,
tetapi itu dapat dilakukan dengan mudah melalui API asli:

1 pembersihan batal () {
2 ...
3 vkDestroySurfaceKHR(instance, permukaan, nullptr);
4 vkDestroyInstance(contoh, nullptr);
5 ...
6 }

Pastikan permukaan dihancurkan sebelum instance.

Meminta dukungan presentasi


Meskipun implementasi Vulkan dapat mendukung integrasi sistem jendela, itu tidak berarti
bahwa setiap perangkat dalam sistem mendukungnya. Oleh karena itu kita perlu memperluas
isDeviceSuitable untuk memastikan bahwa suatu perangkat dapat menampilkan gambar
ke permukaan yang kita buat. Karena presentasi adalah fitur khusus antrean, masalahnya
sebenarnya adalah tentang menemukan keluarga antrean yang mendukung penyajian ke
permukaan yang kami buat.

Sebenarnya mungkin bahwa kelompok antrean yang mendukung perintah menggambar


dan yang mendukung presentasi tidak tumpang tindih. Oleh karena itu, kita harus
mempertimbangkan bahwa mungkin ada antrean presentasi yang berbeda dengan
memodifikasi struktur QueueFamilyIndices:

75
Machine Translated by Google

1 struct QueueFamilyIndices {
2 std::opsional<uint32_t> graphicsFamily;
3 std::opsional<uint32_t> presentFamily;
4
5 bool isComplete()
6 { return graphicsFamily.has_value() &&
presentFamily.has_value();
7 }
8 };

Selanjutnya, kita akan memodifikasi fungsi findQueueFamilies untuk mencari keluarga


antrian yang memiliki kemampuan menampilkan ke permukaan jendela kita. Fungsi untuk
memeriksanya adalah vkGetPhysicalDeviceSurfaceSupportKHR, yang menggunakan
perangkat fisik, indeks keluarga antrean, dan permukaan sebagai parameter. Tambahkan
panggilan ke loop yang sama dengan VK_QUEUE_GRAPHICS_BIT:

1 VkBool32 presentSupport = false; 2


vkGetPhysicalDeviceSurfaceSupportKHR(perangkat, i, permukaan,
&presentSupport);

Kemudian cukup periksa nilai boolean dan simpan indeks antrian keluarga presentasi:

1 if (presentSupport)
2 { indexes.presentFamily = i;
3}

Perhatikan bahwa sangat mungkin bahwa ini akhirnya menjadi keluarga antrian yang sama, tetapi
di seluruh program kami akan memperlakukan mereka seolah-olah mereka adalah antrian terpisah
untuk pendekatan yang seragam. Namun demikian, Anda dapat menambahkan logika untuk secara
eksplisit memilih perangkat fisik yang mendukung gambar dan presentasi dalam antrean yang sama
untuk meningkatkan kinerja.

Membuat antrian presentasi


Satu hal yang tersisa adalah memodifikasi prosedur pembuatan perangkat logis untuk
membuat antrean presentasi dan mengambil pegangan VkQueue. Tambahkan variabel
anggota untuk pegangan:

1 VkQueue sekarangAntrian;

Selanjutnya, kita perlu memiliki beberapa struct VkDeviceQueueCreateInfo untuk membuat antrean
dari kedua keluarga. Cara yang elegan untuk melakukannya adalah dengan membuat sekumpulan
semua kelompok antrean unik yang diperlukan untuk antrean yang diperlukan:

1 #termasuk <set>
2

76
Machine Translated by Google

3 ...
4
5 indeks QueueFamilyIndices = findQueueFamilies(physicalDevice);
6
7 std::vector<VkDeviceQueueCreateInfo> queueCreateInfos; 8 std::set<uint32_t>
uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

9
10 float antrianPrioritas = 1.0f; 11 untuk
(uint32_t queueFamily : uniqueQueueFamilies) { VkDeviceQueueCreateInfo
queueCreateInfo{}; 12 antrianCreateInfo.sType =
13
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
14 queueCreateInfo.queueFamilyIndex = queueFamily;
15 queueCreateInfo.queueCount = 1; queueCreateInfo.pQueuePriorities
16 = &queuePriority; queueCreateInfos.push_back(queueCreateInfo);
17
18 }

Dan modifikasi VkDeviceCreateInfo untuk menunjuk ke vektor:

1 createInfo.queueCreateInfoCount =
static_cast<uint32_t>(queueCreateInfos.size()); 2
createInfo.pQueueCreateInfos = queueCreateInfos.data();

Jika keluarga antrean sama, maka kita hanya perlu melewati indeksnya satu kali.
Terakhir, tambahkan panggilan untuk mengambil pegangan antrean:

1 vkGetDeviceQueue(perangkat, indeks.presentFamily.value(), 0,
&Antrean sekarang);

Jika keluarga antrean sama, kedua pegangan kemungkinan besar akan memiliki nilai yang sama
sekarang. Di bab selanjutnya kita akan melihat rantai pertukaran dan bagaimana mereka
memberi kita kemampuan untuk menyajikan gambar ke permukaan.

kode C++

77
Machine Translated by Google

Tukar rantai

Vulkan tidak memiliki konsep "framebuffer default", sehingga memerlukan infrastruktur yang akan
memiliki buffer yang akan kita render sebelum kita memvisualisasikannya di layar. Infrastruktur ini
dikenal sebagai rantai pertukaran dan harus dibuat secara eksplisit di Vulkan. Rantai pertukaran pada
dasarnya adalah antrian gambar yang menunggu untuk ditampilkan ke layar. Aplikasi kita akan
memperoleh gambar seperti itu untuk digambar, lalu mengembalikannya ke antrean. Bagaimana
sebenarnya antrean bekerja dan kondisi untuk menyajikan gambar dari antrean bergantung pada
bagaimana rantai pertukaran diatur, tetapi tujuan umum dari rantai pertukaran adalah untuk
menyinkronkan penyajian gambar dengan kecepatan penyegaran layar.

Memeriksa dukungan rantai swap


Tidak semua kartu grafis mampu menampilkan gambar langsung ke layar karena berbagai alasan,
misalnya karena dirancang untuk server dan tidak memiliki keluaran tampilan. Kedua, karena
presentasi gambar sangat terkait dengan sistem jendela dan permukaan yang terkait dengan jendela,
ini sebenarnya bukan bagian dari inti Vulkan. Anda harus mengaktifkan ekstensi perangkat
VK_KHR_swapchain setelah meminta dukungannya.

Untuk itu pertama-tama kita akan memperluas fungsi isDeviceSuitable untuk memeriksa apakah
ekstensi ini didukung. Kami sebelumnya telah melihat cara membuat daftar ekstensi yang didukung
oleh VkPhysicalDevice, jadi melakukannya cukup mudah. Perhatikan bahwa file header Vulkan
menyediakan VK_KHR_SWAPCHAIN_EXTENSION_NAME makro yang bagus yang didefinisikan
sebagai VK_KHR_swapchain.
Keuntungan menggunakan makro ini adalah kompiler akan menangkap kesalahan ejaan.

Pertama, nyatakan daftar ekstensi perangkat yang diperlukan, serupa dengan daftar lapisan validasi
yang akan diaktifkan.

1 const std::vector<const char*> deviceExtensions = {


2 VK_KHR_SWAPCHAIN_EXTENSION_NAME
3 };

Selanjutnya, buat fungsi baru checkDeviceExtensionSupport yang dipanggil dari isDeviceSuitable


sebagai pemeriksaan tambahan:

78
Machine Translated by Google

1 bool isDeviceSuitable(perangkat VkPhysicalDevice) {


2 indeks QueueFamilyIndices = findQueueFamilies(device);
3
4 bool extensionsSupported = checkDeviceExtensionSupport(device);
5
6 return indexes.isComplete() && extensionsSupported;
7}
8
9 bool checkDeviceExtensionSupport(VkPhysicalDevice device) { 10
mengembalikan true;
11 }

Ubah badan fungsi untuk menghitung ekstensi dan periksa apakah semua ekstensi yang
diperlukan ada di antaranya.

1 bool checkDeviceExtensionSupport(VkPhysicalDevice device) { uint32_t


nullptr, extensionCount; 2 vkEnumerateDeviceExtensionProperties(perangkat,
3
&jumlahekstensi, nullptr);
4
5 std::vector<VkExtensionProperties>
TersediaExtensions(extensionCount);
6 vkEnumerateDeviceExtensionProperties(perangkat, nullptr,
&extensionCount, availableExtensions.data());
7
8 std::set<std::string>
requiredExtensions(deviceExtensions.begin(),
deviceExtensions.end());
9
10 untuk (const auto& extension : availableExtensions) {
11 wajibEkstensi.hapus(ekstensi.namaekstensi);
12 }
13
14 return requiredExtensions.empty();
15 }

Saya telah memilih untuk menggunakan serangkaian string di sini untuk mewakili ekstensi
wajib yang belum dikonfirmasi. Dengan begitu kita dapat dengan mudah mencentangnya
sambil menghitung urutan ekstensi yang tersedia. Tentu saja Anda juga dapat menggunakan
loop bersarang seperti di checkValidationLayerSupport. Perbedaan kinerja tidak relevan.
Sekarang jalankan kode dan verifikasi bahwa kartu grafis Anda memang mampu membuat
rantai pertukaran. Perlu dicatat bahwa ketersediaan antrean presentasi, seperti yang kita
periksa di bab sebelumnya, menyiratkan bahwa ekstensi rantai swap harus didukung. Namun,
masih bagus untuk menjelaskan hal-hal secara eksplisit, dan ekstensi harus diaktifkan secara
eksplisit.

79
Machine Translated by Google

Mengaktifkan ekstensi perangkat


Menggunakan swapchain harus mengaktifkan ekstensi VK_KHR_swapchain terlebih dahulu.
Mengaktifkan ekstensi hanya memerlukan sedikit perubahan pada struktur pembuatan
perangkat logis:

1 createInfo.enabledExtensionCount =
static_cast<uint32_t>(deviceExtensions.size());
2 createInfo.ppEnabledExtensionNames = deviceExtensions.data();

Pastikan untuk mengganti baris yang ada createInfo.enabledExtensionCount = 0; ketika


Anda melakukannya.

Menanyakan detail dukungan rantai pertukaran


Hanya memeriksa apakah rantai swap tersedia tidak cukup, karena mungkin sebenarnya
tidak kompatibel dengan permukaan jendela kita. Membuat rantai pertukaran juga melibatkan
lebih banyak pengaturan daripada pembuatan instans dan perangkat, jadi kita perlu
menanyakan beberapa detail lebih lanjut sebelum kita dapat melanjutkan.

Pada dasarnya ada tiga jenis properti yang perlu kita periksa:

• Kemampuan permukaan dasar (min/max jumlah gambar dalam rantai pertukaran, min/-
max lebar dan tinggi gambar) • Format permukaan (format piksel, ruang warna) • Mode
presentasi yang tersedia

Mirip dengan findQueueFamilies, kami akan menggunakan struct untuk meneruskan detail
ini setelah mereka ditanyai. Tiga jenis properti yang disebutkan di atas datang dalam bentuk
struct dan daftar struct berikut:

1 struct SwapChainSupportDetails { 2
Kemampuan VkSurfaceCapabilitiesKHR;
3 std::vector<VkSurfaceFormatKHR> format;
4 std::vector<VkPresentModeKHR> presentModes;
5 };

Kami sekarang akan membuat fungsi baru querySwapChainSupport yang akan mengisi
struct ini.

1 SwapChainSupportDetails kueriSwapChainSupport(perangkat VkPhysicalDevice) {

2 Detail SwapChainSupportDetails;
3
4 detail pengembalian ;
5}

80
Machine Translated by Google

Bagian ini membahas cara menanyakan struct yang menyertakan informasi ini. Arti dari
struct ini dan data apa yang dikandungnya dibahas di bagian selanjutnya.

Mari kita mulai dengan kemampuan permukaan dasar. Properti ini mudah untuk dikueri
dan dikembalikan ke struktur VkSurfaceCapabilitiesKHR tunggal.

1 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(perangkat, permukaan,
&details.capabilities);

Fungsi ini mempertimbangkan permukaan jendela VkPhysicalDevice dan VkSurfaceKHR


yang ditentukan saat menentukan kemampuan yang didukung. Semua fungsi kueri
dukungan memiliki keduanya sebagai parameter pertama karena merupakan komponen
inti dari rantai pertukaran.

Langkah selanjutnya adalah menanyakan format permukaan yang didukung. Karena ini
adalah daftar struct, ini mengikuti ritual familiar dari 2 pemanggilan fungsi:

1 format uint32_t Hitung; 2


vkGetPhysicalDeviceSurfaceFormatsKHR(perangkat, permukaan, &formatHitung,
nullptr);

3 4 if (formatCount != 0)
5 { details.formats.resize(formatCount);
6 vkGetPhysicalDeviceSurfaceFormatsKHR(perangkat, permukaan,
&formatCount, detail.formats.data());
7}

Pastikan vektor diubah ukurannya untuk menampung semua format yang tersedia. Dan
terakhir, menanyakan mode presentasi yang didukung bekerja dengan cara yang persis
sama dengan vkGetPhysicalDeviceSurfacePresentModesKHR:

1 uint32_t presentModeCount; 2
vkGetPhysicalDeviceSurfacePresentModesKHR(perangkat, permukaan,
&presentModeCount, nullptr);
3
4 if (presentModeCount != 0)
vkGetPhysicalDeviceSurfacePresentModesKHR(perangkat,
{ details.presentModes.resize(presentModeCount); 5
6 permukaan, &presentModeCount, details.presentModes.data());

7}

Semua detail ada di struct sekarang, jadi mari kita perluas isDeviceSuitable sekali lagi untuk
menggunakan fungsi ini guna memverifikasi bahwa dukungan rantai pertukaran sudah memadai.
Dukungan rantai pertukaran cukup untuk tutorial ini jika setidaknya ada satu format gambar
yang didukung dan satu mode presentasi yang didukung mengingat permukaan jendela yang kita miliki.

1 bool swapChainAdequate = salah; 2 if


(ekstensiDidukung) {

81
Machine Translated by Google

3 SwapChainSupportDetails swapChainSupport =
querySwapChainSupport(perangkat);
4 swapChainAdequate = !swapChainSupport.formats.empty() && !
swapChainSupport.presentModes.empty();
5}

Penting bahwa kami hanya mencoba meminta dukungan rantai pertukaran setelah
memverifikasi bahwa ekstensi tersedia. Baris terakhir dari fungsi berubah menjadi:

1 indeks pengembalian.isComplete () && extensionsSupported &&


swapChainAdequate;

Memilih pengaturan yang tepat untuk rantai pertukaran


Jika kondisi swapChainAdequate terpenuhi maka dukungannya pasti cukup, tetapi mungkin
masih ada banyak mode berbeda dengan berbagai optimalitas.
Kami sekarang akan menulis beberapa fungsi untuk menemukan pengaturan yang tepat
untuk rantai pertukaran terbaik. Ada tiga jenis pengaturan untuk ditentukan:

• Format permukaan (kedalaman


warna) • Mode presentasi (kondisi untuk "menukar" gambar ke layar) • Tingkat
pertukaran (resolusi gambar dalam rantai pertukaran)

Untuk masing-masing pengaturan ini, kami akan memikirkan nilai ideal yang akan kami gunakan jika
tersedia dan jika tidak, kami akan membuat beberapa logika untuk menemukan hal terbaik berikutnya.

Bentuk permukaan

Fungsi untuk pengaturan ini dimulai seperti ini. Nanti kita akan meneruskan anggota format
dari struktur SwapChainSupportDetails sebagai argumen.

1 VkSurfaceFormatKHR memilihSwapSurfaceFormat(const
std::vector<VkSurfaceFormatKHR>& availableFormats) {
2
3}

Setiap entri VkSurfaceFormatKHR berisi format dan anggota colorSpace. Anggota format
menentukan saluran dan jenis warna. Misalnya, VK_FORMAT_B8G8R8A8_SRGB berarti
bahwa kami menyimpan saluran B, G, R, dan alfa dalam urutan tersebut dengan bilangan
bulat tak bertanda 8 bit dengan total 32 bit per piksel.
Anggota colorSpace menunjukkan apakah ruang warna SRGB didukung atau tidak
menggunakan bendera VK_COLOR_SPACE_SRGB_NONLINEAR_KHR. Perhatikan bahwa
flag ini dulu disebut VK_COLORSPACE_SRGB_NONLINEAR_KHR di versi lama spesifikasi.

Untuk ruang warna kami akan menggunakan SRGB jika tersedia, karena menghasilkan
persepsi warna yang lebih akurat. Ini juga merupakan ruang warna standar

82
Machine Translated by Google

untuk gambar, seperti tekstur yang akan kita gunakan nanti. Karena itu kita juga harus
menggunakan format warna SRGB, salah satunya yang paling umum adalah
VK_FORMAT_B8G8R8A8_SRGB.

Mari telusuri daftar dan lihat apakah kombinasi pilihan tersedia:

1 untuk (const auto& availableFormat : availableFormats) {


2 jika (tersediaFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
availableFormat.colorSpace ==
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
3 { kembalikan Format yang tersedia;
4 }
5}

Jika itu juga gagal maka kami dapat mulai memeringkat format yang tersedia berdasarkan seberapa
"bagus" format tersebut, tetapi dalam banyak kasus tidak apa-apa untuk menyelesaikan dengan format
pertama yang ditentukan.

1 VkSurfaceFormatKHR memilihSwapSurfaceFormat(const
std::vector<VkSurfaceFormatKHR>& availableFormats) { for (const
2 auto& availableFormat : availableFormats) {
3 jika (tersediaFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
availableFormat.colorSpace ==
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
4 { kembalikan Format yang tersedia;
5 }
6 }
7
8 return availableFormats[0];
9}

Modus presentasi

Mode presentasi bisa dibilang merupakan pengaturan paling penting untuk rantai swap,
karena mewakili kondisi sebenarnya untuk menampilkan gambar ke layar. Ada empat
kemungkinan mode yang tersedia di Vulkan:

• VK_PRESENT_MODE_IMMEDIATE_KHR: Gambar yang dikirimkan oleh aplikasi Anda


langsung ditransfer ke layar, yang dapat menyebabkan robekan. •
VK_PRESENT_MODE_FIFO_KHR: Rantai pertukaran adalah antrean tempat tampilan
mengambil gambar dari depan antrean saat tampilan di-refresh dan program
menyisipkan gambar yang dirender di belakang antrean. Jika antrian sudah penuh
maka program harus menunggu. Ini paling mirip dengan sinkronisasi vertikal seperti
yang ditemukan di game modern. Saat tampilan disegarkan dikenal sebagai "kosong
vertikal".
• VK_PRESENT_MODE_FIFO_RELAXED_KHR: Mode ini hanya berbeda dari sebelumnya
jika aplikasi terlambat dan antrian terakhir kosong

83
Machine Translated by Google

kosong vertikal. Alih-alih menunggu kosong vertikal berikutnya, gambar langsung


ditransfer saat akhirnya tiba. Ini dapat menyebabkan robekan yang terlihat.

• VK_PRESENT_MODE_MAILBOX_KHR: Ini adalah variasi lain dari mode kedua. Alih-


alih memblokir aplikasi saat antrian penuh, gambar yang sudah antri diganti dengan
yang lebih baru.
Mode ini dapat digunakan untuk merender bingkai secepat mungkin sambil tetap
menghindari robekan, yang menghasilkan lebih sedikit masalah latensi daripada
sinkronisasi vertikal standar. Ini umumnya dikenal sebagai "triple buffering", meskipun
keberadaan tiga buffer saja tidak berarti bahwa framerate tidak terkunci.

Hanya mode VK_PRESENT_MODE_FIFO_KHR yang dijamin tersedia, jadi kita harus


menulis lagi fungsi yang mencari mode terbaik yang tersedia:

1 VkPresentModeKHR pilihSwapPresentMode(const
std::vector<VkPresentModeKHR>& availablePresentModes) { return
VK_PRESENT_MODE_FIFO_KHR;
23}

Saya pribadi berpikir bahwa VK_PRESENT_MODE_MAILBOX_KHR adalah trade-off yang sangat


bagus jika penggunaan energi tidak menjadi perhatian. Ini memungkinkan kami untuk menghindari
robekan sambil tetap mempertahankan latensi yang cukup rendah dengan merender gambar baru
yang paling mutakhir hingga vertikal kosong. Pada perangkat seluler, di mana penggunaan energi
lebih penting, Anda mungkin ingin menggunakan VK_PRESENT_MODE_FIFO_KHR sebagai gantinya.
Sekarang, mari kita lihat daftar untuk melihat apakah VK_PRESENT_MODE_MAILBOX_KHR
tersedia:

1 VkPresentModeKHR pilihSwapPresentMode(const
std::vector<VkPresentModeKHR>& availablePresentModes) { for (const
2 auto& availablePresentMode : availablePresentModes) { if (availablePresentMode
3 == VK_PRESENT_MODE_MAILBOX_KHR) {
4 mengembalikan ModePresentasi yang tersedia;
5 }
6 }
7
8 kembalikan VK_PRESENT_MODE_FIFO_KHR;
9}

Swap sejauh
Itu hanya menyisakan satu properti utama, yang akan kita tambahkan satu fungsi terakhir:

1 VkExtent2D pilihSwapExtent(const VkSurfaceCapabilitiesKHR&


kemampuan) {
2
3}

84
Machine Translated by Google

Batas swap adalah resolusi dari gambar rantai swap dan hampir selalu persis sama dengan
resolusi jendela yang kita gambar dalam piksel (lebih dari itu sebentar lagi). Kisaran resolusi yang
mungkin ditentukan dalam struktur VkSurfaceCapabilitiesKHR. Vulkan memberi tahu kita untuk
mencocokkan resolusi jendela dengan menyetel lebar dan tinggi di anggota currentExtent. Namun,
beberapa pengelola jendela mengizinkan kami untuk berbeda di sini dan ini ditunjukkan dengan
menyetel lebar dan tinggi di CurrentExtent ke nilai khusus: nilai maksimum uint32_t. Dalam hal ini
kami akan memilih resolusi yang paling cocok dengan jendela dalam batas minImageExtent dan
maxImageExtent.

Tapi kita harus menentukan resolusi dalam unit yang benar.

GLFW menggunakan dua unit saat mengukur ukuran: piksel dan koordinat layar. Misalnya, resolusi
{WIDTH, HEIGHT} yang kita tentukan sebelumnya saat membuat jendela diukur dalam koordinat
layar. Tapi Vulkan bekerja dengan piksel, jadi batas rantai pertukaran harus ditentukan dalam
piksel juga. Sayangnya, jika Anda menggunakan layar DPI tinggi (seperti layar Retina Apple),
koordinat layar tidak sesuai dengan piksel. Sebaliknya, karena kerapatan piksel yang lebih tinggi,
resolusi jendela dalam piksel akan lebih besar daripada resolusi dalam koordinat layar. Jadi jika
Vulkan tidak memperbaiki batas swap untuk kita, kita tidak bisa hanya menggunakan {WIDTH,
HEIGHT} yang asli. Sebagai gantinya, kita harus menggunakan glfwGetFramebufferSize untuk
menanyakan resolusi jendela dalam piksel sebelum mencocokkannya dengan jangkauan gambar
minimum dan maksimum.

1 #include <cstdint> // Diperlukan untuk uint32_t 2 #include


<limits> // Diperlukan untuk std::numeric_limits 3 #include <algorithm> //
Diperlukan untuk std::clamp
4
5 ...
6
7 VkExtent2D pilihSwapExtent(const VkSurfaceCapabilitiesKHR&
kapabilitas) { if
8 (capabilities.currentExtent.width !=
std::numeric_limits<uint32_t>::max()) { kembali
9 kemampuan.currentExtent; } else { int lebar, tinggi;
10 glfwGetFramebufferSize(jendela, &lebar, &tinggi);
11
12
13
14 VkExtent2D aktualExtent = {
15 static_cast<uint32_t>(lebar),
16 static_cast<uint32_t>(tinggi)
17 };
18
19 aktualExtent.width = std::clamp(actualExtent.width,
kapabilitas.minImageExtent.width, kapabilitas.maxImageExtent.width);
aktualExtent.height = std::clamp(actualExtent.height,
20

85
Machine Translated by Google

kapabilitas.minImageExtent.height,
kapabilitas.maxImageExtent.height);
21
22 kembali aktualExtent;
23 }
24 }

Fungsi penjepit digunakan di sini untuk mengikat nilai lebar dan tinggi antara luasan
minimum dan maksimum yang diperbolehkan yang didukung oleh implementasi.

Membuat rantai swap


Sekarang kita memiliki semua fungsi pembantu ini yang membantu kita dengan pilihan yang
harus kita buat saat runtime, akhirnya kita memiliki semua informasi yang diperlukan untuk
membuat rantai pertukaran yang berfungsi.

Buat fungsi createSwapChain yang dimulai dengan hasil panggilan ini dan pastikan untuk
memanggilnya dari initVulkan setelah pembuatan perangkat logis.
1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger();
4 buat Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8}
9
10 batal createSwapChain() {
11 SwapChainSupportDetails swapChainSupport =
querySwapChainSupport(Perangkat fisik);
12
13 VkSurfaceFormatKHR surfaceFormat =
pilihSwapSurfaceFormat(swapChainSupport.formats);
14 VkPresentModeKHR presentMode =
pilihSwapPresentMode(swapChainSupport.presentModes); Batas
15 VkExtent2D = pilihSwapExtent(swapChainSupport.capabilities);

16 }

Selain properti ini, kami juga harus memutuskan berapa banyak gambar yang ingin kami
miliki dalam rantai pertukaran. Implementasi menentukan jumlah minimum yang diperlukan
untuk berfungsi:

1 uint32_t imageCount = swapChainSupport.capabilities.minImageCount;

86
Machine Translated by Google

Namun, hanya berpegang pada minimum ini berarti kita terkadang harus menunggu driver
untuk menyelesaikan operasi internal sebelum kita dapat memperoleh gambar lain untuk
dirender. Oleh karena itu disarankan untuk meminta setidaknya satu gambar lebih banyak
dari jumlah minimum:

1 uint32_t imageCount = swapChainSupport.capabilities.minImageCount +


1;

Kita juga harus memastikan untuk tidak melebihi jumlah maksimum gambar saat melakukan
ini, di mana 0 adalah nilai khusus yang berarti tidak ada maksimum:

1 jika (swapChainSupport.capabilities.maxImageCount > 0 && imageCount >


swapChainSupport.capabilities.maxImageCount) { imageCount =
2 swapChainSupport.capabilities.maxImageCount;
3}

Seperti tradisi dengan objek Vulkan, membuat objek rantai pertukaran memerlukan
pengisian struktur yang besar. Itu dimulai dengan sangat akrab:

1 VkSwapchainCreateInfoKHR createInfo{}; 2
buatInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; 3
createInfo.surface = permukaan;

Setelah menentukan permukaan mana yang harus dikaitkan dengan rantai pertukaran,
detail gambar rantai pertukaran ditentukan:

1 createInfo.minImageCount = imageCount; 2
createInfo.imageFormat = surfaceFormat.format; 3
createInfo.imageColorSpace = surfaceFormat.colorSpace; 4
createInfo.imageExtent = luas; 5 createInfo.imageArrayLayers = 1; 6
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

imageArrayLayers menentukan jumlah lapisan yang terdiri dari setiap gambar.


Ini selalu 1 kecuali Anda sedang mengembangkan aplikasi 3D stereoskopik. Bidang bit
imageUsage menentukan jenis operasi apa yang akan kita gunakan untuk gambar dalam
rantai pertukaran. Dalam tutorial ini kita akan merender langsung ke mereka, yang berarti
mereka digunakan sebagai lampiran warna. Mungkin juga Anda merender gambar ke
gambar terpisah terlebih dahulu untuk melakukan operasi seperti pasca-pemrosesan.
Dalam hal ini, Anda dapat menggunakan nilai seperti VK_IMAGE_USAGE_TRANSFER_DST_BIT
dan menggunakan operasi memori untuk mentransfer gambar yang dirender ke gambar
rantai swap.

1 indeks QueueFamilyIndices = findQueueFamilies(physicalDevice); 2 uint32_t


queueFamilyIndices[] = {indices.graphicsFamily.value(),
indexs.presentFamily.value()};
3
4 if (indices.graphicsFamily != indexes.presentFamily) {

87
Machine Translated by Google

5 createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
6 createInfo.queueFamilyIndexCount = 2; createInfo.pQueueFamilyIndices =
7 queueFamilyIndices;
8 } lain {
9 createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
10 createInfo.queueFamilyIndexCount = 0; // Opsional
11 createInfo.pQueueFamilyIndices = nullptr; // Opsional
12 }

Selanjutnya, kita perlu menentukan cara menangani gambar rantai swap yang akan
digunakan di beberapa kelompok antrean. Itu akan terjadi di aplikasi kita jika keluarga antrian
grafis berbeda dari antrian presentasi. Kami akan menggambar pada gambar dalam rantai
pertukaran dari antrean grafik dan kemudian mengirimkannya ke antrean presentasi. Ada
dua cara untuk menangani gambar yang diakses dari banyak antrean:

• VK_SHARING_MODE_EXCLUSIVE: Gambar dimiliki oleh satu kelompok antrian pada


satu waktu dan kepemilikan harus ditransfer secara eksplisit sebelum digunakan
dalam kelompok antrian lain. Opsi ini menawarkan performa terbaik. •
VK_SHARING_MODE_CONCURRENT: Gambar dapat digunakan di banyak antrean
keluarga tanpa transfer kepemilikan eksplisit.

Jika keluarga antrean berbeda, maka kita akan menggunakan mode konkuren dalam tutorial
ini untuk menghindari keharusan melakukan bab kepemilikan, karena ini melibatkan
beberapa konsep yang lebih baik dijelaskan di lain waktu. Mode bersamaan mengharuskan
Anda untuk menentukan terlebih dahulu antara kepemilikan kelompok antrean mana yang
akan dibagikan menggunakan parameter queueFamilyIndexCount dan pQueueFamilyIndices.
Jika kelompok antrean grafis dan kelompok antrean presentasi adalah sama, yang akan
terjadi pada sebagian besar perangkat keras, maka kita harus tetap menggunakan mode
eksklusif, karena mode konkuren mengharuskan Anda menentukan setidaknya dua kelompok
antrean yang berbeda.

1 createInfo.preTransform =
swapChainSupport.capabilities.currentTransform;

Kita dapat menentukan bahwa transformasi tertentu harus diterapkan ke gambar dalam
rantai pertukaran jika didukung (kemampuan didukung Transformasi), seperti rotasi 90
derajat searah jarum jam atau pembalikan horizontal. Untuk menentukan bahwa Anda tidak
menginginkan transformasi apa pun, cukup tentukan transformasi saat ini.

1 buatInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;

Bidang compositeAlpha menentukan apakah saluran alfa harus digunakan untuk berbaur
dengan jendela lain di sistem jendela. Anda hampir selalu ingin mengabaikan saluran alfa,
karenanya VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR.

1 createInfo.presentMode = presentMode; 2
buatInfo.clipped = VK_TRUE;

88
Machine Translated by Google

Anggota presentMode berbicara untuk dirinya sendiri. Jika anggota yang dipotong disetel ke
VK_TRUE maka itu berarti kita tidak peduli dengan warna piksel yang dikaburkan, misalnya
karena ada jendela lain di depannya. Kecuali jika Anda benar-benar harus dapat membaca
kembali piksel ini dan mendapatkan hasil yang dapat diprediksi, Anda akan mendapatkan
performa terbaik dengan mengaktifkan kliping.

1 buatInfo.oldSwapchain = VK_NULL_HANDLE;

Itu menyisakan satu bidang terakhir, oldSwapChain. Dengan Vulkan, rantai pertukaran Anda
mungkin menjadi tidak valid atau tidak dioptimalkan saat aplikasi Anda berjalan, misalnya
karena ukuran jendela diubah. Dalam hal ini rantai swap sebenarnya perlu dibuat ulang dari
awal dan referensi ke yang lama harus ditentukan di bidang ini. Ini adalah topik kompleks
yang akan kita pelajari lebih lanjut di bab mendatang. Untuk saat ini kami berasumsi bahwa
kami hanya akan membuat satu rantai pertukaran.

Sekarang tambahkan anggota kelas untuk menyimpan objek VkSwapchainKHR:

1 VkSwapchainKHR swapChain;

Membuat rantai swap sekarang semudah memanggil vkCreateSwapchainKHR:

1 jika (vkCreateSwapchainKHR(perangkat, &createInfo, nullptr, &swapChain)


!= VK_SUCCESS)
2 { throw std::runtime_error("gagal membuat rantai swap!");
3}

Parameternya adalah perangkat logis, info pembuatan rantai pertukaran, pengalokasi khusus
opsional, dan penunjuk ke variabel untuk menyimpan pegangan. Tidak ada kejutan di sana.
Itu harus dibersihkan menggunakan vkDestroySwapchainKHR sebelum perangkat:

1 pembersihan batal ()
2 { vkDestroySwapchainKHR(perangkat, swapChain, nullptr);
3 ...
4}

Sekarang jalankan aplikasi untuk memastikan bahwa rantai swap berhasil dibuat!
Jika saat ini Anda mendapatkan kesalahan pelanggaran akses di vkCreateSwapchainKHR
atau melihat pesan seperti Gagal menemukan 'vkGetInstanceProcAddress' di lapisan
SteamOverlayVulkanLayer.dll, lihat entri FAQ tentang lapisan Steam over lay.

Coba hapus createInfo.imageExtent = extent; baris dengan lapisan validasi diaktifkan. Anda
akan melihat bahwa salah satu lapisan validasi segera menemukan kesalahan dan pesan
yang berguna dicetak:

89
Machine Translated by Google

Mengambil gambar rantai swap


Rantai pertukaran telah dibuat sekarang, jadi yang tersisa hanyalah mengambil pegangan VkImages
di dalamnya. Kami akan merujuk ini selama operasi rendering di bab selanjutnya. Tambahkan
anggota kelas untuk menyimpan pegangan:

1 std::vector<VkImage> swapChainImages;

Gambar dibuat oleh implementasi untuk rantai swap dan akan dibersihkan secara otomatis setelah
rantai swap dihancurkan, oleh karena itu kita tidak perlu menambahkan kode pembersihan apa pun.

Saya menambahkan kode untuk mengambil pegangan di akhir fungsi createSwapChain, tepat
setelah panggilan vkCreateSwapchainKHR. Mengambilnya sangat mirip dengan waktu lain di mana
kami mengambil array objek dari Vulkan. Ingatlah bahwa kami hanya menentukan jumlah gambar
minimum dalam rantai pertukaran, sehingga penerapannya diizinkan untuk membuat rantai pertukaran
dengan lebih banyak. Itu sebabnya pertama-tama kita akan mengkueri jumlah akhir gambar dengan
vkGetSwapchainImagesKHR, lalu mengubah ukuran wadah dan terakhir memanggilnya lagi untuk
mengambil pegangannya.

1 vkGetSwapchainImagesKHR(perangkat, swapChain, &imageCount, nullptr); 2


swapChainImages.resize(imageCount); 3 vkGetSwapchainImagesKHR(perangkat, swapChain,
&imageCount, swapChainImages.data());

Satu hal lagi, simpan format dan jangkauan yang telah kita pilih untuk gambar rantai swap dalam
variabel anggota. Kami akan membutuhkan mereka di bab-bab selanjutnya.

1 VkSwapchainKHR swapChain; 2
std::vector<VkImage> swapChainImages;
3 VkFormat swapChainImageFormat;
4 VkExtent2D swapChainExtent; 5

6 ...
7
8 swapChainImageFormat = surfaceFormat.format; 9
swapChainExtent = luas;

Kami sekarang memiliki satu set gambar yang dapat digambar dan dapat disajikan ke jendela. Bab
selanjutnya akan mulai membahas bagaimana kita dapat mengatur gambar sebagai target render
dan kemudian kita mulai melihat ke dalam pipa grafik yang sebenarnya dan perintah menggambar!

kode C++

90
Machine Translated by Google

Tampilan gambar

Untuk menggunakan VkImage apa pun, termasuk yang ada di rantai swap, di pipa render kita harus
membuat objek VkImageView. Tampilan gambar secara harfiah adalah tampilan ke dalam gambar.
Ini menjelaskan cara mengakses gambar dan bagian gambar mana yang akan diakses, misalnya
jika harus diperlakukan sebagai tekstur kedalaman tekstur 2D tanpa level mipmapping.

Dalam bab ini kita akan menulis fungsi createImageViews yang membuat tampilan gambar dasar
untuk setiap gambar dalam rantai pertukaran sehingga kita bisa menggunakannya sebagai target
warna nanti.

Pertama tambahkan anggota kelas untuk menyimpan tampilan gambar di:

1 std::vector<VkImageView> swapChainImageViews;

Buat fungsi createImageViews dan panggil tepat setelah pembuatan rantai pertukaran.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger(); buat
4 Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9}
10
11 membatalkan createImageViews()
{ 12
13 }

Hal pertama yang perlu kita lakukan adalah mengubah ukuran daftar agar sesuai dengan semua tampilan gambar
yang akan kita buat:

1 batal createImageViews()
2 { swapChainImageViews.resize(swapChainImages.size());

91
Machine Translated by Google

3
4}

Selanjutnya, atur loop yang mengulang semua gambar rantai swap.

1 untuk (size_t i = 0; i < swapChainImages.size(); i++) {


2
3}

Parameter untuk pembuatan tampilan gambar ditentukan dalam struktur VkImageViewCreateInfo.


Beberapa parameter pertama sangat mudah.
1 VkImageViewCreateInfo createInfo{}; 2
buatInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; 3
createInfo.image = swapChainImages[i];

Bidang viewType dan format menentukan bagaimana data gambar harus ditafsirkan.
Parameter viewType memungkinkan Anda memperlakukan gambar sebagai tekstur 1D,
tekstur 2D, tekstur 3D, dan peta kubus.
1 buatInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; 2
createInfo.format = swapChainImageFormat;

Bidang komponen memungkinkan Anda untuk memutar saluran warna di sekitarnya.


Misalnya, Anda dapat memetakan semua saluran ke saluran merah untuk tekstur
monokrom. Anda juga dapat memetakan nilai konstanta 0 dan 1 ke saluran. Dalam kasus
kami, kami akan tetap menggunakan pemetaan default.
1 createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; 2
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; 3
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; 4
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;

Bidang subresourceRange menjelaskan tujuan gambar dan bagian gambar mana yang
harus diakses. Gambar kami akan digunakan sebagai target warna tanpa level mipmapping
atau banyak lapisan.
1 createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; 2
createInfo.subresourceRange.baseMipLevel = 0; 3 createInfo.subresourceRange.levelCount
= 1; 4 createInfo.subresourceRange.baseArrayLayer = 0; 5
createInfo.subresourceRange.layerCount = 1;

Jika Anda mengerjakan aplikasi 3D stereografis, Anda akan membuat rantai pertukaran
dengan banyak lapisan. Anda kemudian dapat membuat beberapa tampilan gambar untuk
setiap gambar yang mewakili tampilan untuk mata kiri dan kanan dengan mengakses
lapisan yang berbeda.

Membuat tampilan gambar sekarang tinggal memanggil vkCreateImageView:

92
Machine Translated by Google

1 if (vkCreateImageView(device, &createInfo, nullptr,


&swapChainImageViews[i]) != VK_SUCCESS) { throw
2 std::runtime_error("gagal membuat tampilan gambar!");
3}

Tidak seperti gambar, tampilan gambar dibuat secara eksplisit oleh kami, jadi kami perlu
menambahkan loop serupa untuk menghancurkannya lagi di akhir program:

1 pembersihan batal ()
2 { untuk (auto imageView : swapChainImageViews) {
3 vkDestroyImageView(perangkat, imageView, nullptr);
4 }
5
6 ...
7}

Tampilan gambar sudah cukup untuk mulai menggunakan gambar sebagai tekstur,
tetapi belum cukup siap untuk digunakan sebagai target render. Itu membutuhkan satu
langkah tipuan lagi, yang dikenal sebagai framebuffer. Tapi pertama-tama kita harus
menyiapkan pipa grafis.
kode C++

93
Machine Translated by Google

pengantar

Selama beberapa bab berikutnya kita akan menyiapkan pipa grafis yang dikonfigurasi
untuk menggambar segitiga pertama kita. Pipa grafik adalah urutan operasi yang
mengambil simpul dan tekstur mesh Anda sampai ke piksel dalam target render.
Gambaran umum yang disederhanakan ditampilkan di bawah ini:

94
Machine Translated by Google

Assembler input mengumpulkan data vertex mentah dari buffer yang Anda tentukan dan
mungkin juga menggunakan buffer indeks untuk mengulang elemen tertentu tanpa harus
menduplikasi data vertex itu sendiri.

Vertex shader dijalankan untuk setiap vertex dan umumnya menerapkan transformasi

95
Machine Translated by Google

untuk mengubah posisi verteks dari ruang model ke ruang layar. Itu juga melewati data per simpul
ke bawah pipa.

Shader tessellation memungkinkan Anda untuk membagi geometri berdasarkan aturan tertentu
untuk meningkatkan kualitas mesh. Ini sering digunakan untuk membuat permukaan seperti
dinding bata dan tangga terlihat kurang rata saat berada di dekatnya.

Shader geometri dijalankan pada setiap primitif (segitiga, garis, titik) dan dapat membuangnya
atau menampilkan lebih banyak primitif daripada yang masuk. Ini mirip dengan shader tessel
lation, tetapi jauh lebih fleksibel. Namun, itu tidak banyak digunakan dalam aplikasi saat ini karena
kinerjanya tidak begitu baik pada kebanyakan kartu grafis kecuali untuk GPU terintegrasi Intel.

Tahap rasterisasi mendiskritisasi primitif menjadi fragmen. Ini adalah elemen piksel yang mereka
isi di framebuffer. Fragmen apa pun yang berada di luar layar akan dibuang dan atribut yang
dikeluarkan oleh vertex shader diinterpolasi ke seluruh fragmen, seperti yang ditunjukkan pada
gambar. Biasanya fragmen yang berada di belakang fragmen primitif lainnya juga dibuang di sini
karena pengujian mendalam.

Shader fragmen dipanggil untuk setiap fragmen yang bertahan dan menentukan buffer bingkai
mana yang menjadi tujuan penulisan fragmen dan dengan nilai warna dan kedalaman yang mana.
Hal ini dapat dilakukan dengan menggunakan data interpolasi dari vertex shader, yang dapat
mencakup hal-hal seperti koordinat tekstur dan normal untuk pencahayaan.

Tahap pencampuran warna menerapkan operasi untuk mencampur fragmen berbeda yang
dipetakan ke piksel yang sama di framebuffer. Fragmen dapat dengan mudah menimpa satu
sama lain, dijumlahkan atau dicampur berdasarkan transparansi.

Tahapan dengan warna hijau dikenal sebagai tahapan fungsi tetap . Tahapan ini memungkinkan
Anda mengubah operasinya menggunakan parameter, tetapi cara kerjanya sudah ditentukan
sebelumnya.

Tahapan dengan warna oranye di sisi lain dapat diprogram, yang berarti Anda dapat mengunggah
kode Anda sendiri ke kartu grafis untuk menerapkan operasi yang Anda inginkan. Ini
memungkinkan Anda untuk menggunakan shader fragmen, misalnya, untuk mengimplementasikan
apa pun mulai dari tekstur dan pencahayaan hingga pelacak sinar. Program-program ini berjalan
di banyak inti GPU secara bersamaan untuk memproses banyak objek, seperti simpul dan
fragmen secara paralel.

Jika Anda telah menggunakan API lama seperti OpenGL dan Direct3D sebelumnya, maka Anda
akan terbiasa untuk dapat mengubah pengaturan saluran apa pun sesuka hati dengan panggilan
seperti glBlendFunc dan OMSetBlendState. Pipa grafis di Vulkan hampir sepenuhnya tidak dapat
diubah, jadi Anda harus membuat ulang pipa dari awal jika Anda ingin mengubah shader, mengikat
framebuffer yang berbeda, atau mengubah fungsi campuran. Sisi negatifnya adalah Anda harus
membuat sejumlah saluran pipa yang mewakili semua kombinasi status berbeda yang ingin Anda
gunakan dalam operasi rendering.
Namun, karena semua operasi yang akan Anda lakukan di dalam pipeline telah diketahui
sebelumnya, pengemudi dapat mengoptimalkannya dengan jauh lebih baik.

96
Machine Translated by Google

Beberapa tahapan yang dapat diprogram bersifat opsional berdasarkan apa yang ingin Anda lakukan.
Misalnya, tahapan teselasi dan geometri dapat dinonaktifkan jika Anda hanya menggambar geometri
sederhana. Jika Anda hanya tertarik pada nilai kedalaman maka Anda dapat menonaktifkan tahap
shader fragmen, yang berguna untuk pembuatan peta bayangan.

Di bab berikutnya, pertama-tama kita akan membuat dua tahapan yang dapat diprogram yang
diperlukan untuk meletakkan segitiga di layar: shader verteks dan shader fragmen. Konfigurasi fungsi
tetap seperti blending mode, viewport, rasterisasi akan diatur di bab setelahnya. Bagian terakhir dari
pengaturan pipa grafis di Vulkan melibatkan spesifikasi framebuffer input dan output.

Buat fungsi createGraphicsPipeline yang dipanggil tepat setelah createImageViews di initVulkan.


Kami akan mengerjakan fungsi ini di seluruh bab berikut.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger(); buat
4 Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9 buatGraphicsPipeline();
10 }
11
12 ...
13
14 batal createGraphicsPipeline() {
15
16 }

kode C++

97
Machine Translated by Google

Modul shader

Tidak seperti API sebelumnya, kode shader di Vulkan harus ditentukan dalam format
bytecode sebagai kebalikan dari sintaksis yang dapat dibaca manusia seperti GLSL dan
HLSL. Format kode byte ini disebut SPIR-V dan dirancang untuk digunakan dengan Vulkan
dan OpenCL (keduanya Khronos API). Ini adalah format yang dapat digunakan untuk
menulis grafis dan menghitung shader, tetapi kami akan fokus pada shader yang digunakan
dalam pipeline grafis Vulkan dalam tutorial ini.

Keuntungan menggunakan format bytecode adalah kompiler yang ditulis oleh vendor GPU
untuk mengubah kode shader menjadi kode asli jauh lebih sederhana. Masa lalu telah
menunjukkan bahwa dengan sintaks yang dapat dibaca manusia seperti GLSL, beberapa
vendor GPU agak fleksibel dengan interpretasi standar mereka. Jika Anda kebetulan menulis
shader non-sepele dengan GPU dari salah satu vendor ini, maka Anda berisiko driver vendor
lain menolak kode Anda karena kesalahan sintaks, atau lebih buruk lagi, shader Anda
berjalan berbeda karena bug kompiler. Dengan format bytecode langsung seperti SPIR-V
yang diharapkan dapat dihindari.

Namun, bukan berarti kita perlu menulis bytecode ini dengan tangan.
Khronos telah merilis kompiler independen vendor mereka sendiri yang mengkompilasi
GLSL ke SPIR-V. Kompiler ini dirancang untuk memverifikasi bahwa kode shader Anda
sepenuhnya memenuhi standar dan menghasilkan satu biner SPIR-V yang dapat Anda
kirimkan dengan program Anda. Anda juga dapat menyertakan kompiler ini sebagai pustaka
untuk menghasilkan SPIR-V saat runtime, tetapi kami tidak akan melakukannya dalam
tutorial ini. Meskipun kita dapat menggunakan kompiler ini secara langsung melalui
glslangValidator.exe, sebagai gantinya kita akan menggunakan glslc.exe oleh Google.
Keuntungan glslc adalah menggunakan format parameter yang sama dengan kompiler
terkenal seperti GCC dan Clang dan menyertakan beberapa fungsi tambahan seperti include.
Keduanya sudah termasuk dalam Vulkan SDK, jadi Anda tidak perlu mengunduh tambahan apa pun.

GLSL adalah bahasa shading dengan sintaks C-style. Program yang ditulis di dalamnya
memiliki fungsi utama yang dipanggil untuk setiap objek. Alih-alih menggunakan parameter
untuk masukan dan nilai kembalian sebagai keluaran, GLSL menggunakan variabel global
untuk menangani masukan dan keluaran. Bahasa ini menyertakan banyak fitur untuk
membantu pemrograman grafis, seperti vektor bawaan dan primitif matriks. Fungsi untuk
operasi seperti perkalian silang, perkalian matriks-vektor, dan refleksi di sekitar vektor
disertakan. Jenis vektor disebut vec dengan angka yang menunjukkan jumlah

98
Machine Translated by Google

elemen. Misalnya, posisi 3D akan disimpan dalam vec3. Dimungkinkan untuk mengakses
komponen tunggal melalui anggota seperti .x, tetapi juga memungkinkan untuk membuat vektor
baru dari banyak komponen secara bersamaan. Misalnya, ekspresi vec3(1.0, 2.0, 3.0).xy akan
menghasilkan vec2. Pembangun vektor juga dapat mengambil kombinasi objek vektor dan nilai
skalar.
Misalnya, vec3 dapat dibangun dengan vec3(vec2(1.0, 2.0), 3.0).

Seperti yang disebutkan di bab sebelumnya, kita perlu menulis shader vertex dan shader
fragmen untuk mendapatkan segitiga di layar. Dua bagian berikutnya akan mencakup kode
GLSL dari masing-masingnya dan setelah itu saya akan menunjukkan kepada Anda bagaimana
menghasilkan dua binari SPIR-V dan memuatnya ke dalam program.

Shader vertex
Vertex shader memproses setiap vertex yang masuk. Dibutuhkan atributnya, seperti koordinat
posisi dunia, warna, normal dan tekstur sebagai masukan. Keluarannya adalah posisi akhir
dalam koordinat klip dan atribut yang perlu diteruskan ke shader fragmen, seperti koordinat
warna dan tekstur. Nilai-nilai ini kemudian akan diinterpolasi pada fragmen oleh rasterizer untuk
menghasilkan gradien yang mulus.

Koordinat klip adalah vektor empat dimensi dari vertex shader yang kemudian diubah menjadi
koordinat perangkat yang dinormalisasi dengan membagi seluruh vektor dengan komponen
terakhirnya. Koordinat perangkat yang dinormalisasi ini adalah koordinat homo gen yang
memetakan framebuffer ke sistem koordinat [-1, 1] oleh [-1, 1] yang terlihat seperti berikut:

Anda seharusnya sudah terbiasa dengan ini jika Anda pernah mencoba grafik komputer
sebelumnya. Jika Anda pernah menggunakan OpenGL sebelumnya, Anda akan melihat bahwa
tanda koordinat Y sekarang terbalik. Koordinat Z sekarang menggunakan rentang yang sama
seperti di Direct3D, dari 0 hingga 1.

Untuk segitiga pertama kami, kami tidak akan menerapkan transformasi apa pun, kami hanya akan menentukan

99
Machine Translated by Google

posisi ketiga simpul secara langsung sebagai koordinat perangkat yang dinormalisasi untuk
membuat bentuk berikut:

Kita dapat langsung menampilkan koordinat perangkat yang dinormalisasi dengan


mengeluarkannya sebagai koordinat klip dari vertex shader dengan komponen terakhir disetel
ke 1. Dengan begitu pembagian untuk mengubah koordinat klip menjadi koordinat perangkat
yang dinormalisasi tidak akan mengubah apa pun.

Biasanya koordinat ini akan disimpan dalam buffer vertex, tetapi membuat buffer vertex di
Vulkan dan mengisinya dengan data tidaklah mudah. Oleh karena itu saya telah memutuskan
untuk menunda sampai setelah kami puas melihat segitiga muncul di layar. Sementara itu,
kita akan melakukan sesuatu yang sedikit tidak ortodoks: sertakan koordinat langsung di
dalam vertex shader. Kodenya terlihat seperti ini:

1 #versi 450
2

3 posisi vec2[3] = vec2[]( vec2(0,0,


vec2(-0,5,
-0,5),
0,5)4 vec2(0,5, 0,5),
5
6
7 );

8 9 batal utama() {
10 gl_Posisi = vec4(posisi[gl_VertexIndex], 0.0, 1.0);
11 }

Fungsi utama dipanggil untuk setiap simpul. Variabel gl_VertexIndex bawaan berisi indeks
dari simpul saat ini. Ini biasanya merupakan indeks ke dalam buffer vertex, tetapi dalam
kasus kami ini akan menjadi indeks ke dalam array hardcoded dari data vertex. Posisi setiap
simpul diakses dari array konstanta di dalam shader dan digabungkan dengan komponen
dummy z dan w untuk menghasilkan posisi dalam koordinat klip. Variabel bawaan gl_Position
berfungsi sebagai

100
Machine Translated by Google

keluaran.

Pengubah fragmen

Segitiga yang dibentuk oleh posisi dari vertex shader mengisi area di layar dengan fragmen.
Shader fragmen dipanggil pada fragmen ini untuk menghasilkan warna dan kedalaman
untuk framebuffer (atau framebuffer).
Shader fragmen sederhana yang menghasilkan warna merah untuk seluruh segitiga terlihat
seperti ini:

1 #versi 450
2

3 tata letak(lokasi = 0) out vec4 outColor; 4

5 batal main()
6 { outColor = vec4(1.0, 0.0, 0.0, 1.0);
7}

Fungsi main dipanggil untuk setiap fragmen seperti fungsi utama shader vertex dipanggil
untuk setiap vertex. Warna dalam GLSL adalah vektor 4 komponen dengan saluran R, G, B,
dan alfa dalam rentang [0, 1]. Tidak seperti gl_Position di vertex shader, tidak ada variabel
bawaan untuk menampilkan warna untuk fragmen saat ini. Anda harus menentukan variabel
keluaran Anda sendiri untuk setiap framebuffer di mana pengubah layout(location = 0)
menentukan indeks buffer bingkai. Warna merah ditulis ke variabel outColor ini yang
ditautkan ke framebuffer pertama (dan satu-satunya) pada indeks 0.

Warna per-simpul

Membuat seluruh segitiga menjadi merah tidak terlalu menarik, bukankah hal seperti berikut
ini terlihat jauh lebih bagus?

101
Machine Translated by Google

Kami harus membuat beberapa perubahan pada kedua shader untuk melakukannya.
Pertama, kita perlu menentukan warna yang berbeda untuk masing-masing dari tiga simpul.
Vertex shader sekarang harus menyertakan array dengan warna seperti halnya untuk posisi:
1 vec3 warna[3] = vec3[]( vec3(1.0,
2 0.0, 0.0), vec3(0.0, 1.0,
3 0.0), vec3(0.0, 0.0, 1.0)
4
5 );

Sekarang kita hanya perlu meneruskan warna per-vertex ini ke shader fragmen sehingga
dapat menampilkan nilai interpolasinya ke framebuffer. Tambahkan output untuk warna ke
vertex shader dan tulis di fungsi utama:

1 tata letak(lokasi = 0) keluar vec3 fragColor;


2
3 batal utama() {
4 gl_Posisi = vec4(posisi[gl_VertexIndex], 0.0, 1.0); fragWarna =
5 warna[gl_VertexIndex];
6}

Selanjutnya, kita perlu menambahkan input yang cocok di shader fragmen:

1 tata letak (lokasi = 0) di vec3 fragColor; 2

3 batal utama() {
4 outColor = vec4(fragColor, 1.0);
5}

Variabel input tidak harus menggunakan nama yang sama, mereka akan dihubungkan
bersama menggunakan indeks yang ditentukan oleh arahan lokasi. Fungsi utama telah
dimodifikasi untuk menampilkan warna bersama dengan nilai alfa.
Seperti yang ditunjukkan pada gambar di atas, nilai untuk fragColor akan diinterpolasi secara
otomatis untuk fragmen di antara tiga simpul, menghasilkan gradien yang mulus.

Mengkompilasi shader
Buat direktori bernama shader di direktori akar proyek Anda dan simpan vertex shader dalam
file bernama shader.vert dan shader fragmen dalam file bernama shader.frag di direktori itu.
Shader GLSL tidak memiliki ekstensi resmi, tetapi keduanya biasanya digunakan untuk
membedakannya.
Isi shader.vert harus:

1 #versi 450
2
3 tata letak(lokasi = 0) keluar vec3 fragColor;

102
Machine Translated by Google

4
5 posisi vec2[3] = vec2[]( vec2(0,0, -0,5),
6 vec2(0,5, 0,5), vec2(-0,5, 0,5)
7
8
9 );
10
11 vec3 warna[3] = vec3[]( vec3(1.0,
12 0.0, 0.0), vec3(0.0, 1.0, 0.0),
13 vec3(0.0, 0.0, 1.0)
14
15 );
16
17 batal utama() {
18 gl_Posisi = vec4(posisi[gl_VertexIndex], 0.0, 1.0); fragWarna =
19 warna[gl_VertexIndex];
20 }

Dan isi shader.frag seharusnya:


1 #versi 450
2

3 tata letak(lokasi = 0) di vec3 fragColor;


4
5 tata letak(lokasi = 0) out vec4 outColor;
6
7 batal utama() {
8 outColor = vec4(fragColor, 1.0);
9}

Kita sekarang akan mengkompilasi ini menjadi bytecode SPIR-V menggunakan program
glslc.
Windows

Buat file compile.bat dengan konten berikut:

1 C:/VulkanSDK/xxxx/Bin32/glslc.exe shader.vert -o vert.spv 2 C:/VulkanSDK/xxxx/Bin32/


glslc.exe shader.frag -o frag.spv 3 jeda

Ganti jalur ke glslc.exe dengan jalur tempat Anda menginstal Vulkan SDK. Klik dua
kali file tersebut untuk menjalankannya.
Linux

Buat file compile.sh dengan konten berikut:

1 /home/user/VulkanSDK/xxxx/x86_64/bin/glslc shader.vert -o vert.spv 2 /home/user/VulkanSDK/xxxx/


x86_64/bin/glslc shader.frag -o frag.spv

103
Machine Translated by Google

Ganti jalur ke glslc dengan jalur tempat Anda menginstal Vulkan SDK. Jadikan skrip dapat dieksekusi
dengan chmod +x compile.sh dan jalankan.

Akhir dari instruksi khusus platform

Kedua perintah ini memberi tahu kompiler untuk membaca file sumber GLSL dan menampilkan file
bytecode SPIR-V menggunakan flag -o (output).

Jika shader Anda mengandung kesalahan sintaks maka kompiler akan memberi tahu Anda
nomor baris dan masalahnya, seperti yang Anda harapkan. Coba tinggalkan titik koma
misalnya dan jalankan skrip kompilasi lagi. Coba juga jalankan kompiler tanpa argumen apa
pun untuk melihat jenis flag apa yang didukungnya. Itu dapat, misalnya, juga mengeluarkan
bytecode ke dalam format yang dapat dibaca manusia sehingga Anda dapat melihat dengan
tepat apa yang dilakukan shader Anda dan pengoptimalan apa pun yang telah diterapkan pada tahap ini.

Mengompilasi shader pada baris perintah adalah salah satu opsi yang paling mudah dan ini adalah
opsi yang akan kita gunakan dalam tutorial ini, tetapi juga memungkinkan untuk mengompilasi shader
langsung dari kode Anda sendiri. Vulkan SDK menyertakan lib shaderc, yang merupakan pustaka
untuk mengompilasi kode GLSL ke SPIR-V dari dalam program Anda.

Memuat shader
Sekarang kita memiliki cara untuk memproduksi shader SPIR-V, saatnya untuk memuatnya
ke dalam program kita untuk menghubungkannya ke pipa grafis di beberapa titik. Kami
pertama-tama akan menulis fungsi pembantu sederhana untuk memuat data biner dari file.
1 #termasuk <fstream>
2
3 ...
4
5 statis std::vector<char> readFile(const std::string& nama file) {
6 std::ifstream file(nama file, std::ios::ate | std::ios::binary);
7
8 if (!file.is_open()) { throw
9 std::runtime_error("gagal membuka file!");
10 }
11 }

Fungsi readFile akan membaca semua byte dari file yang ditentukan dan mengembalikannya dalam
array byte yang dikelola oleh std::vector. Kami mulai dengan membuka file dengan dua tanda:

• makan: Mulai membaca di akhir file • biner: Membaca


file sebagai file biner (hindari transformasi teks)

Keuntungan mulai membaca di akhir file adalah kita dapat menggunakan posisi baca untuk
menentukan ukuran file dan mengalokasikan buffer:

104
Machine Translated by Google

1 size_t fileUkuran = (size_t) file.tellg(); 2 std::vector<char>


buffer(Ukuranfile);

Setelah itu, kita dapat mencari kembali ke awal file dan membaca semua byte sekaligus:

1 file.seekg(0); 2
file.baca(buffer.data(), ukuranfile);

Dan terakhir tutup file dan kembalikan byte:

1 file.close();
2
3 buffer kembali ;

Kami sekarang akan memanggil fungsi ini dari createGraphicsPipeline untuk memuat bytecode
dari dua shader:

1 batal buatGraphicsPipeline() { 2
otomatis vertShaderCode = readFile ("shaders/vert.spv"); otomatis
3 fragShaderCode = readFile ("shaders/frag.spv");
4}

Pastikan shader dimuat dengan benar dengan mencetak ukuran buffer dan memeriksa
apakah cocok dengan ukuran file sebenarnya dalam byte. Perhatikan bahwa kode tidak perlu
dihentikan null karena ini adalah kode biner dan nanti kita akan menjelaskan ukurannya
secara eksplisit.

Membuat modul shader


Sebelum kita dapat meneruskan kode ke pipeline, kita harus membungkusnya dalam objek
VkShaderModule. Mari buat fungsi pembantu createShaderModule untuk melakukannya.

1 VkShaderModule createShaderModule(const std::vector<char>& kode) {


2
3}

Fungsi akan mengambil buffer dengan bytecode sebagai parameter dan membuat
VkShaderModule darinya.

Membuat modul shader itu sederhana, kita hanya perlu menentukan pointer ke buffer dengan
bytecode dan panjangnya. Informasi ini ditentukan dalam struktur VkShaderModuleCreateInfo.
Satu tangkapan adalah bahwa ukuran bytecode ditentukan dalam byte, tetapi pointer bytecode
adalah pointer uint32_t daripada pointer char. Oleh karena itu kita perlu mentransmisikan
pointer dengan reinterpret_cast seperti yang ditunjukkan di bawah ini. Saat Anda melakukan
gips seperti ini, Anda juga perlu memastikan bahwa data memenuhi persyaratan penyelarasan
uint32_t.

105
Machine Translated by Google

Beruntung bagi kami, data disimpan dalam std::vector di mana pengalokasi default sudah
memastikan bahwa data memenuhi persyaratan penyelarasan kasus terburuk.
1 VkShaderModuleCreateInfo createInfo{}; 2
buatInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; 3
createInfo.codeSize = kode.ukuran(); 4 createInfo.pCode = reinterpret_cast<const
uint32_t*>(code.data());

VkShaderModule kemudian dapat dibuat dengan panggilan ke vkCreateShaderModule:

1 VkShaderModule shaderModule; 2 if
(vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) !=
VK_SUCCESS) { throw std::runtime_error("gagal membuat modul
3 shader!");
4}

Parameternya sama dengan yang ada di fungsi pembuatan objek sebelumnya: perangkat
logis, penunjuk untuk membuat struktur info, penunjuk opsional ke pengalokasi khusus dan
menangani variabel keluaran. Buffer dengan kode dapat segera dibebaskan setelah
membuat modul shader. Jangan lupa mengembalikan modul shader yang dibuat:

1 mengembalikan shaderModule;

Modul shader hanyalah pembungkus tipis di sekitar bytecode shader yang sebelumnya
telah kami muat dari file dan fungsi yang ditentukan di dalamnya. Kompilasi dan penautan
bytecode SPIR-V ke kode mesin untuk dieksekusi oleh GPU tidak akan terjadi hingga alur
grafik dibuat. Itu berarti bahwa kita diizinkan untuk menghancurkan modul shader lagi
segera setelah pembuatan pipa selesai, itulah sebabnya kita akan menjadikannya variabel
lokal dalam fungsi createGraphicsPipeline alih-alih anggota kelas:

1 batal buatGraphicsPipeline() {
2 otomatis vertShaderCode = readFile ("shaders/vert.spv"); otomatis
3 fragShaderCode = readFile ("shaders/frag.spv");
4
5 VkShaderModule hostShaderModule =
createShaderModule(hostShaderCode);
6 VkShaderModule fragShaderModule =
createShaderModule(fragShaderCode);

Pembersihan kemudian akan terjadi di akhir fungsi dengan menambahkan dua panggilan
ke vkDestroyShaderModule. Semua sisa kode di bab ini akan disisipkan sebelum baris ini.

1 ...
2 vkDestroyShaderModule(perangkat, fragShaderModule, nullptr);
3 vkDestroyShaderModule(perangkat, vertShaderModule, nullptr);
4}

106
Machine Translated by Google

Pembuatan panggung shader


Untuk benar-benar menggunakan shader, kita perlu menetapkannya ke tahapan pipeline tertentu melalui
struktur VkPipelineShaderStageCreateInfo sebagai bagian dari proses pembuatan pipeline yang
sebenarnya.

Kita akan mulai dengan mengisi struktur vertex shader, sekali lagi di fungsi createGraphicsPipeline.

1 VkPipelineShaderStageCreateInfo hostShaderStageInfo{}; 2
hostShaderStageInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3 vertShaderStageInfo.tahap = VK_SHADER_STAGE_VERTEX_BIT;

Langkah pertama, selain anggota sType wajib, adalah memberi tahu Vulkan di tahap pipeline mana
shader akan digunakan. Ada nilai enum untuk setiap tahapan yang dapat diprogram yang dijelaskan di
bab sebelumnya.

1 hostShaderStageInfo.module = hostShaderModule; 2
hostShaderStageInfo.pName = "utama";

Dua anggota berikutnya menentukan modul shader yang berisi kode, dan fungsi yang akan dipanggil,
yang dikenal sebagai titik masuk . Artinya, menggabungkan beberapa shader fragmen menjadi satu
modul shader dan menggunakan titik masuk yang berbeda untuk membedakan perilakunya dapat
dilakukan. Namun, dalam hal ini kami akan tetap menggunakan main standar.

Ada satu anggota (opsional) lagi, pSpecializationInfo, yang tidak akan kita gunakan di sini, tetapi perlu
didiskusikan. Ini memungkinkan Anda menentukan nilai untuk konstanta shader. Anda dapat
menggunakan modul shader tunggal di mana perilakunya dapat dikonfigurasi pada pembuatan pipa
dengan menentukan nilai yang berbeda untuk konstanta yang digunakan di dalamnya. Ini lebih efisien
daripada mengonfigurasi shader menggunakan variabel pada waktu render, karena kompiler dapat
melakukan pengoptimalan seperti menghilangkan pernyataan if yang bergantung pada nilai ini. Jika
Anda tidak memiliki konstanta seperti itu, maka Anda dapat menyetel anggota ke nullptr, yang dilakukan
inisialisasi struct kami secara otomatis.

Memodifikasi struktur agar sesuai dengan shader fragmen itu mudah:

1 VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; 2
fragShaderStageInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; 3
fragShaderStageInfo.tahap = VK_SHADER_STAGE_FRAGMENT_BIT; 4
fragShaderStageInfo.module = fragShaderModule; 5 fragShaderStageInfo.pName =
"utama";

Selesaikan dengan mendefinisikan array yang berisi dua struct ini, yang nantinya akan kita gunakan
untuk mereferensikannya dalam langkah pembuatan pipeline yang sebenarnya.

107
Machine Translated by Google

1 VkPipelineShaderStageCreateInfo shaderStages[] =
{vertShaderStageInfo, fragShaderStageInfo};

Itu saja yang ada untuk menggambarkan tahapan pipa yang dapat diprogram. Di bab
selanjutnya kita akan melihat tahapan fungsi tetap.

Kode C++ / Vertex shader / Fragment shader

108
Machine Translated by Google

Fungsi tetap

API grafis yang lebih lama menyediakan status default untuk sebagian besar tahapan pipa
grafis. Di Vulkan Anda harus eksplisit tentang segala hal, mulai dari ukuran area pandang
hingga fungsi pencampuran warna. Dalam bab ini kita akan mengisi semua struktur untuk
mengonfigurasi operasi fungsi tetap ini.

Masukan simpul
Struktur VkPipelineVertexInputStateCreateInfo menjelaskan format data vertex yang akan
diteruskan ke shader vertex. Ini menggambarkan ini kira-kira dalam dua cara:

• Bindings: spasi antar data dan apakah data per-vertex atau


per-instance (lihat instans)
• Deskripsi atribut: jenis atribut yang diteruskan ke vertex shader,
yang mengikat untuk memuatnya dari dan di offset mana

Karena kita sulit mengkodekan data vertex secara langsung di shader vertex, kita akan mengisi
struktur ini untuk menentukan bahwa tidak ada data vertex yang akan dimuat untuk saat ini.
Kita akan membahasnya kembali di bab buffer vertex.

1 VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; 2
vertexInputInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3 vertexInputInfo.vertexBindingDescriptionCount = 0; 4
vertexInputInfo.pVertexBindingDescriptions = nullptr; // Opsional 5
vertexInputInfo.vertexAttributeDescriptionCount = 0; 6 vertexInputInfo.pVertexAttributeDescriptions
= nullptr; // Opsional

Anggota pVertexBindingDescriptions dan pVertexAttributeDescriptions menunjuk ke array struct


yang menjelaskan detail yang disebutkan di atas untuk memuat data vertex. Tambahkan
struktur ini ke fungsi createGraphicsPipeline tepat setelah larik shaderStages.

109
Machine Translated by Google

Majelis masukan
Struktur VkPipelineInputAssemblyStateCreateInfo menjelaskan dua hal: jenis geometri apa yang
akan diambil dari simpul dan jika restart primitif harus diaktifkan. Yang pertama ditentukan dalam
anggota topologi dan dapat memiliki nilai seperti:

• VK_PRIMITIVE_TOPOLOGY_POINT_LIST: titik dari setiap simpul •


VK_PRIMITIVE_TOPOLOGY_LINE_LIST: garis dari setiap 2 simpul tanpa
penggunaan kembali

• VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: simpul akhir dari setiap baris digunakan sebagai


simpul awal untuk baris berikutnya
• VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: segitiga dari setiap 3 simpul
tanpa menggunakan kembali

• VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: simpul kedua dan ketiga dari setiap


segitiga digunakan sebagai dua simpul pertama dari segitiga berikutnya

Biasanya, simpul dimuat dari buffer simpul dengan indeks secara berurutan, tetapi dengan buffer
elemen Anda dapat menentukan indeks untuk digunakan sendiri.
Ini memungkinkan Anda melakukan pengoptimalan seperti menggunakan kembali simpul. Jika
Anda menyetel anggota primitifRestartEnable ke VK_TRUE, maka dimungkinkan untuk memisahkan
garis dan segitiga dalam mode topologi _STRIP dengan menggunakan indeks khusus 0xFFFF atau
0xFFFFFFFF.

Kami bermaksud untuk menggambar segitiga sepanjang tutorial ini, jadi kami akan tetap berpegang
pada data berikut untuk strukturnya:

1 VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; 2
masukanAssembly.sType =
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3 masukanAssembly.topologi = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; 4
inputAssembly.primitiveRestartEnable = VK_FALSE;

Area pandang dan gunting


Area pandang pada dasarnya menggambarkan wilayah framebuffer tempat output akan dirender.
Ini hampir selalu (0, 0) ke (lebar, tinggi) dan dalam tutorial ini juga akan demikian.

1 VkViewport viewport{}; 2 area


pandang.x = 0,0f; 3 viewport.y =
0,0f; 4 viewport.width = (float)
swapChainExtent.width; 5 viewport.height = (float)
swapChainExtent.height; 6 viewport.minDepth = 0,0f; 7 viewport.maxDepth
= 1,0f;

110
Machine Translated by Google

Ingatlah bahwa ukuran rantai pertukaran dan gambarnya mungkin berbeda dari WIDTH dan HEIGHT
jendela. Gambar rantai swap akan digunakan sebagai framebuffer nanti, jadi kita harus tetap pada
ukurannya.

Nilai minDepth dan maxDepth menentukan rentang nilai kedalaman yang akan digunakan untuk
framebuffer. Nilai ini harus berada dalam kisaran [0.0f, 1.0f], tetapi minDepth mungkin lebih tinggi
dari maxDepth. Jika Anda tidak melakukan sesuatu yang istimewa, Anda harus tetap berpegang
pada nilai standar 0.0f dan 1.0f.

Sementara area pandang menentukan transformasi dari gambar ke framebuffer, persegi panjang
gunting menentukan di wilayah mana piksel sebenarnya akan disimpan. Setiap piksel di luar persegi
panjang gunting akan dibuang oleh rasterizer. Mereka berfungsi seperti filter daripada transformasi.
Perbedaannya digambarkan di bawah ini. Perhatikan bahwa persegi panjang gunting kiri hanyalah
salah satu dari banyak kemungkinan yang akan menghasilkan gambar tersebut, asalkan ukurannya
lebih besar dari viewport.

Dalam tutorial ini kami hanya ingin menggambar ke seluruh framebuffer, jadi kami akan menentukan
persegi panjang gunting yang menutupi seluruhnya:

1 gunting VkRect2D{}; 2
gunting.offset = {0, 0}; 3 gunting.luas
= swapChainExtent;

Sekarang area pandang dan persegi panjang gunting ini perlu digabungkan menjadi status area
pandang menggunakan struct VkPipelineViewportStateCreateInfo. Dimungkinkan untuk menggunakan
beberapa area pandang dan persegi panjang gunting pada beberapa kartu grafis, jadi anggotanya
mereferensikan lariknya. Menggunakan beberapa memerlukan pengaktifan fitur GPU (lihat
pembuatan perangkat logis).

1 VkPipelineViewportStateCreateInfo viewportState{}; 2 viewportState.sType


= VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

111
Machine Translated by Google

3 viewportState.viewportCount = 1; 4
viewportState.pViewports = &viewport; 5
viewportState.scissorCount = 1; 6
viewportState.pScissors = &gunting;

Rasterisasi
Rasterizer mengambil geometri yang dibentuk oleh simpul dari vertex shader dan
mengubahnya menjadi fragmen untuk diwarnai oleh shader fragmen. Itu juga melakukan
pengujian kedalaman, pemusnahan wajah dan uji gunting, dan itu dapat dikonfigurasi untuk
menghasilkan fragmen yang mengisi seluruh poligon atau hanya bagian tepinya (render
bingkai gambar). Semua ini dikonfigurasi menggunakan struktur
VkPipelineRasterizationStateCreateInfo.

1 rasterizer VkPipelineRasterizationStateCreateInfo{}; 2
rasterizer.sType =
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3 rasterizer.depthClampEnable = VK_FALSE;

Jika depthClampEnable disetel ke VK_TRUE, maka fragmen yang berada di luar bidang
dekat dan jauh akan dijepit ke sana, bukannya membuangnya. Ini berguna dalam beberapa
kasus khusus seperti peta bayangan. Menggunakan ini membutuhkan pengaktifan fitur
GPU.

1 rasterizer.rasterizerDiscardEnable = VK_FALSE;

Jika rasterizerDiscardEnable disetel ke VK_TRUE, geometri tidak akan pernah melewati


tahap rasterizer. Ini pada dasarnya menonaktifkan output apa pun ke buffer bingkai.

1 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

PolygonMode menentukan bagaimana fragmen dihasilkan untuk geometri. Mode berikut


tersedia:

• VK_POLYGON_MODE_FILL: mengisi area poligon dengan fragmen •


VK_POLYGON_MODE_LINE: tepi poligon digambar sebagai garis •
VK_POLYGON_MODE_POINT: simpul poligon digambar sebagai titik

Menggunakan mode apa pun selain isi memerlukan pengaktifan fitur GPU.

1 rasterizer.lineWidth = 1,0f;

Anggota lineWidth langsung, itu menggambarkan ketebalan garis dalam hal jumlah
fragmen. Lebar garis maksimum yang didukung bergantung pada perangkat keras dan
garis apa pun yang lebih tebal dari 1,0f mengharuskan Anda untuk mengaktifkan fitur GPU
wideLines.

112
Machine Translated by Google

1 rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; 2
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

Variabel cullMode menentukan jenis pemusnahan wajah yang akan digunakan. Anda dapat menonaktifkan
pemusnahan, pemusnahan muka depan, pemusnahan muka belakang atau keduanya. Variabel frontFace
menentukan urutan vertex untuk wajah yang dianggap menghadap ke depan dan dapat searah jarum jam atau
berlawanan arah jarum jam.

1 rasterizer.depthBiasEnable = VK_FALSE; 2
rasterizer.depthBiasConstantFactor = 0,0f; // Opsional 3 rasterizer.depthBiasClamp =
0.0f; // Opsional 4 rasterizer.depthBiasSlopeFactor = 0.0f; // Opsional

Rasterizer dapat mengubah nilai kedalaman dengan menambahkan nilai konstanta atau membiaskannya
berdasarkan kemiringan fragmen. Ini terkadang digunakan untuk pemetaan bayangan, tetapi kami tidak akan
menggunakannya. Cukup atur depthBiasEnable ke VK_FALSE.

Multisampling
Struktur VkPipelineMultisampleStateCreateInfo mengonfigurasi multisam pling, yang merupakan salah satu
cara untuk melakukan anti-aliasing. Ini bekerja dengan menggabungkan hasil shader fragmen dari beberapa
poligon yang di-raster ke piksel yang sama. Ini terutama terjadi di sepanjang tepi, yang juga merupakan tempat
terjadinya artefak aliasing yang paling mencolok. Karena tidak perlu menjalankan shader fragmen beberapa
kali jika hanya satu poligon yang dipetakan ke piksel, biayanya jauh lebih murah daripada hanya merender ke
resolusi yang lebih tinggi lalu menurunkan skala.

Mengaktifkannya memerlukan pengaktifan fitur GPU.

1 multisampling VkPipelineMultisampleStateCreateInfo{}; 2 multisampling.sType =


VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; 3
multisampling.sampleShadingEnable = VK_FALSE; 4 multisampling.rasterizationSamples
= VK_SAMPLE_COUNT_1_BIT; 5 multisampling.minSampleShading = 1,0f; // Opsional 6
multisampling.pSampleMask = nullptr; // Opsional 7 multisampling.alphaToCoverageEnable =
VK_FALSE; // Opsional 8 multisampling.alphaToOneEnable = VK_FALSE; // Opsional

Kita akan meninjau kembali multisampling di bab selanjutnya, untuk saat ini biarkan tetap dinonaktifkan.

Pengujian kedalaman dan stensil


Jika Anda menggunakan buffer kedalaman dan/atau stensil, Anda juga perlu mengonfigurasi pengujian
kedalaman dan stensil menggunakan VkPipelineDepthStencilStateCreateInfo.
Kami tidak memilikinya sekarang, jadi kami cukup meneruskan nullptr alih-alih penunjuk ke struct seperti itu.
Kami akan membahasnya kembali di bab buffering kedalaman.

113
Machine Translated by Google

Pencampuran warna

Setelah shader fragmen mengembalikan warna, itu perlu digabungkan dengan warna yang
sudah ada di framebuffer. Transformasi ini dikenal sebagai pencampuran warna dan ada
dua cara untuk melakukannya:

• Campurkan nilai lama dan baru untuk menghasilkan warna


akhir • Gabungkan nilai lama dan baru menggunakan operasi bitwise

Ada dua jenis struct untuk mengonfigurasi pencampuran warna. Struktur pertama,
VkPipelineColorBlendAttachmentState berisi konfigurasi per pada framebuffer yang
dilampirkan dan struktur kedua, VkPipelineColorBlendStateCreateInfo berisi pengaturan
pencampuran warna global . Dalam kasus kami, kami hanya memiliki satu buffer bingkai:

1 VkPipelineColorBlendAttachmentState colorBlendAttachment{}; 2
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT |
VK_COLOR_COMPONENT_A_BIT; 3 colorBlendAttachment.blendEnable =
VK_FALSE; 4 colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; //

Opsional
5 colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; //
Opsional
6 colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Opsional 7
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; //
Opsional
8 warnaBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; //
Opsional
9 colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Opsional

Struktur per-framebuffer ini memungkinkan Anda mengonfigurasi cara pertama pencampuran warna.
Operasi yang akan dilakukan paling baik didemonstrasikan menggunakan pseudocode
berikut:
1 jika (blendEnable)
2 { finalColor.rgb = (srcColorBlendFactor * newColor.rgb)
<colorBlendOp> (dstColorBlendFactor * oldColor.rgb); finalColor.a
3 = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp>
(dstAlphaBlendFactor * oldColor.a); 4 } lain {

finalColor = newColor;
56}
7
8 finalColor = finalColor & colorWriteMask;

Jika blendEnable disetel ke VK_FALSE, maka warna baru dari shader fragmen diteruskan
tanpa dimodifikasi. Jika tidak, dua operasi pencampuran

114
Machine Translated by Google

dilakukan untuk menghitung warna baru. Warna yang dihasilkan adalah DAN dengan
colorWriteMask untuk menentukan saluran mana yang benar-benar dilewati.

Cara paling umum untuk menggunakan pencampuran warna adalah menerapkan pencampuran
alfa, di mana kita ingin warna baru dicampur dengan warna lama berdasarkan keburamannya.
FinalColor kemudian harus dihitung sebagai berikut:

1 finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; 2 finalColor.a =


newAlpha.a;

Ini dapat dicapai dengan parameter berikut:

1 colorBlendAttachment.blendEnable = VK_TRUE; 2
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; 3
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;

4 colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; 5
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; 6
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; 7
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

Anda dapat menemukan semua kemungkinan operasi dalam enumerasi VkBlendFactor dan
VkBlendOp dalam spesifikasi.

Struktur kedua mereferensikan susunan struktur untuk semua framebuffer dan memungkinkan
Anda menyetel konstanta campuran yang dapat Anda gunakan sebagai faktor campuran dalam
perhitungan yang disebutkan di atas.

1 VkPipelineColorBlendStateCreateInfo colorBlending{}; 2
colorBlending.sType =
VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3 colorBlending.logicOpEnable = VK_FALSE; 4 colorBlending.logicOp =
VK_LOGIC_OP_COPY; // Opsional 5 colorBlending.attachmentCount = 1; 6
colorBlending.pAttachments = &colorBlendAttachment; 7 colorBlending.blendConstants[0]
= 0.0f; // Opsional 8 colorBlending.blendConstants[1] = 0.0f; // Opsional 9
colorBlending.blendConstants[2] = 0.0f; // Opsional 10 colorBlending.blendConstants[3]
= 0.0f; // Opsional

Jika Anda ingin menggunakan metode pencampuran kedua (kombinasi bitwise), maka Anda
harus menyetel logicOpEnable ke VK_TRUE. Operasi bitwise kemudian dapat ditentukan di
bidang logicOp. Perhatikan bahwa ini akan secara otomatis menonaktifkan metode pertama,
seolah-olah Anda telah menyetel blendEnable ke VK_FALSE untuk setiap framebuffer yang
terpasang! ColorWriteMask juga akan digunakan dalam mode ini untuk menentukan saluran
mana dalam framebuffer yang akan terpengaruh. Dimungkinkan juga untuk menonaktifkan kedua
mode, seperti yang telah kita lakukan di sini, dalam hal ini warna fragmen akan ditulis ke
framebuffer yang tidak dimodifikasi.

115
Machine Translated by Google

Keadaan dinamis
Jumlah terbatas dari status yang telah kita tentukan di struct sebelumnya sebenarnya bisa diubah
tanpa membuat ulang pipeline. Contohnya adalah ukuran area pandang, lebar garis, dan
konstanta campuran. Jika Anda ingin melakukannya, Anda harus mengisi struktur
VkPipelineDynamicStateCreateInfo seperti ini:

1 std::vector<VkDynamicState> dynamicStates = { 2
VK_DYNAMIC_STATE_VIEWPORT,
3 VK_DYNAMIC_STATE_LINE_WIDTH
4 };
5
6 VkPipelineDynamicStateCreateInfo dynamicState{}; 7
dynamicState.sType =
VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
8 dynamicState.dynamicStateCount =
static_cast<uint32_t>(dynamicStates.size()); 9
dynamicState.pDynamicStates = dynamicStates.data();

Ini akan menyebabkan konfigurasi nilai-nilai ini diabaikan dan Anda akan diminta untuk
menentukan data pada waktu menggambar. Kami akan kembali ke ini di bab mendatang. Struktur
ini dapat diganti dengan nullptr nanti jika Anda tidak memiliki status dinamis.

Tata letak pipa


Anda dapat menggunakan nilai seragam dalam shader, yang merupakan global mirip dengan
variabel keadaan dinamis yang dapat diubah pada waktu menggambar untuk mengubah perilaku
shader Anda tanpa harus membuatnya kembali. Mereka biasanya digunakan untuk meneruskan
matriks transformasi ke vertex shader, atau untuk membuat sampler tekstur di shader fragmen.

Nilai seragam ini perlu ditentukan selama pembuatan pipa dengan membuat objek
VkPipelineLayout. Meskipun kita tidak akan menggunakannya sampai bab selanjutnya, kita
masih diminta untuk membuat tata letak pipa kosong.

Buat anggota kelas untuk menampung objek ini, karena kita akan merujuknya dari fungsi lain di
lain waktu:

1 VkPipelineLayout pipelineLayout;

Dan kemudian buat objek dalam fungsi createGraphicsPipeline :

1 VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; 2
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3 pipelineLayoutInfo.setLayoutCount = 0; // Opsional 4
pipelineLayoutInfo.pSetLayouts = nullptr; // Opsional

116
Machine Translated by Google

5 pipelineLayoutInfo.pushConstantRangeCount = 0; // Opsional 6
pipelineLayoutInfo.pPushConstantRanges = nullptr; // Opsional
7

8 jika (vkCreatePipelineLayout(perangkat, &pipelineLayoutInfo, nullptr,


&pipelineLayout) != VK_SUCCESS)
9 { throw std::runtime_error("gagal membuat tata letak pipa!");
10 }

Strukturnya juga menentukan konstanta push, yang merupakan cara lain untuk
meneruskan nilai dinamis ke shader yang mungkin akan kita bahas di bab mendatang.
Tata letak pipa akan direferensikan sepanjang masa program, sehingga harus
dihancurkan di bagian akhir:
1 pembersihan batal ()
{ vkDestroyPipelineLayout(perangkat, pipelineLayout, nullptr); 2
3 ...
4}

Kesimpulan

Itu saja untuk semua keadaan fungsi tetap! Ini banyak pekerjaan untuk mengatur
semua ini dari awal, tetapi keuntungannya adalah kita sekarang hampir sepenuhnya
menyadari semua yang terjadi di pipa grafis! Hal ini mengurangi kemungkinan
terjadinya perilaku yang tidak diharapkan karena status default komponen tertentu
tidak seperti yang Anda harapkan.
Namun ada satu objek lagi yang harus dibuat sebelum kita akhirnya bisa membuat
pipa grafik dan itu adalah render pass.

Kode C++ / Vertex shader / Fragment shader

117
Machine Translated by Google

Render pass

Mempersiapkan

Sebelum kita dapat menyelesaikan pembuatan pipeline, kita perlu memberi tahu Vulkan tentang
lampiran framebuffer yang akan digunakan saat merender. Kita perlu menentukan berapa banyak
buffer warna dan kedalaman yang akan ada, berapa banyak sampel yang akan digunakan untuk
masing-masing buffer, dan bagaimana isinya harus ditangani selama operasi rendering. Semua
informasi ini dibungkus dalam objek render pass , yang akan kita buat fungsi createRenderPass baru.
Panggil fungsi ini dari initVulkan sebelum createGraphicsPipeline.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger(); buat
4 Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9 buatRenderPass();
10 buatGraphicsPipeline();
11 }
12
13 ...
14
15 batal createRenderPass() {

16 17 }

Deskripsi lampiran
Dalam kasus kami, kami hanya akan memiliki satu lampiran penyangga warna yang diwakili oleh
salah satu gambar dari rantai pertukaran.

118
Machine Translated by Google

1 batal createRenderPass()
2 { VkAttachmentDescription colorAttachment{};
3 colorAttachment.format = swapChainImageFormat;
4 colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
5}

Format lampiran warna harus sesuai dengan format gambar rantai pertukaran, dan kami belum
melakukan apa pun dengan multisampling, jadi kami akan tetap menggunakan 1 sampel.

1 colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; 2
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

LoadOp dan storeOp menentukan apa yang harus dilakukan dengan data dalam lampiran
sebelum perenderan dan setelah perenderan. Kami memiliki pilihan berikut untuk loadOp:

• VK_ATTACHMENT_LOAD_OP_LOAD: Pertahankan isi yang ada di at


keterikatan
• VK_ATTACHMENT_LOAD_OP_CLEAR: Menghapus nilai menjadi konstanta di
Mulailah

• VK_ATTACHMENT_LOAD_OP_DONT_CARE: Konten yang ada tidak ditentukan;


kami tidak peduli tentang mereka

Dalam kasus kita, kita akan menggunakan operasi clear untuk menghapus framebuffer menjadi
hitam sebelum menggambar frame baru. Hanya ada dua kemungkinan untuk storeOp:

• VK_ATTACHMENT_STORE_OP_STORE: Konten hasil render akan disimpan di memori


dan dapat dibaca nanti • VK_ATTACHMENT_STORE_OP_DONT_CARE: Konten
framebuffer akan
tidak ditentukan setelah operasi rendering

Kami tertarik untuk melihat segitiga yang dirender di layar, jadi kami akan melanjutkan operasi
toko di sini.

1 colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; 2
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

LoadOp dan storeOp berlaku untuk data warna dan kedalaman, dan stencilLoadOp /
stencilStoreOp berlaku untuk data stensil. Aplikasi kita tidak akan melakukan apa pun dengan
buffer stensil, sehingga hasil pemuatan dan penyimpanan tidak relevan.

1 colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; 2
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

Tekstur dan buffer bingkai di Vulkan diwakili oleh objek VkImage dengan format piksel tertentu,
namun tata letak piksel dalam memori dapat berubah berdasarkan apa yang Anda coba
lakukan dengan gambar.

Beberapa tata letak yang paling umum adalah:

119
Machine Translated by Google

• VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: Gambar digunakan sebagai warna pada


keterikatan
• VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: Gambar yang akan ditampilkan di swap
rantai
• VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: Gambar yang akan digunakan sebagai tujuan
untuk operasi penyalinan memori

Kita akan membahas topik ini secara lebih mendalam di bab tekstur, tetapi yang penting untuk diketahui
saat ini adalah bahwa gambar perlu dialihkan ke tata letak tertentu yang sesuai untuk operasi yang akan
dilakukan selanjutnya.

InitialLayout menentukan tata letak mana yang akan dimiliki gambar sebelum proses render dimulai.
finalLayout menentukan tata letak untuk secara otomatis bertransisi ke saat render pass selesai.
Menggunakan VK_IMAGE_LAYOUT_UNDEFINED untuk initialLayout berarti kita tidak peduli dengan tata
letak gambar sebelumnya. Peringatan dari nilai khusus ini adalah bahwa konten gambar tidak dijamin akan
dipertahankan, tetapi itu tidak masalah karena kita akan pergi untuk membersihkannya. Kami ingin gambar
siap untuk presentasi menggunakan rantai pertukaran setelah rendering, itulah sebabnya kami
menggunakan VK_IMAGE_LAYOUT_PRESENT_SRC_KHR sebagai finalLayout.

Subpass dan referensi lampiran


Satu pass render dapat terdiri dari beberapa subpass. Subpasses adalah operasi rendering berikutnya
yang bergantung pada konten framebuffer di pass sebelumnya, misalnya urutan efek pasca-pemrosesan
yang diterapkan satu demi satu. Jika Anda mengelompokkan operasi rendering ini ke dalam satu pass
render, Vulkan dapat mengurutkan ulang operasi dan menghemat bandwidth memori untuk kemungkinan
kinerja yang lebih baik. Namun, untuk segitiga pertama kita, kita akan tetap menggunakan satu subpass.

Setiap subpass mereferensikan satu atau lebih lampiran yang telah kami jelaskan menggunakan struktur
di bagian sebelumnya. Referensi ini sendiri adalah struct VkAttachmentReference yang terlihat seperti ini:

1 VkAttachmentReference colorAttachmentRef{}; 2
colorAttachmentRef.attachment = 0; 3 colorAttachmentRef.layout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

Parameter lampiran menentukan lampiran mana yang menjadi referensi berdasarkan indeksnya dalam
larik deskripsi lampiran. Larik kita terdiri dari satu VkAttachmentDescription, jadi indeksnya adalah 0. Tata
letak menentukan tata letak mana yang kita inginkan untuk lampiran selama subpass yang menggunakan
referensi ini. Vulkan akan secara otomatis mentransisikan lampiran ke tata letak ini saat subpass dimulai.
Kami bermaksud menggunakan lampiran untuk berfungsi sebagai

120
Machine Translated by Google

penyangga warna dan tata letak VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL


akan memberi kita kinerja terbaik, seperti namanya.

Subpass dijelaskan menggunakan struktur VkSubpassDescription:

1 subpass VkSubpassDescription{}; 2
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;

Vulkan mungkin juga mendukung subpass komputasi di masa mendatang, jadi kami harus
menjelaskan secara eksplisit bahwa ini adalah subpass grafis. Selanjutnya, kami menentukan
referensi ke lampiran warna:

1 subpass.colorAttachmentCount = 1; 2
subpass.pColorAttachments = &colorAttachmentRef;

Indeks lampiran dalam larik ini direferensikan langsung dari shader fragmen dengan
direktif layout(location = 0)out vec4 outColor!

Jenis lampiran lain berikut ini dapat direferensikan oleh subpass:

• pInputAttachments: Lampiran yang dibaca dari shader •


pResolveAttachments: Lampiran yang digunakan untuk lampiran warna multisampling
hal
• pDepthStencilAttachment: Lampiran untuk data kedalaman dan stensil •
pPreserveAttachments: Lampiran yang tidak digunakan oleh subpass ini,
tetapi untuk itu data harus dipertahankan

Berikan izin
Sekarang lampiran dan referensi subpass dasar telah dijelaskan, kita dapat membuat
pass render itu sendiri. Buat variabel anggota kelas baru untuk menampung objek
VkRenderPass tepat di atas variabel pipelineLayout:
1 VkRenderPass renderPass;
2 Tata letak pipa VkPipelineLayout;

Objek render pass kemudian dapat dibuat dengan mengisi struktur VkRenderPassCreateInfo
dengan array lampiran dan subpass. Objek VkAttachmentReference mereferensikan lampiran
menggunakan indeks larik ini.
1 VkRenderPassCreateInfo renderPassInfo{}; 2
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
3 renderPassInfo.attachmentCount = 1; 4 renderPassInfo.pAttachments =
&colorAttachment; 5 renderPassInfo.subpassCount = 1; 6 renderPassInfo.pSubpasses
= &subpass;

7
8 if (vkCreateRenderPass(perangkat, &renderPassInfo, nullptr,
&renderPass) != VK_SUCCESS) {

121
Machine Translated by Google

9 throw std::runtime_error("gagal membuat render pass!");


10 }

Sama seperti tata letak pipa, render pass akan direferensikan di seluruh program, jadi
seharusnya hanya dibersihkan di bagian akhir:

1 pembersihan batal ()
2 { vkDestroyPipelineLayout(perangkat, pipelineLayout, nullptr);
3 vkDestroyRenderPass(perangkat, renderPass, nullptr);
4 ...
5}

Itu banyak pekerjaan, tetapi di bab berikutnya semuanya bersatu untuk akhirnya membuat
objek pipa grafis!

Kode C++ / Vertex shader / Fragment shader

122
Machine Translated by Google

Kesimpulan

Kita sekarang dapat menggabungkan semua struktur dan objek dari bab sebelumnya
untuk membuat saluran grafik! Inilah jenis objek yang kita miliki sekarang, sebagai
rekap cepat:

• Tahapan shader: modul shader yang menentukan fungsionalitas tahapan yang


dapat diprogram dari pipa grafis
• Status fungsi tetap: semua struktur yang menentukan tahapan fungsi tetap dari
pipeline, seperti perakitan input, rasterizer, viewport, dan pencampuran warna

• Tata letak pipa: nilai seragam dan dorong yang direferensikan oleh shader yang
dapat diperbarui pada waktu pengundian • Render pass: lampiran yang
direferensikan oleh tahapan pipa dan mereka
penggunaan

Semua gabungan ini sepenuhnya menentukan fungsionalitas pipa grafik, jadi kita
sekarang dapat mulai mengisi struktur VkGraphicsPipelineCreateInfo di akhir fungsi
createGraphicsPipeline. Tapi sebelum panggilan ke vkDestroyShaderModule karena ini
masih digunakan selama pembuatan.

1 VkGraphicsPipelineCreateInfo pipelineInfo{}; 2
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3 pipelineInfo.stageCount = 2; 4 pipelineInfo.pStages = shaderStages;

Kita mulai dengan mereferensikan array struct VkPipelineShaderStageCreateInfo.

1 pipelineInfo.pVertexInputState = &vertexInputInfo; 2
pipelineInfo.pInputAssemblyState = &inputAssembly; 3
pipelineInfo.pViewportState = &viewportState; 4
pipelineInfo.pRasterizationState = &rasterizer; 5
pipelineInfo.pMultisampleState = &multisampling; 6
pipelineInfo.pDepthStencilState = nullptr; // Opsional 7
pipelineInfo.pColorBlendState = &colorBlending; 8
pipelineInfo.pDynamicState = nullptr; // Opsional

123
Machine Translated by Google

Kemudian kami mereferensikan semua struktur yang menjelaskan tahap fungsi tetap.

1 pipelineInfo.layout = pipelineLayout;

Setelah itu muncul tata letak pipa, yang merupakan pegangan Vulkan daripada penunjuk
struct.

1 pipelineInfo.renderPass = renderPass; 2
pipelineInfo.subpass = 0;

Dan akhirnya kita memiliki referensi untuk render pass dan indeks dari sub pass dimana
pipa grafis ini akan digunakan. Dimungkinkan juga untuk menggunakan render pass lain
dengan pipeline ini alih-alih instance khusus ini, tetapi pass tersebut harus kompatibel
dengan renderPass. Persyaratan kompatibilitas dijelaskan di sini, tetapi kami tidak akan
menggunakan fitur tersebut dalam tutorial ini.

1 pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Opsional 2


pipelineInfo.basePipelineIndex = -1; // Opsional

Sebenarnya ada dua parameter lagi: basePipelineHandle dan basePipelineIndex. Vulkan


memungkinkan Anda membuat pipeline grafis baru dengan mengambil dari pipeline yang
sudah ada. Gagasan tentang turunan pipa adalah lebih murah untuk menyiapkan saluran
pipa ketika mereka memiliki banyak fungsi yang sama dengan saluran pipa yang ada dan
peralihan antar saluran pipa dari induk yang sama juga dapat dilakukan lebih cepat. Anda
dapat menentukan pegangan pipa yang ada dengan basePipelineHandle atau
mereferensikan pipa lain yang akan dibuat oleh indeks dengan basePipelineIndex. Saat ini
hanya ada satu pipa, jadi kami hanya akan menentukan pegangan nol dan indeks yang
tidak valid.
Nilai ini hanya digunakan jika bendera VK_PIPELINE_CREATE_DERIVATIVE_BIT juga
ditentukan di bidang bendera VkGraphicsPipelineCreateInfo.

Sekarang bersiaplah untuk langkah terakhir dengan membuat anggota kelas untuk memegang
Objek VkPipeline:

1 VkPipeline graphicsPipeline;

Dan terakhir buat pipa grafis:

1 jika (vkCreateGraphicsPipelines(perangkat, VK_NULL_HANDLE, 1,


&pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { throw
2 std::runtime_error("gagal membuat pipa grafis!");
3}

Fungsi vkCreateGraphicsPipelines sebenarnya memiliki lebih banyak parameter daripada


fungsi pembuatan objek biasa di Vulkan. Ini dirancang untuk mengambil beberapa objek
VkGraphicsPipelineCreateInfo dan membuat beberapa objek VkPipeline dalam satu
panggilan.

Parameter kedua, yang kami berikan argumen VK_NULL_HANDLE, mereferensikan objek


VkPipelineCache opsional. Cache pipa dapat digunakan

124
Machine Translated by Google

untuk menyimpan dan menggunakan kembali data yang relevan dengan pembuatan
pipeline di beberapa panggilan ke vkCreateGraphicsPipelines dan bahkan di seluruh
eksekusi program jika cache disimpan ke file. Ini memungkinkan untuk secara signifikan
mempercepat pembuatan pipa di lain waktu. Kami akan membahas ini di bab cache pipa.

Pipa grafis diperlukan untuk semua operasi menggambar umum, jadi itu juga harus
dihancurkan di akhir program:

1 pembersihan batal ()
2 { vkDestroyPipeline(perangkat, graphicsPipeline, nullptr);
3 vkDestroyPipelineLayout(perangkat, pipelineLayout, nullptr);
4 ...
5}

Sekarang jalankan program Anda untuk memastikan bahwa semua kerja keras ini telah
menghasilkan pembuatan pipa yang berhasil! Kami sudah cukup dekat untuk melihat
sesuatu muncul di layar. Dalam beberapa bab berikutnya kita akan mengatur framebuffer
sebenarnya dari gambar rantai swap dan menyiapkan perintah menggambar.

Kode C++ / Vertex shader / Fragment shader

125
Machine Translated by Google

Framebuffer

Kita telah berbicara banyak tentang framebuffer dalam beberapa bab terakhir dan kita telah
menyiapkan render pass untuk mengharapkan satu framebuffer dengan format yang sama dengan
gambar rantai swap, tetapi kita belum benar-benar membuatnya.

Lampiran yang ditentukan selama pembuatan pass render diikat dengan membungkusnya menjadi
objek VkFramebuffer. Objek framebuffer mereferensikan semua objek VkImageView yang mewakili
lampiran. Dalam kasus kami, itu hanya akan menjadi satu: lampiran warna. Namun, gambar yang
harus kita gunakan untuk lampiran bergantung pada gambar mana yang dikembalikan oleh rantai
pertukaran saat kita mengambilnya untuk presentasi. Itu berarti kita harus membuat buffer bingkai
untuk semua gambar dalam rantai pertukaran dan menggunakan salah satu yang sesuai dengan
gambar yang diambil pada waktu menggambar.

Untuk itu, buat anggota kelas std::vector lain untuk menampung framebuffer:

1 std::vector<VkFramebuffer> swapChainFramebuffer;

Kita akan membuat objek untuk array ini dalam fungsi baru createFramebuffers yang dipanggil dari
initVulkan tepat setelah membuat pipa grafis:

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger(); buat
4 Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9 buatRenderPass();
10 buatGraphicsPipeline();
11 buatFramebuffer();
12 }
13
14 ...
15
16 batal createFramebuffers() {

126
Machine Translated by Google

17
18 }

Mulailah dengan mengubah ukuran wadah untuk menampung semua framebuffer:

1 batal createFramebuffers() {
2 swapChainFramebuffers.resize(swapChainImageViews.size());
3}

Kami kemudian akan beralih melalui tampilan gambar dan membuat framebuffer darinya:

1 untuk (size_t i = 0; i < swapChainImageViews.size(); i++) { 2


Lampiran VkImageView[] =
3 { swapChainImageViews[i]
4 };
5
6 VkFramebufferCreateInfo framebufferInfo{};
7 framebufferInfo.sType =
VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
8 framebufferInfo.renderPass = renderPass;
9 framebufferInfo.attachmentCount = 1;
10 framebufferInfo.pAttachments = lampiran;
11 framebufferInfo.width = swapChainExtent.width;
12 framebufferInfo.height = swapChainExtent.height;
13 framebufferInfo.layers = 1;
14
15 if (vkCreateFramebuffer(device, &framebufferInfo, nullptr,
&swapChainFramebuffers[i]) != VK_SUCCESS) { throw
16 std::runtime_error("gagal membuat framebuffer!");
17 }
18 }

Seperti yang Anda lihat, pembuatan framebuffer cukup mudah. Pertama-tama kita harus
menentukan dengan renderPass mana framebuffer harus kompatibel. Anda hanya dapat
menggunakan framebuffer dengan render pass yang kompatibel dengannya, yang secara
kasar berarti mereka menggunakan jumlah dan jenis lampiran yang sama.

Parameter attachmentCount dan pAttachments menentukan objek VkImageView yang harus


diikat ke masing-masing deskripsi lampiran dalam array render pass pAttachment.

Parameter lebar dan tinggi cukup jelas dan lapisan mengacu pada jumlah lapisan dalam
susunan gambar. Gambar rantai pertukaran kami adalah gambar tunggal, jadi jumlah
lapisannya adalah 1.

Kita harus menghapus framebuffers sebelum tampilan gambar dan render pass yang menjadi
dasarnya, tetapi hanya setelah kita selesai merender:

1 pembersihan batal () {

127
Machine Translated by Google

2 untuk ( framebuffer otomatis : swapChainFramebuffers) {


3 vkDestroyFramebuffer(perangkat, framebuffer, nullptr);
4 }
5
6 ...
7}

Kami sekarang telah mencapai tonggak di mana kami memiliki semua objek yang
diperlukan untuk rendering. Di bab selanjutnya kita akan menulis perintah menggambar
pertama yang sebenarnya.

Kode C++ / Vertex shader / Fragment shader

128
Machine Translated by Google

Buffer perintah

Perintah di Vulkan, seperti operasi menggambar dan transfer memori, tidak dijalankan secara langsung
menggunakan pemanggilan fungsi. Anda harus merekam semua operasi yang ingin Anda lakukan di
objek buffer perintah. Keuntungannya adalah ketika kita siap memberi tahu Vulkan apa yang ingin kita
lakukan, semua perintah dikirimkan bersama dan Vulkan dapat memproses perintah dengan lebih
efisien karena semuanya tersedia bersama. Selain itu, ini memungkinkan perekaman perintah terjadi
di banyak utas jika diinginkan.

Kolam komando
Kita harus membuat kumpulan perintah sebelum kita dapat membuat buffer perintah.
Kumpulan perintah mengelola memori yang digunakan untuk menyimpan buffer dan buffer perintah
dialokasikan dari mereka. Tambahkan anggota kelas baru untuk menyimpan a
VkCommandPool:

1 kumpulan perintah VkCommandPool;

Kemudian buat fungsi baru createCommandPool dan panggil dari initVulkan setelah framebuffer dibuat.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger(); buat
4 Permukaan(); pickPhysicalDevice();
5 createLogicalDevice();
6 buatSwapChain();
7 createImageViews();
8 buatRenderPass();
9 buatGraphicsPipeline();
10 buatFramebuffer(); buatCommandPool();
11
12
13 }
14

129
Machine Translated by Google

15 ...
16
17 batal buatCommandPool() {
18
19 }

Pembuatan kumpulan perintah hanya membutuhkan dua parameter:

1 QueueFamilyIndices queueFamilyIndices =
findQueueFamilies(physicalDevice);
2

3 VkCommandPoolCreateInfo poolInfo{}; 4 poolInfo.sType


= VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; 5 poolInfo.flags =
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; 6 poolInfo.queueFamilyIndex
= queueFamilyIndices.graphicsFamily.value();

Ada dua kemungkinan bendera untuk kumpulan perintah:

• VK_COMMAND_POOL_CREATE_TRANSIENT_BIT: Petunjuk bahwa buffer perintah sangat


sering direkam ulang dengan perintah baru (dapat mengubah perilaku alokasi memori) •
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT: Izinkan buffer perintah
direkam ulang satu per satu, tanpa tanda ini semuanya harus disetel ulang bersama

Kami akan merekam buffer perintah setiap frame, jadi kami ingin dapat menyetel ulang dan merekam
ulang di atasnya. Jadi, kita perlu menyetel flag bit VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT untuk kumpulan
perintah kita.

Buffer perintah dijalankan dengan mengirimkannya ke salah satu antrean perangkat, seperti antrean
grafik dan presentasi yang kami ambil. Setiap kumpulan perintah hanya dapat mengalokasikan buffer
perintah yang dikirimkan pada satu jenis antrian.
Kami akan merekam perintah untuk menggambar, itulah sebabnya kami memilih keluarga antrean grafis.

1 jika (vkCreateCommandPool(perangkat, &poolInfo, nullptr, &commandPool) !=


VK_SUCCESS)
2 { throw std::runtime_error("gagal membuat kumpulan perintah!");
3}

Selesaikan pembuatan kumpulan perintah menggunakan fungsi vkCreateCommandPool. Itu tidak


memiliki parameter khusus. Perintah akan digunakan di seluruh program untuk menggambar sesuatu di
layar, jadi kumpulan hanya boleh dihancurkan di bagian akhir:

1 pembersihan batal ()
2 { vkDestroyCommandPool(perangkat, commandPool, nullptr);
3

130
Machine Translated by Google

4 ...
5}

Alokasi buffer perintah

Kita sekarang dapat mulai mengalokasikan buffer perintah.

Buat objek VkCommandBuffer sebagai anggota kelas. Buffer perintah akan dibebaskan
secara otomatis saat kumpulan perintahnya dihancurkan, jadi kita tidak memerlukan
pembersihan eksplisit.

1 VkCommandBuffer perintahBuffer;

Sekarang kita akan mulai mengerjakan fungsi createCommandBuffer untuk mengalokasikan


buffer perintah tunggal dari kumpulan perintah.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger();
4 buat Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9 buatRenderPass();
10 buatGraphicsPipeline();
11 buatFramebuffer();
12 buatCommandPool();
13 buatCommandBuffer();
14 }
15
16 ...
17
18 batal buatCommandBuffer() {
19
20 }

Buffer perintah dialokasikan dengan fungsi vkAllocateCommandBuffers, yang menggunakan


struct VkCommandBufferAllocateInfo sebagai parameter yang menentukan kumpulan perintah
dan jumlah buffer untuk dialokasikan:

1 VkCommandBufferAllocateInfo allocInfo{}; 2 allocInfo.sType =


VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; 3 allocInfo.commandPool =
commandPool; 4 alokasiInfo.tingkat = VK_COMMAND_BUFFER_LEVEL_PRIMARY; 5
allocInfo.commandBufferCount = 1;

131
Machine Translated by Google

7 jika (vkAllocateCommandBuffers(perangkat, &allocInfo, &commandBuffer) !=


VK_SUCCESS)
8 { throw std::runtime_error("gagal mengalokasikan buffer perintah!");
9}

Parameter level menentukan apakah buffer perintah yang dialokasikan adalah buffer
perintah primer atau sekunder.

• VK_COMMAND_BUFFER_LEVEL_PRIMARY: Dapat dikirimkan ke antrean untuk


dieksekusi, tetapi tidak dapat dipanggil dari buffer perintah lain. •
VK_COMMAND_BUFFER_LEVEL_SECONDARY: Tidak dapat dikirimkan secara langsung,
tetapi dapat dipanggil dari buffer perintah utama.

Kami tidak akan menggunakan fungsi buffer perintah sekunder di sini, tetapi Anda dapat
membayangkan bahwa menggunakan kembali operasi umum dari buffer perintah utama
akan sangat membantu.

Karena kita hanya mengalokasikan satu buffer perintah, parameter commandBufferCount


hanya satu.

Perekaman buffer perintah


Sekarang kita akan mulai mengerjakan fungsi recordCommandBuffer yang menulis
perintah yang ingin kita jalankan ke dalam buffer perintah. VkCommandBuffer yang
digunakan akan diteruskan sebagai parameter, serta indeks dari gambar swapchain saat
ini yang ingin kita tulis.

1 catatan batalCommandBuffer (VkCommandBuffer commandBuffer, uint32_t


indeksgambar) {
2
3}

Kami selalu mulai merekam buffer perintah dengan memanggil vkBeginCommandBuffer


dengan struktur kecil VkCommandBufferBeginInfo sebagai argumen yang menetapkan
beberapa detail tentang penggunaan buffer perintah khusus ini.

1 VkCommandBufferBeginInfo beginInfo{}; 2
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
3 beginInfo.flags = 0; // Opsional 4 beginInfo.pInheritanceInfo = nullptr; // Opsional
5

6 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { throw


std::runtime_error("gagal mulai merekam perintah 7 buffer!");

8}

Parameter flags menentukan bagaimana kita akan menggunakan buffer perintah. Nilai
berikut tersedia:

132
Machine Translated by Google

• VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT: Perintah
buffer akan direkam ulang tepat setelah mengeksekusinya
sekali. • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT: Ini
adalah buffer perintah sekunder yang seluruhnya berada dalam satu pass render. •
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT: Buffer perintah dapat
dikirim ulang saat sedang menunggu eksekusi.

Tidak satu pun dari bendera ini yang berlaku untuk kami saat ini.

Parameter pInheritanceInfo hanya relevan untuk buffer perintah sekunder. Ini menentukan
status mana yang akan diwarisi dari buffer perintah utama panggilan.

Jika buffer perintah sudah direkam sekali, panggilan ke vkBeginCommandBuffer akan secara implisit
meresetnya. Tidak mungkin menambahkan perintah ke buffer di lain waktu.

Memulai render pass


Menggambar dimulai dengan memulai render pass dengan vkCmdBeginRenderPass. Pass
render dikonfigurasi menggunakan beberapa parameter dalam struktur VkRenderPassBeginInfo.

1 VkRenderPassBeginInfo renderPassInfo{}; 2
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; 3
renderPassInfo.renderPass = renderPass; 4 renderPassInfo.framebuffer =
swapChainFramebuffers[imageIndex];

Parameter pertama adalah render pass itu sendiri dan lampiran yang akan diikat. Kami
membuat framebuffer untuk setiap gambar rantai swap yang ditentukan sebagai lampiran
warna. Jadi kita perlu mengikat framebuffer untuk gambar swapchain yang ingin kita gambar.
Dengan menggunakan parameter imageIndex yang diteruskan, kita dapat memilih framebuffer
yang tepat untuk image swapchain saat ini.
1 renderPassInfo.renderArea.offset = {0, 0}; 2
renderPassInfo.renderArea.extent = swapChainExtent;

Dua parameter berikutnya menentukan ukuran area render. Area render menentukan tempat
pemuatan dan penyimpanan shader akan berlangsung. Piksel di luar wilayah ini akan memiliki
nilai yang tidak ditentukan. Itu harus sesuai dengan ukuran lampiran untuk kinerja terbaik.

1 VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; 2


renderPassInfo.clearValueCount = 1; 3 renderPassInfo.pClearValues =
&clearColor;

Dua parameter terakhir menentukan nilai bersih yang akan digunakan untuk VK_ATTACHMENT_LOAD_OP_CLEAR,
yang kami gunakan sebagai operasi muat untuk lampiran warna. Saya telah mendefinisikan warna bening
menjadi hitam dengan opasitas 100%.

133
Machine Translated by Google

1 vkCmdBeginRenderPass(commandBuffer, &renderPassInfo,
VK_SUBPASS_CONTENTS_INLINE);

Render pass sekarang dapat dimulai. Semua fungsi yang merekam perintah dapat dikenali dari
awalan vkCmd mereka. Semuanya kembali kosong, jadi tidak akan ada penanganan kesalahan
hingga kami selesai merekam.

Parameter pertama untuk setiap perintah selalu merupakan buffer perintah untuk merekam
perintah. Parameter kedua menentukan detail pass render yang baru saja kita sediakan.
Parameter terakhir mengontrol bagaimana perintah menggambar dalam pass render akan
disediakan. Itu dapat memiliki salah satu dari dua nilai:

• VK_SUBPASS_CONTENTS_INLINE: Perintah render pass akan disematkan dalam buffer


perintah utama itu sendiri dan tidak ada buffer perintah sekunder yang akan dijalankan.

• VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS: Perintah render pass


akan dijalankan dari buffer perintah sekunder.

Kami tidak akan menggunakan buffer perintah sekunder, jadi kami akan menggunakan opsi
pertama.

Perintah menggambar dasar


Kami sekarang dapat mengikat pipa grafis:

1 vkCmdBindPipeline(Buffer perintah, VK_PIPELINE_BIND_POINT_GRAPHICS,


pipa grafis);

Parameter kedua menentukan apakah objek pipa adalah grafik atau menghitung pipa. Kami
sekarang telah memberi tahu Vulkan operasi mana yang harus dijalankan dalam pipa grafis dan
lampiran mana yang akan digunakan dalam shader fragmen, jadi yang tersisa hanyalah memberi
tahu Vulkan untuk menggambar segitiga:

1 vkCmdDraw(commandBuffer, 3, 1, 0, 0);

Fungsi vkCmdDraw sebenarnya agak antiklimaks, tetapi sangat sederhana karena semua
informasi yang kami tentukan sebelumnya. Ini memiliki parameter berikut, selain dari buffer
perintah:

• vertexCount: Meskipun kita tidak memiliki buffer vertex, secara teknis kita
masih memiliki 3 simpul untuk menggambar.

• instanceCount: Digunakan untuk pembuatan instance, gunakan 1 jika Anda tidak melakukannya
itu.
• firstVertex: Digunakan sebagai offset ke dalam buffer vertex, mendefinisikan yang terendah
nilai gl_VertexIndex.
• firstInstance: Digunakan sebagai offset untuk instance rendering, menentukan nilai terendah
dari gl_InstanceIndex.

134
Machine Translated by Google

Menyelesaikan
Render pass sekarang dapat diakhiri:

1 vkCmdEndRenderPass(commandBuffer);

Dan kami telah selesai merekam buffer perintah:

1 jika (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { 2


throw std::runtime_error("gagal merekam buffer perintah!");
3}

Di bab selanjutnya kita akan menulis kode untuk loop utama, yang akan memperoleh
image dari rantai swap, merekam dan menjalankan buffer perintah, lalu mengembalikan
image yang sudah selesai ke rantai swap.

Kode C++ / Vertex shader / Fragment shader

135
Machine Translated by Google

Rendering dan presentasi

Ini adalah bab di mana semuanya akan datang bersama-sama. Kita akan menulis fungsi drawFrame
yang akan dipanggil dari loop utama untuk meletakkan segitiga di layar. Mari kita mulai dengan
membuat fungsi dan memanggilnya dari mainLoop:

1 void mainLoop() { while (!


2 glfwWindowShouldClose(window)) { glfwPollEvents();
3 drawFrame();
4
}
56}
7
8 ...
9
10 batal drawFrame() {
11
12 }

Garis besar bingkai

Pada tingkat tinggi, merender bingkai di Vulkan terdiri dari serangkaian langkah umum:

• Tunggu hingga bingkai sebelumnya selesai •


Dapatkan gambar dari rantai penukaran • Rekam
buffer perintah yang menarik adegan ke gambar tersebut • Kirimkan buffer perintah
yang direkam • Sajikan gambar rantai penukaran

Sementara kita akan memperluas fungsi menggambar di bab selanjutnya, untuk saat ini ini
adalah inti dari loop render kita.

136
Machine Translated by Google

Sinkronisasi
Filosofi desain inti di Vulkan adalah bahwa sinkronisasi eksekusi pada GPU bersifat eksplisit.
Urutan operasi terserah kita untuk menentukan menggunakan berbagai primitif sinkronisasi yang
memberi tahu pengemudi urutan yang kita inginkan untuk menjalankan sesuatu. Ini berarti bahwa
banyak panggilan Vulkan API yang mulai menjalankan pekerjaan pada GPU tidak sinkron, fungsi
akan kembali sebelum operasi telah selesai.

Di chapter ini ada beberapa event yang perlu kita urutkan secara eksplisit karena terjadi di GPU,
seperti:

• Dapatkan gambar dari rantai pertukaran •


Jalankan perintah yang menggambar ke gambar yang diperoleh •
Sajikan gambar itu ke layar untuk presentasi, kembalikan ke
rantai pertukaran

Masing-masing peristiwa ini digerakkan menggunakan satu pemanggilan fungsi, tetapi semuanya
dijalankan secara asinkron. Panggilan fungsi akan kembali sebelum operasi benar-benar selesai
dan urutan eksekusi juga tidak ditentukan. Itu kurang beruntung, karena setiap operasi bergantung
pada penyelesaian sebelumnya.
Jadi kita perlu mengeksplorasi primitif mana yang dapat kita gunakan untuk mencapai urutan yang
diinginkan.

Semafor
Sebuah semaphore digunakan untuk menambah urutan antara operasi antrian. Operasi antrian
mengacu pada pekerjaan yang kita serahkan ke antrian, baik dalam buffer perintah atau dari dalam
fungsi seperti yang akan kita lihat nanti. Contoh antrian adalah antrian grafik dan antrian presentasi.
Semafor digunakan untuk mengurutkan pekerjaan di dalam antrean yang sama dan di antara
antrean yang berbeda.

Kebetulan ada dua jenis semafor di Vulkan, biner dan garis waktu.
Karena hanya semaphore biner yang akan digunakan dalam tutorial ini, kita tidak akan membahas
semaphore timeline. Penyebutan lebih lanjut istilah semafor secara eksklusif mengacu pada
semafor biner.

Sebuah semaphore adalah unsignaled atau signaled. Itu memulai hidup sebagai tanpa tanda.
Cara kita menggunakan semafor untuk memesan operasi antrian adalah dengan menyediakan
semafor yang sama sebagai semafor 'sinyal' dalam satu operasi antrian dan sebagai semafor
'tunggu' dalam operasi antrian lainnya. Sebagai contoh, katakanlah kita memiliki semafor S dan
operasi antrian A dan B yang ingin kita jalankan secara berurutan. Apa yang kami katakan kepada
Vulkan adalah bahwa operasi A akan 'memberi sinyal' pada semaphore S ketika selesai memotong
exe, dan operasi B akan 'menunggu' pada semaphore S sebelum mulai dieksekusi.
Saat operasi A selesai, semafor S akan diberi sinyal, sementara operasi B tidak akan dimulai
sampai S diberi sinyal. Setelah operasi B mulai dijalankan, semaphore S secara otomatis direset
kembali menjadi tidak bersinyal, memungkinkannya untuk digunakan kembali.

Kode semu dari apa yang baru saja dijelaskan:

137
Machine Translated by Google

1 VkCommandBuffer A, B = ... // ... // merekam buffer perintah


= buat semaphore 2 VkSemaphore S
3
4 // enqueue A, beri sinyal S setelah selesai - segera mulai mengeksekusi 5
vkQueueSubmit(kerja: A, sinyal: S, tunggu: Tidak ada)
6
7 // enqueue B, tunggu S untuk memulai 8
vkQueueSubmit(kerja: B, sinyal: Tidak ada, tunggu: S)

Perhatikan bahwa dalam cuplikan kode ini, kedua panggilan ke vkQueueSubmit() segera kembali -
penantian hanya terjadi di GPU. CPU terus berjalan tanpa pemblokiran. Untuk membuat CPU
menunggu, kita memerlukan primitif sinkronisasi yang berbeda, yang sekarang akan kita jelaskan.

Pagar

Pagar memiliki tujuan yang sama, yaitu digunakan untuk menyinkronkan eksekusi, tetapi untuk
memerintahkan eksekusi pada CPU, atau dikenal sebagai host. Sederhananya, jika tuan rumah
perlu mengetahui kapan GPU menyelesaikan sesuatu, kami menggunakan pagar.

Mirip dengan semafor, pagar berada dalam keadaan bersinyal atau tidak bersinyal. Kapan pun kami
mengirimkan pekerjaan untuk dieksekusi, kami dapat memasang pagar pada pekerjaan itu. Saat
pekerjaan selesai, pagar akan diberi tanda. Kemudian kita bisa membuat tuan rumah menunggu
pagar diberi tanda, menjamin pekerjaan sudah selesai sebelum tuan rumah melanjutkan.

Contoh nyata adalah mengambil tangkapan layar. Katakanlah kita telah melakukan pekerjaan yang
diperlukan pada GPU. Sekarang perlu mentransfer gambar dari GPU ke host dan kemudian
menyimpan memori ke file. Kami memiliki buffer perintah A yang mengeksekusi transfer dan pagar
F. Kami mengirimkan buffer perintah A dengan pagar F, lalu segera beri tahu tuan rumah untuk
menunggu F memberi sinyal. Ini menyebabkan host memblokir hingga buffer perintah A menyelesaikan
eksekusi. Dengan demikian kita aman membiarkan tuan rumah menyimpan file ke disk, karena
transfer memori telah selesai.

Kode semu untuk apa yang dijelaskan:

1 VkCommandBuffer A = ... // merekam buffer perintah dengan transfer


2 VkFence F = ... // buat pagar
3
4 // enqueue A, segera mulai bekerja, beri sinyal F setelah selesai 5
vkQueueSubmit(kerja: A, pagar: F)
6
7 vkWaitForFence(F) // memblokir eksekusi sampai A selesai mengeksekusi 8

9 save_screenshot_to_disk() // tidak dapat berjalan sampai transfer selesai


selesai

138
Machine Translated by Google

Berbeda dengan contoh semaphore, contoh ini memblokir eksekusi host. Ini berarti tuan rumah
tidak akan melakukan apapun kecuali menunggu sampai eksekusi selesai. Untuk kasus ini, kami
harus memastikan transfer selesai sebelum kami dapat menyimpan tangkapan layar ke disk.

Secara umum, lebih baik untuk tidak memblokir host kecuali diperlukan. Kami ingin memberi makan
GPU dan host dengan pekerjaan yang berguna untuk dilakukan. Menunggu pagar untuk memberi
sinyal bukanlah pekerjaan yang berguna. Jadi kami lebih suka semafor, atau primitif sinkronisasi
lainnya yang belum tercakup, untuk menyinkronkan pekerjaan kami.

Pagar harus diatur ulang secara manual untuk mengembalikannya ke keadaan tanpa sinyal. Ini
karena pagar digunakan untuk mengontrol eksekusi tuan rumah, sehingga tuan rumah memutuskan
kapan mengatur ulang pagar. Bandingkan ini dengan semafor yang digunakan untuk memesan
pekerjaan pada GPU tanpa melibatkan host.

Singkatnya, semafor digunakan untuk menentukan urutan eksekusi operasi pada GPU sementara
pagar digunakan untuk menjaga agar CPU dan GPU tetap sinkron satu sama lain.

Apa yang harus dipilih?

Kami memiliki dua primitif sinkronisasi untuk digunakan dan dua tempat yang nyaman untuk
menerapkan sinkronisasi: operasi Swapchain dan menunggu frame sebelumnya selesai. Kami ingin
menggunakan semaphore untuk operasi swapchain karena terjadi pada GPU, jadi kami tidak ingin
membuat tuan rumah menunggu jika kami dapat membantunya. Untuk menunggu frame sebelumnya
selesai, kami ingin menggunakan pagar untuk alasan sebaliknya, karena kami membutuhkan tuan
rumah untuk menunggu. Ini agar kami tidak menggambar lebih dari satu bingkai sekaligus. Karena
kami merekam ulang buffer perintah setiap frame, kami tidak dapat merekam pekerjaan frame
berikutnya ke buffer perintah hingga frame saat ini selesai dieksekusi, karena kami tidak ingin
menimpa konten buffer perintah saat ini saat GPU sedang digunakan dia.

Membuat objek sinkronisasi


Kita memerlukan satu semaphore untuk menandakan bahwa sebuah gambar telah diperoleh dari
swapchain dan siap untuk dirender, satu lagi untuk menandakan bahwa rendering telah selesai dan
presentasi dapat dilakukan, dan sebuah pagar untuk memastikan hanya satu frame yang dirender
pada satu waktu .

Buat tiga anggota kelas untuk menyimpan objek semafor dan objek pagar ini:

1 VkSemaphore imageTersediaSemaphore;
2 VkSemaphore renderFinishedSemaphore;
3 VkFence diFlightFence;

Untuk membuat semaphore, kita akan menambahkan fungsi last create untuk bagian tutorial ini:
createSyncObjects:

139
Machine Translated by Google

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger();
4 buat Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9 buatRenderPass();
10 buatGraphicsPipeline();
11 buatFramebuffer();
12 buatCommandPool();
13 buatCommandBuffer();
14 buatSyncObjects();
15 }
16
17 ...
18

19 batal createSyncObjects() {
20
21 }

Membuat semaphore memerlukan pengisian VkSemaphoreCreateInfo, tetapi dalam versi


API saat ini sebenarnya tidak ada bidang yang diperlukan selain sType:

1 batal createSyncObjects()
2 { VkSemaphoreCreateInfo semaphoreInfo{};
3 semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
4}

Versi Vulkan API atau ekstensi yang akan datang dapat menambahkan fungsionalitas untuk
flag dan parameter pNext seperti yang dilakukannya untuk struktur lainnya.

Membuat pagar membutuhkan mengisi VkFenceCreateInfo:


1 VkFenceCreateInfo fenceInfo{}; 2 fenceInfo.sType
= VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;

Membuat semafor dan pagar mengikuti pola yang sudah dikenal dengan vkCreateSemaphore
& vkCreateFence: 1 if (vkCreateSemaphore(device, &semaphoreInfo, nullptr,

&imageAvailableSemaphore) != VK_SUCCESS || vkCreateSemaphore(device, &semaphoreInfo,


nullptr, &renderFinishedSemaphore) != VK_SUCCESS) || vkCreateFence(perangkat,
2 &fenceInfo, nullptr, &inFlightFence) != VK_SUCCESS){

140
Machine Translated by Google

4
5 throw std::runtime_error("gagal membuat semafor!");
6}

Semafor dan pagar harus dibersihkan di akhir program, ketika semua perintah telah selesai
dan tidak diperlukan lagi sinkronisasi:

1 pembersihan batal ()
2 { vkDestroySemaphore(perangkat, imageAvailableSemaphore, nullptr);
3 vkDestroySemaphore(perangkat, renderFinishedSemaphore, nullptr);
4 vkDestroyFence(perangkat, inFlightFence, nullptr);

Ke fungsi gambar utama!

Menunggu bingkai sebelumnya


Di awal frame, kita ingin menunggu hingga frame sebelumnya selesai, sehingga buffer perintah
dan semaphore tersedia untuk digunakan. Untuk melakukannya, kami memanggil
vkWaitForFences:

1 batal drawFrame()
2 { vkWaitForFences(perangkat, 1, &inFlightFence, VK_TRUE, UINT64_MAX);
3}

Fungsi vkWaitForFences mengambil susunan pagar dan menunggu tuan rumah untuk salah
satu atau semua pagar untuk diberi sinyal sebelum kembali. VK_TRUE yang kami lewati di
sini menunjukkan bahwa kami ingin menunggu semua pagar, tetapi untuk satu pagar saja
tidak masalah. Fungsi ini juga memiliki parameter batas waktu yang kami atur ke nilai
maksimum bilangan bulat 64 bit unsigned, UINT64_MAX, yang secara efektif menonaktifkan
batas waktu.

Setelah menunggu, kita perlu mengatur ulang pagar secara manual ke keadaan tidak bersinyal
dengan panggilan vkResetFences:

1 vkResetFences(perangkat, 1, &inFlightFence);

Sebelum kita dapat melanjutkan, ada sedikit kesalahan dalam desain kita. Pada frame pertama
kita memanggil drawFrame(), yang segera menunggu inFlightFence untuk diberi sinyal.
inFlightFence hanya diberi sinyal setelah bingkai selesai dirender, namun karena ini adalah
bingkai pertama, tidak ada bingkai sebelumnya untuk memberi sinyal pagar! Jadi
vkWaitForFences() memblokir tanpa batas waktu, menunggu sesuatu yang tidak akan pernah
terjadi.

Dari sekian banyak solusi untuk dilema ini, ada solusi cerdas yang dibangun di dalam API.
Buat pagar dalam keadaan bersinyal, sehingga panggilan pertama ke vkWaitForFences()
segera kembali karena pagar sudah bersinyal.

Untuk melakukan ini, kami menambahkan bendera VK_FENCE_CREATE_SIGNALED_BIT ke


VkFenceCreateInfo:

141
Machine Translated by Google

1 batal createSyncObjects() {
2 ...
3
4 VkFenceCreateInfo fenceInfo{};
5 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
6 fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
7
8 ...
9}

Memperoleh gambar dari rantai swap


Hal berikutnya yang perlu kita lakukan dalam fungsi drawFrame adalah memperoleh gambar
dari rantai pertukaran. Ingatlah bahwa rantai swap adalah fitur ekstensi, jadi kita harus
menggunakan fungsi dengan konvensi penamaan vk*KHR:

1 batal drawFrame() {
2 uint32_t imageIndex;
3 vkAcquireNextImageKHR(perangkat, swapChain, UINT64_MAX,
imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
4}

Dua parameter pertama vkAcquireNextImageKHR adalah perangkat logis dan rantai


pertukaran tempat kami ingin memperoleh gambar. Parameter ketiga menentukan batas
waktu dalam nanodetik agar gambar tersedia. Menggunakan nilai maksimum 64 bit unsigned
integer berarti kita secara efektif menonaktifkan batas waktu.

Dua parameter berikutnya menentukan objek sinkronisasi yang akan diberi sinyal saat mesin
presentasi selesai menggunakan gambar. Itulah titik waktu di mana kita bisa mulai
menggambarnya. Dimungkinkan untuk menentukan semafor, pagar atau keduanya. Kami
akan menggunakan imageAvailableSemaphore kami untuk tujuan itu di sini.

Parameter terakhir menentukan variabel untuk menampilkan indeks dari gambar rantai swap
yang telah tersedia. Indeks mengacu pada VkImage di array swapChainImages kami. Kami
akan menggunakan indeks itu untuk memilih VkFrameBuffer.

Merekam buffer perintah


Dengan imageIndex menentukan gambar rantai swap untuk digunakan di tangan, kita
sekarang dapat merekam buffer perintah. Pertama, kita memanggil vkResetCommandBuffer
pada buffer perintah untuk memastikannya dapat direkam.

1 vkResetCommandBuffer(commandBuffer, 0);

142
Machine Translated by Google

Parameter kedua vkResetCommandBuffer adalah flag VkCommandBufferResetFlagBits. Karena kami


tidak ingin melakukan sesuatu yang istimewa, kami membiarkannya sebagai 0.
Sekarang panggil fungsi recordCommandBuffer untuk merekam perintah yang kita inginkan.

1 recordCommandBuffer(commandBuffer, imageIndex);

Dengan buffer perintah yang direkam sepenuhnya, sekarang kami dapat mengirimkannya.

Mengirimkan buffer perintah


Pengajuan antrian dan sinkronisasi dikonfigurasi melalui parameter dalam struktur VkSubmitInfo.

1 VkSubmitInfo kirimInfo{}; 2
kirimInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; 3

4 VkSemaphore waitSemaphores[] = {imageAvailableSemaphore}; 5


VkPipelineStageFlags waitStages[] =
{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; 6
kirimInfo.waitSemaphoreCount = 1; 7 submitInfo.pWaitSemaphores =
waitSemaphores; 8 submitInfo.pWaitDstStageMask = waitStages;

Tiga parameter pertama menentukan semaphore mana yang akan menunggu sebelum
eksekusi dimulai dan tahap mana dari pipeline yang akan menunggu. Kami ingin menunggu
dengan menulis warna ke gambar sampai tersedia, jadi kami menentukan tahap pipa grafis
yang menulis ke lampiran warna. Artinya secara teoritis implementasinya sudah bisa mulai
mengeksekusi vertex shader kita dan semacamnya sementara gambarnya belum tersedia.
Setiap entri dalam array waitStages sesuai dengan semaphore dengan indeks yang sama di
pWaitSemaphores.

1 kirimInfo.commandBufferCount = 1; 2
submitInfo.pCommandBuffers = commandBuffer;

Dua parameter berikutnya menentukan buffer perintah mana yang benar-benar dikirim untuk
dieksekusi. Kami cukup mengirimkan buffer perintah tunggal yang kami miliki.

1 VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; 2


kirimInfo.signalSemaphoreCount = 1; 3 submitInfo.pSignalSemaphores =
signalSemaphores;

Parameter signalSemaphoreCount dan pSignalSemaphores menentukan semaphore mana


yang akan diberi sinyal setelah buffer perintah selesai dieksekusi. Dalam kasus kami, kami
menggunakan renderFinishedSemaphore untuk tujuan itu.

1 jika (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) !=


VK_SUKSES) {

143
Machine Translated by Google

2 throw std::runtime_error("gagal mengirimkan buffer perintah undian!");

3}

Kami sekarang dapat mengirimkan buffer perintah ke antrean grafis menggunakan


vkQueueSubmit. Fungsi mengambil larik struktur VkSubmitInfo sebagai argumen untuk
efisiensi saat beban kerja jauh lebih besar. Parameter terakhir mereferensikan pagar
opsional yang akan diberi sinyal saat buffer perintah menyelesaikan eksekusi. Ini
memungkinkan kami untuk mengetahui kapan buffer perintah aman untuk digunakan
kembali, jadi kami ingin memberikannya diFlightFence. Sekarang pada frame berikutnya,
CPU akan menunggu buffer perintah ini selesai dieksekusi sebelum merekam perintah
baru ke dalamnya.

Subpass dependensi
Ingatlah bahwa subpass dalam render pass secara otomatis menangani transisi tata letak
gambar. Transisi ini dikontrol oleh dependensi subpass, yang menentukan dependensi
memori dan eksekusi di antara subpass. Kami hanya memiliki satu subpass saat ini, tetapi
operasi tepat sebelum dan tepat setelah subpass ini juga dihitung sebagai "subpass"
implisit.

Ada dua dependensi bawaan yang menangani transisi di awal render pass dan di akhir
render pass, tetapi yang pertama tidak terjadi pada waktu yang tepat. Diasumsikan bahwa
transisi terjadi pada awal pipa, tetapi kami belum mendapatkan gambarnya pada saat itu!
Ada dua cara untuk mengatasi masalah ini.

Kita dapat mengubah waitStages untuk imageAvailableSemaphore menjadi


VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT untuk memastikan bahwa proses render tidak
dimulai hingga gambar tersedia, atau kita dapat membuat proses render menunggu untuk
tahap VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT. Saya telah
memutuskan untuk menggunakan opsi kedua di sini, karena ini adalah alasan yang bagus
untuk melihat dependensi subpass dan cara kerjanya.

Ketergantungan subpass ditentukan dalam struktur VkSubpassDependency. Buka fungsi


createRenderPass dan tambahkan satu:

1 ketergantungan VkSubpassDependency{};
2 dependensi.srcSubpass = VK_SUBPASS_EXTERNAL; 3
dependensi.dstSubpass = 0;

Dua bidang pertama menentukan indeks dependensi dan subpass dependen. Nilai khusus
VK_SUBPASS_EXTERNAL mengacu pada subpass implisit sebelum atau setelah render
pass tergantung pada apakah itu ditentukan dalam srcSubpass atau dstSubpass. Indeks 0
mengacu pada subpass kami, yang merupakan yang pertama dan satu-satunya.
dstSubpass harus selalu lebih tinggi dari srcSubpass untuk mencegah siklus dalam grafik
dependensi (kecuali salah satu subpass adalah VK_SUBPASS_EXTERNAL).

144
Machine Translated by Google

1 dependensi.srcStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
2 dependensi.srcAccessMask = 0;

Dua bidang berikutnya menentukan operasi untuk menunggu dan tahapan di mana operasi
ini terjadi. Kita harus menunggu rantai swap selesai membaca dari gambar sebelum kita
dapat mengaksesnya. Hal ini dapat dilakukan dengan menunggu pada tahap keluaran
lampiran warna itu sendiri.

1 dependensi.dstStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
2 dependensi.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

Operasi yang harus menunggu ini berada di tahap penempelan warna dan melibatkan
penulisan penempelan warna. Pengaturan ini akan mencegah terjadinya transisi hingga
benar-benar diperlukan (dan diizinkan): saat kita ingin mulai menulis warna padanya.

1 renderPassInfo.dependencyCount = 1; 2
renderPassInfo.pDependencies = &ketergantungan;

Struktur VkRenderPassCreateInfo memiliki dua kolom untuk menentukan array dependensi.

Presentasi
Langkah terakhir menggambar bingkai adalah mengirimkan hasilnya kembali ke rantai
pertukaran agar akhirnya muncul di layar. Presentasi dikonfigurasikan melalui struktur
VkPresentInfoKHR di akhir fungsi drawFrame.

1 VkPresentInfoKHR presentInfo{}; 2
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
3
4 presentInfo.waitSemaphoreCount = 1; 5
presentInfo.pWaitSemaphores = signalSemaphores;

Dua parameter pertama menentukan semafor mana yang akan ditunggu sebelum
presentasi dapat terjadi, seperti halnya VkSubmitInfo. Karena kita ingin menunggu buffer
perintah untuk menyelesaikan eksekusi, sehingga segitiga kita tergambar, kita mengambil
semaphore yang akan diberi sinyal dan menunggunya, jadi kita menggunakan signalSemaphores.

1 VkSwapchainKHR swapChains[] = {swapChain}; 2


presentInfo.swapchainCount = 1; 3 presentInfo.pSwapchains
= swapChains; 4 presentInfo.pImageIndices = &imageIndex;

Dua parameter berikutnya menentukan rantai pertukaran untuk menampilkan gambar dan
indeks gambar untuk setiap rantai pertukaran. Ini hampir selalu menjadi satu.

145
Machine Translated by Google

1 presentInfo.pResults = nullptr; // Opsional

Ada satu parameter opsional terakhir yang disebut pResults. Ini memungkinkan Anda menentukan
larik nilai VkResult untuk memeriksa setiap rantai pertukaran individual jika presentasi berhasil.
Tidak perlu jika Anda hanya menggunakan satu rantai swap, karena Anda cukup menggunakan
nilai kembalian dari fungsi sekarang.

1 vkQueuePresentKHR(presentQueue, &presentInfo);

Fungsi vkQueuePresentKHR mengirimkan permintaan untuk menyajikan gambar ke rantai


pertukaran. Kita akan menambahkan penanganan kesalahan untuk vkAcquireNextImageKHR dan
vkQueuePresentKHR di bab berikutnya, karena kegagalannya tidak berarti program harus
dihentikan, tidak seperti fungsi yang telah kita lihat sejauh ini.

Jika Anda melakukan semuanya dengan benar sampai saat ini, maka Anda sekarang akan melihat
sesuatu yang menyerupai berikut ketika Anda menjalankan program Anda:

Segitiga berwarna ini mungkin terlihat sedikit berbeda dari yang biasa Anda lihat di
tutorial grafis. Itu karena tutorial ini memungkinkan shader melakukan interpolasi
dalam ruang warna linier dan mengonversi ke ruang warna sRGB sesudahnya. Lihat
posting blog ini untuk diskusi tentang perbedaannya.

146
Machine Translated by Google

Hore! Sayangnya, Anda akan melihat bahwa saat lapisan validasi diaktifkan, program macet
segera setelah Anda menutupnya. Pesan yang dicetak ke terminal dari debugCallback
memberi tahu kami alasannya:

Ingatlah bahwa semua operasi di drawFrame tidak sinkron. Itu berarti bahwa ketika kita
keluar dari loop di mainLoop, operasi menggambar dan presentasi mungkin masih
berlangsung. Membersihkan sumber daya saat itu terjadi adalah ide yang buruk.

Untuk memperbaiki masalah itu, kita harus menunggu perangkat logis menyelesaikan
operasi sebelum keluar dari mainLoop dan menghancurkan jendela:

1 void mainLoop() { while


2 (!glfwWindowShouldClose(window)) { glfwPollEvents();
3 drawFrame();
4
5 }
6
7 vkDeviceWaitIdle(perangkat);
8}

Anda juga dapat menunggu operasi dalam antrean perintah tertentu selesai dengan
vkQueueWaitIdle. Fungsi-fungsi ini dapat digunakan sebagai cara yang sangat mendasar
untuk melakukan sinkronisasi. Anda akan melihat bahwa program sekarang keluar tanpa
masalah saat menutup jendela.

Kesimpulan

Sedikit lebih dari 900 baris kode kemudian, kita akhirnya sampai pada tahap melihat sesuatu
muncul di layar! Bootstrap program Vulkan jelas merupakan pekerjaan yang sulit, tetapi
pesan yang dapat diambil adalah bahwa Vulkan memberi Anda kontrol yang sangat besar
melalui kejelasannya. Saya menyarankan Anda untuk meluangkan waktu sekarang untuk
membaca ulang kode dan membangun model mental dari tujuan semua objek Vulkan dalam
program dan bagaimana mereka berhubungan satu sama lain. Kami akan membangun di
atas pengetahuan itu untuk memperluas fungsionalitas program mulai saat ini.

Bab selanjutnya akan memperluas loop render untuk menangani banyak frame dalam penerbangan.

Kode C++ / Vertex shader / Fragment shader

147
Machine Translated by Google

Bingkai dalam penerbangan

Bingkai dalam penerbangan

Saat ini loop render kami memiliki satu kekurangan yang mencolok. Kita diminta untuk menunggu
frame sebelumnya selesai sebelum kita dapat memulai rendering berikutnya yang mengakibatkan
pemalasan host yang tidak perlu.

Cara untuk memperbaikinya adalah dengan memungkinkan beberapa frame untuk di-flight sekaligus,
artinya, memungkinkan rendering satu frame tidak mengganggu perekaman berikutnya. Bagaimana
kita melakukan ini? Sumber daya apa pun yang diakses dan dimodifikasi selama rendering harus
digandakan. Jadi, kita membutuhkan banyak buffer perintah, semafor, dan pagar. Di bab-bab
selanjutnya kita juga akan menambahkan banyak contoh sumber daya lain, jadi kita akan melihat
konsep ini muncul kembali.

Mulailah dengan menambahkan sebuah konstanta di bagian atas program yang menentukan berapa
banyak frame yang harus diproses secara bersamaan:

1 const int MAX_FRAMES_IN_FLIGHT = 2;

Kami memilih nomor 2 karena kami tidak ingin CPU terlalu jauh di depan GPU. Dengan 2 frame
dalam penerbangan, CPU dan GPU dapat mengerjakan tugasnya sendiri secara bersamaan. Jika
CPU selesai lebih awal, CPU akan menunggu hingga GPU selesai merender sebelum mengirimkan
lebih banyak pekerjaan. Dengan 3 bingkai atau lebih dalam penerbangan, CPU dapat mengungguli
GPU, menambahkan bingkai latensi.
Umumnya, latensi ekstra tidak diinginkan. Tetapi memberikan kontrol aplikasi atas jumlah frame
dalam penerbangan adalah contoh lain dari Vulkan yang eksplisit.

Setiap frame harus memiliki buffer perintah, kumpulan semafor, dan pagar sendiri.
Ganti nama lalu ubah menjadi std::vectors dari objek:

1 std::vector<VkCommandBuffer> commandBuffers;
2
3 ...
4
5 std::vector<VkSemaphore> imageAvailableSemaphores; 6
std::vector<VkSemaphore> renderFinishedSemaphores; 7
std::vector<VkFence> diFlightFences;

148
Machine Translated by Google

Kemudian kita perlu membuat beberapa buffer perintah. Ganti nama createCommandBuffer
menjadi createCommandBuffers. Selanjutnya kita perlu mengubah ukuran vektor buffer perintah
menjadi ukuran MAX_FRAMES_IN_FLIGHT, mengubah VkCommandBufferAllocateInfo agar
memuat banyak buffer perintah, dan kemudian mengubah tujuan ke vektor buffer perintah kita:

1 batal buatCommandBuffers() {
2 commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
3 ...
4 allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();
5
6 if (vkAllocateCommandBuffers(device, &allocInfo,
commandBuffers.data()) != VK_SUCCESS) { throw
7 std::runtime_error("gagal mengalokasikan perintah
penyangga!");
8 }
9}

Fungsi createSyncObjects harus diubah untuk membuat semua objek:

1 batal createSyncObjects()
2 { imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
3 renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
4 inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
5
6 VkSemaphoreCreateInfo semaphoreInfo{};
7 semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
8
9 VkFenceCreateInfo fenceInfo{};
10 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
11 fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
12
13 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
14 if (vkCreateSemaphore(perangkat, &semaphoreInfo, nullptr,
&imageAvailableSemaphores[i]) != VK_SUCCESS ||
15 vkCreateSemaphore(perangkat, &semaphoreInfo, nullptr,
&renderFinishedSemaphores[i]) != VK_SUCCESS ||
16 vkCreateFence(perangkat, &fenceInfo, nullptr,
&inFlightFences[i]) != VK_SUCCESS) {
17
18 throw std::runtime_error("gagal membuat objek
sinkronisasi untuk sebuah bingkai!");
19 }
}
20 21 }

Demikian pula, mereka semua juga harus dibersihkan:

149
Machine Translated by Google

1 pembersihan batal ()
2 { untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
3 vkDestroySemaphore(perangkat, renderFinishedSemaphores[i], nullptr);
vkDestroySemaphore(perangkat, imageAvailableSemaphores[i],
4 nullptr); vkDestroyFence(perangkat, inFlightFences[i], nullptr);

5
6 }
7
8 ...
9}

Ingat, karena buffer perintah dibebaskan untuk kita saat kita membebaskan kumpulan perintah,
tidak ada tambahan yang harus dilakukan untuk pembersihan buffer perintah.

Untuk menggunakan objek yang tepat di setiap frame, kita perlu melacak frame saat ini.
Kami akan menggunakan indeks bingkai untuk tujuan itu:

1 uint32_t bingkai saat ini = 0;

Fungsi drawFrame sekarang dapat dimodifikasi untuk menggunakan objek yang tepat:

1 batal drawFrame()
2 { vkWaitForFences(perangkat, 1, &inFlightFences[currentFrame],
VK_TRUE, UINT64_MAX);
3 vkResetFences(perangkat, 1, &inFlightFences[BingkaiArus]);
4
5 vkAcquireNextImageKHR(perangkat, swapChain, UINT64_MAX,
imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE,
&imageIndex);
6
7 ...
8
9 vkResetCommandBuffer(commandBuffers[BingkaiArus], 0);
10 recordCommandBuffer(commandBuffers[CurrentFrame], imageIndex);
11
12 ...
13
14 submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
15
16 ...
17
18 VkSemaphore waitSemaphores[] =
{imageAvailableSemaphores[currentFrame]};
19
20 ...
21

150
Machine Translated by Google

22 VkSemaphore signalSemaphores[] =
{renderFinishedSemaphores[currentFrame]};
23
24 ...
25
26 jika (vkQueueSubmit(graphicsQueue, 1, &submitInfo,
inFlightFences[currentFrame]) != VK_SUCCESS) {
27 }

Tentu saja, kita tidak boleh lupa untuk maju ke bingkai berikutnya setiap saat:

1 batal drawFrame() {
2 ...
3
4 currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
5}

Dengan menggunakan operator modulo (%), kami memastikan bahwa indeks frame berputar
setelah setiap frame yang diantrekan MAX_FRAMES_IN_FLIGHT.

Kami sekarang telah mengimplementasikan semua sinkronisasi yang diperlukan untuk


memastikan bahwa tidak ada lebih dari MAX_FRAMES_IN_FLIGHT frame pekerjaan yang
diantrekan dan bahwa frame ini tidak saling melangkahi. Perhatikan bahwa tidak apa-apa
untuk bagian lain dari kode, seperti pembersihan akhir, untuk mengandalkan sinkronisasi
yang lebih kasar seperti vkDeviceWaitIdle. Anda harus memutuskan pendekatan mana yang
akan digunakan berdasarkan persyaratan kinerja.

Untuk mempelajari lebih lanjut tentang sinkronisasi melalui contoh, lihat ikhtisar ekstensif ini
oleh Khronos.

Di bab selanjutnya kita akan membahas satu hal kecil lagi yang diperlukan untuk program
Vulkan yang berperilaku baik.

Kode C++ / Vertex shader / Fragment shader

151
Machine Translated by Google

Rekreasi rantai tukar


pengantar

Aplikasi yang kita miliki sekarang berhasil menggambar segitiga, tetapi ada beberapa keadaan yang
belum ditangani dengan baik. Permukaan jendela dapat berubah sedemikian rupa sehingga rantai
pertukaran tidak lagi kompatibel dengannya. Salah satu alasan yang dapat menyebabkan hal ini
terjadi adalah ukuran jendela yang berubah.
Kami harus menangkap peristiwa ini dan membuat ulang rantai pertukaran.

Membuat ulang rantai swap


Buat fungsi recreateSwapChain baru yang memanggil createSwapChain dan semua fungsi
pembuatan untuk objek yang bergantung pada rantai swap atau ukuran jendela.

1 batal buat ulangSwapChain() {


2 vkDeviceWaitIdle(perangkat);
3
4 buatSwapChain();
5 createImageViews();
6 buatRenderPass();
7 buatGraphicsPipeline();
8 buatFramebuffer();
9}

Pertama-tama kita panggil vkDeviceWaitIdle, karena sama seperti di bab sebelumnya, kita tidak
boleh menyentuh resource yang mungkin masih digunakan. Jelas, hal pertama yang harus kita
lakukan adalah membuat ulang rantai pertukaran itu sendiri. Tampilan gambar perlu dibuat ulang
karena didasarkan langsung pada gambar rantai pertukaran. Render pass perlu dibuat ulang karena
bergantung pada format gambar rantai pertukaran. Format gambar rantai swap jarang berubah
selama operasi seperti pengubahan ukuran jendela, tetapi masih harus ditangani. Ukuran viewport
dan scissor persegi panjang ditentukan selama pembuatan pipeline grafik, sehingga pipeline juga
perlu dibangun kembali. Dimungkinkan untuk menghindari ini dengan menggunakan dinamis

152
Machine Translated by Google

state untuk area pandang dan persegi panjang gunting. Terakhir, framebuffer secara langsung
bergantung pada gambar rantai swap.

Untuk memastikan bahwa versi lama dari objek ini dibersihkan sebelum membuatnya
kembali, kita harus memindahkan beberapa kode pembersihan ke fungsi terpisah yang
dapat kita panggil dari fungsi recreateSwapChain. Sebut saja cleanupSwapChain:

1 batal pembersihanSwapChain() {
2
3}
4
5 batal buat ulangSwapChain()
{ vkDeviceWaitIdle(perangkat); 6
7
8 cleanupSwapChain();
9
10 buatSwapChain();
11 createImageViews();
12 buatRenderPass();
13 buatGraphicsPipeline();
14 buatFramebuffer();
15 }

kami akan memindahkan kode pembersihan semua objek yang dibuat ulang sebagai bagian dari
penyegaran rantai pertukaran dari pembersihan ke pembersihanSwapChain:

1 batal cleanupSwapChain()
2 { untuk (size_t i = 0; i < swapChainFramebuffers.size(); i++)
3 { vkDestroyFramebuffer(perangkat, swapChainFramebuffers[i], nullptr);

4 }
5
6 vkDestroyPipeline(perangkat, graphicsPipeline, nullptr);
7 vkDestroyPipelineLayout(perangkat, pipelineLayout, nullptr);
8 vkDestroyRenderPass(perangkat, renderPass, nullptr);
9
10 untuk (size_t i = 0; i < swapChainImageViews.size(); i++) {
11 vkDestroyImageView(perangkat, swapChainImageViews[i], nullptr);
12 }
13
14 vkDestroySwapchainKHR(perangkat, swapChain, nullptr);
15 }
16
17 pembersihan batal () {
18 cleanupSwapChain();
19

153
Machine Translated by Google

20 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {


21 vkDestroySemaphore(perangkat, renderFinishedSemaphores[i], nullptr);
vkDestroySemaphore(perangkat, imageAvailableSemaphores[i],
22 nullptr); vkDestroyFence(perangkat, inFlightFences[i], nullptr);

23
24 }
25
26 vkDestroyCommandPool(perangkat, commandPool, nullptr);
27
28 vkDestroyDevice(perangkat, nullptr);
29
30 if (enableValidationLayers) {
31 HancurkanDebugUtilsMessengerEXT(instance, debugMessenger,
nullptr);
32 }
33
34 vkDestroySurfaceKHR(instance, permukaan, nullptr);
35 vkDestroyInstance(contoh, nullptr);
36
37 glfwDestroyWindow(jendela);
38
39 glfwHentikan();
40 }

Perhatikan bahwa di chooseSwapExtent kami sudah meminta resolusi jendela baru untuk
memastikan bahwa gambar rantai swap memiliki ukuran (baru) yang tepat, jadi tidak perlu
mengubah pilihSwapExtent (ingat bahwa kami sudah harus menggunakan
glfwGetFramebufferSize untuk mendapatkan resolusi permukaan di piksel saat membuat
rantai swap).

Hanya itu yang diperlukan untuk membuat ulang rantai pertukaran! Namun, kerugian dari
pendekatan ini adalah kita harus menghentikan semua rendering sebelum membuat rantai
pertukaran baru. Dimungkinkan untuk membuat rantai swap baru saat menggambar perintah
pada gambar dari rantai swap lama masih dalam penerbangan. Anda harus meneruskan
rantai swap sebelumnya ke bidang oldSwapChain di struktur VkSwapchainCreateInfoKHR
dan menghancurkan rantai swap lama segera setelah Anda selesai menggunakannya.

Rantai pertukaran yang kurang optimal atau kedaluwarsa

Sekarang kita hanya perlu mencari tahu kapan rekreasi rantai swap diperlukan dan
memanggil fungsi recreateSwapChain baru kita. Untungnya, Vulkan biasanya hanya
memberi tahu kami bahwa rantai pertukaran tidak lagi memadai selama presentasi. Fungsi
vkAcquireNextImageKHR dan vkQueuePresentKHR dapat mengembalikan nilai khusus
berikut untuk menunjukkan hal ini.

154
Machine Translated by Google

• VK_ERROR_OUT_OF_DATE_KHR: Rantai pertukaran menjadi tidak kompatibel dengan


permukaan dan tidak lagi dapat digunakan untuk rendering. Biasanya terjadi setelah
mengubah ukuran jendela. • VK_SUBOPTIMAL_KHR: Rantai swap masih dapat digunakan
untuk berhasil mempresentasikan ke permukaan, tetapi properti permukaan tidak lagi sama
persis.

1 Hasil VkResult = vkAcquireNextImageKHR(perangkat, swapChain,


UINT64_MAX, imageAvailableSemaphores[currentFrame],
VK_NULL_HANDLE, &indeksgambar);
2
3 if (hasil == VK_ERROR_OUT_OF_DATE_KHR)
4 { recreateSwapChain(); kembali;
5
6 } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
throw std::runtime_error("gagal memperoleh gambar rantai swap!");
78}

Jika rantai pertukaran ternyata kedaluwarsa saat mencoba memperoleh gambar, maka tidak
mungkin lagi menampilkannya. Oleh karena itu kita harus segera membuat ulang rantai swap dan
coba lagi di panggilan drawFrame berikutnya.

Anda juga dapat memutuskan untuk melakukan itu jika rantai pertukaran kurang optimal, tetapi
saya telah memilih untuk tetap melanjutkan dalam kasus itu karena kami telah memperoleh gambar.
Baik VK_SUCCESS dan VK_SUBOPTIMAL_KHR dianggap sebagai kode pengembalian “sukses”.

1 hasil = vkQueuePresentKHR(presentQueue, &presentInfo);


2
3 if (hasil == VK_ERROR_OUT_OF_DATE_KHR || hasil ==
VK_SUBOPTIMAL_KHR) { recreateSwapChain(); 5 } else if (hasil !
4 = VK_SUCCESS) {

6 throw std::runtime_error("gagal menampilkan gambar rantai swap!");


7}
8
9 BingkaiArus = (BingkaiArus + 1) % MAX_FRAMES_IN_FLIGHT;

Fungsi vkQueuePresentKHR mengembalikan nilai yang sama dengan arti yang sama. Dalam hal
ini kami juga akan membuat ulang rantai swap jika kurang optimal, karena kami menginginkan hasil
terbaik.

Memperbaiki kebuntuan
Jika kami mencoba menjalankan kode sekarang, kemungkinan akan menemui jalan buntu. Setelah
menyadap kode, kami menemukan bahwa aplikasi mencapai vkWaitForFences tetapi tidak pernah
melewatinya. Ini karena ketika vkAcquireNextImageKHR mengembalikan
VK_ERROR_OUT_OF_DATE_KHR, kami membuat ulang rantai pertukaran dan kemudian kembali dari

155
Machine Translated by Google

drawFrame. Tapi sebelum itu terjadi, pagar bingkai saat ini ditunggu dan disetel ulang. Karena
kami segera kembali, tidak ada pekerjaan yang dikirimkan untuk dieksekusi dan pagar tidak akan
pernah diberi sinyal, menyebabkan vkWaitForFences berhenti selamanya.

Syukurlah ada perbaikan sederhana. Tunda pengaturan ulang pagar sampai setelah kita tahu pasti
kita akan mengirimkan pekerjaan dengannya. Jadi, jika kita kembali lebih awal, pagar masih diberi
sinyal dan vkWaitForFences tidak akan menemui jalan buntu saat berikutnya kita menggunakan
objek pagar yang sama.

Awal drawFrame sekarang akan terlihat seperti ini:

1 vkWaitForFences(perangkat, 1, &inFlightFences[currentFrame], VK_TRUE,


UINT64_MAX);
2

3 uint32_t imageIndex;
4 Hasil VkResult = vkAcquireNextImageKHR(perangkat, swapChain,
UINT64_MAX, imageAvailableSemaphores[currentFrame],
VK_NULL_HANDLE, &indeksgambar);
5
6 if (hasil == VK_ERROR_OUT_OF_DATE_KHR)
7 { recreateSwapChain(); kembali;
8
9 } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
10 throw std::runtime_error("gagal memperoleh gambar rantai swap!");
11 }
12

13 // Setel ulang pagar hanya jika kami mengirimkan pekerjaan 14


vkResetFences(device, 1, &inFlightFences[currentFrame]);

Penanganan mengubah ukuran secara eksplisit

Meskipun banyak driver dan platform memicu VK_ERROR_OUT_OF_DATE_KHR secara otomatis


setelah pengubahan ukuran jendela, ini tidak dijamin akan terjadi. Itu sebabnya kami akan menambahkan
beberapa kode tambahan untuk juga menangani pengubahan ukuran secara eksplisit. Pertama, tambahkan
variabel anggota baru yang menandai bahwa perubahan ukuran telah terjadi:

1 std::vector<VkFence> diFlightFences; 2

3 bool framebufferResized = false;

Fungsi drawFrame kemudian harus dimodifikasi untuk juga memeriksa bendera ini:

1 if (hasil == VK_ERROR_OUT_OF_DATE_KHR || hasil ==


VK_SUBOPTIMAL_KHR || framebufferResized) { framebufferResized
2 = false; buat ulangSwapChain(); 4 } else if (hasil != VK_SUCCESS)
3 {

156
Machine Translated by Google

5 ...
6}

Penting untuk melakukan ini setelah vkQueuePresentKHR untuk memastikan bahwa semafor
berada dalam status yang konsisten, jika tidak, semafor bersinyal mungkin tidak akan pernah
ditunggu dengan benar. Sekarang untuk benar-benar mendeteksi perubahan ukuran, kita dapat
menggunakan fungsi glfwSetFramebufferSizeCallback di kerangka kerja GLFW untuk
menyiapkan panggilan balik:
1 void initWindow()
2 { glfwInit();
3
4 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
5
6 jendela = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr,
nullptr);
7 glfwSetFramebufferSizeCallback(jendela,
framebufferResizeCallback);
8}
9
10 framebufferResizeCallback statis kosong (jendela GLFWwindow *, lebar int ,
int tinggi) {
11
12 }

Alasan kami membuat fungsi statis sebagai callback adalah karena GLFW tidak tahu
cara memanggil fungsi anggota dengan tepat menggunakan penunjuk this ke instance
HelloTriangleApplication kami.

Namun, kami mendapatkan referensi ke GLFWwindow di callback dan ada fungsi GLFW
lain yang memungkinkan Anda menyimpan penunjuk arbitrer di dalamnya:
glfwSetWindowUserPointer:
1 jendela = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); 2
glfwSetWindowUserPointer(jendela, ini); 3 glfwSetFramebufferSizeCallback(window,
framebufferResizeCallback);

Nilai ini sekarang dapat diambil dari dalam callback dengan glfwGetWindowUserPointer untuk
mengatur flag dengan benar:
1 static void framebufferResizeCallback(jendela GLFWwindow*, lebar int, tinggi int )
{ aplikasi otomatis =
2
reinterpret_cast<HelloTriangleApplication*>(glfwGetWindowUserPointer(window)); app-
3 >framebufferResized = true;
4}

Sekarang coba jalankan program dan ubah ukuran jendela untuk melihat apakah framebuffer
benar-benar diubah ukurannya dengan jendela.

157
Machine Translated by Google

Penanganan minimalisasi
Ada kasus lain di mana rantai swap mungkin sudah ketinggalan zaman dan itu adalah
jenis khusus pengubahan ukuran jendela: minimalisasi jendela. Kasus ini istimewa
karena akan menghasilkan ukuran penyangga bingkai sebesar 0. Dalam tutorial ini kita
akan mengatasinya dengan menjeda hingga jendela berada di latar depan lagi dengan
memperluas fungsi recreateSwapChain:

1 batal buat ulangSwapChain() { 2


int lebar = 0, tinggi = 0;
3 glfwGetFramebufferSize(jendela, &lebar, &tinggi); while (lebar
4 == 0 || tinggi == 0) {
5 glfwGetFramebufferSize(jendela, &lebar, &tinggi);
6 glfwWaitEvents();
7 }
8
9 vkDeviceWaitIdle(perangkat);
10
11 ...
12 }

Panggilan awal ke glfwGetFramebufferSize menangani kasus di mana ukurannya sudah


benar dan glfwWaitEvents tidak perlu menunggu.

Selamat, Anda sekarang telah menyelesaikan program Vulkan berperilaku baik pertama
Anda! Pada bab selanjutnya kita akan menghilangkan vertex yang di-hardcode di vertex
shader dan benar-benar menggunakan buffer vertex.

Kode C++ / Vertex shader / Fragment shader

158
Machine Translated by Google

Deskripsi masukan titik

pengantar

Dalam beberapa bab berikutnya, kita akan mengganti data vertex yang di-hardcode di vertex
shader dengan buffer vertex di memori. Kita akan mulai dengan pendekatan termudah untuk
membuat buffer terlihat CPU dan menggunakan memcpy untuk menyalin data vertex ke
dalamnya secara langsung, dan setelah itu kita akan melihat bagaimana menggunakan buffer
staging untuk menyalin data vertex ke memori performa tinggi.

Shader vertex

Pertama, ubah vertex shader agar tidak lagi menyertakan data vertex dalam kode shader itu
sendiri. Vertex shader mengambil input dari buffer vertex menggunakan kata kunci in.

1 #versi 450
2

3 tata letak(lokasi = 0) di vec2 inPosition; 4 tata letak(lokasi = 1) di


vec3 inColor; 5

6 tata letak(lokasi = 0) keluar vec3 fragColor; 7

8 batal utama() {
9 gl_Position = vec4(inPosition, 0.0, 1.0); fragColor =
10 inColor;
11 }

Variabel inPosition dan inColor adalah atribut vertex. Itu adalah properti yang ditentukan per-
vertex dalam buffer vertex, sama seperti kita secara manual menentukan posisi dan warna per
vertex menggunakan dua larik. Pastikan untuk mengkompilasi ulang vertex shader!

Sama seperti fragColor, anotasi layout(location = x) menetapkan indeks ke input yang nantinya
dapat kita gunakan untuk mereferensikannya. Penting untuk mengetahui hal itu

159
Machine Translated by Google

beberapa jenis, seperti vektor dvec3 64 bit, menggunakan banyak slot. Itu berarti indeks
setelahnya harus minimal 2 lebih tinggi:

1 tata letak(lokasi = 0) di dvec3 inPosition; 2 tata


letak(lokasi = 2) di vec3 inColor;

Anda dapat menemukan info lebih lanjut tentang kualifikasi tata letak di wiki OpenGL.

Data simpul

Kami sedang memindahkan data vertex dari kode shader ke array dalam kode program
kami. Mulailah dengan menyertakan perpustakaan GLM, yang memberi kita jenis terkait
aljabar linier seperti vektor dan matriks. Kita akan menggunakan tipe ini untuk menentukan
vektor posisi dan warna.

1 #sertakan <glm/glm.hpp>

Buat struktur baru bernama Vertex dengan dua atribut yang akan kita gunakan di shader
vertex di dalamnya:

1 struct Puncak
2 { glm::vec2 pos;
3 glm::vec3 warna;
4 };

GLM dengan mudah memberi kita tipe C++ yang sama persis dengan tipe vektor yang
digunakan dalam bahasa shader.
1 const std::vector<Vertex> simpul = {
2 {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}}, {{0.5f, 0.5f}, {0.0f,
3 1.0f, 0.0f}}, {{-0.5f, 0.5 f}, {0.0f, 0.0f, 1.0f}}
4
5 };

Sekarang gunakan struktur Vertex untuk menentukan larik data vertex. Kami menggunakan
nilai posisi dan warna yang persis sama seperti sebelumnya, tetapi sekarang digabungkan
menjadi satu larik simpul. Ini dikenal sebagai interleaving vertex atribut.

Deskripsi yang mengikat

Langkah selanjutnya adalah memberi tahu Vulkan cara meneruskan format data ini ke
vertex shader setelah diunggah ke memori GPU. Ada dua jenis struktur yang diperlukan
untuk menyampaikan informasi ini.

Struktur pertama adalah VkVertexInputBindingDescription dan kita akan menambahkan


fungsi anggota ke struktur Vertex untuk mengisinya dengan data yang tepat.

160
Machine Translated by Google

1 struct Puncak
2 { glm::vec2 pos;
3 glm::vec3 warna;
4
5 static VkVertexInputBindingDescription getBindingDescription() {
6 VkVertexInputBindingDescription bindingDescription{};
7
8 return bindingDescription;
9 }
10 };

Pengikatan simpul menjelaskan kecepatan memuat data dari memori di seluruh simpul. Ini
menentukan jumlah byte antara entri data dan apakah akan pindah ke entri data berikutnya setelah
setiap titik atau setelah setiap instance.

1 VkVertexInputBindingDescription bindingDescription{}; 2
bindingDescription.binding = 0; 3 bindingDescription.stride = sizeof(Vertex); 4
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

Semua data per-vertex kami dikemas bersama dalam satu larik, jadi kami hanya akan memiliki satu
pengikatan. Parameter pengikatan menentukan indeks pengikatan dalam larik pengikatan. Parameter
stride menentukan jumlah byte dari satu entri ke entri berikutnya, dan parameter inputRate dapat
memiliki salah satu dari nilai berikut:

• VK_VERTEX_INPUT_RATE_VERTEX: Pindah ke entri data berikutnya setelah masing-masing


puncak

• VK_VERTEX_INPUT_RATE_INSTANCE: Pindah ke entri data berikutnya setelahnya


setiap contoh

Kami tidak akan menggunakan perenderan instan, jadi kami akan tetap menggunakan data per-vertex.

Deskripsi atribut
Struktur kedua yang menjelaskan cara menangani input vertex adalah VkVertexInputAttributeDescription.
Kami akan menambahkan fungsi pembantu lain ke Vertex untuk mengisi struct ini.

1 #termasuk <array> 2

3 ...
4
5 statis std::array<VkVertexInputAttributeDescription, 2>
getAttributeDescriptions()
6 { std::array<VkVertexInputAttributeDescription, 2>
atributKeterangan{};

161
Machine Translated by Google

7
8 return atributDeskripsi;
9}

Seperti yang ditunjukkan oleh prototipe fungsi, akan ada dua dari struktur ini.
Struktur deskripsi atribut menjelaskan cara mengekstraksi atribut simpul dari potongan data
simpul yang berasal dari deskripsi yang mengikat. Kami memiliki dua atribut, posisi dan
warna, jadi kami membutuhkan dua struct deskripsi atribut.

1 atributDeskripsi[0].binding = 0; 2
atributDeskripsi[0].lokasi = 0; 3
atributKeterangan[0].format = VK_FORMAT_R32G32_SFLOAT; 4
atributDeskripsi[0].offset = offsetof(Vertex, pos);

Parameter binding memberi tahu Vulkan dari mana binding data per-vertex berasal. Parameter
lokasi mereferensikan arahan lokasi input di vertex shader. Masukan pada vertex shader
dengan lokasi 0 adalah posisi, yang memiliki dua komponen float 32-bit.

Parameter format menjelaskan jenis data untuk atribut. Agak membingungkan, format
ditentukan menggunakan pencacahan yang sama dengan format warna.
Jenis dan format shader berikut biasanya digunakan bersama:

• mengambang:
VK_FORMAT_R32_SFLOAT • vec2:
VK_FORMAT_R32G32_SFLOAT • vec3:
VK_FORMAT_R32G32B32_SFLOAT • vec4: VK_FORMAT_R32G32B32A32_SFLOAT

Seperti yang Anda lihat, Anda harus menggunakan format di mana jumlah saluran warna
cocok dengan jumlah komponen dalam tipe data shader. Diperbolehkan menggunakan lebih
banyak saluran daripada jumlah komponen di shader, tetapi saluran tersebut akan dibuang
secara diam-diam. Jika jumlah saluran lebih rendah dari jumlah komponen, maka komponen
BGA akan menggunakan nilai default (0, 0, 1).
Jenis warna (SFLOAT, UINT, SINT) dan lebar bit juga harus sesuai dengan jenis input shader.
Lihat contoh berikut:

• ivec2: VK_FORMAT_R32G32_SINT, vektor 2 komponen bertanda tangan 32 bit


bilangan bulat

• uvec4: VK_FORMAT_R32G32B32A32_UINT, vektor 4 komponen dari bilangan bulat 32


bit tak bertanda
• ganda: VK_FORMAT_R64_SFLOAT, pelampung presisi ganda (64-bit)

Parameter format secara implisit menentukan ukuran byte dari data atribut dan parameter
offset menentukan jumlah byte sejak awal data per-vertex untuk dibaca. Pengikatan memuat
satu Vertex pada satu waktu dan atribut posisi (pos) berada pada offset 0 byte dari awal struct
ini. Ini dihitung secara otomatis menggunakan makro offsetof.

1 atributDeskripsi[1].binding = 0;

162
Machine Translated by Google

2 atributKeterangan[1].lokasi = 1; 3
atributKeterangan[1].format = VK_FORMAT_R32G32B32_SFLOAT; 4
atributDeskripsi[1].offset = offsetof(Vertex, warna);

Atribut warna dijelaskan dengan cara yang hampir sama.

Input titik pipa


Kita sekarang perlu menyiapkan pipa grafis untuk menerima data vertex dalam format ini
dengan mereferensikan struktur di createGraphicsPipeline. Temukan struct vertexInputInfo
dan modifikasi untuk mereferensikan dua deskripsi:

1 auto bindingDescription = Vertex::getBindingDescription(); 2 atributDeskripsi


otomatis = Vertex::getAttributeDescriptions(); 3

4 vertexInputInfo.vertexBindingDescriptionCount = 1; 5
vertexInputInfo.vertexAttributeDescriptionCount =
static_cast<uint32_t>(attributeDescriptions.size());
6 vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; 7
vertexInputInfo.pVertexAttributeDescriptions = atributDescriptions.data();

Pipeline sekarang siap untuk menerima data vertex dalam format wadah vertex dan
meneruskannya ke shader vertex kita. Jika Anda menjalankan program sekarang dengan
mengaktifkan lapisan validasi, Anda akan melihat bahwa ia mengeluh bahwa tidak ada
buffer vertex yang terikat pada pengikatan. Langkah selanjutnya adalah membuat buffer
vertex dan memindahkan data vertex ke sana sehingga GPU dapat mengaksesnya.

Kode C++ / Vertex shader / Fragment shader

163
Machine Translated by Google

Pembuatan buffer vertex

pengantar

Buffer di Vulkan adalah wilayah memori yang digunakan untuk menyimpan data arbitrer yang dapat
dibaca oleh kartu grafis. Mereka dapat digunakan untuk menyimpan data vertex, yang akan kita
lakukan di bab ini, tetapi mereka juga dapat digunakan untuk banyak tujuan lain yang akan kita
jelajahi di bab selanjutnya. Tidak seperti objek Vulkan yang telah kita tangani sejauh ini, buffer tidak
secara otomatis mengalokasikan memori untuk dirinya sendiri. Pekerjaan dari bab-bab sebelumnya
telah menunjukkan bahwa Vulkan API membuat programmer mengendalikan hampir semua hal dan
manajemen memori adalah salah satunya.

Pembuatan penyangga

Buat fungsi baru createVertexBuffer dan panggil dari initVulkan tepat sebelum createCommandBuffers.

1 batal initVulkan()
2 { createInstance();
3 setupDebugMessenger(); buat
4 Permukaan();
5 pickPhysicalDevice();
6 createLogicalDevice();
7 buatSwapChain();
8 createImageViews();
9 buatRenderPass();
10 buatGraphicsPipeline();
11 buatFramebuffer();
12 buatCommandPool();
13 createVertexBuffer();
14 buatCommandBuffers();
15 buatSyncObjects();
16 }
17
18 ...

164
Machine Translated by Google

19
20 batal createVertexBuffer() {
21
22 }

Membuat buffer mengharuskan kita mengisi struktur VkBufferCreateInfo.

1 VkBufferCreateInfo bufferInfo{}; 2
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; 3
bufferInfo.size = sizeof(vertices[0]) * vertices.size();

Bidang pertama dari struct adalah ukuran, yang menentukan ukuran buffer dalam byte.
Menghitung ukuran byte dari data vertex sangat mudah dengan sizeof.

1 bufferInfo.penggunaan = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;

Kolom kedua adalah penggunaan, yang menunjukkan untuk tujuan apa data dalam buffer akan
digunakan. Dimungkinkan untuk menentukan beberapa tujuan menggunakan bitwise atau. Kasus
penggunaan kami akan menjadi buffer vertex, kami akan melihat jenis penggunaan lain di bab
mendatang.

1 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

Sama seperti image dalam rantai swap, buffer juga dapat dimiliki oleh keluarga antrian tertentu atau
dibagikan di antara banyak antrian sekaligus. Buffer hanya akan digunakan dari antrean grafik, jadi
kami dapat tetap menggunakan akses eksklusif.

Parameter flags digunakan untuk mengonfigurasi memori buffer jarang, yang saat ini tidak relevan.
Kami akan membiarkannya pada nilai default 0.

Kita sekarang dapat membuat buffer dengan vkCreateBuffer. Tentukan anggota kelas untuk
memegang gagang buffer dan beri nama vertexBuffer.

1 VkBuffer vertexBuffer;
2
3 ...
4
5 batal createVertexBuffer()
6 { VkBufferCreateInfo bufferInfo{};
7 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
8 bufferInfo.size = sizeof(vertices[0]) * vertices.size(); bufferInfo.penggunaan =
9 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; bufferInfo.sharingMode =
10 VK_SHARING_MODE_EXCLUSIVE;
11
12 jika (vkCreateBuffer(perangkat, &bufferInfo, nullptr, &vertexBuffer)
!= VK_SUCCESS)
13 { throw std::runtime_error("gagal membuat buffer vertex!");
14 }
15 }

165
Machine Translated by Google

Buffer harus tersedia untuk digunakan dalam merender perintah hingga akhir program dan
tidak bergantung pada rantai pertukaran, jadi kami akan membersihkannya dalam fungsi
pembersihan asli:

1 pembersihan batal () {
2 cleanupSwapChain();
3
4 vkDestroyBuffer(perangkat, vertexBuffer, nullptr);
5
6 ...
7}

Persyaratan memori
Buffer telah dibuat, tetapi sebenarnya belum ada memori yang ditetapkan untuknya. Langkah
pertama mengalokasikan memori untuk buffer adalah menanyakan persyaratan memorinya
menggunakan fungsi vkGetBufferMemoryRequirements yang diberi nama tepat.

1 VkMemoryRequirements memRequirements; 2
vkGetBufferMemoryRequirements(perangkat, vertexBuffer,
&memPersyaratan);

Struktur VkMemoryRequirements memiliki tiga bidang:

• ukuran: Ukuran jumlah memori yang diperlukan dalam byte, mungkin berbeda dari
bufferInfo.size.
• keselarasan: Offset dalam byte di mana buffer dimulai di wilayah memori yang
dialokasikan, bergantung pada bufferInfo.usage dan bufferInfo.flags. • memoryTypeBits:
Bidang bit dari jenis memori yang cocok untuk
penyangga.

Kartu grafis dapat menawarkan berbagai jenis memori untuk dialokasikan. Setiap jenis
memori bervariasi dalam hal operasi yang diizinkan dan karakteristik kinerja.
Kita perlu menggabungkan persyaratan buffer dan persyaratan aplikasi kita sendiri untuk
menemukan jenis memori yang tepat untuk digunakan. Mari buat fungsi baru findMemoryType
untuk tujuan ini.

1 uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags


properti) {
2
3}

Pertama kita perlu menanyakan info tentang jenis memori yang tersedia menggunakan
vkGetPhysicalDeviceMemoryProperties.

1 VkPhysicalDeviceMemoryProperties memProperties; 2
vkGetPhysicalDeviceMemoryProperties(Perangkat fisik, &Properti mem);

166
Machine Translated by Google

Struktur VkPhysicalDeviceMemoryProperties memiliki dua array memoryTypes dan


memoryHeaps. Tumpukan memori adalah sumber daya memori yang berbeda seperti
VRAM khusus dan ruang swap dalam RAM ketika VRAM habis. Berbagai jenis memori
ada di dalam tumpukan ini. Saat ini kami hanya akan menyibukkan diri dengan jenis
memori dan bukan asalnya, tetapi Anda dapat membayangkan bahwa ini dapat
memengaruhi kinerja.

Pertama mari kita cari jenis memori yang cocok untuk buffer itu sendiri:

1 untuk (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { 2


if (typeFilter & (1 << i)) {
3 kembalikan saya;

4 }
5}
6
7 throw std::runtime_error("gagal menemukan jenis memori yang cocok!");

Parameter typeFilter akan digunakan untuk menentukan bidang bit dari jenis memori yang
sesuai. Itu berarti bahwa kita dapat menemukan indeks dari jenis memori yang sesuai
hanya dengan mengulanginya dan memeriksa apakah bit yang sesuai diatur ke 1.

Namun, kami tidak hanya tertarik pada tipe memori yang cocok untuk buffer vertex. Kita
juga harus bisa menulis data verteks kita ke memori itu.
Array memoryTypes terdiri dari struct VkMemoryType yang menentukan heap dan properti
dari setiap jenis memori. Properti menentukan fitur khusus dari memori, seperti mampu
memetakannya sehingga kita dapat menulisnya dari CPU.
Properti ini ditunjukkan dengan VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, tetapi
kita juga perlu menggunakan properti VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.
Kita akan melihat mengapa ketika kita memetakan memori.

Kami sekarang dapat memodifikasi loop untuk juga memeriksa dukungan properti ini:

1 untuk (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { 2


jika ((typeFilter & (1 << i)) &&
(memProperties.memoryTypes[i].propertyFlags & properti) == properti) { return
i;
3
4 }
5}

Kita mungkin memiliki lebih dari satu properti yang diinginkan, jadi kita harus memeriksa
apakah hasil bitwise AND bukan hanya bukan nol, tetapi sama dengan bidang bit properti
yang diinginkan. Jika ada tipe memori yang cocok untuk buffer yang juga memiliki semua
properti yang kita perlukan, maka kita mengembalikan indeksnya, jika tidak, kita melemparkan pengecualian.

167
Machine Translated by Google

Alokasi memori
Kami sekarang memiliki cara untuk menentukan jenis memori yang tepat, sehingga kami benar-
benar dapat mengalokasikan memori dengan mengisi struktur VkMemoryAllocateInfo.

1 VkMemoryAllocateInfo allocInfo{}; 2
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; 3
allocInfo.allocationSize = memRequirements.size; 4 allocInfo.memoryTypeIndex =
findMemoryType(memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

Alokasi memori sekarang sesederhana menentukan ukuran dan jenis, keduanya berasal dari
persyaratan memori buffer vertex dan properti yang diinginkan. Buat anggota kelas untuk menyimpan
pegangan ke memori dan mengalokasikannya dengan vkAllocateMemory.

1 VkBuffer vertexBuffer;
2 VkDeviceMemory vertexBufferMemory; 3

4 ...
5
6 if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) !
= VK_SUCCESS) { throw std::runtime_error("gagal
7 mengalokasikan memori buffer vertex!");

8}

Jika alokasi memori berhasil, sekarang kita dapat mengaitkan memori ini dengan buffer menggunakan
vkBindBufferMemory:

1 vkBindBufferMemory(perangkat, vertexBuffer, vertexBufferMemory, 0);

Tiga parameter pertama cukup jelas dan parameter keempat adalah offset dalam wilayah memori.
Karena memori ini dialokasikan secara khusus untuk buffer vertex ini, offsetnya hanya 0. Jika
offsetnya bukan nol, maka harus dapat dibagi oleh memRequirements.alignment.

Tentu saja, seperti alokasi memori dinamis di C++, memori harus dibebaskan di beberapa titik.
Memori yang terikat ke objek buffer dapat dibebaskan setelah buffer tidak lagi digunakan, jadi mari
kita bebaskan setelah buffer dihancurkan:

1 pembersihan batal () {
2 cleanupSwapChain();
3
4 vkDestroyBuffer(perangkat, vertexBuffer, nullptr);
5 vkFreeMemory(perangkat, vertexBufferMemory, nullptr);

168
Machine Translated by Google

Mengisi buffer vertex


Sekarang saatnya untuk menyalin data vertex ke buffer. Ini dilakukan dengan memetakan memori
buffer ke dalam memori yang dapat diakses CPU dengan vkMapMemory.

1 data kosong* ;
2 vkMapMemory(perangkat, vertexBufferMemory, 0, bufferInfo.size, 0,
&data);

Fungsi ini memungkinkan kita untuk mengakses wilayah dari sumber daya memori tertentu yang
ditentukan oleh offset dan ukuran. Offset dan ukuran di sini masing-masing adalah 0 dan
bufferInfo.size. Dimungkinkan juga untuk menentukan nilai khusus VK_WHOLE_SIZE untuk
memetakan semua memori. Parameter kedua hingga terakhir dapat digunakan untuk menentukan
flag, tetapi belum ada yang tersedia di API saat ini. Itu harus disetel ke nilai 0. Parameter terakhir
menentukan keluaran untuk penunjuk ke memori yang dipetakan.

1 data kosong* ;
2 vkMapMemory(perangkat, vertexBufferMemory, 0, bufferInfo.size, 0,
&data);
3 memcpy(data, vertices.data(), (size_t) bufferInfo.size); 4
vkUnmapMemory(perangkat, vertexBufferMemory);

Anda sekarang dapat memcpy data vertex ke memori yang dipetakan dan menghapusnya lagi
menggunakan vkUnmapMemory. Sayangnya driver mungkin tidak segera menyalin data ke buffer
memory, misalnya karena caching. Mungkin juga penulisan ke buffer belum terlihat di memori
yang dipetakan.
Ada dua cara untuk mengatasi masalah itu:

• Gunakan tumpukan memori yang koheren host, ditunjukkan dengan VK_MEMORY_PROPERTY_HOST_COHERENT_BIT


• Panggil vkFlushMappedMemoryRanges setelah menulis ke memori yang dipetakan, dan panggil
vkInvalidateMappedMemoryRanges sebelum membaca dari memori yang dipetakan

Kami menggunakan pendekatan pertama, yang memastikan bahwa memori yang dipetakan selalu
cocok dengan konten memori yang dialokasikan. Perlu diingat bahwa ini dapat menyebabkan
kinerja yang sedikit lebih buruk daripada pembilasan eksplisit, tetapi kita akan melihat mengapa
hal itu tidak menjadi masalah di bab selanjutnya.

Membilas rentang memori atau menggunakan tumpukan memori yang koheren berarti pengemudi
akan mengetahui tulisan kita ke buffer, tetapi itu tidak berarti bahwa tulisan tersebut benar-benar
terlihat di GPU. Transfer data ke GPU adalah operasi yang terjadi di latar belakang dan
spesifikasinya hanya memberi tahu kami bahwa ini dijamin selesai pada panggilan berikutnya ke
vkQueueSubmit.

169
Machine Translated by Google

Mengikat buffer vertex


Yang tersisa sekarang hanyalah mengikat buffer vertex selama operasi rendering.
Kami akan memperluas fungsi recordCommandBuffer untuk melakukan itu.
1 vkCmdBindPipeline(Buffer perintah, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipa grafis);
2
3 VkBuffer vertexBuffer[] = {vertexBuffer};
4 offset VkDeviceSize[] = {0}; 5
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offset);
6
7 vkCmdDraw(commandBuffer, static_cast<uint32_t>(vertices.size()), 1,
0, 0);

Fungsi vkCmdBindVertexBuffers digunakan untuk mengikat buffer vertex untuk


mengikat, seperti yang kita atur di bab sebelumnya. Dua parameter pertama, selain
buffer perintah, tentukan offset dan jumlah binding yang akan kita tentukan untuk buffer
vertex. Dua parameter terakhir menentukan array buffer vertex untuk diikat dan offset
byte untuk mulai membaca data vertex.
Anda juga harus mengubah panggilan ke vkCmdDraw untuk meneruskan jumlah simpul
dalam buffer sebagai kebalikan dari angka hardcode 3.

Sekarang jalankan program dan Anda akan melihat segitiga familiar lagi:

170
Machine Translated by Google

Coba ubah warna simpul atas menjadi putih dengan memodifikasi simpulnya
Himpunan:

1 const std::vector<Vertex> simpul = {


2 {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}}, {{0.5f, 0.5f}, {0.0f,
3 1.0f, 0.0f}}, {{-0.5f, 0.5 f}, {0.0f, 0.0f, 1.0f}}
4
5 };

Jalankan program lagi dan Anda akan melihat yang berikut:

Di bab berikutnya, kita akan melihat cara lain untuk menyalin data vertex ke buffer vertex
yang menghasilkan performa lebih baik, tetapi memerlukan lebih banyak usaha.

Kode C++ / Vertex shader / Fragment shader

171
Machine Translated by Google

Penyangga pementasan

pengantar

Buffer vertex yang kami miliki saat ini berfungsi dengan benar, tetapi jenis memori yang memungkinkan
kami untuk mengaksesnya dari CPU mungkin bukan jenis memori yang paling optimal untuk dibaca
oleh kartu grafis itu sendiri. Memori paling optimal memiliki flag
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT dan biasanya tidak dapat diakses oleh CPU pada
kartu grafis khusus. Dalam bab ini kita akan membuat dua buffer vertex. Satu staging buffer dalam
memori yang dapat diakses CPU untuk mengunggah data dari array vertex ke, dan buffer vertex
terakhir dalam memori lokal perangkat.
Kami kemudian akan menggunakan perintah salinan buffer untuk memindahkan data dari buffer staging
ke buffer vertex yang sebenarnya.

Antrean transfer
Perintah salinan buffer memerlukan kelompok antrian yang mendukung operasi transfer, yang
ditunjukkan menggunakan VK_QUEUE_TRANSFER_BIT. Kabar baiknya adalah bahwa setiap keluarga
antrian dengan kemampuan VK_QUEUE_GRAPHICS_BIT atau VK_QUEUE_COMPUTE_BIT sudah
secara implisit mendukung operasi VK_QUEUE_TRANSFER_BIT. Implementasi tidak diharuskan
untuk mencantumkannya secara eksplisit di queueFlags dalam kasus tersebut.

Jika Anda menyukai tantangan, Anda masih dapat mencoba menggunakan kelompok antrean yang
berbeda khusus untuk operasi transfer. Ini akan meminta Anda untuk melakukan modifikasi berikut
pada program Anda:

• Modifikasi QueueFamilyIndices dan findQueueFamilies untuk secara eksplisit mencari kelompok


antrian dengan bit VK_QUEUE_TRANSFER_BIT, tetapi bukan VK_QUEUE_GRAPHICS_BIT.

• Modifikasi createLogicalDevice untuk meminta pegangan ke antrean transfer • Buat kumpulan


perintah kedua untuk buffer perintah yang dikirimkan
pada keluarga antrian transfer
• Ubah sharingMode resource menjadi VK_SHARING_MODE_CONCURRENT
dan tentukan keluarga antrian grafik dan transfer
• Mengirimkan perintah transfer apa pun seperti vkCmdCopyBuffer (yang akan kita gunakan di bab
ini) ke antrean transfer alih-alih antrean grafik

172
Machine Translated by Google

Ini sedikit pekerjaan, tetapi itu akan mengajari Anda banyak hal tentang bagaimana sumber daya
dibagikan di antara keluarga antrean.

Mengabstraksi pembuatan buffer


Karena kita akan membuat banyak buffer di bab ini, sebaiknya pindahkan pembuatan buffer
ke fungsi pembantu. Buat fungsi baru createBuffer dan pindahkan kode di createVertexBuffer
(kecuali pemetaan) ke sana.

1 void createBuffer(Ukuran VkDeviceSize, penggunaan VkBufferUsageFlags, properti


VkMemoryPropertyFlags, VkBuffer& buffer, VkDeviceMemory& bufferMemory)
{ VkBufferCreateInfo bufferInfo{}; bufferInfo.sType =
2 VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.ukuran =
3 ukuran; bufferInfo.penggunaan = penggunaan; bufferInfo.sharingMode =
4 VK_SHARING_MODE_EXCLUSIVE;
5
6
7
8 jika (vkCreateBuffer(perangkat, &bufferInfo, nullptr, &buffer) !=
VK_SUCCESS)
9 { throw std::runtime_error("gagal membuat buffer!");
10 }
11
12 VkMemoryRequirements memRequirements;
13 vkGetBufferMemoryRequirements(perangkat, buffer, &memRequirements);
14
15 VkMemoryAllocateInfo allocInfo{};
16 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
17 allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex
18 =
findMemoryType(memRequirements.memoryTypeBits, properti);
19
20 jika (vkAllocateMemory(perangkat, &allocInfo, nullptr, &bufferMemory)
!= VK_SUCCESS)
21 { throw std::runtime_error("gagal mengalokasikan buffer
Penyimpanan!");
22 }
23

vkBindBufferMemory(perangkat, buffer, bufferMemory, 0);


24 25 }

Pastikan untuk menambahkan parameter untuk ukuran buffer, properti memori, dan
penggunaan sehingga kita dapat menggunakan fungsi ini untuk membuat berbagai jenis
buffer. Dua parameter terakhir adalah variabel keluaran untuk menulis pegangan.

Anda sekarang dapat menghapus pembuatan buffer dan kode alokasi memori dari

173
Machine Translated by Google

createVertexBuffer dan panggil saja createBuffer sebagai gantinya:


1 void createVertexBuffer() { VkDeviceSize
2 bufferSize = sizeof(vertices[0]) * vertices.size(); buatBuffer(Ukuran buffer,
3 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer,
vertexBufferMemory);
4
5 batal* data;
6 vkMapMemory(perangkat, vertexBufferMemory, 0, bufferSize, 0, &data); memcpy(data,
7 vertices.data(), (size_t) bufferSize); vkUnmapMemory(perangkat, vertexBufferMemory);
8
9}

Jalankan program Anda untuk memastikan bahwa buffer vertex masih berfungsi dengan baik.

Menggunakan penyangga pementasan

Kita sekarang akan mengubah createVertexBuffer untuk hanya menggunakan buffer host yang terlihat
sebagai buffer sementara dan menggunakan perangkat lokal sebagai buffer vertex yang sebenarnya.

1 void createVertexBuffer() { VkDeviceSize


2 bufferSize = sizeof(vertices[0]) * vertices.size();
3
4 Pementasan VkBufferBuffer;
5 Pementasan VkDeviceMemoryBufferMemory;
6 buatBuffer(Ukuran buffer, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
stagingBufferMemory);
7
8 batal* data;
9 vkMapMemory(perangkat, stagingBufferMemory, 0, bufferSize, 0,
&data);
10 memcpy(data, vertices.data(), (size_t) bufferSize);
11 vkUnmapMemory(perangkat, stagingBufferMemory);
12
13 createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer,
vertexBufferMemory);
14 }

Kami sekarang menggunakan stagingBuffer baru dengan stagingBufferMemory untuk memetakan


dan menyalin data vertex. Dalam bab ini kita akan menggunakan dua flag penggunaan buffer baru:

174
Machine Translated by Google

• VK_BUFFER_USAGE_TRANSFER_SRC_BIT: Buffer dapat digunakan sebagai sumber dalam


operasi transfer memori.
• VK_BUFFER_USAGE_TRANSFER_DST_BIT: Buffer dapat digunakan sebagai tujuan
dalam operasi transfer memori.

VertexBuffer sekarang dialokasikan dari jenis memori lokal perangkat, yang umumnya berarti
bahwa kita tidak dapat menggunakan vkMapMemory. Namun, kita dapat menyalin data dari
stagingBuffer ke vertexBuffer. Kita harus menunjukkan bahwa kita bermaksud melakukannya
dengan menentukan flag sumber transfer untuk stagingBuffer dan flag tujuan transfer untuk
vertexBuffer, bersama dengan flag penggunaan buffer vertex.

Kita sekarang akan menulis sebuah fungsi untuk menyalin konten dari satu buffer ke buffer
lainnya, yang disebut copyBuffer.

1 batal copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize


ukuran) {
2
3}

Operasi transfer memori dijalankan menggunakan buffer perintah, seperti halnya perintah
menggambar. Oleh karena itu pertama-tama kita harus mengalokasikan buffer perintah sementara.
Anda mungkin ingin membuat kumpulan perintah terpisah untuk jenis buffer berumur pendek
ini, karena penerapannya mungkin dapat menerapkan pengoptimalan alokasi memori. Anda
harus menggunakan flag VK_COMMAND_POOL_CREATE_TRANSIENT_BIT selama
pembuatan kumpulan perintah dalam kasus tersebut.

1 batal copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize


size)
2 { VkCommandBufferAllocateInfo allocInfo{};
3 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
4 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandPool
5 = commandPool; allocInfo.commandBufferCount = 1;
6
7
8 VkCommandBuffer perintahBuffer;
9 vkAllocateCommandBuffers(perangkat, &allocInfo, &commandBuffer);
10 }

Dan segera mulai merekam buffer perintah:

1 VkCommandBufferBeginInfo beginInfo{}; 2
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; 3
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
4
5 vkBeginCommandBuffer(commandBuffer, &beginInfo);

175
Machine Translated by Google

Kami hanya akan menggunakan buffer perintah satu kali dan menunggu dengan kembali beralih
dari fungsi sampai operasi penyalinan selesai exe Ini adalah praktik yang baik untuk memberi tahu
pengemudi tentang niat kami menggunakan pemotongan.
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT.

1 VkBufferCopy copyRegion{}; 2
copyRegion.srcOffset = 0; // Opsional 3
copyRegion.dstOffset = 0; // Opsional 4 copyRegion.size
= ukuran; 5 vkCmdCopyBuffer(commandBuffer,
srcBuffer, dstBuffer, 1, &copyRegion);

Isi buffer ditransfer menggunakan perintah vkCmdCopyBuffer. Dibutuhkan buffer sumber dan
tujuan sebagai argumen, dan array wilayah untuk disalin. Region didefinisikan dalam struct
VkBufferCopy dan terdiri dari offset buffer sumber, offset buffer tujuan, dan ukuran.
VK_WHOLE_SIZE tidak dapat ditentukan di sini, tidak seperti perintah vkMapMemory.

1 vkEndCommandBuffer(commandBuffer);

Buffer perintah ini hanya berisi perintah salin, jadi kami dapat berhenti merekam setelah itu.
Sekarang jalankan buffer perintah untuk menyelesaikan transfer:
1 VkSubmitInfo kirimInfo{}; 2
kirimInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; 3
kirimInfo.commandBufferCount = 1; 4 submitInfo.pCommandBuffers
= &commandBuffer; 5

6 vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); 7


vkQueueWaitIdle(graphicsQueue);

Berbeda dengan perintah undian, tidak ada acara yang perlu kita tunggu saat ini.
Kami hanya ingin segera mengeksekusi transfer pada buffer. Ada lagi dua kemungkinan cara untuk
menunggu transfer ini selesai. Kita bisa menggunakan pagar dan menunggu dengan
vkWaitForFences, atau cukup menunggu antrian transfer menjadi tidak aktif dengan
vkQueueWaitIdle. Pagar akan memungkinkan Anda untuk menjadwalkan beberapa transfer secara
bersamaan dan menunggu semuanya selesai, alih-alih mengeksekusi satu per satu. Itu dapat
memberi pengemudi lebih banyak peluang untuk mengoptimalkan.

1 vkFreeCommandBuffers(perangkat, commandPool, 1, &commandBuffer);

Jangan lupa untuk membersihkan buffer perintah yang digunakan untuk operasi transfer.

Kita sekarang dapat memanggil copyBuffer dari fungsi createVertexBuffer untuk memindahkan
data vertex ke buffer lokal perangkat:

1 buatBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer,
vertexBufferMemory);

176
Machine Translated by Google

3 copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

Setelah menyalin data dari staging buffer ke device buffer, kita harus membersihkannya:

1 ...
2
3 copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
4
5 vkDestroyBuffer(perangkat, stagingBuffer, nullptr);
6 vkFreeMemory(perangkat, stagingBufferMemory, nullptr);
7}

Jalankan program Anda untuk memverifikasi bahwa Anda melihat segitiga familiar lagi.
Peningkatan mungkin tidak terlihat saat ini, tetapi data verteksnya sekarang sedang
dimuat dari memori performa tinggi. Ini penting saat kita akan mulai merender geometri
yang lebih kompleks.

Kesimpulan

Perlu dicatat bahwa dalam aplikasi dunia nyata, Anda tidak seharusnya memanggil
vkAllocateMemory untuk setiap buffer individu. Jumlah maksimum alokasi memori
simultan dibatasi oleh batas perangkat fisik maxMemoryAllocationCount, yang mungkin
serendah 4096 bahkan pada perangkat keras kelas atas seperti NVIDIA GTX 1080.
Cara yang tepat untuk mengalokasikan memori untuk sejumlah besar objek pada waktu
yang sama adalah membuat pengalokasi khusus yang membagi satu alokasi di antara
banyak objek berbeda dengan menggunakan parameter offset yang telah kita lihat di
banyak fungsi.

Anda dapat mengimplementasikan sendiri pengalokasi tersebut, atau menggunakan


pustaka VulkanMem oryAllocator yang disediakan oleh inisiatif GPUOpen. Namun,
untuk tutorial ini tidak apa-apa untuk menggunakan alokasi terpisah untuk setiap sumber
daya, karena kami tidak akan mencapai batasan ini untuk saat ini.

Kode C++ / Vertex shader / Fragment shader

177
Machine Translated by Google

Penyangga indeks

pengantar

Jala 3D yang akan Anda render dalam aplikasi dunia nyata akan sering berbagi simpul di
antara banyak segitiga. Ini sudah terjadi bahkan dengan sesuatu yang sederhana seperti
menggambar persegi panjang:

Menggambar persegi panjang membutuhkan dua segitiga, yang berarti kita membutuhkan
penyangga simpul dengan 6 simpul. Masalahnya adalah data dari dua simpul perlu
diduplikasi sehingga terjadi redundansi 50%. Ini hanya menjadi lebih buruk dengan jerat
yang lebih kompleks, di mana simpul digunakan kembali dalam jumlah rata-rata 3 segitiga.
Solusi untuk masalah ini adalah dengan menggunakan buffer indeks.

Buffer indeks pada dasarnya adalah array pointer ke buffer vertex. Ini memungkinkan Anda
untuk menyusun ulang data simpul, dan menggunakan kembali data yang ada untuk
beberapa simpul. Ilustrasi di atas menunjukkan seperti apa buffer indeks untuk persegi
panjang jika kita memiliki buffer simpul yang berisi masing-masing dari empat simpul unik.
Tiga indeks pertama menentukan segitiga kanan atas dan tiga indeks terakhir

178
Machine Translated by Google

tentukan simpul untuk segitiga kiri bawah.

Pembuatan buffer indeks


Dalam bab ini kita akan memodifikasi data simpul dan menambahkan data indeks untuk menggambar
persegi panjang seperti yang ada di ilustrasi. Ubah data simpul untuk mewakili empat sudut:

1 const std::vector<Vertex> simpul = {


2 {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, {{0.5f, -0.5f}, {0.0f, 1.0f,
3 0.0f}}, {{0.5f, 0,5f}, {0,0f, 0,0f, 1,0f}}, {{-0,5f, 0,5f}, {1,0f,
4 1,0f, 1,0f}}
5
6 };

Pojok kiri atas berwarna merah, kanan atas berwarna hijau, kanan bawah berwarna
biru dan kiri bawah berwarna putih. Kami akan menambahkan indeks array baru untuk
mewakili konten buffer indeks. Itu harus cocok dengan indeks dalam ilustrasi untuk
menggambar segitiga kanan atas dan segitiga kiri bawah.
1 indeks const std::vector<uint16_t> = { 0, 1, 2, 2, 3, 0
2
3 };

Dimungkinkan untuk menggunakan uint16_t atau uint32_t untuk buffer indeks Anda
tergantung pada jumlah entri di simpul. Kami dapat tetap menggunakan uint16_t untuk saat
ini karena kami menggunakan kurang dari 65535 simpul unik.

Sama seperti data vertex, indeks perlu diunggah ke VkBuffer agar GPU dapat mengaksesnya.
Tentukan dua anggota kelas baru untuk menyimpan sumber daya untuk buffer indeks:

1 VkBuffer vertexBuffer;
2 VkDeviceMemory vertexBufferMemory;
3 VkBuffer indexBuffer;
4 VkDeviceMemory indexBufferMemory;

Fungsi createIndexBuffer yang akan kita tambahkan sekarang hampir identik


buatVertexBuffer:

1 batal initVulkan() {
2 ...
3 createVertexBuffer();
4 createIndexBuffer();
...
56}
7
8 membatalkan createIndexBuffer() {

179
Machine Translated by Google

9 VkDeviceSize bufferSize = sizeof(indeks[0]) * indeks.ukuran();


10
11 Pementasan VkBufferBuffer;
12 Pementasan VkDeviceMemoryBufferMemory;
13 buatBuffer(Ukuran buffer, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
stagingBufferMemory);
14
15 batal* data;
16 vkMapMemory(perangkat, stagingBufferMemory, 0, bufferSize, 0,
&data);
17 memcpy(data, indexes.data(), (size_t) bufferSize);
18 vkUnmapMemory(perangkat, stagingBufferMemory);
19
20 createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer,
indexBufferMemory);
21
22 copyBuffer(stagingBuffer, indexBuffer, bufferSize);
23
24 vkDestroyBuffer(perangkat, stagingBuffer, nullptr);
25 vkFreeMemory(perangkat, stagingBufferMemory, nullptr);
26 }

Hanya ada dua perbedaan mencolok. Ukuran buffer sekarang sama dengan jumlah indeks
dikali ukuran tipe indeks, baik uint16_t atau uint32_t.
Penggunaan indexBuffer harus VK_BUFFER_USAGE_INDEX_BUFFER_BIT bukan
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, yang masuk akal. Selain itu, prosesnya
persis sama. Kami membuat buffer pementasan untuk menyalin konten indeks dan
kemudian menyalinnya ke buffer indeks lokal perangkat terakhir.

Buffer indeks harus dibersihkan di akhir program, seperti buffer vertex:

1 pembersihan batal ()
{2 cleanupSwapChain();
3
4 vkDestroyBuffer(perangkat, indexBuffer, nullptr);
5 vkFreeMemory(perangkat, indexBufferMemory, nullptr);
6
7 vkDestroyBuffer(perangkat, vertexBuffer, nullptr);
8 vkFreeMemory(perangkat, vertexBufferMemory, nullptr);
9
10 ...
11 }

180
Machine Translated by Google

Menggunakan buffer indeks


Menggunakan buffer indeks untuk menggambar melibatkan dua perubahan untuk membuatBuffer Perintah.
Pertama-tama kita perlu mengikat buffer indeks, seperti yang kita lakukan untuk buffer vertex.
Perbedaannya adalah Anda hanya dapat memiliki satu buffer indeks. Sayangnya tidak mungkin
untuk menggunakan indeks yang berbeda untuk setiap atribut simpul, jadi kami masih harus
menduplikat data simpul sepenuhnya meskipun hanya satu atribut yang bervariasi.

1 vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers,
offset);
2
3 vkCmdBindIndexBuffer(commandBuffer[i], indexBuffer, 0,
VK_INDEX_TYPE_UINT16);

Buffer indeks terikat dengan vkCmdBindIndexBuffer yang memiliki buffer indeks, offset byte ke
dalamnya, dan jenis data indeks sebagai parameter.
Seperti disebutkan sebelumnya, jenis yang mungkin adalah VK_INDEX_TYPE_UINT16 dan
VK_INDEX_TYPE_UINT32.

Mengikat buffer indeks saja belum mengubah apa pun, kami juga perlu mengubah perintah
menggambar untuk memberi tahu Vulkan agar menggunakan buffer indeks. Hapus baris
vkCmdDraw dan ganti dengan vkCmdDrawIndexed:

1 vkCmdDrawIndexed(commandBuffers[i],
static_cast<uint32_t>(indeks.ukuran()), 1, 0, 0, 0);

Panggilan ke fungsi ini sangat mirip dengan vkCmdDraw. Dua parameter pertama menentukan
jumlah indeks dan jumlah instance. Kami tidak menggunakan instans, jadi cukup tentukan 1
instans. Jumlah indeks mewakili jumlah simpul yang akan diteruskan ke buffer simpul. Parameter
selanjutnya menentukan offset ke buffer indeks, menggunakan nilai 1 akan menyebabkan kartu
grafis mulai membaca indeks kedua. Parameter kedua hingga terakhir menentukan offset untuk
ditambahkan ke indeks dalam buffer indeks. Parameter terakhir menentukan offset untuk
instancing, yang tidak kami gunakan.

Sekarang jalankan program Anda dan Anda akan melihat yang berikut:

181
Machine Translated by Google

Anda sekarang tahu cara menghemat memori dengan menggunakan kembali simpul dengan
buffer indeks. Ini akan menjadi sangat penting di bab mendatang di mana kita akan memuat
model 3D yang rumit.

Bab sebelumnya telah menyebutkan bahwa Anda harus mengalokasikan banyak sumber
daya seperti buffer dari satu alokasi memori, tetapi sebenarnya Anda harus melangkah lebih
jauh. Pengembang driver menyarankan agar Anda juga menyimpan beberapa buffer, seperti
buffer vertex dan indeks, ke dalam satu VkBuffer dan menggunakan offset dalam perintah
seperti vkCmdBindVertexBuffers. Keuntungannya adalah data Anda lebih ramah cache
dalam hal ini, karena lebih dekat satu sama lain. Bahkan dimungkinkan untuk menggunakan
kembali potongan memori yang sama untuk banyak sumber daya jika mereka tidak
digunakan selama operasi render yang sama, asalkan datanya disegarkan, tentu saja. Ini
dikenal sebagai aliasing dan beberapa fungsi Vulkan memiliki tanda eksplisit untuk
menentukan bahwa Anda ingin melakukan ini.

Kode C++ / Vertex shader / Fragment shader

182
Machine Translated by Google

Tata letak deskriptor dan buffer

pengantar

Kami sekarang dapat meneruskan atribut arbitrer ke vertex shader untuk setiap ver tex,
tetapi bagaimana dengan variabel global? Kita akan beralih ke grafik 3D dari bab ini dan itu
membutuhkan matriks model-view-projection. Kita dapat memasukkannya sebagai data
vertex, tetapi itu membuang-buang memori dan mengharuskan kita untuk memperbarui
buffer vertex setiap kali transformasi berubah. Transformasi dapat dengan mudah mengubah
setiap frame.

Cara yang tepat untuk mengatasi ini di Vulkan adalah dengan menggunakan deskriptor
sumber daya. Deskriptor adalah cara bagi shader untuk mengakses sumber daya secara
bebas seperti buffer dan gambar. Kita akan menyiapkan buffer yang berisi matriks
transformasi dan membuat vertex shader mengaksesnya melalui deskriptor. Penggunaan
deskriptor terdiri dari tiga bagian:

• Menentukan tata letak deskriptor selama pembuatan pipeline


• Mengalokasikan set deskriptor dari kumpulan deskriptor •
Mengikat set deskriptor selama rendering

Tata letak deskriptor menentukan jenis sumber daya yang akan diakses oleh saluran pipa,
seperti pass render yang menentukan jenis lampiran yang akan diakses. Set deskriptor
menentukan buffer aktual atau sumber gambar yang akan diikat ke deskriptor, seperti
halnya framebuffer menentukan tampilan gambar aktual untuk diikat untuk membuat
lampiran pass. Set deskriptor kemudian terikat untuk perintah menggambar seperti buffer
vertex dan framebuffer.

Ada banyak jenis deskriptor, tetapi dalam bab ini kita akan bekerja dengan objek penyangga
seragam (UBO). Kita akan melihat jenis deskriptor lain di bab selanjutnya, tetapi proses
dasarnya sama. Katakanlah kita memiliki data yang ingin dimiliki vertex shader dalam
struktur C seperti ini:

1 struct UniformBufferObject
2 { glm::mat4 model; glm::mat4
3 lihat; glm::mat4 proj;
4

183
Machine Translated by Google

5 };

Kemudian kita dapat menyalin data ke VkBuffer dan mengaksesnya melalui deskriptor
objek buffer seragam dari vertex shader seperti ini: 1 layout(binding = 0) uniform
UniformBufferObject { mat4 model; tampilan mat4; program mat4; 5 } ubo;
2
3
4

6
7 batal utama() {
8 gl_Position = ubo.proj * ubo.view * ubo.model * vec4(dalamPosisi,
0,0, 1,0);
9 fragColor = inColor;
10 }

Kita akan memperbarui model, tampilan, dan matriks proyeksi setiap bingkai untuk
membuat persegi panjang dari bab sebelumnya berputar dalam 3D.

Shader vertex
Ubah vertex shader untuk menyertakan objek buffer seragam seperti yang ditentukan
di atas. Saya akan berasumsi bahwa Anda sudah familiar dengan transformasi MVP.
Jika tidak, lihat sumber yang disebutkan di bab pertama.
1 #versi 450
2

3 tata letak(binding = 0) seragam UniformBufferObject { mat4 model; tampilan


4 mat4; program mat4; 7 } ubo;
5
6

8
9 tata letak(lokasi = 0) di vec2 inPosition; 10 tata letak(lokasi =
1) di vec3 inColor;
11
12 tata letak(lokasi = 0) keluar vec3 fragColor;
13
14 batal utama() {
15 gl_Position = ubo.proj * ubo.view * ubo.model * vec4(dalamPosisi,
0,0, 1,0);
16 fragColor = inColor;
17 }

Perhatikan bahwa urutan deklarasi seragam, masuk dan keluar tidak masalah. Direktif
yang mengikat mirip dengan direktif lokasi untuk atribut. Akan

184
Machine Translated by Google

untuk merujuk pengikatan ini dalam tata letak deskriptor. Baris dengan gl_Position
diubah untuk menggunakan transformasi untuk menghitung posisi akhir dalam koordinat
klip. Berbeda dengan segitiga 2D, komponen terakhir dari koordinat klip mungkin bukan
1, yang akan menghasilkan pembagian saat diubah menjadi koordinat perangkat akhir
yang dinormalisasi di layar. Ini digunakan dalam proyeksi perspektif sebagai pembagian
perspektif dan sangat penting untuk membuat objek yang lebih dekat terlihat lebih
besar daripada objek yang lebih jauh.

Tata letak set deskriptor


Langkah selanjutnya adalah mendefinisikan UBO di sisi C++ dan memberi tahu Vulkan
tentang deskriptor ini di vertex shader.

1 struct UniformBufferObject { glm::mat4


glm::mat4
model;
proj; 2 glm::mat4 tampilan;
3
4
5 };

Kami dapat mencocokkan definisi di shader dengan menggunakan tipe data di GLM.
Data dalam matriks adalah biner yang kompatibel dengan cara yang diharapkan oleh shader,
jadi nanti kita dapat memcpy UniformBufferObject ke VkBuffer.

Kami perlu memberikan detail tentang setiap pengikatan deskriptor yang digunakan dalam
shader untuk pembuatan pipa, seperti yang harus kami lakukan untuk setiap atribut simpul
dan indeks lokasinya. Kami akan menyiapkan fungsi baru untuk mendefinisikan semua
informasi ini yang disebut createDescriptorSetLayout. Itu harus dipanggil tepat sebelum
pembuatan pipa, karena kita akan membutuhkannya di sana.
1 batal initVulkan() {
2 ...
3 createDescriptorSetLayout();
4 buatGraphicsPipeline();
5 ...
6}
7
8 ...
9
10 batal createDescriptorSetLayout() { 11 12 }

Setiap pengikatan perlu dijelaskan melalui VkDescriptorSetLayoutBinding


struct.

1 batal createDescriptorSetLayout() {
2 VkDescriptorSetLayoutBinding uboLayoutBinding{};

185
Machine Translated by Google

3 uboLayoutBinding.binding = 0;
4 uboLayoutBinding.descriptorType =
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
5 uboLayoutBinding.descriptorCount = 1;
6}

Dua bidang pertama menentukan pengikatan yang digunakan dalam shader dan jenis
descriptor, yang merupakan objek buffer seragam. Variabel shader dapat mewakili larik
objek buffer seragam, dan descriptorCount menentukan jumlah nilai dalam larik. Ini dapat
digunakan untuk menentukan transformasi untuk masing-masing tulang dalam kerangka
untuk animasi kerangka, misalnya.
Transformasi MVP kami berada dalam satu objek buffer seragam, jadi kami menggunakan
descriptorCount dari 1.

1 uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

Kita juga perlu menentukan di tahap shader mana deskriptor akan direferensikan. Bidang
stageFlags dapat berupa kombinasi nilai VkShaderStageFlagBits atau nilai
VK_SHADER_STAGE_ALL_GRAPHICS. Dalam kasus kami, kami hanya merujuk
deskriptor dari vertex shader.

1 uboLayoutBinding.pImmutableSamplers = nullptr; // Opsional

Kolom pImmutableSamplers hanya relevan untuk descriptor terkait pengambilan sampel


gambar, yang akan kita lihat nanti. Anda dapat membiarkan ini ke nilai standarnya.

Semua binding deskriptor digabungkan menjadi satu objek VkDescriptorSetLayout. Tentukan


anggota kelas baru di atas pipelineLayout:

1 VkDescriptorSetLayout deskriptorSetLayout;
2 Tata letak pipa VkPipelineLayout;

Kita kemudian dapat membuatnya menggunakan vkCreateDescriptorSetLayout. Fungsi ini


menerima VkDescriptorSetLayoutCreateInfo sederhana dengan larik binding:

1 VkDescriptorSetLayoutCreateInfo layoutInfo{}; 2
layoutInfo.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
3 layoutInfo.bindingCount = 1; 4
layoutInfo.pBindings = &uboLayoutBinding; 5

6 if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) !


= VK_SUCCESS) { throw std::runtime_error("gagal membuat descriptor
7 set layout!");

8}

186
Machine Translated by Google

Kita perlu menentukan tata letak set deskriptor selama pembuatan pipa untuk memberi tahu
Vulkan deskriptor mana yang akan digunakan oleh shader. Deskripsi
set tata letak ditentukan dalam objek tata letak pipa. Ubah VkPipelineLayoutCreateInfo untuk
mereferensikan objek tata letak:

1 VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; 2
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3 pipelineLayoutInfo.setLayoutCount = 1; 4
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

Anda mungkin bertanya-tanya mengapa mungkin untuk menentukan beberapa tata letak set
deskriptor di sini, karena satu saja sudah menyertakan semua binding. Kita akan
membahasnya kembali di bab berikutnya, di mana kita akan melihat kumpulan deskriptor
dan set deskriptor.

Tata letak deskriptor harus tetap ada sementara kami dapat membuat saluran grafik baru
yaitu hingga program berakhir:

1 pembersihan batal () {
2 cleanupSwapChain();
3
4 vkDestroyDescriptorSetLayout(perangkat, deskriptorSetLayout,
nullptr);
5
6 ...
7}

Penyangga seragam

Di bab selanjutnya kita akan menentukan buffer yang berisi data UBO untuk shader, tetapi
kita perlu membuat buffer ini terlebih dahulu. Kami akan menyalin data baru ke buffer
seragam setiap frame, jadi tidak masuk akal untuk memiliki buffer staging. Itu hanya akan
menambah biaya tambahan dalam kasus ini dan kemungkinan menurunkan kinerja alih-alih
memperbaikinya.

Kita harus memiliki banyak buffer, karena beberapa frame mungkin sedang berjalan pada
waktu yang sama dan kita tidak ingin mengupdate buffer dalam persiapan frame berikutnya
sementara yang sebelumnya masih membaca darinya! Jadi, kita perlu memiliki buffer
seragam sebanyak yang kita miliki dalam penerbangan, dan menulis ke buffer seragam yang
saat ini tidak sedang dibaca oleh GPU

Untuk itu, tambahkan anggota kelas baru untuk uniformBuffers, dan uniformBuffersMemory:

1 VkBuffer indexBuffer;
2 VkDeviceMemory indexBufferMemory;
3
4 std::vector<VkBuffer> uniformBuffer;

187
Machine Translated by Google

5 std::vector<VkDeviceMemory> uniformBuffersMemory;

Demikian pula, buat fungsi baru createUniformBuffers yang dipanggil setelah createIndexBuffer
dan alokasikan buffer:

1 batal initVulkan() {
2 ...
3 createVertexBuffer();
4 createIndexBuffer();
5 createUniformBuffers();
6 ...
7}
8
9 ...
10
11 batal createUniformBuffers() {
12 VkDeviceSize bufferSize = sizeof(UniformBufferObject);
13
14 uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
15 uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
16
17 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
18 buatBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
uniformBuffers[i], uniformBuffersMemory[i]);
19 }
20 }

Kami akan menulis fungsi terpisah yang memperbarui buffer seragam dengan
transformasi baru setiap bingkai, jadi tidak akan ada vkMapMemory di sini. Data
uniform akan digunakan untuk semua panggilan draw, jadi buffer yang memuatnya
hanya boleh dihancurkan saat kita berhenti merender. Karena ini juga bergantung
pada jumlah gambar rantai pertukaran, yang dapat berubah setelah rekreasi, kami
akan membersihkannya di cleanupSwapChain:

1 batal pembersihanSwapChain()
{2 ...
3
4 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
5 vkDestroyBuffer(perangkat, uniformBuffers[i], nullptr);
6 vkFreeMemory(perangkat, uniformBuffersMemory[i], nullptr);
}
78}

Ini berarti kita juga perlu membuatnya kembali di recreateSwapChain:

188
Machine Translated by Google

1 batal buat ulangSwapChain() {


2 ...
3
4 buatFramebuffer();
5 createUniformBuffers();
6 buatCommandBuffers();
7}

Memperbarui data seragam


Buat fungsi baru updateUniformBuffer dan tambahkan panggilan dari fungsi drawFrame tepat
setelah kita mengetahui gambar rantai pertukaran mana yang akan kita peroleh:

1 batal drawFrame() {
2 ...
3
4 uint32_t imageIndex;
5 Hasil VkResult = vkAcquireNextImageKHR(perangkat, swapChain,
UINT64_MAX, imageAvailableSemaphores[currentFrame],
VK_NULL_HANDLE, &indeksgambar);
6
7 ...
8
9 updateUniformBuffer(imageIndex);
10
11 VkSubmitInfo kirimInfo{};
12 kirimInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
13
14 ...
15 }
16
17 ...
18
19 batal updateUniformBuffer(uint32_t currentImage) {
20
21 }

Fungsi ini akan menghasilkan transformasi baru setiap frame untuk membuat geometri
berputar. Kita perlu menyertakan dua header baru untuk mengimplementasikan fungsi ini:

1 #define GLM_FORCE_RADIANS
2 #include <glm/glm.hpp> 3
#include <glm/gtc/matrix_transform.hpp>
4

189
Machine Translated by Google

5 #termasuk <chrono>

Header glm/gtc/matrix_transform.hpp memperlihatkan fungsi yang dapat digunakan untuk


menghasilkan transformasi model seperti glm::rotate, transformasi tampilan seperti glm::lookAt
dan transformasi proyeksi seperti glm::perspective.
Definisi GLM_FORCE_RADIANS diperlukan untuk memastikan bahwa fungsi seperti glm::rotate
menggunakan radian sebagai argumen, untuk menghindari kemungkinan kebingungan.

Header pustaka standar chrono memperlihatkan fungsi untuk melakukan ketepatan waktu yang tepat.
Kami akan menggunakan ini untuk memastikan bahwa geometri berputar 90 derajat per detik
terlepas dari frame rate.

1 pembaruan batalUniformBuffer (uint32_t currentImage) { 2


waktu mulai otomatis statis =
std::krono::jam_resolusi_tinggi::sekarang();
3
4 auto currentTime = std::chrono::high_resolution_clock::now(); waktu apung =
5 std::chrono::durasi<float,
std::chrono::seconds::period>(currentTime - startTime).count();

6}

Fungsi updateUniformBuffer akan dimulai dengan beberapa logika untuk menghitung waktu
dalam detik sejak rendering dimulai dengan akurasi floating point.

Kita sekarang akan mendefinisikan transformasi model, view dan proyeksi dalam objek buffer
uni form. Rotasi model akan menjadi rotasi sederhana di sekitar sumbu Z menggunakan variabel
waktu:

1 UniformBufferObject ubo{}; 2
ubo.model = glm::rotate(glm::mat4(1.0f), waktu * glm::radians(90.0f), glm::vec3(0.0f, 0.0f,
1.0f));

Fungsi glm::rotate menggunakan transformasi, sudut rotasi, dan sumbu rotasi yang ada sebagai
parameter. Konstruktor glm::mat4(1.0f) mengembalikan matriks identitas. Menggunakan sudut
rotasi waktu * glm::radians(90.0f) memenuhi tujuan rotasi 90 derajat per detik.

1 batuk.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f),


glm::vec3(0.0f, 0.0f , 1.0f));

Untuk transformasi tampilan, saya memutuskan untuk melihat geometri dari atas pada sudut 45
derajat. Fungsi glm::lookAt mengambil posisi mata, posisi tengah, dan sumbu atas sebagai
parameter.

1 ubo.proj = glm::perspektif(glm::radians(45.0f), swapChainExtent.width /


(float) swapChainExtent.height, 0.1f, 10.0f);

190
Machine Translated by Google

Saya telah memilih untuk menggunakan proyeksi perspektif dengan bidang pandang
vertikal 45 derajat. Parameter lainnya adalah rasio aspek, bidang pandang dekat dan jauh.
Penting untuk menggunakan luas rantai swap saat ini untuk menghitung rasio aspek untuk
memperhitungkan lebar dan tinggi baru jendela setelah pengubahan ukuran.

1 ubo.proj[1][1] *= -1;

GLM awalnya dirancang untuk OpenGL, di mana koordinat Y dari koordinat klip dibalik.
Cara termudah untuk mengkompensasinya adalah membalik tanda pada faktor penskalaan
sumbu Y dalam matriks proyeksi. Jika Anda tidak melakukan ini, gambar akan dirender
terbalik.

Semua transformasi didefinisikan sekarang, sehingga kita dapat menyalin data dalam objek
buffer seragam ke buffer seragam saat ini. Ini terjadi dengan cara yang persis sama seperti
yang kita lakukan untuk buffer vertex, kecuali tanpa buffer staging:

1 data kosong* ;
2 vkMapMemory(perangkat, uniformBuffersMemory[currentImage], 0,
sizeof(ubo), 0, &data);
3 memcpy(data, &ubo, ukuran(ubo)); 4
vkUnmapMemory(perangkat, uniformBuffersMemory[currentImage]);

Menggunakan UBO dengan cara ini bukanlah cara yang paling efisien untuk meneruskan
nilai yang sering berubah ke shader. Cara yang lebih efisien untuk melewatkan buffer kecil
data ke shader adalah konstanta push. Kita mungkin melihat ini di bab mendatang.

Di bab selanjutnya kita akan melihat set deskriptor, yang sebenarnya akan mengikat
VkBuffer ke deskriptor buffer seragam sehingga shader dapat mengakses data transformasi
ini.

Kode C++ / Vertex shader / Fragment shader

191
Machine Translated by Google

Kumpulan dan kumpulan deskriptor

pengantar

Tata letak deskriptor dari bab sebelumnya menjelaskan jenis deskriptor yang dapat diikat.
Dalam bab ini kita akan membuat set deskriptor untuk setiap sumber daya VkBuffer untuk
mengikatnya ke deskriptor buffer seragam.

Kumpulan deskriptor
Set deskriptor tidak dapat dibuat secara langsung, mereka harus dialokasikan dari
kumpulan seperti buffer perintah. Setara untuk set deskriptor secara mengejutkan disebut
kumpulan deskriptor. Kami akan menulis fungsi baru createDescriptorPool untuk menyiapkannya.
1 batal initVulkan() {
2 ...
3 createUniformBuffers();
4 createDescriptorPool();
5 ...
6}
7
8 ...
9

10 batal createDescriptorPool() { 11

12 }

Pertama-tama kita perlu menjelaskan tipe deskriptor mana yang akan berisi set deskriptor
kita dan berapa banyak, menggunakan struktur VkDescriptorPoolSize.

1 VkDescriptorPoolSize poolSize{}; 2
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 3
poolSize.descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

192
Machine Translated by Google

Kami akan mengalokasikan salah satu deskriptor ini untuk setiap frame. Struktur ukuran
kumpulan ini direferensikan oleh VkDescriptorPoolCreateInfo utama:

1 VkDescriptorPoolCreateInfo poolInfo{}; 2
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; 3
poolInfo.poolSizeCount = 1; 4 poolInfo.pPoolSizes = &poolSize;

Selain jumlah maksimum deskriptor individu yang tersedia, kami juga perlu menentukan
jumlah maksimum set deskriptor yang dapat dialokasikan:

1 poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

Struktur memiliki flag opsional yang mirip dengan kumpulan perintah yang menentukan apakah
set deskriptor individual dapat dibebaskan atau tidak: VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT.
Kami tidak akan menyentuh set deskriptor setelah membuatnya, jadi kami tidak memerlukan
flag ini. Anda dapat meninggalkan bendera dengan nilai default 0.

1 VkDescriptorPool descriptorPool;
2
3 ...
4

5 if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) !=


VK_SUCCESS) { throw std::runtime_error("gagal membuat
6 descriptor pool!");
7}

Tambahkan anggota kelas baru untuk menyimpan pegangan kumpulan deskriptor dan panggil
vkCreateDescriptorPool untuk membuatnya.

Set deskriptor
Kami sekarang dapat mengalokasikan set deskriptor sendiri. Tambahkan fungsi
createDescriptorSets untuk tujuan itu:

1 batal initVulkan() {
2 ...
3 createDescriptorPool();
4 createDescriptorSets();
...
56}
7

8 batal buat ulangSwapChain() {


9 ...
10 createDescriptorPool();
11 createDescriptorSets();

193
Machine Translated by Google

12 ...
13 }
14
15 ...
16
17 batal createDescriptorSets() {
18
19 }

Alokasi set deskriptor dijelaskan dengan struct VkDescriptorSetAllocateInfo. Anda perlu


menentukan kumpulan deskriptor yang akan dialokasikan, jumlah set deskriptor yang
akan dialokasikan, dan tata letak deskriptor untuk mendasarinya:

1 std::vector<VkDescriptorSetLayout> tata letak (MAX_FRAMES_IN_FLIGHT,


descriptorSetLayout); 2 VkDescriptorSetAllocateInfo allocInfo{}; 3
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
4 allocInfo.descriptorPool = descriptorPool; 5 allocInfo.descriptorSetCount =
static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

6 allocInfo.pSetLayouts = layouts.data();

Dalam kasus kami, kami akan membuat satu set deskriptor untuk setiap gambar rantai
pertukaran, semua dengan tata letak yang sama. Sayangnya kami membutuhkan semua salinan
tata letak karena fungsi selanjutnya mengharapkan larik yang cocok dengan jumlah set.

Tambahkan anggota kelas untuk memegang pegangan set deskriptor dan mengalokasikannya
dengan vkAllocateDescriptorSets:

1 VkDescriptorPool descriptorPool; 2
std::vector<VkDescriptorSet> descriptorSets;
3
4 ...
5
6 descriptorSets.resize(MAX_FRAMES_IN_FLIGHT); 7 if
(vkAllocateDescriptorSets(device, &allocInfo,
descriptorSets.data()) != VK_SUCCESS) { throw
8 std::runtime_error("gagal mengalokasikan set deskriptor!");
9}

Anda tidak perlu membersihkan set deskriptor secara eksplisit, karena set deskriptor
akan dibebaskan secara otomatis saat kumpulan deskriptor dihancurkan. Panggilan ke
vkAllocateDescriptorSets akan mengalokasikan set deskriptor, masing-masing dengan
satu deskriptor buffer seragam.

1 pembersihan batal () {
2 ...
3 vkDestroyDescriptorPool(perangkat, descriptorPool, nullptr);

194
Machine Translated by Google

4
5 vkDestroyDescriptorSetLayout(perangkat, deskriptorSetLayout,
nullptr);
6 ...
7}

Set deskriptor telah dialokasikan sekarang, tetapi deskriptor di dalamnya masih perlu
dikonfigurasi. Kami sekarang akan menambahkan loop untuk mengisi setiap deskriptor:

1 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { 2

3}

Deskriptor yang merujuk ke buffer, seperti deskriptor buffer seragam kami, dikonfigurasi
dengan struktur VkDescriptorBufferInfo. Struktur ini menentukan buffer dan region di
dalamnya yang berisi data untuk deskriptor.

1 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {


2 VkDescriptorBufferInfo bufferInfo{};
3 bufferInfo.buffer = uniformBuffers[i];
4 bufferInfo.offset = 0; bufferInfo.range =
sizeof(UniformBufferObject);
56}

Jika Anda menimpa seluruh buffer, seperti dalam kasus ini, Anda juga dapat menggunakan
nilai VK_WHOLE_SIZE untuk rentang. Konfigurasi deskriptor diperbarui menggunakan
fungsi vkUpdateDescriptorSets, yang menggunakan array struct VkWriteDescriptorSet
sebagai parameter.

1 VkWriteDescriptorSet descriptorWrite{}; 2
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; 3
descriptorWrite.dstSet = descriptorSets[i]; 4 descriptorWrite.dstBinding = 0; 5
descriptorWrite.dstArrayElement = 0;

Dua bidang pertama menentukan set deskriptor untuk memperbarui dan pengikatan. Kami
memberikan indeks pengikatan buffer seragam kami 0. Ingatlah bahwa deskriptor dapat
berupa array, jadi kami juga perlu menentukan indeks pertama dalam array yang ingin
kami perbarui. Kami tidak menggunakan array, jadi indeksnya hanya 0.

1 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 2
descriptorWrite.descriptorCount = 1;

Kita perlu menentukan jenis deskriptor lagi. Dimungkinkan untuk memperbarui beberapa
deskriptor sekaligus dalam sebuah array, mulai dari indeks dstArrayElement. Bidang
descriptorCount menentukan berapa banyak elemen array yang ingin Anda perbarui.

1 deskriptorWrite.pBufferInfo = &bufferInfo;

195
Machine Translated by Google

2 descriptorWrite.pImageInfo = nullptr; // Opsional 3


descriptorWrite.pTexelBufferView = nullptr; // Opsional

Kolom terakhir mereferensikan array dengan struct descriptorCount yang benar-benar


mengonfigurasi deskriptor. Itu tergantung pada jenis deskriptor mana dari ketiganya yang benar-
benar perlu Anda gunakan. Bidang pBufferInfo digunakan untuk deskriptor yang merujuk ke
data buffer, pImageInfo digunakan untuk deskriptor yang merujuk ke data gambar, dan
pTexelBufferView digunakan untuk deskriptor yang merujuk ke tampilan buffer.
Deskriptor kami didasarkan pada buffer, jadi kami menggunakan pBufferInfo.

1 vkUpdateDescriptorSets(perangkat, 1, &descriptorWrite, 0, nullptr);

Pembaruan diterapkan menggunakan vkUpdateDescriptorSets. Ia menerima dua jenis larik


sebagai parameter: larik VkWriteDescriptorSet dan larik VkCopyDescriptorSet. Yang terakhir
dapat digunakan untuk menyalin deskriptor satu sama lain, seperti namanya.

Menggunakan set deskriptor


Kita sekarang perlu memperbarui fungsi createCommandBuffers untuk benar-benar mengikat
set deskriptor yang tepat untuk setiap gambar rantai swap ke deskriptor di shader dengan
vkCmdBindDescriptorSets. Ini perlu dilakukan sebelum panggilan vkCmdDrawIndexed:

1 vkCmdBindDescriptorSets(commandBuffers[i],
VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1,
&descriptorSets[i], 0, nullptr); 2 vkCmdDrawIndexed(commandBuffers[i],
static_cast<uint32_t>(indeks.ukuran()), 1, 0, 0, 0);

Tidak seperti buffer verteks dan indeks, set deskriptor tidak unik untuk jaringan pipa grafis.
Oleh karena itu, kita perlu menentukan apakah kita ingin mengikat set deskriptor ke pipa grafik
atau komputasi. Parameter berikutnya adalah tata letak yang menjadi dasar deskriptor. Tiga
parameter berikutnya menentukan indeks set deskriptor pertama, jumlah set yang akan diikat,
dan larik set yang akan diikat.
Kami akan kembali ke ini sebentar lagi. Dua parameter terakhir menentukan larik offset yang
digunakan untuk deskriptor dinamis. Kita akan melihat ini di bab mendatang.

Jika Anda menjalankan program Anda sekarang, Anda akan melihat bahwa sayangnya tidak
ada yang terlihat. Masalahnya adalah karena pembalikan Y yang kita lakukan dalam matriks
proyeksi, simpul sekarang digambar dalam urutan berlawanan arah jarum jam, bukan searah
jarum jam. Hal ini menyebabkan pemusnahan backface muncul dan mencegah geometri apa
pun untuk digambar. Buka fungsi createGraphicsPipeline dan ubah frontFace di
VkPipelineRasterizationStateCreateInfo untuk memperbaikinya:

1 rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; 2
rasterizer.muka depan = VK_FRONT_FACE_COUNTER_CLOCKWISE;

196
Machine Translated by Google

Jalankan program Anda lagi dan sekarang Anda akan melihat yang berikut ini:

Persegi panjang telah berubah menjadi persegi karena matriks proyeksi kini mengoreksi rasio
aspek. UpdateUniformBuffer menangani pengubahan ukuran layar, jadi kita tidak perlu membuat
ulang set deskriptor di recreateSwapChain.

Persyaratan keselarasan
Satu hal yang telah kami bahas sejauh ini adalah bagaimana persisnya data dalam struktur C++
harus cocok dengan definisi seragam di shader. Tampaknya cukup jelas untuk menggunakan
tipe yang sama di keduanya:

1 struct UniformBufferObject { glm::mat4


glm::mat4
model;
proj; 2 glm::mat4 tampilan;
3
4
5 };
6
7 tata letak(mengikat = 0) seragam UniformBufferObject { mat4 model;
8 tampilan mat4; program mat4; 11 } ubo;
9
10

197
Machine Translated by Google

Namun, itu tidak semua ada untuk itu. Misalnya, coba modifikasi struct dan shader agar
terlihat seperti ini:

1 struct UniformBufferObject {
2 glm::vec2 foo;
3 glm::mat4 model;
4 glm::mat4 lihat;
5 glm::mat4 proj;
6 };
7
8 tata letak(mengikat = 0) seragam UniformBufferObject { 9 vec2
foo; model mat4; tampilan mat4; program mat4; 13 } ubo;
10
11
12

Kompilasi ulang shader dan program Anda dan jalankan dan Anda akan menemukan bahwa
kotak warna-warni yang Anda kerjakan sejauh ini telah hilang! Itu karena kami belum
memperhitungkan persyaratan penyelarasan.

Vulkan mengharapkan data dalam struktur Anda diselaraskan dalam memori dengan cara
tertentu, misalnya:

• Skalar harus disejajarkan dengan N (= 4 byte diberikan float 32 bit). •


Sebuah vec2 harus disejajarkan dengan 2N (= 8 byte) • Sebuah vec3 atau
vec4 harus disejajarkan dengan 4N (= 16 byte) • Struktur bersarang harus
disejajarkan dengan penyelarasan dasar anggotanya yang dibulatkan ke atas hingga
kelipatan 16.
• Matriks mat4 harus memiliki perataan yang sama dengan vec4.

Anda dapat menemukan daftar lengkap persyaratan penyelarasan dalam spesifikasi.

Shader asli kami dengan hanya tiga bidang mat4 sudah memenuhi persyaratan penyelarasan.
Karena setiap mat4 berukuran 4 x 4 x 4 = 64 byte, model memiliki offset 0, view memiliki
offset 64 dan proj memiliki offset 128. Semua ini adalah kelipatan 16 dan itulah mengapa
bekerja dengan baik.

Struktur baru dimulai dengan vec2 yang hanya berukuran 8 byte dan karenanya membuang
semua offset. Sekarang model memiliki offset 8, lihat offset 72 dan proj offset 136, tidak ada
yang merupakan kelipatan 16. Untuk memperbaiki masalah ini, kita dapat menggunakan
specifier alignas yang diperkenalkan di C++11:

1 struct UniformBufferObject { glm::vec2


2 foo; alignas(16) glm::mat4
3 model; glm::mat4 lihat; glm::mat4 proj;
4
5
6 };

198
Machine Translated by Google

Jika sekarang Anda mengkompilasi dan menjalankan program Anda lagi, Anda akan melihat bahwa
shader menerima nilai matriksnya dengan benar sekali lagi.

Untungnya, ada cara untuk tidak terlalu sering memikirkan persyaratan penyelarasan ini .
Kita dapat mendefinisikan GLM_FORCE_DEFAULT_ALIGNED_GENTYPES tepat sebelum
menyertakan GLM:

1 #define GLM_FORCE_RADIANS
2 #define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
3 #termasuk <glm/glm.hpp>

Ini akan memaksa GLM untuk menggunakan versi vec2 dan mat4 yang memiliki persyaratan
penyelarasan yang sudah ditentukan untuk kita. Jika Anda menambahkan definisi ini maka
Anda dapat menghapus penentu alignas dan program Anda akan tetap berfungsi.

Sayangnya metode ini dapat rusak jika Anda mulai menggunakan struktur bersarang.
Pertimbangkan definisi berikut dalam kode C++:
1 struct Foo {
2 glm::vec2 v;
3 };
4
5 struct UniformBufferObject {
6 foo f1;
7 foo f2;
8 };

Dan definisi shader berikut:


1 struct Foo
2 { vec2 v;
3 };
4
5 tata letak(mengikat = 0) seragam UniformBufferObject { 6
foo f1;
7 foo f2;
8 } domba;

Dalam hal ini f2 akan memiliki offset 8 sedangkan seharusnya memiliki offset 16 karena
merupakan struktur bersarang. Dalam hal ini Anda harus menentukan perataan sendiri:

1 struct UniformBufferObject {
sejajar(16)
foo Foo
f1; 2
3 f2;
4 };

Gotcha ini adalah alasan bagus untuk selalu eksplisit tentang penyelarasan. Dengan begitu
Anda tidak akan lengah dengan gejala aneh dari kesalahan penyelarasan.

199
Machine Translated by Google

1 struct UniformBufferObject {
2 menyelaraskan(16) glm::mat4 model;
3 menyelaraskan(16) glm::mat4 tampilan;
4 menyelaraskan(16) glm::mat4 proj;
5 };

Jangan lupa untuk mengkompilasi ulang shader Anda setelah menghapus kolom foo.

Beberapa set deskriptor


Seperti yang diisyaratkan oleh beberapa panggilan struktur dan fungsi, sebenarnya mungkin untuk
mengikat beberapa set deskriptor secara bersamaan. Anda perlu menentukan tata letak deskriptor
untuk setiap kumpulan deskriptor saat membuat tata letak pipeline. Shader kemudian dapat
mereferensikan set deskriptor tertentu seperti ini:

1 tata letak(set = 0, pengikatan = 0) seragam UniformBufferObject { ... }

Anda dapat menggunakan fitur ini untuk meletakkan deskriptor yang bervariasi per objek dan
deskriptor yang dibagikan ke dalam set deskriptor terpisah. Dalam hal ini, Anda menghindari
pengikatan ulang sebagian besar deskriptor di seluruh panggilan draw yang berpotensi lebih efisien.

Kode C++ / Vertex shader / Fragment shader

200
Machine Translated by Google

Gambar-gambar

pengantar

Geometri telah diwarnai menggunakan warna per-simpul sejauh ini, yang merupakan
pendekatan yang agak terbatas. Pada bagian tutorial ini kita akan mengimplementasikan
pemetaan tekstur untuk membuat geometri terlihat lebih menarik. Ini juga akan memungkinkan
kita memuat dan menggambar model 3D dasar di bab mendatang.

Menambahkan tekstur ke aplikasi kita akan melibatkan langkah-langkah berikut:

• Membuat objek gambar yang didukung oleh memori


perangkat • Mengisinya dengan piksel dari file gambar •
Membuat sampel gambar • Menambahkan deskriptor sampel
gambar gabungan ke warna sampel dari teks
hukum

Kami telah bekerja dengan objek gambar sebelumnya, tetapi itu dibuat secara otomatis oleh
ekstensi rantai pertukaran. Kali ini kita harus membuatnya sendiri. Membuat gambar dan
mengisinya dengan data mirip dengan pembuatan buffer vertex. Kita akan mulai dengan
membuat resource staging dan mengisinya dengan data piksel lalu kita salin ini ke objek
gambar akhir yang akan kita gunakan untuk rendering.
Meskipun dimungkinkan untuk membuat gambar pementasan untuk tujuan ini, Vulkan juga
memungkinkan Anda menyalin piksel dari VkBuffer ke gambar dan API untuk ini sebenarnya
lebih cepat pada beberapa perangkat keras. Pertama-tama kita akan membuat buffer ini dan
mengisinya dengan nilai piksel, lalu kita akan membuat gambar untuk menyalin pikselnya.
Membuat gambar tidak jauh berbeda dengan membuat buffer. Ini melibatkan menanyakan
persyaratan memori, mengalokasikan memori perangkat dan mengikatnya, seperti yang telah
kita lihat sebelumnya.

Namun, ada hal ekstra yang harus kita perhatikan saat bekerja dengan gambar. Gambar
dapat memiliki tata letak berbeda yang memengaruhi cara piksel diatur dalam memori. Karena
cara kerja perangkat keras grafis, hanya menyimpan piksel baris demi baris mungkin tidak
menghasilkan kinerja terbaik, misalnya. Saat melakukan operasi apa pun pada gambar, Anda
harus memastikan gambar tersebut memiliki tata letak yang optimal untuk digunakan dalam
operasi tersebut. Kami sebenarnya sudah melihat beberapa tata letak ini ketika kami
menentukan pass render:

201
Machine Translated by Google

• VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: Optimal untuk presentasi •


VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: Optimal sebagai lampiran
untuk menulis warna dari shader fragmen
• VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: Optimal sebagai sumber dalam sebuah trans
untuk operasi, seperti vkCmdCopyImageToBuffer
• VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: Optimal sebagai tujuan dalam
operasi transfer, seperti vkCmdCopyBufferToImage
• VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: Optimal untuk pengambilan sampel
dari shader

Salah satu cara paling umum untuk mentransisikan tata letak gambar adalah penghalang pipa.
Penghalang pipa terutama digunakan untuk menyinkronkan akses ke sumber daya, seperti
memastikan bahwa gambar telah ditulis sebelum dibaca, tetapi juga dapat digunakan untuk tata letak
transisi. Dalam bab ini kita akan melihat bagaimana penghalang jalur pipa digunakan untuk tujuan
ini. Penghalang juga dapat digunakan untuk mentransfer kepemilikan keluarga antrean saat
menggunakan VK_SHARING_MODE_EXCLUSIVE.

Pustaka gambar
Ada banyak pustaka yang tersedia untuk memuat gambar, dan Anda bahkan dapat menulis kode
Anda sendiri untuk memuat format sederhana seperti BMP dan PPM. Dalam tutorial ini kita akan
menggunakan library stb_image dari koleksi stb. Keuntungannya adalah semua kode berada dalam
satu file, sehingga tidak memerlukan konfigurasi build yang rumit. Unduh stb_image.h dan simpan di
lokasi yang nyaman, seperti direktori tempat Anda menyimpan GLFW dan GLM. Tambahkan lokasi
ke jalur sertakan Anda.

Studio visual

Tambahkan direktori dengan stb_image.h di dalamnya ke jalur Direktori Sertakan Tambahan.

Makefile

Tambahkan direktori dengan stb_image.h ke direktori include untuk GCC:

1 VULKAN_SDK_PATH = /rumah/pengguna/VulkanSDK/xxxx/x86_64 2
STB_INCLUDE_PATH = /rumah/pengguna/perpustakaan/stb

202
Machine Translated by Google

3
4 ...
5
6 CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/sertakan
-I$(STB_INCLUDE_PATH)

Memuat gambar
Sertakan perpustakaan gambar seperti ini:

1 #define STB_IMAGE_IMPLEMENTATION
2 #termasuk <stb_image.h>

Header hanya mendefinisikan prototipe fungsi secara default. Satu file kode perlu menyertakan
header dengan definisi STB_IMAGE_IMPLEMENTATION untuk menyertakan badan fungsi,
jika tidak, kita akan mendapatkan kesalahan penautan.

1 batal initVulkan() {
2 ...
3 buatCommandPool();
4 buatTeksturGambar();
5 createVertexBuffer();
6 ...
7}
8
9 ...
10
11 batal createTextureImage() {
12
13 }

Buat fungsi baru createTextureImage di mana kita akan memuat gambar dan mengunggahnya
ke objek gambar Vulkan. Kita akan menggunakan buffer perintah, sehingga harus dipanggil
setelah createCommandPool.

Buat tekstur direktori baru di sebelah direktori shaders untuk menyimpan gambar tekstur.
Kita akan memuat gambar bernama texture.jpg dari direktori itu.
Saya telah memilih untuk menggunakan gambar berlisensi CC0 berikut yang diubah ukurannya menjadi 512 x
512 piksel, tetapi jangan ragu untuk memilih gambar apa pun yang Anda inginkan. Perpustakaan mendukung
format file gambar yang paling umum, seperti JPEG, PNG, BMP dan GIF.

203
Machine Translated by Google

Memuat gambar dengan perpustakaan ini sangat mudah:

1 void createTextureImage() { 2 int


texWidth, texHeight,
stbi_uc* texChannels;
piksel = stbi_load("textures/texture.jpg",
3 &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); VkDeviceSize imageSize
= texWidth * texHeight * 4;
4
5
6 if (!pixels) { throw
7 std::runtime_error("gagal memuat gambar tekstur!");
}
89}

Fungsi stbi_load mengambil jalur file dan jumlah saluran untuk dimuat sebagai argumen.
Nilai STBI_rgb_alpha memaksa gambar untuk dimuat dengan saluran alfa, meskipun tidak
memilikinya, yang bagus untuk konsistensi dengan tekstur lain di masa mendatang. Tiga
parameter tengah adalah keluaran untuk

204
Machine Translated by Google

lebar, tinggi, dan jumlah sebenarnya saluran dalam gambar. Pointer yang dikembalikan adalah
elemen pertama dalam array nilai piksel. Piksel ditata baris demi baris dengan 4 byte per piksel
dalam kasus STBI_rgb_alpha dengan total nilai texWidth * texHeight * 4.

Penyangga pementasan

Kami sekarang akan membuat buffer di memori host yang terlihat sehingga kami dapat
menggunakan vkMapMemory dan menyalin piksel ke dalamnya. Tambahkan variabel untuk
buffer sementara ini ke fungsi createTextureImage:

1 VkBuffer stagingBuffer;
2 VkDeviceMemory stagingBufferMemory;

Buffer harus berada dalam memori host yang terlihat sehingga kita dapat memetakannya dan
dapat digunakan sebagai sumber transfer sehingga kita dapat menyalinnya ke gambar nanti:

1 buatBuffer(ukurangambar, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
stagingBufferMemory);

Kami kemudian dapat langsung menyalin nilai piksel yang kami dapatkan dari perpustakaan
pemuatan gambar ke buffer:

1 data kosong* ;
2 vkMapMemory(perangkat, stagingBufferMemory, 0, imageSize, 0, &data);
3 memcpy(data, pixels, static_cast<size_t>(imageSize)); 4
vkUnmapMemory(perangkat, stagingBufferMemory);

Jangan lupa untuk membersihkan susunan piksel asli sekarang:

1 stbi_image_free(piksel);

Gambar Tekstur
Meskipun kita dapat menyiapkan shader untuk mengakses nilai piksel dalam buffer, lebih baik
menggunakan objek gambar di Vulkan untuk tujuan ini. Objek gambar akan mempermudah dan
mempercepat pengambilan warna dengan memungkinkan kita menggunakan koordinat 2D,
salah satunya. Piksel di dalam objek gambar dikenal sebagai texel dan kita akan menggunakan
nama itu mulai saat ini. Tambahkan anggota kelas baru berikut:

1 VkImage teksturGambar;
2 VkDeviceMemory teksturImageMemory;

Parameter untuk gambar ditentukan dalam struct VkImageCreateInfo:

205
Machine Translated by Google

1 VkImageCreateInfo imageInfo{}; 2
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; 3
imageInfo.imageType = VK_IMAGE_TYPE_2D; 4 imageInfo.extent.width
= static_cast<uint32_t>(texWidth); 5 imageInfo.extent.height =
static_cast<uint32_t>(texHeight); 6 imageInfo.extent.depth = 1; 7
imageInfo.mipLevels = 1; 8 imageInfo.arrayLayers = 1;

Jenis gambar, ditentukan dalam bidang imageType, memberi tahu Vulkan dengan jenis
sistem koordinat apa texel dalam gambar akan ditangani. Dimungkinkan untuk membuat
gambar 1D, 2D dan 3D. Gambar satu dimensi dapat digunakan untuk menyimpan array
data atau gradien, gambar dua dimensi terutama digunakan untuk tekstur, dan gambar tiga
dimensi dapat digunakan untuk menyimpan volume voxel, misalnya. Bidang jangkauan
menentukan dimensi gambar, pada dasarnya berapa banyak texel yang ada di setiap
sumbu. Itu sebabnya kedalaman harus 1 bukan 0. Tekstur kami tidak akan menjadi array
dan kami tidak akan menggunakan mipmapping untuk
sekarang.

1 imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB;

Vulkan mendukung banyak kemungkinan format gambar, tetapi kita harus menggunakan
format yang sama untuk texel seperti piksel dalam buffer, jika tidak, operasi penyalinan
akan gagal.

1 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;

Bidang ubin dapat memiliki salah satu dari dua nilai:

• VK_IMAGE_TILING_LINEAR: Texel ditata dalam urutan baris utama seperti


susunan piksel
• VK_IMAGE_TILING_OPTIMAL: Texel ditata dalam de implementasi
didenda untuk akses yang optimal

Berbeda dengan tata letak gambar, mode ubin tidak dapat diubah di lain waktu. Jika Anda
ingin dapat langsung mengakses texel di memori gambar, maka Anda harus menggunakan
VK_IMAGE_TILING_LINEAR. Kita akan menggunakan buffer pementasan alih-alih gambar
pementasan, jadi ini tidak diperlukan. Kami akan menggunakan VK_IMAGE_TILING_OPTIMAL
untuk akses efisien dari shader.

1 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

Hanya ada dua kemungkinan nilai untuk tata letak awal gambar:

• VK_IMAGE_LAYOUT_UNDEFINED: Tidak dapat digunakan oleh GPU dan transisi


pertama akan membuang texel. • VK_IMAGE_LAYOUT_PREINITIALIZED: Tidak
dapat digunakan oleh GPU, tetapi transisi pertama akan mempertahankan texel.

206
Machine Translated by Google

Ada beberapa situasi di mana texel perlu dipertahankan selama transisi pertama. Namun, salah
satu contohnya adalah jika Anda ingin menggunakan gambar sebagai gambar pementasan yang
dikombinasikan dengan tata letak VK_IMAGE_TILING_LINEAR. Dalam hal ini, Anda ingin
mengunggah data texel ke dalamnya dan kemudian mentransisikan gambar menjadi sumber
transfer tanpa kehilangan data. Namun, dalam kasus kita, pertama-tama kita akan mentransisi
gambar menjadi tujuan transfer dan kemudian menyalin data texel ke sana dari objek buffer, jadi
kita tidak memerlukan properti ini dan dapat menggunakan VK_IMAGE_LAYOUT_UNDEFINED
dengan aman.

1 imageInfo.penggunaan = VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT;

Bidang penggunaan memiliki semantik yang sama dengan yang selama pembuatan buffer.
Gambar akan digunakan sebagai tujuan untuk salinan buffer, sehingga harus diatur sebagai
tujuan transfer. Kami juga ingin dapat mengakses gambar dari shader untuk mewarnai mesh
kami, jadi penggunaannya harus menyertakan VK_IMAGE_USAGE_SAMPLED_BIT.

1 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

Gambar hanya akan digunakan oleh satu kelompok antrean: yang mendukung operasi transfer
grafik (dan karenanya juga).

1 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; 2
imageInfo.flags = 0; // Opsional

Bendera sampel terkait dengan multisampling. Ini hanya relevan untuk gambar yang akan
digunakan sebagai lampiran, jadi tetap gunakan satu sampel. Ada beberapa flag opsional untuk
gambar yang terkait dengan gambar renggang. Gambar jarang adalah gambar di mana hanya
wilayah tertentu yang benar-benar didukung oleh memori. Jika Anda menggunakan tekstur 3D
untuk medan voxel, misalnya, Anda dapat menggunakan ini untuk menghindari pengalokasian
memori untuk menyimpan nilai "udara" dalam jumlah besar. Kami tidak akan menggunakannya
dalam tutorial ini, jadi biarkan nilai defaultnya 0.

1 if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { throw


std::runtime_error("gagal membuat gambar!");
2
3}

Gambar dibuat menggunakan vkCreateImage, yang tidak memiliki parameter penting. Ada
kemungkinan VK_FORMAT_R8G8B8A8_SRGB untuk mat tidak didukung oleh perangkat keras
grafis. Anda harus memiliki daftar alternatif yang dapat diterima dan memilih yang terbaik yang
didukung. Namun, dukungan untuk format khusus ini sangat luas sehingga kami akan melewatkan
langkah ini. Menggunakan format yang berbeda juga membutuhkan konversi yang mengganggu.
Kita akan membahasnya kembali di bab buffer kedalaman, di mana kita akan mengimplementasikan
sistem seperti itu.

1 VkMemoryRequirements memRequirements;

207
Machine Translated by Google

2 vkGetImageMemoryRequirements(perangkat, textureImage, &memPersyaratan);


3

4 VkMemoryAllocateInfo allocInfo{}; 5 allocInfo.sType


= VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; 6 allocInfo.allocationSize =
memRequirements.size; 7 allocInfo.memoryTypeIndex =
findMemoryType(memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

8
9 if (vkAllocateMemory(device, &allocInfo, nullptr,
&textureImageMemory) != VK_SUCCESS) { throw
10 std::runtime_error("gagal mengalokasikan memori gambar!");
11 }
12

13 vkBindImageMemory(perangkat, textureImage, textureImageMemory, 0);

Mengalokasikan memori untuk gambar bekerja dengan cara yang persis sama seperti
mengalokasikan memori untuk buffer. Gunakan vkGetImageMemoryRequirements
sebagai ganti vkGetBufferMemoryRequirements, dan gunakan vkBindImageMemory
sebagai ganti vkBindBufferMemory.

Fungsi ini sudah semakin besar dan akan ada kebutuhan untuk membuat lebih banyak
gambar di bab selanjutnya, jadi kita harus mengabstraksi pembuatan gambar menjadi
fungsi createImage, seperti yang kita lakukan untuk buffer. Buat fungsi dan pindahkan
pembuatan objek gambar dan alokasi memori ke sana:

1 batal createImage(lebar uint32_t, tinggi uint32_t, format VkFormat, ubin


VkImageTiling, penggunaan VkImageUsageFlags, properti
VkMemoryPropertyFlags, VkImage& image, VkDeviceMemory& imageMemory)
{ VkImageCreateInfo imageInfo{}; imageInfo.sType =
2 VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageInfo.imageType =
3 VK_IMAGE_TYPE_2D; imageInfo.extent.width = lebar; imageInfo.extent.height
4 = tinggi; imageInfo.extent.depth = 1; imageInfo.mipLevels = 1;
5 imageInfo.arrayLayers = 1; infogambar.format = format; imageInfo.tiling = ubin;
6 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageInfo.usage
7 = penggunaan; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
8 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
9
10
11
12
13
14
15
16
17 if (vkCreateImage(perangkat, &informasigambar, nullptr, &gambar) !=
VK_SUCCESS) {

208
Machine Translated by Google

18 throw std::runtime_error("gagal membuat gambar!");


19 }
20
21 VkMemoryRequirements memRequirements;
22 vkGetImageMemoryRequirements(perangkat, gambar, &memPersyaratan);
23
24 VkMemoryAllocateInfo allocInfo{};
25 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
26 allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex
27 =
findMemoryType(memRequirements.memoryTypeBits, properti);
28
29 jika (vkAllocateMemory(perangkat, &allocInfo, nullptr, &imageMemory)
!= VK_SUCCESS)
30 { throw std::runtime_error("gagal mengalokasikan memori gambar!");
31 }
32
vkBindImageMemory(perangkat, gambar, memorigambar, 0);
33 34 }

Saya telah membuat lebar, tinggi, format, mode ubin, penggunaan, dan parameter properti
memori, karena ini semua akan bervariasi antara gambar yang akan kita buat sepanjang
tutorial ini.

Fungsi createTextureImage sekarang dapat disederhanakan menjadi:

1 void createTextureImage() { int


2 texWidth, texHeight, texChannels; stbi_uc*
3 piksel = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels,
STBI_rgb_alpha); VkDeviceSize imageSize = texWidth * texHeight * 4;
4
5
6 if (!pixels) { throw
7 std::runtime_error("gagal memuat gambar tekstur!");
8 }
9
10 Pementasan VkBufferBuffer;
11 Pementasan VkDeviceMemoryBufferMemory;
12 buatBuffer(ukurangambar, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
stagingBufferMemory);
13
14 batal* data;
15 vkMapMemory(perangkat, stagingBufferMemory, 0, imageSize, 0, &data);
16 memcpy(data, pixels, static_cast<size_t>(imageSize)); vkUnmapMemory(perangkat,
17 stagingBufferMemory);

209
Machine Translated by Google

18
19 stbi_image_free(piksel);
20
21 buatGambar(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage,
textureImageMemory);
22 }

Transisi tata letak


Fungsi yang akan kita tulis sekarang melibatkan perekaman dan eksekusi buffer perintah lagi,
jadi sekarang saat yang tepat untuk memindahkan logika itu ke dalam satu atau dua fungsi
pembantu:

1 VkCommandBuffer beginSingleTimeCommands()
2 { VkCommandBufferAllocateInfo allocInfo{}; allocInfo.sType =
3 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.level =
4 VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandPool = commandPool;
5 allocInfo.commandBufferCount = 1;
6
7
8 VkCommandBuffer perintahBuffer;
9 vkAllocateCommandBuffers(perangkat, &allocInfo, &commandBuffer);
10
11 VkCommandBufferBeginInfo beginInfo{};
12 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
13 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
14
15 vkBeginCommandBuffer(commandBuffer, &beginInfo);
16
17 kembalikan perintahBuffer;
18 }
19

20 batal endSingleTimeCommands(VkCommandBuffer commandBuffer)


{ vkEndCommandBuffer(commandBuffer); 21
22
23 VkSubmitInfo kirimInfo{};
24 kirimInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
25 kirimInfo.commandBufferCount = 1; submitInfo.pCommandBuffers =
26 &commandBuffer;
27
28 vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
29 vkQueueWaitIdle(graphicsQueue);

210
Machine Translated by Google

30
31 vkFreeCommandBuffers(perangkat, commandPool, 1, &commandBuffer);
32 }

Kode untuk fungsi ini didasarkan pada kode yang ada di copyBuffer. Anda sekarang dapat
menyederhanakan fungsi itu menjadi:

1 batal copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize


ukuran) {
2 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
3
4 VkBufferCopy copyRegion{};
5 copyRegion.size = ukuran;
6 vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

7
8 endSingleTimeCommands(commandBuffer);
9}

Jika kita masih menggunakan buffer, maka kita sekarang dapat menulis fungsi untuk merekam dan
menjalankan vkCmdCopyBufferToImage untuk menyelesaikan pekerjaan, tetapi perintah ini
mengharuskan gambar berada dalam tata letak yang benar terlebih dahulu. Buat fungsi baru untuk
menangani transisi tata letak:

1 batal transisiImageLayout (gambar VkImage, format VkFormat,


VkImageLayout oldLayout, VkImageLayout newLayout) {
2 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
3
4 endSingleTimeCommands(commandBuffer);
5}

Salah satu cara paling umum untuk melakukan transisi tata letak adalah menggunakan penghalang
memori gambar. Penghalang jalur pipa seperti itu umumnya digunakan untuk menyinkronkan akses
ke sumber daya, seperti memastikan bahwa penulisan ke buffer selesai sebelum membacanya, tetapi
juga dapat digunakan untuk mentransisi tata letak gambar dan mentransfer kepemilikan keluarga
antrean saat VK_SHARING_MODE_EXCLUSIVE digunakan. Ada penghalang memori buffer yang
setara untuk melakukan ini untuk buffer.

1 penghalang VkImageMemoryBarrier{}; 2
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; 3 barrier.oldLayout
= oldLayout; 4 barrier.newLayout = newLayout;

Dua bidang pertama menentukan transisi tata letak. Dimungkinkan untuk


menggunakan VK_IMAGE_LAYOUT_UNDEFINED sebagai oldLayout jika Anda tidak peduli dengan
konten gambar yang ada.

1 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; 2
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;

211
Machine Translated by Google

Jika Anda menggunakan penghalang untuk mentransfer kepemilikan kelompok antrean,


maka kedua bidang ini harus menjadi indeks dari kelompok antrean. Mereka harus disetel
ke VK_QUEUE_FAMILY_IGNORED jika Anda tidak ingin melakukan ini (bukan nilai default!).

1 penghalang.gambar =
gambar; 2 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
3 barrier.subresourceRange.baseMipLevel = 0; 4 barrier.subresourceRange.levelCount
= 1; 5 barrier.subresourceRange.baseArrayLayer = 0; 6 barrier.subresourceRange.layerCount
= 1;

Gambar dan subresourceRange menentukan gambar yang terpengaruh dan bagian tertentu
dari gambar. Gambar kami bukan array dan tidak memiliki level ping mipmap, jadi hanya
satu level dan layer yang ditentukan.

1 barrier.srcAccessMask = 0; // TODO 2
barrier.dstAccessMask = 0; // MELAKUKAN

Penghalang terutama digunakan untuk tujuan sinkronisasi, jadi Anda harus menentukan
jenis operasi mana yang melibatkan sumber daya yang harus terjadi sebelum penghalang,
dan operasi mana yang melibatkan sumber daya yang harus menunggu di penghalang.
Kita perlu melakukannya meskipun sudah menggunakan vkQueueWaitIdle untuk
menyinkronkan secara manual. Nilai yang tepat bergantung pada tata letak lama dan baru,
jadi kita akan kembali ke sini setelah mengetahui transisi mana yang akan kita gunakan.

1
2 vkCmdPipelineBarrier(perintahBuffer,
3 0 /* SEMUA */, 0 /* SEMUA */, 0,
4 0, nullptr, 0, nullptr, 1, &barrier
5
6
7
8 );

Semua jenis penghalang pipa dikirimkan menggunakan fungsi yang sama. Parameter
pertama setelah buffer perintah menentukan di tahap pipa mana operasi terjadi yang harus
terjadi sebelum penghalang. Parameter kedua menentukan tahap pipa di mana operasi
akan menunggu penghalang. Tahapan pipeline yang boleh Anda tentukan sebelum dan
sesudah penghalang bergantung pada cara Anda menggunakan sumber daya sebelum dan
sesudah penghalang. Nilai yang diizinkan tercantum dalam tabel spesifikasi ini. Misalnya,
jika Anda akan membaca dari seragam setelah penghalang, Anda akan menentukan
penggunaan VK_ACCESS_UNIFORM_READ_BIT dan shader paling awal yang akan
membaca dari seragam sebagai tahap pipa, misalnya
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT. Tidak masuk akal untuk menentukan
tahapan pipeline non-shader untuk jenis penggunaan ini dan lapisan validasi akan
memperingatkan Anda saat Anda menentukan tahapan pipeline yang tidak sesuai dengan
jenis penggunaan.

212
Machine Translated by Google

Parameter ketiga adalah 0 atau VK_DEPENDENCY_BY_REGION_BIT. Yang terakhir


mengubah penghalang menjadi kondisi per wilayah. Itu berarti implementasinya
diperbolehkan untuk sudah mulai membaca dari bagian-bagian sumber daya yang ditulis
sejauh ini, misalnya.

Tiga pasang parameter terakhir mereferensikan rangkaian penghalang pipa dari tiga jenis
yang tersedia: penghalang memori, penghalang memori buffer, dan penghalang memori
gambar seperti yang kita gunakan di sini. Perhatikan bahwa kita belum menggunakan
parameter VkFormat, tetapi kita akan menggunakannya untuk transisi khusus di bab buffer
kedalaman.

Menyalin buffer ke gambar


Sebelum kita kembali ke createTextureImage, kita akan menulis satu lagi fungsi pembantu:
copyBufferToImage:

1 batal copyBufferToImage (buffer VkBuffer, gambar VkImage, uint32_t


lebar, tinggi uint32_t) {
2 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
3
4 endSingleTimeCommands(commandBuffer);
5}

Sama seperti salinan buffer, Anda perlu menentukan bagian mana dari buffer yang akan
disalin ke bagian mana dari gambar. Ini terjadi melalui struktur VkBufferImageCopy:

1 region VkBufferImageCopy{}; 2
wilayah.bufferOffset = 0; 3
wilayah.bufferRowLength = 0; 4
wilayah.bufferImageHeight = 0;
5

6 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; 7
region.imageSubresource.mipLevel = 0; 8 region.imageSubresource.baseArrayLayer
= 0; 9 region.imageSubresource.layerCount = 1;

10

11 region.imageOffset = {0, 0, 0}; 12


region.imageExtent = { 13 lebar, tinggi, 1

14
15
16 };

Sebagian besar bidang ini cukup jelas. BufferOffset menentukan offset byte dalam buffer
tempat nilai piksel dimulai. Bidang bufferRowLength dan bufferImageHeight menentukan
bagaimana piksel ditata dalam memori. Untuk

213
Machine Translated by Google

misalnya, Anda dapat memiliki beberapa byte padding di antara baris gambar. Menentukan 0
untuk keduanya menunjukkan bahwa piksel dikemas rapat seperti dalam kasus kami. Bidang
imageSubresource, imageOffset, dan imageExtent menunjukkan bagian mana dari gambar yang
ingin kita salin pikselnya.

Operasi buffer ke penyalinan gambar diantrekan menggunakan fungsi vkCmdCopyBufferToImage:

1
2
3
4 vkCmdCopyBufferToImage( commandBuffer, buffer, gambar,
5 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
6 1, &wilayah
7
8 );

Parameter keempat menunjukkan tata letak mana yang sedang digunakan gambar. Saya berasumsi
di sini bahwa gambar telah dialihkan ke tata letak yang optimal untuk menyalin piksel. Saat ini kami
hanya menyalin satu bongkahan piksel ke seluruh gambar, tetapi mungkin untuk menentukan larik
VkBufferImageCopy untuk melakukan banyak salinan berbeda dari buffer ini ke gambar dalam satu
operasi.

Mempersiapkan gambar tekstur


Kami sekarang memiliki semua alat yang kami butuhkan untuk menyelesaikan pengaturan gambar
tekstur, jadi kami akan kembali ke fungsi createTextureImage. Hal terakhir yang kami lakukan
adalah membuat gambar tekstur. Langkah selanjutnya adalah menyalin staging buffer ke gambar
tekstur. Ini melibatkan dua langkah:

• Transisi gambar tekstur ke VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL • Jalankan


buffer ke operasi penyalinan gambar

Ini mudah dilakukan dengan fungsi yang baru saja kita buat:

1 transisiImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
2 copyBufferToImage(stagingBuffer, textureImage,
static_cast<uint32_t>(texWidth),
static_cast<uint32_t>(texHeight));

Gambar dibuat dengan tata letak VK_IMAGE_LAYOUT_UNDEFINED, sehingga salah satunya


harus ditetapkan sebagai tata letak lama saat mentransisikan textureImage. Ingatlah bahwa kita
dapat melakukan ini karena kita tidak peduli dengan isinya sebelum melakukan operasi penyalinan.

Untuk dapat memulai pengambilan sampel dari gambar tekstur di shader, kita memerlukan satu
transisi terakhir untuk menyiapkannya untuk akses shader:

214
Machine Translated by Google

1 transisiImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

Topeng penghalang transisi

Jika Anda menjalankan aplikasi dengan lapisan validasi yang diaktifkan sekarang, Anda akan melihat
bahwa aplikasi tersebut mengeluh tentang topeng akses dan tahapan pipa di transitionImageLayout
yang tidak valid. Kami masih perlu mengaturnya berdasarkan tata letak dalam transisi.

Ada dua transisi yang perlu kita tangani:

• Tidak ditentukan ÿ tujuan transfer: transfer tulis yang tidak perlu menunggu
pada apa pun
• Tujuan transfer ÿ pembacaan shader: pembacaan shader harus menunggu trans fer write,
khususnya shader membaca di shader fragmen, karena di situlah kita akan menggunakan
tekstur

Aturan ini ditentukan menggunakan topeng akses dan tahapan pipa berikut:

1 VkPipelineStageFlags sourceStage;
2 VkPipelineStageFlags tahap tujuan;
3
4 jika (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout ==
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { barrier.srcAccessMask =
5 0; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
6
7
8 sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
9 destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
10 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout ==
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { barrier.srcAccessMask =
11 VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask =
12 VK_ACCESS_SHADER_READ_BIT;
13
14 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
15 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
16 } lain {
17 throw std::invalid_argument(" transisi tata letak tidak didukung!");
18 }
19
20 vkCmdPipelineBarrier( commandBuffer,
21 sourceStage,
22 destinationStage, 0, 0, nullptr,
23
24

215
Machine Translated by Google

25 0, nullptr, 1,
26 &penghalang
27 );

Seperti yang Anda lihat pada tabel di atas, penulisan transfer harus terjadi pada tahap transfer pipa.
Karena penulisan tidak harus menunggu apa pun, Anda dapat menentukan masker akses kosong dan
tahap saluran pipa sedini mungkin VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT untuk operasi pra-
penghalang. Perlu dicatat bahwa VK_PIPELINE_STAGE_TRANSFER_BIT bukanlah tahapan nyata
dalam pipa grafis dan komputasi. Ini lebih merupakan tahap semu di mana transfer terjadi. Lihat
dokumentasi untuk informasi lebih lanjut dan contoh tahapan semu lainnya.

Gambar akan ditulis dalam tahap pipeline yang sama dan selanjutnya dibaca oleh shader fragmen,
itulah sebabnya kami menentukan akses pembacaan shader dalam tahap pipeline shader fragmen.

Jika kami perlu melakukan lebih banyak transisi di masa mendatang, kami akan memperluas fungsinya.
Aplikasi sekarang harus berjalan dengan sukses, meskipun tentu saja belum ada perubahan visual.

Satu hal yang perlu diperhatikan adalah bahwa pengiriman buffer perintah menghasilkan sinkronisasi
VK_ACCESS_HOST_WRITE_BIT implisit di awal. Karena fungsi transitionImageLayout menjalankan
buffer perintah hanya dengan satu perintah, Anda dapat menggunakan sinkronisasi implisit ini dan
menyetel srcAccessMask ke 0 jika Anda memerlukan ketergantungan VK_ACCESS_HOST_WRITE_BIT
dalam transisi tata letak. Terserah Anda apakah Anda ingin secara eksplisit atau tidak, tetapi saya
pribadi bukan penggemar mengandalkan operasi "tersembunyi" seperti OpenGL ini.

Sebenarnya ada jenis tata letak gambar khusus yang mendukung semua operasi,
VK_IMAGE_LAYOUT_GENERAL. Masalahnya, tentu saja, itu tidak selalu menawarkan kinerja terbaik
untuk operasi apa pun. Ini diperlukan untuk beberapa kasus khusus, seperti menggunakan gambar
sebagai input dan output, atau untuk membaca gambar setelah meninggalkan tata letak yang telah
diinisialisasi sebelumnya.

Semua fungsi pembantu yang mengirimkan perintah sejauh ini telah diatur untuk dijalankan secara
sinkron dengan menunggu antrean menganggur. Untuk aplikasi praktis, disarankan untuk
menggabungkan operasi ini dalam buffer perintah tunggal dan menjalankannya secara asinkron untuk
throughput yang lebih tinggi, terutama transisi dan penyalinan dalam fungsi createTextureImage.
Cobalah untuk bereksperimen dengan ini dengan membuat setupCommandBuffer di mana fungsi
helper merekam perintah, dan tambahkan flushSetupCommands untuk menjalankan perintah yang
telah direkam sejauh ini. Sebaiknya lakukan ini setelah pemetaan tekstur berfungsi untuk memeriksa
apakah sumber daya tekstur masih diatur dengan benar.

216
Machine Translated by Google

Membersihkan

Selesaikan fungsi createTextureImage dengan membersihkan staging buffer dan


memorinya di akhir:

1 transisiImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
2
3 vkDestroyBuffer(perangkat, stagingBuffer, nullptr);
4 vkFreeMemory(perangkat, stagingBufferMemory, nullptr);
5}

Gambar tekstur utama digunakan hingga akhir program:

1 pembersihan batal ()
{2 cleanupSwapChain();
3
4 vkDestroyImage(perangkat, textureImage, nullptr);
5 vkFreeMemory(perangkat, textureImageMemory, nullptr);
6
7 ...
8}

Gambar sekarang berisi tekstur, tetapi kita masih memerlukan cara untuk mengaksesnya
dari pipa grafis. Kami akan mengerjakannya di bab berikutnya.

Kode C++ / Vertex shader / Fragment shader

217
Machine Translated by Google

Tampilan gambar dan sampler

Dalam bab ini kita akan membuat dua sumber daya lagi yang diperlukan untuk pipa
grafis untuk membuat sampel gambar. Sumber daya pertama adalah salah satu yang
telah kita lihat sebelumnya saat bekerja dengan gambar rantai swap, tetapi yang
kedua baru - ini berkaitan dengan bagaimana shader akan membaca texel dari gambar.

Tampilan gambar tekstur


Kita telah melihat sebelumnya, dengan gambar rantai swap dan framebuffer, bahwa
gambar diakses melalui tampilan gambar, bukan secara langsung. Kita juga perlu
membuat tampilan gambar seperti itu untuk gambar tekstur.

Tambahkan anggota kelas untuk memegang VkImageView untuk gambar tekstur dan
buat fungsi baru createTextureImageView tempat kita akan membuatnya:

1 VkImageView teksturImageView;
2
3 ...
4
5 batal initVulkan() {
6 ...
7 buatTeksturGambar();
8 createTextureImageView();
9 createVertexBuffer();
10 ...
11 }
12
13 ...
14

15 batal createTextureImageView() { 16 17 }

Kode untuk fungsi ini dapat didasarkan langsung pada createImageViews. Hanya dua perubahan
yang harus Anda lakukan adalah format dan gambarnya:

218
Machine Translated by Google

1 VkImageViewCreateInfo viewInfo{}; 2 viewInfo.sType


= VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; 3 viewInfo.image = textureImage;
4 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; 5 lihatInfo.format =
VK_FORMAT_R8G8B8A8_SRGB; 6 viewInfo.subresourceRange.aspectMask =
VK_IMAGE_ASPECT_COLOR_BIT; 7 viewInfo.subresourceRange.baseMipLevel = 0; 8
viewInfo.subresourceRange.levelCount = 1; 9 viewInfo.subresourceRange.baseArrayLayer = 0; 10
viewInfo.subresourceRange.layerCount = 1;

Saya telah meninggalkan inisialisasi viewInfo.components eksplisit, karena


VK_COMPONENT_SWIZZLE_IDENTITY tetap didefinisikan sebagai 0. Selesaikan
pembuatan tampilan gambar dengan memanggil vkCreateImageView:

1 jika (vkCreateImageView(perangkat, &viewInfo, nullptr, &textureImageView)


!= VK_SUCCESS)
{ throw std::runtime_error("gagal membuat tampilan gambar tekstur!");
23}

Karena begitu banyak logika yang digandakan dari createImageViews, Anda mungkin ingin
mengabstraksikannya menjadi fungsi createImageView baru:

1 VkImageView createImageView(Gambar VkImage, format VkFormat) { VkImageViewCreateInfo


2 viewInfo{}; viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
3 viewInfo.gambar = gambar; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
4 viewInfo.format = format; viewInfo.subresourceRange.aspectMask =
5 VK_IMAGE_ASPECT_COLOR_BIT; viewInfo.subresourceRange.baseMipLevel = 0;
6 viewInfo.subresourceRange.levelCount = 1; viewInfo.subresourceRange.baseArrayLayer =
7 0; viewInfo.subresourceRange.layerCount = 1;
8
9
10
11
12
13 VkImageView imageView; if
14 (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS)
{ throw std::runtime_error("gagal membuat gambar tekstur
15
melihat!");
16 }
17
18 kembalikan tampilan gambar;
19 }

Fungsi createTextureImageView sekarang dapat disederhanakan menjadi:

219
Machine Translated by Google

1 batal createTextureImageView()
2 { textureImageView = createImageView(textureImage,
VK_FORMAT_R8G8B8A8_SRGB);
3}

Dan createImageViews dapat disederhanakan menjadi:

1 batal createImageViews()
2 { swapChainImageViews.resize(swapChainImages.size());
3
4 untuk (uint32_t i = 0; i < swapChainImages.size(); i++) {
5 swapChainImageViews[i] = buatImageView(swapChainImages[i],
swapChainImageFormat);
6 }
7}

Pastikan untuk menghancurkan tampilan gambar di akhir program, tepat sebelum menghancurkan
gambar itu sendiri:

1 pembersihan batal ()
2 { cleanupSwapChain();
3
4 vkDestroyImageView(perangkat, textureImageView, nullptr);
5
6 vkDestroyImage(perangkat, textureImage, nullptr);
7 vkFreeMemory(perangkat, textureImageMemory, nullptr);

Sampler
Shader dapat membaca texel langsung dari gambar, tetapi itu tidak umum jika digunakan sebagai
tekstur. Tekstur biasanya diakses melalui sampler, yang akan menerapkan pemfilteran dan
transformasi untuk menghitung warna akhir yang diambil.

Filter ini berguna untuk menangani masalah seperti oversampling. Pertimbangkan tekstur yang
dipetakan ke geometri dengan lebih banyak fragmen daripada texel. Jika Anda hanya mengambil
texel terdekat untuk koordinat tekstur di setiap fragmen, maka Anda akan mendapatkan hasil
seperti gambar pertama:

220
Machine Translated by Google

Jika Anda menggabungkan 4 texel terdekat melalui interpolasi linier, maka Anda akan
mendapatkan hasil yang lebih mulus seperti di sebelah kanan. Tentu saja aplikasi Anda
mungkin memiliki persyaratan gaya seni yang lebih sesuai dengan gaya kiri (pikirkan
Minecraft), tetapi kanan lebih disukai dalam aplikasi grafis konvensional. Objek sampler
secara otomatis menerapkan pemfilteran ini untuk Anda saat membaca warna dari tekstur.

Undersampling adalah masalah sebaliknya, di mana Anda memiliki lebih banyak texel
daripada fragmen. Ini akan menyebabkan artefak saat mengambil sampel pola frekuensi
tinggi seperti tekstur kotak-kotak pada sudut yang tajam:

Seperti yang ditunjukkan pada gambar kiri, tekstur berubah menjadi buram di kejauhan.
Solusi untuk ini adalah penyaringan anisotropik, yang juga dapat diterapkan secara otomatis
oleh sampler.

Selain filter ini, sampler juga dapat menangani transformasi. Ini menentukan apa yang terjadi
ketika Anda mencoba membaca texel di luar gambar melalui mode pengalamatannya.
Gambar di bawah menampilkan beberapa kemungkinan:

221
Machine Translated by Google

Kami sekarang akan membuat fungsi createTextureSampler untuk mengatur objek sampler
tersebut. Kami akan menggunakan sampler itu untuk membaca warna dari tekstur di
shader nanti.

1 batal initVulkan() {
2 ...
3 buatTeksturGambar();
4 createTextureImageView();
5 createTextureSampler();
6 ...
7}
8
9 ...
10
11 membatalkan createTextureSampler()
{ 12
13 }

Sampler dikonfigurasikan melalui struktur VkSamplerCreateInfo, yang menentukan semua filter dan
transformasi yang harus diterapkan.

1 VkSamplerCreateInfo samplerInfo{}; 2
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; 3
samplerInfo.magFilter = VK_FILTER_LINEAR; 4 samplerInfo.minFilter =
VK_FILTER_LINEAR;

Bidang magFilter dan minFilter menentukan cara menginterpolasi texel yang diperbesar atau diperkecil.
Pembesaran menyangkut masalah oversampling yang dijelaskan di atas, dan minifikasi menyangkut
undersampling. Pilihannya adalah VK_FILTER_NEAREST dan VK_FILTER_LINEAR, sesuai dengan
mode yang ditunjukkan pada gambar di atas.

1 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
2 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
3 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;

Mode pengalamatan dapat ditentukan per sumbu menggunakan bidang addressMode.


Nilai yang tersedia tercantum di bawah ini. Sebagian besar ditunjukkan dalam

222
Machine Translated by Google

gambar di atas. Perhatikan bahwa sumbu disebut U, V dan W bukannya X, Y dan Z.


Ini adalah konvensi untuk koordinat ruang tekstur.

• VK_SAMPLER_ADDRESS_MODE_REPEAT: Ulangi tekstur saat akan


di luar dimensi gambar.
• VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT: Seperti mengulang, tetapi
membalikkan koordinat untuk mencerminkan gambar saat melampaui dimensi. •
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE: Ambil warna tepi yang paling
dekat dengan koordinat di luar dimensi gambar. •
VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE: Seperti penjepit ke
tepi, tetapi sebaliknya menggunakan tepi yang berlawanan dengan tepi terdekat. •
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER: Mengembalikan warna solid
saat mengambil sampel di luar dimensi gambar.

Tidak masalah mode pengalamatan mana yang kita gunakan di sini, karena kita tidak akan
mengambil sampel di luar gambar dalam tutorial ini. Namun, mode pengulangan mungkin
merupakan mode yang paling umum, karena dapat digunakan untuk tekstur ubin seperti lantai
dan dinding.

1 samplerInfo.anisotropyEnable = VK_TRUE; 2
samplerInfo.maxAnisotropy = ???;

Kedua bidang ini menentukan apakah penyaringan anisotropik harus digunakan. Tidak ada
alasan untuk tidak menggunakan ini kecuali kinerja menjadi perhatian. Bidang maxAnisotropy
membatasi jumlah sampel texel yang dapat digunakan untuk menghitung warna akhir. Nilai
yang lebih rendah menghasilkan kinerja yang lebih baik, tetapi kualitasnya lebih rendah. Untuk
mengetahui nilai mana yang dapat kita gunakan, kita perlu mengambil properti dari perangkat
fisik seperti ini:

1 properti VkPhysicalDeviceProperties{}; 2
vkGetPhysicalDeviceProperties(Perangkat fisik, & properti);

Jika Anda melihat dokumentasi untuk struktur VkPhysicalDeviceProperties, Anda akan melihat
bahwa itu berisi batasan nama anggota VkPhysicalDeviceLimits. Struktur ini pada gilirannya
memiliki anggota bernama maxSamplerAnisotropy dan ini adalah nilai maksimum yang dapat
kita tentukan untuk maxAnisotropy. Jika kita ingin mendapatkan kualitas maksimal, kita cukup
menggunakan nilai itu secara langsung:

1 samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy;

Anda dapat mengkueri properti di awal program dan menyebarkannya ke fungsi yang
membutuhkannya, atau mengkuerinya dalam fungsi createTextureSampler itu sendiri.

1 samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;

Bidang borderColor menentukan warna mana yang dikembalikan saat mengambil sampel di
luar gambar dengan mode pengalamatan penjepit ke tepi. Dimungkinkan untuk mengembalikan
hitam, putih atau transparan baik dalam format float atau int. Anda tidak dapat menentukan
warna baki arbi.

223
Machine Translated by Google

1 samplerInfo.unnormalizedCoordinates = VK_FALSE;

Kolom unnormalizedCoordinates menentukan sistem koordinat mana yang ingin Anda


gunakan untuk mengalamatkan texel dalam gambar. Jika kolom ini adalah VK_TRUE,
Anda cukup menggunakan koordinat dalam rentang [0, texWidth) dan [0, texHeight). Jika
VK_FALSE, maka texel dialamatkan menggunakan rentang [0, 1) pada semua sumbu.
Aplikasi dunia nyata hampir selalu menggunakan koordinat yang dinormalisasi, karena
dengan demikian dimungkinkan untuk menggunakan tekstur dengan berbagai resolusi
dengan koordinat yang sama persis.

1 samplerInfo.compareEnable = VK_FALSE; 2
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;

Jika fungsi perbandingan diaktifkan, maka texel pertama-tama akan dibandingkan dengan
sebuah nilai, dan hasil perbandingan tersebut digunakan dalam operasi pemfilteran. Ini
terutama digunakan untuk pemfilteran persentase lebih dekat pada peta bayangan. Kita akan
melihat ini di bab mendatang.

1 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; 2
samplerInfo.mipLodBias = 0,0f; 3 samplerInfo.minLod = 0,0f; 4 samplerInfo.maxLod =
0,0f;

Semua bidang ini berlaku untuk mipmapping. Kita akan melihat mipmapping di bab
selanjutnya, tetapi pada dasarnya ini adalah jenis filter lain yang dapat diterapkan.

Fungsi sampler sekarang sepenuhnya ditentukan. Tambahkan anggota kelas untuk


memegang pegangan objek sampler dan buat sampler dengan vkCreateSampler:
1 VkImageView teksturImageView;
2 VkSampler teksturSampler;
3
4 ...
5

6 batal createTextureSampler() { 7
...
8
9 if (vkCreateSampler(device, &samplerInfo, nullptr,
&textureSampler) != VK_SUCCESS) { throw
10 std::runtime_error("gagal membuat sampler tekstur!");

}
11 12 }

Perhatikan bahwa sampler tidak mereferensikan VkImage di mana pun. Sampler adalah
objek berbeda yang menyediakan antarmuka untuk mengekstrak warna dari tekstur. Itu
dapat diterapkan pada gambar apa pun yang Anda inginkan, apakah itu 1D, 2D atau 3D. Ini adalah

224
Machine Translated by Google

berbeda dari banyak API lama, yang menggabungkan gambar tekstur dan pemfilteran
menjadi satu keadaan.

Hancurkan sampler di akhir program saat kita tidak lagi dapat mengakses gambar:

1 pembersihan batal () {
2 cleanupSwapChain();
3
4 vkDestroySampler(perangkat, teksturSampler, nullptr);
5 vkDestroyImageView(perangkat, textureImageView, nullptr);
6
7 ...
8}

Fitur perangkat anisotropi


Jika Anda menjalankan program sekarang, Anda akan melihat pesan lapisan validasi
seperti ini:

Itu karena pemfilteran anisotropik sebenarnya adalah fitur perangkat opsional. Kita perlu
memperbarui fungsi createLogicalDevice untuk memintanya:

1 VkPhysicalDeviceFeatures deviceFeatures{}; 2
deviceFeatures.samplerAnisotropy = VK_TRUE;

Dan meskipun sangat tidak mungkin kartu grafis modern tidak mendukungnya, kita harus
memperbarui isDeviceSuitable untuk memeriksa apakah tersedia:

1 bool isDeviceSuitable(perangkat VkPhysicalDevice) { 2


...
3
4 VkFisikPerangkatFitur didukungFitur;
5 vkGetFiturPerangkatFisik(perangkat, &Fitur yang didukung);
6
7 kembalikan indeks.isLengkap() && ekstensiDidukung &&
swapChainMemadai && didukungFitur.samplerAnisotropi;
8}

vkGetPhysicalDeviceFeatures menggunakan ulang struktur VkPhysicalDeviceFeatures untuk


menunjukkan fitur mana yang didukung daripada diminta dengan menyetel nilai boolean.

225
Machine Translated by Google

Alih-alih memaksakan ketersediaan pemfilteran anisotropik, dimungkinkan juga untuk tidak


menggunakannya dengan pengaturan kondisional:

1 samplerInfo.anisotropyEnable = VK_FALSE; 2
samplerInfo.maxAnisotropy = 1.0f;

Pada bab selanjutnya kita akan memaparkan objek gambar dan sampler ke shader untuk
menggambar tekstur ke persegi.

Kode C++ / Vertex shader / Fragment shader

226
Machine Translated by Google

Sampler gambar gabungan

pengantar

Kami melihat deskriptor untuk pertama kalinya di bagian buffer seragam dari tutorial. Dalam bab ini
kita akan melihat jenis deskriptor baru: sampler gambar gabungan. Deskriptor ini memungkinkan
shader untuk mengakses sumber gambar melalui objek sampler seperti yang kita buat di bab
sebelumnya.

Kita akan mulai dengan memodifikasi tata letak deskriptor, kumpulan deskriptor, dan set deskriptor
untuk menyertakan deskriptor sampler gambar gabungan tersebut. Setelah itu, kita akan
menambahkan koordinat tekstur ke Vertex dan memodifikasi shader fragmen untuk membaca warna
dari tekstur alih-alih hanya menginterpolasi warna verteks.

Memperbarui deskriptor

Jelajahi fungsi createDescriptorSetLayout dan tambahkan VkDescriptorSetLayoutBinding untuk deskriptor sampler gambar
gabungan. Kami hanya akan meletakkannya di penjilidan setelah buffer seragam:

1 VkDescriptorSetLayoutBinding samplerLayoutBinding{}; 2
samplerLayoutBinding.binding = 1; 3 samplerLayoutBinding.descriptorCount
= 1; 4 samplerLayoutBinding.descriptorType =

VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
5 samplerLayoutBinding.pImmutableSamplers = nullptr; 6
samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; 7

8 std::array<VkDescriptorSetLayoutBinding, 2> binding =


{uboLayoutBinding, samplerLayoutBinding}; 9
VkDescriptorSetLayoutCreateInfo layoutInfo{}; 10 layoutInfo.sType
=
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
11 layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size()); 12 layoutInfo.pBindings =
bindings.data();

227
Machine Translated by Google

Pastikan untuk menyetel stageFlags untuk menunjukkan bahwa kami bermaksud


menggunakan deskriptor sampler gambar gabungan dalam shader fragmen. Di situlah
warna fragmen akan ditentukan. Dimungkinkan untuk menggunakan pengambilan sampel
tekstur di vertex shader, misalnya untuk secara dinamis mengubah bentuk kisi-kisi simpul
dengan peta ketinggian.

Kita juga harus membuat kumpulan deskriptor yang lebih besar untuk memberi ruang bagi
alokasi sampler gambar gabungan dengan menambahkan VkPoolSize lain bertipe
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER ke VkDescriptorPoolCreateInfo.
Buka fungsi createDescriptorPool dan modifikasi untuk menyertakan VkDescriptorPoolSize
untuk deskriptor ini:

1 std::array<VkDescriptorPoolSize, 2> poolSizes{}; 2 ukuran


kolam[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 3
poolSizes[0].descriptorCount =
static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
4 ukuran kolam[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
5 poolSizes[1].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

6
7 VkDescriptorPoolCreateInfo poolInfo{}; 8
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
9 poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size()); 10
poolInfo.pPoolSizes = poolSizes.data(); 11 poolInfo.maxSets =
static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

Kumpulan deskriptor yang tidak memadai adalah contoh bagus masalah yang tidak akan
ditangkap oleh lapisan validasi: Pada Vulkan 1.1, vkAllocateDescriptorSets mungkin gagal
dengan kode kesalahan VK_ERROR_POOL_OUT_OF_MEMORY jika kumpulan tidak
cukup besar, tetapi driver juga dapat mencoba menyelesaikan masalah secara internal.
Ini berarti bahwa kadang-kadang (bergantung pada perangkat keras, ukuran kumpulan,
dan ukuran alokasi) driver akan membiarkan kami lolos dengan alokasi yang melebihi
batas kumpulan deskriptor kami. Di lain waktu, vkAllocateDescriptorSets akan gagal dan
mengembalikan VK_ERROR_POOL_OUT_OF_MEMORY. Ini bisa sangat membuat
frustasi jika alokasi berhasil pada beberapa mesin, tetapi gagal pada yang lain.

Karena Vulkan mengalihkan tanggung jawab alokasi ke driver, tidak lagi menjadi
persyaratan ketat untuk hanya mengalokasikan sebanyak mungkin deskriptor dari jenis
tertentu (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, dll.) seperti yang
ditentukan oleh anggota descriptorCount terkait untuk pembuatan kumpulan deskriptor.
Namun, tetap praktik terbaik untuk melakukannya, dan di masa mendatang,
VK_LAYER_KHRONOS_validation akan memperingatkan tentang jenis masalah ini jika
Anda mengaktifkan Validasi Praktik Terbaik.

Langkah terakhir adalah mengikat sumber gambar dan sampler yang sebenarnya ke
deskriptor di set deskriptor. Buka fungsi createDescriptorSets.

1 untuk (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {

228
Machine Translated by Google

2 VkDescriptorBufferInfo bufferInfo{};
3 bufferInfo.buffer = uniformBuffers[i]; bufferInfo.offset
4 = 0; bufferInfo.range = sizeof(UniformBufferObject);
5
6
7 VkDescriptorImageInfo imageInfo{};
8 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
9 imageInfo.imageView = textureImageView; imageInfo.sampler = teksturSampler;
10
11
12 ...
13 }

Sumber daya untuk struktur sampler gambar gabungan harus ditentukan dalam struct
VkDescriptorImageInfo, sama seperti sumber daya buffer untuk deskriptor buffer seragam
yang ditentukan dalam struct VkDescriptorBufferInfo. Di sinilah objek dari bab sebelumnya
berkumpul.

1 std::array<VkWriteDescriptorSet, 2> descriptorWrites{};


2

3 descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; 4
descriptorWrites[0].dstSet = descriptorSets[i]; 5 descriptorWrites[0].dstBinding = 0; 6
descriptorWrites[0].dstArrayElement = 0; 7 descriptorWrites[0].descriptorType =

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
8 descriptorWrites[0].descriptorCount = 1; 9
descriptorWrites[0].pBufferInfo = &bufferInfo;
10

11 descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; 12
descriptorWrites[1].dstSet = descriptorSets[i]; 13 descriptorWrites[1].dstBinding = 1; 14
descriptorWrites[1].dstArrayElement = 0; 15 descriptorWrites[1].descriptorType =
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;

16 descriptorWrites[1].descriptorCount = 1; 17
descriptorWrites[1].pImageInfo = &imageInfo; 18

19 vkUpdateDescriptorSets(perangkat,
static_cast<uint32_t>(descriptorWrites.size()),
descriptorWrites.data(), 0, nullptr);

Deskriptor harus diperbarui dengan info gambar ini, sama seperti buffer. Kali ini kami
menggunakan array pImageInfo, bukan pBufferInfo. Deskriptor sekarang siap digunakan
oleh shader!

229
Machine Translated by Google

Koordinat tekstur
Ada satu unsur penting untuk pemetaan tekstur yang masih belum ada, yaitu koordinat
sebenarnya untuk setiap simpul. Koordinat menentukan bagaimana gambar sebenarnya
dipetakan ke geometri.
1 struct Puncak
2 { glm::vec2 pos;
3 glm::vec3 warna;
4 glm::vec2 texCoord;
5
6 static VkVertexInputBindingDescription getBindingDescription()
7 { VkVertexInputBindingDescription bindingDescription{}; bindingDescription.binding
8 = 0; bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate =
9 VK_VERTEX_INPUT_RATE_VERTEX;
10
11
12 return bindingDescription;
13 }
14
15 statis std::array<VkVertexInputAttributeDescription, 3>
getAttributeDescriptions()
16 { std::array<VkVertexInputAttributeDescription, 3>
atributKeterangan{};
17
18 atributDeskripsi[0].binding = 0;
19 atributDeskripsi[0].lokasi = 0;
20 atributKeterangan[0].format = VK_FORMAT_R32G32_SFLOAT;
21 atributDeskripsi[0].offset = offsetof(Vertex, pos);
22
23 atributDeskripsi[1].binding = 0;
24 atributDeskripsi[1].lokasi = 1;
25 atributKeterangan[1].format = VK_FORMAT_R32G32B32_SFLOAT;
26 atributDeskripsi[1].offset = offsetof(Vertex, warna);
27
28 atributDeskripsi[2].binding = 0;
29 atributDeskripsi[2].lokasi = 2;
30 atributKeterangan[2].format = VK_FORMAT_R32G32_SFLOAT;
31 atributDeskripsi[2].offset = offsetof(Vertex, texCoord);
32
33 return atributDeskripsi;
34 }
35 };

Ubah struct Vertex untuk menyertakan vec2 untuk koordinat tekstur. Pastikan
juga untuk menambahkan VkVertexInputAttributeDescription agar kita dapat
menggunakan koordinat tekstur akses sebagai input di vertex shader. Itu perlu untuk bisa

230
Machine Translated by Google

untuk meneruskannya ke shader fragmen untuk interpolasi di seluruh permukaan


kotak.
1 const std::vector<Vertex> simpul = {
2 {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f, -0.5f}, {0.0f, 1.0f,
3 0.0f }, {0.0f, 0.0f}}, {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, {{-0.5f,
4 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
5
6 };

Dalam tutorial ini, saya hanya akan mengisi persegi dengan tekstur dengan menggunakan
koordinat dari 0, 0 di pojok kiri atas hingga 1, 1 di pojok kanan bawah. Jangan ragu untuk
bereksperimen dengan koordinat yang berbeda. Coba gunakan koordinat di bawah 0 atau
di atas 1 untuk melihat mode pengalamatan beraksi!

Shader
Langkah terakhir adalah memodifikasi shader untuk mengambil sampel warna dari tekstur.
Pertama-tama kita perlu memodifikasi vertex shader untuk melewati koordinat tekstur ke
shader fragmen:

1 tata letak(lokasi = 0) di vec2 inPosition; 2 tata letak(lokasi = 1) di


vec3 inColor; 3 tata letak(lokasi = 2) di vec2 inTexCoord; 4

5 tata letak(lokasi = 0) keluar vec3 fragColor; 6 tata


letak(lokasi = 1) keluar vec2 fragTexCoord;

7 8 batal utama() {
9 gl_Position = ubo.proj * ubo.view * ubo.model * vec4(dalamPosisi,
0,0, 1,0);
10 fragColor = inColor;
11 fragTexCoord = inTexCoord;
12 }

Sama seperti warna per titik, nilai fragTexCoord akan diinterpolasi dengan lancar di seluruh
area persegi oleh rasterizer. Kita dapat memvisualisasikan ini dengan membuat shader
fragmen mengeluarkan koordinat tekstur sebagai warna:
1 #versi 450
2
3 tata letak(lokasi = 0) di vec3 fragColor; 4 tata
letak(lokasi = 1) di vec2 fragTexCoord;
5
6 tata letak(lokasi = 0) out vec4 outColor;
7
8 batal utama() {

231
Machine Translated by Google

9 outColor = vec4(fragTexCoord, 0.0, 1.0);


10 }

Anda akan melihat sesuatu seperti gambar di bawah ini. Jangan lupa untuk mengkompilasi
ulang shader!

Saluran hijau mewakili koordinat horizontal dan saluran merah koordinat vertikal. Sudut hitam
dan kuning memastikan bahwa koordinat tekstur diinterpolasi dengan benar dari 0, 0 hingga 1,
1 melintasi bujur sangkar.
Memvisualisasikan data menggunakan warna adalah program shader yang setara dengan printf
debugging, karena tidak ada pilihan yang lebih baik!

Deskriptor sampler gambar gabungan direpresentasikan dalam GLSL dengan seragam sampler.
Tambahkan referensi untuk itu di shader fragmen:

1 layout(binding = 1) sampler2D texSampler seragam;

Ada jenis sampler1D dan sampler3D yang setara untuk jenis gambar lainnya.
Pastikan untuk menggunakan penjilidan yang benar di sini.

1 batal main() {
2 outColor = tekstur(texSampler, fragTexCoord);
3}

232
Machine Translated by Google

Tekstur diambil sampelnya menggunakan fungsi tekstur bawaan. Dibutuhkan sampler dan
koordinat sebagai argumen. Sampler secara otomatis menangani pemfilteran dan transformasi
di latar belakang. Anda sekarang akan melihat tekstur pada kotak ketika Anda menjalankan
aplikasi:

Cobalah bereksperimen dengan mode pengalamatan dengan menskalakan koordinat tekstur


ke nilai yang lebih tinggi dari 1. Misalnya, shader fragmen berikut menghasilkan gambar di
bawah saat menggunakan VK_SAMPLER_ADDRESS_MODE_REPEAT:

1 batal main() {
2 outColor = tekstur(texSampler, fragTexCoord * 2.0);
3}

233
Machine Translated by Google

Anda juga dapat memanipulasi warna tekstur menggunakan warna simpul:

1 batal main() {
2 outColor = vec4(fragColor * tekstur(texSampler,
askTexCoord).rgb, 1.0);
3}

Saya telah memisahkan saluran RGB dan alfa di sini untuk tidak menskalakan saluran alfa.

234
Machine Translated by Google

Anda sekarang tahu cara mengakses gambar di shader! Ini adalah teknik yang sangat kuat
bila dikombinasikan dengan gambar yang juga ditulis dalam framebuffer. Anda dapat
menggunakan gambar ini sebagai input untuk mengimplementasikan efek keren seperti
pasca-pemrosesan dan tampilan kamera dalam dunia 3D.

Kode C++ / Vertex shader / Fragment shader

235
Machine Translated by Google

Buffer kedalaman

pengantar

Geometri yang telah kita kerjakan sejauh ini diproyeksikan ke dalam 3D, tetapi masih
sepenuhnya datar. Dalam bab ini kita akan menambahkan koordinat Z ke posisi untuk
menyiapkan mesh 3D. Kami akan menggunakan koordinat ketiga ini untuk menempatkan
persegi di atas persegi saat ini untuk melihat masalah yang muncul saat geometri tidak
diurutkan berdasarkan kedalaman.

geometri 3D
Ubah struct Vertex untuk menggunakan vektor 3D untuk posisinya, dan perbarui format
dalam VkVertexInputAttributeDescription yang sesuai:
1 struct Vertex
2 { glm::vec3 pos;
3 glm::vec3 warna;
4 glm::vec2 texCoord;
5
6 ...
7
8 statis std::array<VkVertexInputAttributeDescription, 3>
getAttributeDescriptions()
9 { std::array<VkVertexInputAttributeDescription, 3>
atributKeterangan{};
10
11 atributDeskripsi[0].binding = 0;
12 atributDeskripsi[0].lokasi = 0;
13 atributKeterangan[0].format = VK_FORMAT_R32G32B32_SFLOAT;
14 atributDeskripsi[0].offset = offsetof(Vertex, pos);
15
16 ...
17 }
18 };

236
Machine Translated by Google

Selanjutnya, perbarui vertex shader untuk menerima dan mengubah koordinat 3D sebagai masukan.
Jangan lupa untuk mengkompilasi ulang setelah itu!

1 tata letak(lokasi = 0) di vec3 inPosition;


2
3 ...
4
5 batal utama() {
6 gl_Position = ubo.proj * ubo.view * ubo.model * vec4(dalamPosisi,
1.0);
7 fragColor = inColor;
8 fragTexCoord = inTexCoord;
9}

Terakhir, perbarui wadah simpul untuk menyertakan koordinat Z:

1 const std::vector<Vertex> simpul = { {{-0.5f, -0.5f, 0.0f},


2 {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.5f , -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f,
3 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f }, {1.0f, 1.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f,
4 1.0f}}
5
6 };

Jika Anda menjalankan aplikasi Anda sekarang, maka Anda akan melihat hasil yang persis
sama seperti sebelumnya. Saatnya menambahkan beberapa geometri ekstra untuk membuat
pemandangan lebih menarik, dan untuk mendemonstrasikan masalah yang akan kita tangani di bab ini.
Gandakan simpul untuk menentukan posisi persegi tepat di bawah yang sekarang seperti ini:

Gunakan koordinat Z -0,5f dan tambahkan indeks yang sesuai untuk kuadrat ekstra:

1 const std::vector<Vertex> simpul = { {{-0.5f, -0.5f, 0.0f},


2 {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},

237
Machine Translated by Google

3 {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f, 0.5f, 0.0f}, {0.0f,
4 0.0 f, 1.0f}, {1.0f, 1.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}},
5
6
7 {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.5f, -0.5f, -0.5f}, { 0.0f,
8 1.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f} },
9 {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}
10
11 };
12
13 indeks const std::vector<uint16_t> = { 0, 1, 2, 2,
14 3, 0, 4, 5, 6, 6, 7, 4
15
16 };

Jalankan program Anda sekarang dan Anda akan melihat sesuatu yang menyerupai ilustrasi
Escher:

Masalahnya adalah bahwa fragmen kotak bawah digambar di atas fragmen kotak atas,
hanya karena itu muncul belakangan di array indeks.
Ada dua cara untuk menyelesaikan ini:

• Urutkan semua panggilan undian berdasarkan kedalaman dari belakang ke depan

238
Machine Translated by Google

• Gunakan pengujian kedalaman dengan penyangga kedalaman

Pendekatan pertama umumnya digunakan untuk menggambar objek transparan, karena transparansi
tanpa keteraturan merupakan tantangan yang sulit dipecahkan. Namun, masalah pengurutan fragmen
berdasarkan kedalaman jauh lebih umum diselesaikan dengan menggunakan buffer kedalaman.
Buffer kedalaman adalah lampiran tambahan yang menyimpan kedalaman untuk setiap posisi, seperti
lampiran warna yang menyimpan warna setiap posisi.
Setiap kali rasterizer menghasilkan sebuah fragmen, uji kedalaman akan memeriksa apakah fragmen
baru lebih dekat dari yang sebelumnya. Jika tidak, maka fragmen baru akan dibuang. Fragmen yang
lulus uji kedalaman menulis kedalamannya sendiri ke buffer kedalaman. Dimungkinkan untuk
memanipulasi nilai ini dari shader fragmen, sama seperti Anda dapat memanipulasi keluaran warna.

1 #define GLM_FORCE_RADIANS 2
#define GLM_FORCE_DEPTH_ZERO_TO_ONE 3
#include <glm/glm.hpp> 4 #include <glm/gtc/
matrix_transform.hpp>

Matriks proyeksi perspektif yang dihasilkan oleh GLM akan menggunakan rentang kedalaman
OpenGL dari -1.0 hingga 1.0 secara default. Kita perlu mengonfigurasinya untuk menggunakan
rentang Vulkan dari 0,0 hingga 1,0 menggunakan definisi GLM_FORCE_DEPTH_ZERO_TO_ONE.

Kedalaman gambar dan tampilan

Lampiran kedalaman didasarkan pada gambar, seperti lampiran warna. Perbedaannya adalah rantai
swap tidak akan secara otomatis membuat gambar kedalaman untuk kita. Kami hanya membutuhkan
satu gambar kedalaman, karena hanya satu operasi menggambar yang berjalan sekaligus. Gambar
kedalaman lagi akan membutuhkan trifecta sumber daya: gambar, memori dan tampilan gambar.

1 VkImage kedalamanGambar;
2 VkDeviceMemory depthImageMemory;
3 VkImageView depthImageView;

Buat fungsi baru createDepthResources untuk menyiapkan sumber daya ini:

1 batal initVulkan() {
2 ...
3 buatCommandPool();
4 createDepthResources();
5 buatTeksturGambar();
...
67}
8
9 ...
10
11 batal createDepthResources() {

239
Machine Translated by Google

12
13 }

Membuat gambar kedalaman cukup mudah. Itu harus memiliki resolusi yang sama dengan
lampiran warna, ditentukan oleh batas rantai pertukaran, penggunaan gambar yang sesuai untuk
lampiran kedalaman, ubin optimal, dan memori lokal perangkat.
Satu-satunya pertanyaan adalah: apa format yang tepat untuk gambar kedalaman? Formatnya
harus mengandung komponen kedalaman, yang ditunjukkan dengan _D??_ di VK_FORMAT_.

Berbeda dengan gambar tekstur, kita tidak memerlukan format tertentu, karena kita tidak akan
langsung mengakses texel dari program. Itu hanya perlu memiliki akurasi yang masuk akal,
setidaknya 24 bit adalah hal yang umum dalam aplikasi dunia nyata.
Ada beberapa format yang sesuai dengan persyaratan ini:

• VK_FORMAT_D32_SFLOAT: float 32-bit untuk kedalaman


• VK_FORMAT_D32_SFLOAT_S8_UINT: float bertanda 32-bit untuk kedalaman dan
komponen stensil 8 bit • VK_FORMAT_D24_UNORM_S8_UINT: float 24-bit untuk
kedalaman dan stensil 8 bit
komponen

Komponen stensil digunakan untuk pengujian stensil, yang merupakan pengujian tambahan yang
dapat digabungkan dengan pengujian kedalaman. Kita akan melihat ini di bab mendatang.

Kita cukup menggunakan format VK_FORMAT_D32_SFLOAT, karena dukungan untuk itu sangat
umum (lihat database perangkat keras), tetapi bagus untuk menambahkan beberapa fleksibilitas
ekstra ke aplikasi kita jika memungkinkan. Kita akan menulis fungsi findSupportedFormat yang
mengambil daftar format kandidat dalam urutan dari yang paling diinginkan hingga yang paling
tidak diinginkan, dan memeriksa mana yang pertama didukung:

1 VkFormat findSupportedFormat(const std::vector<VkFormat>&


kandidat, ubin VkImageTiling, fitur VkFormatFeatureFlags) {

2
3}

Dukungan format bergantung pada mode ubin dan penggunaan, jadi kami juga harus memasukkan
ini sebagai parameter. Dukungan format dapat ditanyakan menggunakan fungsi
vkGetPhysicalDeviceFormatProperties :

1 untuk (format VkFormat : kandidat) {


2 alat peraga VkFormatProperties;
3 vkGetPhysicalDeviceFormatProperties(Perangkat fisik, format,
&Atribut);
4}

Struktur VkFormatProperties berisi tiga bidang:

• linearTilingFeatures: Gunakan kasus yang didukung dengan linear tiling

240
Machine Translated by Google

• optimalTilingFeatures: Gunakan kasus yang didukung dengan optimal


ubin
• bufferFeatures: Gunakan kasus yang didukung untuk buffer

Hanya dua yang pertama yang relevan di sini, dan yang kami periksa bergantung pada
parameter ubin fungsi:

1 jika (ubin == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures


& fitur) == fitur) { kembali format;
2 3 } else if (tile ==
VK_IMAGE_TILING_OPTIMAL &&
(props.optimalTilingFeatures & features) == fitur) { return format;
4
5}

Jika tidak ada format kandidat yang mendukung penggunaan yang diinginkan, maka kita dapat
mengembalikan nilai khusus atau hanya memberikan pengecualian:

1 VkFormat findSupportedFormat(const std::vector<VkFormat>&


kandidat, ubin VkImageTiling, fitur VkFormatFeatureFlags) { untuk (format VkFormat :
kandidat) {
2
3 alat peraga VkFormatProperties;
4 vkGetPhysicalDeviceFormatProperties(Perangkat fisik, format,
&Atribut);
5
6 jika (ubin == VK_IMAGE_TILING_LINEAR &&
(props.linearTilingFeatures & features) == fitur) { return format;
7
8 } lain jika (ubin == VK_IMAGE_TILING_OPTIMAL &&
(props.optimalTilingFeatures & features) == fitur) { return format;
9
10 }
11 }
12
13 throw std::runtime_error("gagal menemukan format yang didukung!");
14 }

Kami akan menggunakan fungsi ini sekarang untuk membuat fungsi pembantu findDepthFormat untuk
memilih format dengan komponen kedalaman yang mendukung penggunaan sebagai lampiran kedalaman:

1 VkFormat findDepthFormat() {
2 kembali findSupportedFormat(
3 {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT,
VK_FORMAT_D24_UNORM_S8_UINT},
4 VK_IMAGE_TILING_OPTIMAL,
5 VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT
6 );

241
Machine Translated by Google

7}

Pastikan untuk menggunakan flag VK_FORMAT_FEATURE_ bukan VK_IMAGE_USAGE_


dalam kasus ini. Semua format kandidat ini berisi komponen kedalaman, tetapi dua format
terakhir juga berisi komponen stensil. Kami belum akan menggunakannya, tetapi kami perlu
mempertimbangkannya saat melakukan transisi tata letak pada gambar dengan format ini.
Tambahkan fungsi pembantu sederhana yang memberi tahu kami jika format kedalaman yang
dipilih berisi komponen stensil:

1 bool hasStencilComponent(format VkFormat) { 2


kembalikan format == VK_FORMAT_D32_SFLOAT_S8_UINT || format ==
VK_FORMAT_D24_UNORM_S8_UINT;
3}

Panggil fungsi untuk menemukan format kedalaman dari createDepthResources:

1 VkFormat depthFormat = findDepthFormat();

Kami sekarang memiliki semua informasi yang diperlukan untuk menjalankan fungsi helper
createImage dan createImageView kami:

1 buatGambar(swapChainExtent.width, swapChainExtent.height, depthFormat,


VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage,
depthImageMemory); 2 depthImageView = createImageView(depthImage,
depthFormat);

Namun, fungsi createImageView saat ini mengasumsikan bahwa subresource selalu


VK_IMAGE_ASPECT_COLOR_BIT, jadi kita perlu mengubah bidang tersebut menjadi parameter:

1 VkImageView buatImageView(gambar VkImage, format VkFormat,


VkImageAspectFlags aspekFlags) {
2 ...
3 viewInfo.subresourceRange.aspectMask = aspekFlags;
4 ...
5}

Perbarui semua panggilan ke fungsi ini untuk menggunakan aspek yang tepat:

1 swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat,


VK_IMAGE_ASPECT_COLOR_BIT);
2 ...
3 depthImageView = buatImageView(kedalamanGambar, depthFormat,
VK_IMAGE_ASPECT_DEPTH_BIT);
4 ...
5 textureImageView = createImageView(textureImage,
VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT);

242
Machine Translated by Google

Itu saja untuk membuat gambar kedalaman. Kita tidak perlu memetakannya atau menyalin gambar
lain ke dalamnya, karena kita akan menghapusnya di awal render pass seperti lampiran warna.

Secara eksplisit mentransisikan gambar kedalaman


Kita tidak perlu secara eksplisit mentransisi tata letak gambar ke depth at tachment karena kita
akan menanganinya di render pass. Namun, untuk kelengkapan saya akan tetap menjelaskan
prosesnya di bagian ini. Anda dapat melewatkannya jika Anda suka.

Lakukan panggilan ke transitionImageLayout di akhir fungsi createDepthResources seperti ini:

1 transisiImageLayout(kedalamanGambar, depthFormat,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

Tata letak yang tidak ditentukan dapat digunakan sebagai tata letak awal, karena tidak ada konten
gambar kedalaman yang penting. Kita perlu mengupdate beberapa logika di transitionImageLayout
untuk menggunakan aspek subresource yang tepat:

1 if (Layout baru == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)


{ barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; 2
3
4 if (hasStencilComponent(format)) {
5 barrier.subresourceRange.aspectMask |=
VK_IMAGE_ASPECT_STENCIL_BIT;
6 7 } lain}
{
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
89}

Meskipun kita tidak menggunakan komponen stensil, kita perlu menyertakannya dalam transisi
tata letak gambar kedalaman.

Terakhir, tambahkan topeng akses dan tahapan pipa yang benar:

1 jika (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout ==


VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { barrier.srcAccessMask
2 = 0; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
3
4
5 sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
6 destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
7 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout
== VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { barrier.srcAccessMask
8 = VK_ACCESS_TRANSFER_WRITE_BIT;

243
Machine Translated by Google

9 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
10
11 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
12 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; 13 }
else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout ==
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
14 { barrier.srcAccessMask = 0;
15 barrier.dstAccessMask =
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
16
17 sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
18 destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
19 } lain {
20 throw std::invalid_argument(" transisi tata letak tidak didukung!");
21 }

Buffer kedalaman akan dibaca dari untuk melakukan uji kedalaman untuk melihat apakah fragmen
terlihat, dan akan ditulis saat fragmen baru digambar. Pembacaan terjadi di tahap
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT dan penulisan di tahap
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT. Anda harus memilih tahapan pipeline
paling awal yang cocok dengan operasi yang ditentukan, sehingga siap untuk digunakan sebagai
lampiran kedalaman saat diperlukan.

Berikan izin
Kami sekarang akan memodifikasi createRenderPass untuk menyertakan lampiran kedalaman.
Pertama tentukan VkAttachmentDescription:

1 VkAttachmentDescription depthAttachment{}; 2
depthAttachment.format = findDepthFormat(); 3
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; 4
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; 5
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; 6
depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; 7
depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; 8
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; 9 depthAttachment.finalLayout
= VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

Formatnya harus sama dengan kedalaman gambar itu sendiri. Kali ini kita tidak peduli tentang
penyimpanan data kedalaman (storeOp), karena tidak akan digunakan setelah gambar selesai.
Hal ini memungkinkan perangkat keras untuk melakukan optimalisasi tambahan. Sama seperti
buffer warna, kami tidak peduli dengan konten kedalaman sebelumnya, jadi kami dapat
menggunakan VK_IMAGE_LAYOUT_UNDEFINED sebagai initialLayout.

244
Machine Translated by Google

1 VkAttachmentReference depthAttachmentRef{}; 2
depthAttachmentRef.attachment = 1; 3
depthAttachmentRef.layout =
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

Tambahkan referensi ke lampiran untuk subpass pertama (dan satu-satunya):

1 subpass VkSubpassDescription{}; 2
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; 3
subpass.colorAttachmentCount = 1; 4 subpass.pColorAttachments =
&colorAttachmentRef; 5 subpass.pDepthStencilAttachment = &depthAttachmentRef;

Tidak seperti lampiran warna, subpass hanya dapat menggunakan satu kedalaman (+ stensil)
pada lampiran. Tidak masuk akal untuk melakukan pengujian mendalam pada banyak buffer.

1 std::array<VkAttachmentDescription, 2> lampiran = {colorAttachment,


depthAttachment}; 2 VkRenderPassCreateInfo renderPassInfo{};
3 renderPassInfo.sType =
VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; 4
renderPassInfo.attachmentCount =
static_cast<uint32_t>(attachments.size()); 5
renderPassInfo.pAttachments = lampiran.data(); 6
renderPassInfo.subpassCount = 1; 7 renderPassInfo.pSubpasses =
&subpass; 8 renderPassInfo.dependencyCount = 1; 9
renderPassInfo.pDependencies = &ketergantungan;

Selanjutnya, perbarui struct VkRenderPassCreateInfo untuk merujuk ke kedua lampiran.

1 dependensi.srcStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
| VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; 2
dependensi.dstStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
| VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; 3
dependensi.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

Terakhir, kita perlu memperluas dependensi subpass untuk memastikan bahwa tidak ada konflik
antara transisi gambar kedalaman dan dihapus sebagai bagian dari operasi pemuatannya. Gambar
kedalaman pertama kali diakses pada tahap pipeline pengujian fragmen awal dan karena kita
memiliki operasi pemuatan yang dihapus, kita harus menentukan masker akses untuk penulisan.

245
Machine Translated by Google

Framebuffer

Langkah selanjutnya adalah memodifikasi pembuatan framebuffer untuk mengikat gambar


kedalaman ke lampiran kedalaman. Buka createFramebuffers dan tentukan tampilan
kedalaman gambar sebagai lampiran kedua:

1 std::array<VkImageView, 2> lampiran =


2 { swapChainImageViews[i], depthImageView
3
4 };
5
6 VkFramebufferCreateInfo framebufferInfo{}; 7
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; 8
framebufferInfo.renderPass = renderPass; 9 framebufferInfo.attachmentCount =

static_cast<uint32_t>(attachments.size());
10 framebufferInfo.pAttachments = lampiran.data(); 11
framebufferInfo.width = swapChainExtent.width; 12 framebufferInfo.height
= swapChainExtent.height; 13 framebufferInfo.layers = 1;

Lampiran warna berbeda untuk setiap gambar rantai pertukaran, tetapi gambar kedalaman
yang sama dapat digunakan oleh semuanya karena hanya satu subpass yang berjalan
pada waktu yang sama karena semafor kami.
Anda juga harus memindahkan panggilan ke createFramebuffers untuk memastikan bahwa
panggilan tersebut dipanggil setelah tampilan gambar kedalaman benar-benar dibuat:

1 batal initVulkan() {
2 ...
3 createDepthResources();
4 buatFramebuffer();
5 ...
6}

Hapus nilai

Karena kami sekarang memiliki banyak lampiran dengan VK_ATTACHMENT_LOAD_OP_CLEAR,


kami juga perlu menentukan beberapa nilai yang jelas. Pergi ke createCommandBuffers dan
buat array struct VkClearValue:

1 std::array<VkClearValue, 2> clearValues{}; 2


clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; 3
clearValues[1].depthStencil = {1.0f, 0};
4
5 renderPassInfo.clearValueCount =
static_cast<uint32_t>(clearValues.size());

246
Machine Translated by Google

6 renderPassInfo.pClearValues = clearValues.data();

Kisaran kedalaman buffer kedalaman adalah 0,0 hingga 1,0 di Vulkan, di mana 1,0 terletak pada
bidang pandangan jauh dan 0,0 pada bidang pandangan dekat. Nilai awal pada setiap titik
dalam penyangga kedalaman harus merupakan kedalaman terjauh yang mungkin, yaitu 1,0.

Perhatikan bahwa urutan clearValues harus identik dengan urutan lampiran Anda.

Kedalaman dan kondisi stensil


Lampiran kedalaman siap untuk digunakan sekarang, tetapi pengujian kedalaman masih harus
diaktifkan di pipa grafis. Ini dikonfigurasi melalui struct VkPipelineDepthStencilStateCreateInfo:

1 VkPipelineDepthStencilStateCreateInfo depthStencil{}; 2
depthStencil.sType =
VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3 depthStencil.depthTestEnable = VK_TRUE; 4 depthStencil.depthWriteEnable
= VK_TRUE;

Kolom depthTestEnable menentukan apakah kedalaman fragmen baru harus dibandingkan


dengan buffer kedalaman untuk melihat apakah fragmen tersebut harus dibuang. Bidang
depthWriteEnable menentukan apakah kedalaman fragmen baru yang lulus uji kedalaman harus
benar-benar ditulis ke buffer kedalaman.

1 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;

Bidang depthCompareOp menentukan perbandingan yang dilakukan untuk menyimpan atau


menghapus fragmen. Kami berpegang pada konvensi lebih rendah kedalaman = lebih dekat,
sehingga kedalaman fragmen baru harus lebih sedikit.

1 depthStencil.depthBoundsTestEnable = VK_FALSE; 2
depthStencil.minDepthBounds = 0,0f; // Opsional 3
depthStencil.maxDepthBounds = 1.0f; // Opsional

Bidang depthBoundsTestEnable, minDepthBounds, dan maxDepthBounds digunakan untuk


pengujian batas kedalaman opsional. Pada dasarnya, ini memungkinkan Anda untuk hanya
menyimpan fragmen yang berada dalam rentang kedalaman yang ditentukan. Kami tidak akan
menggunakan fungsi ini.

1 depthStencil.stencilTestEnable = VK_FALSE; 2
depthStencil.depan = {}; // Opsional 3 depthStencil.back =
{}; // Opsional

Tiga kolom terakhir mengonfigurasi operasi buffer stensil, yang juga tidak akan kita gunakan
dalam tutorial ini. Jika Anda ingin menggunakan operasi ini, Anda harus memastikan bahwa
format gambar kedalaman/stensil berisi komponen stensil.

247
Machine Translated by Google

1 pipelineInfo.pDepthStencilState = &depthStencil;

Perbarui struct VkGraphicsPipelineCreateInfo untuk merujuk status stensil kedalaman


yang baru saja kita isi. Status stensil kedalaman harus selalu ditentukan jika pass render
berisi lampiran stensil kedalaman.

Jika Anda menjalankan program Anda sekarang, maka Anda akan melihat bahwa fragmen
geometri sekarang diurutkan dengan benar:

Menangani pengubahan ukuran jendela

Resolusi buffer kedalaman harus berubah saat jendela diubah ukurannya agar sesuai
dengan resolusi lampiran warna yang baru. Perluas fungsi recreateSwapChain untuk
membuat ulang sumber daya kedalaman dalam hal ini:

1 void recreateSwapChain() { lebar


2 int = 0, tinggi = 0; while (lebar ==
3 0 || tinggi == 0) {
4 glfwGetFramebufferSize(jendela, &lebar, &tinggi);
5 glfwWaitEvents();
6 }
7
8 vkDeviceWaitIdle(perangkat);

248
Machine Translated by Google

9
10 cleanupSwapChain();
11
12 buatSwapChain();
13 createImageViews();
14 buatRenderPass();
15 buatGraphicsPipeline();
16 createDepthResources();
17 buatFramebuffer();
18 createUniformBuffers();
19 createDescriptorPool();
20 createDescriptorSets();
21 buatCommandBuffers();
22 }

Operasi pembersihan harus dilakukan dalam fungsi pembersihan rantai pertukaran:

1 batal pembersihanSwapChain()
2 { vkDestroyImageView(perangkat, depthImageView, nullptr);
3 vkDestroyImage(perangkat, depthImage, nullptr);
4 vkFreeMemory(perangkat, depthImageMemory, nullptr);
5
6 ...
7}

Selamat, aplikasi Anda sekarang akhirnya siap untuk merender geometri 3D arbitrer dan
membuatnya tampak benar. Kita akan mencobanya di bab selanjutnya dengan
menggambar model bertekstur!

Kode C++ / Vertex shader / Fragment shader

249
Machine Translated by Google

Memuat model
pengantar

Program Anda sekarang siap untuk merender jaring 3D bertekstur, tetapi geometri saat ini di larik
simpul dan indeks belum terlalu menarik. Dalam bab ini kita akan memperluas program untuk
memuat simpul dan indeks dari file model aktual untuk membuat kartu grafis benar-benar
berfungsi.

Banyak tutorial API grafis membuat pembaca menulis pemuat OBJ mereka sendiri di bab seperti
ini. Masalahnya adalah aplikasi 3D yang menarik dari jarak jauh akan segera membutuhkan fitur
yang tidak didukung oleh format file ini, seperti animasi kerangka. Kita akan memuat data mesh
dari model OBJ di bab ini, tetapi kita akan lebih fokus pada integrasi data mesh dengan program
itu sendiri daripada detail memuatnya dari file.

Perpustakaan

Kami akan menggunakan pustaka tinyobjloader untuk memuat simpul dan wajah dari file OBJ.
Cepat dan mudah untuk diintegrasikan karena merupakan pustaka file tunggal seperti stb_image.
Buka repositori yang ditautkan di atas dan unduh file tiny_obj_loader.h ke folder di direktori
perpustakaan Anda. Pastikan untuk menggunakan versi file dari cabang master karena rilis resmi
terbaru sudah usang.

Studio visual

Tambahkan direktori dengan tiny_obj_loader.h di dalamnya ke jalur Direktori Sertakan


Tambahan.

250
Machine Translated by Google

Makefile

Tambahkan direktori dengan tiny_obj_loader.h ke direktori include untuk GCC:

1 VULKAN_SDK_PATH = /home/user/VulkanSDK/xxxx/x86_64 2
STB_INCLUDE_PATH = /home/user/libraries/ stb 3 TINYOBJ_INCLUDE_PATH = /
home/user/libraries/tinyobjloader 4

5 ...
6
7 CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/sertakan
-I$(STB_INCLUDE_PATH) -I$(TINYOBJ_INCLUDE_PATH)

Jaring sampel
Dalam bab ini kita belum akan mengaktifkan pencahayaan, jadi ada baiknya menggunakan model
sampel yang memiliki pencahayaan yang dimasukkan ke dalam tekstur. Cara mudah untuk
menemukan model seperti itu adalah dengan mencari pindaian 3D di Sketchfab. Banyak model di
situs tersebut tersedia dalam format OBJ dengan lisensi permisif.

Untuk tutorial ini saya memutuskan untuk menggunakan model kamar Viking oleh nigelgoh (CC
BY 4.0). Saya men-tweak ukuran dan orientasi model untuk menggunakannya sebagai pengganti
geometri saat ini:

• viking_room.obj •
viking_room.png

Jangan ragu untuk menggunakan model Anda sendiri, tetapi pastikan hanya terdiri dari satu bahan
dan memiliki dimensi sekitar 1,5 x 1,5 x 1,5 unit. Jika lebih besar dari itu, maka Anda harus
mengubah matriks tampilan. Letakkan file model di direktori model baru di sebelah shader dan
tekstur, dan letakkan gambar tekstur di direktori tekstur.

Letakkan dua variabel konfigurasi baru di program Anda untuk menentukan model dan jalur tekstur:

1 const uint32_t LEBAR = 800;

251
Machine Translated by Google

2 const uint32_t HEIGHT = 600;


3
4 const std::string MODEL_PATH = "model/kamar viking.obj"; 5 const
std::string TEXTURE_PATH = "textures/viking_room.png";

Dan perbarui createTextureImage untuk menggunakan variabel jalur ini:

1 stbi_uc* piksel = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels,


STBI_rgb_alpha);

Memuat simpul dan indeks


Kita akan memuat simpul dan indeks dari file model sekarang, jadi Anda harus menghapus
larik simpul dan indeks global sekarang. Ganti dengan wadah non-const sebagai anggota
kelas:

1 std::vector<Vertex> simpul; 2
std::vector<uint32_t> indeks;
3 VkBuffer vertexBuffer;
4 VkDeviceMemory vertexBufferMemory;

Anda harus mengubah jenis indeks dari uint16_t menjadi uint32_t, karena akan ada lebih
banyak simpul daripada 65535. Ingat juga untuk mengubah parameter vkCmdBindIndexBuffer:

1 vkCmdBindIndexBuffer(commandBuffer[i], indexBuffer, 0,
VK_INDEX_TYPE_UINT32);

Pustaka tinyobjloader disertakan dengan cara yang sama seperti pustaka STB. Sertakan file
tiny_obj_loader.h dan pastikan untuk mendefinisikan TINYOBJLOADER_IMPLEMENTATION dalam
satu file sumber untuk menyertakan badan fungsi dan menghindari kesalahan linker:

1 #define TINYOBJLOADER_IMPLEMENTATION
2 #termasuk <tiny_obj_loader.h>

Kita sekarang akan menulis fungsi loadModel yang menggunakan pustaka ini untuk mengisi
wadah simpul dan indeks dengan data simpul dari mesh. Itu harus dipanggil di suatu tempat
sebelum buffer vertex dan indeks dibuat:

1 batal initVulkan() {
2 ...
3 loadModel();
4 createVertexBuffer();
5 createIndexBuffer();
...
67}
8

252
Machine Translated by Google

9 ...
10
11 batal loadModel() {
12
13 }

Sebuah model dimuat ke dalam struktur data perpustakaan dengan memanggil fungsi
tinyobj::LoadObj :
1 batal loadModel() {
2 tinyobj::attrib_t attrib;
3 std::vector<tinyobj::shape_t> bentuk;
4 std::vector<tinyobj::material_t> bahan; std::string
5 memperingatkan, err;
6
7 if (!tinyobj::LoadObj(&attrib, &bentuk, &bahan, &peringatan, &err,
MODEL_PATH.c_str()))
8 { lempar std::runtime_error(warn + err);
}
9 10 }

File OBJ terdiri dari posisi, normal, koordinat tekstur, dan wajah. Wajah terdiri dari jumlah
simpul yang berubah-ubah, di mana setiap simpul mengacu pada posisi, koordinat normal
dan/atau tekstur berdasarkan indeks. Ini memungkinkan untuk tidak hanya menggunakan
kembali seluruh simpul, tetapi juga atribut individu.

Kontainer attrib menampung semua koordinat posisi, normal, dan tekstur dalam vektor
attrib.vertices, attrib.normals, dan attrib.texcoords. Wadah bentuk berisi semua objek terpisah
dan wajahnya. Setiap wajah terdiri dari array simpul, dan setiap simpul berisi indeks posisi,
atribut koordinat normal dan tekstur. Model OBJ juga dapat menentukan bahan dan tekstur
per permukaan, tetapi kami akan mengabaikannya.

String err berisi kesalahan dan string peringatan berisi peringatan yang terjadi saat memuat
file, seperti definisi material yang hilang. Memuat hanya benar-benar gagal jika fungsi
LoadObj mengembalikan false. Seperti disebutkan di atas, wajah dalam file OBJ sebenarnya
dapat berisi jumlah simpul yang berubah-ubah, sedangkan aplikasi kita hanya dapat merender
segitiga. Untungnya LoadObj memiliki parameter opsional untuk secara otomatis melakukan
pelacakan wajah tersebut, yang diaktifkan secara default.

Kita akan menggabungkan semua permukaan dalam file menjadi satu model, jadi ulangi
semua bentuk:

1 untuk (const auto& bentuk : bentuk) {


2
3}

253
Machine Translated by Google

Fitur triangulasi telah memastikan bahwa ada tiga simpul per wajah, jadi sekarang kita
dapat langsung melakukan iterasi pada simpul dan membuangnya langsung ke vektor
simpul kita:

1 for (const auto& shape : shapes) { for


2 (const auto& index : shape.mesh.indices) { Vertex vertex{};
3
4
5 simpul.push_back(puncak);
6 indeks.push_back(indeks.ukuran());
7 }
8}

Untuk penyederhanaan, kita akan mengasumsikan bahwa setiap simpul adalah unik
untuk saat ini, sehingga indeks kenaikan otomatis yang sederhana. Variabel indeks
bertipe tinyobj::index_t, yang berisi anggota vertex_index, normal_index dan
texcoord_index. Kita perlu menggunakan indeks ini untuk mencari atribut vertex yang
sebenarnya dalam array attrib:

1 vertex.pos =
2 { attrib.vertices[3 * index.vertex_index + 0], attrib.vertices[3
3 * index.vertex_index + 1], attrib.vertices[3 *
4 index.vertex_index + 2]
5 };
6
7 vertex.texCoord =
8 { attrib.texcoords[2 * index.texcoord_index + 0],
9 attrib.texcoords[2 * index.texcoord_index + 1]
10 };
11
12 simpul.warna = {1.0f, 1.0f, 1.0f};

Sayangnya array attrib.vertices adalah array nilai float dan bukan glm::vec3, jadi Anda
perlu mengalikan indeks dengan 3. Demikian pula, ada dua komponen koordinat tekstur
per entri. Offset 0, 1 dan 2 digunakan untuk mengakses komponen X, Y dan Z, atau
komponen U dan V dalam kasus koordinat tekstur.

Jalankan program Anda sekarang dengan pengoptimalan diaktifkan (misalnya mode Rilis
di Visual Studio dan dengan -O3 compiler flag untuk GCC'). Ini perlu, karena jika tidak
memuat model akan sangat lambat. Anda akan melihat sesuatu seperti berikut:

254
Machine Translated by Google

Bagus, geometrinya terlihat benar, tapi ada apa dengan teksturnya? Format OBJ
mengasumsikan sistem koordinat di mana koordinat vertikal 0 berarti bagian bawah
gambar, namun kami telah mengunggah gambar kami ke Vulkan dengan orientasi atas
ke bawah di mana 0 berarti bagian atas gambar. Selesaikan ini dengan membalik
komponen vertikal koordinat tekstur:
1 vertex.texCoord =
2 { attrib.texcoords[2 * index.texcoord_index + 0], 1.0f -
3 attrib.texcoords[2 * index.texcoord_index + 1]
4 };

Ketika Anda menjalankan program Anda lagi, Anda sekarang akan melihat hasil yang benar:

255
Machine Translated by Google

Semua kerja keras itu akhirnya terbayar dengan demo seperti ini!

Saat model berputar, Anda mungkin memperhatikan bahwa bagian belakang (bagian
belakang dinding) terlihat agak lucu. Ini normal dan hanya karena modelnya tidak
benar-benar dirancang untuk dilihat dari sisi itu.

Deduplikasi simpul
Sayangnya kami belum benar-benar memanfaatkan buffer indeks. Vektor simpul mengandung
banyak data simpul yang digandakan, karena banyak simpul yang termasuk dalam banyak segitiga.
Kita harus menyimpan hanya simpul unik dan menggunakan buffer indeks untuk menggunakannya
kembali setiap kali muncul. Cara mudah untuk mengimplementasikan ini adalah dengan menggunakan
peta atau unordered_map untuk melacak simpul unik dan indeks masing-masing:

1 #termasuk <unordered_map>
2
3 ...
4
5 std::unordered_map<Vertex, uint32_t> uniqueVertices{};
6
7 for (const auto& shape : shapes) { for (const
8 auto& index : shape.mesh.indices) {

256
Machine Translated by Google

9 Simpul simpul{};
10
11 ...
12
13 if (uniqueVertices.count(vertex) == 0)
14 { uniqueVertices[vertex] =
static_cast<uint32_t>(vertices.size());
15 simpul.push_back(puncak);
16 }
17
18 indexes.push_back(uniqueVertices[vertex]);
19 }
20 }

Setiap kali kita membaca sebuah simpul dari file OBJ, kita memeriksa apakah kita telah
melihat sebuah simpul dengan posisi dan koordinat tekstur yang sama persis sebelumnya.
Jika tidak, kami menambahkannya ke simpul dan menyimpan indeksnya di wadah
uniqueVertices. Setelah itu kami menambahkan indeks dari simpul baru ke indeks. Jika kita
telah melihat vertex yang persis sama sebelumnya, maka kita mencari indeksnya di
uniqueVertices dan menyimpan indeks itu di indeks.

Program akan gagal dikompilasi sekarang, karena menggunakan tipe yang ditentukan
pengguna seperti struct Vertex kami sebagai kunci dalam tabel hash mengharuskan kami untuk
mengimplementasikan dua fungsi: uji kesetaraan dan perhitungan hash. Yang pertama mudah
diimplementasikan dengan mengganti operator == di struktur Vertex:

1 bool operator==(const Vertex& lainnya) const { 2


return pos == pos lain && color == other.color && texCoord == other.texCoord;

3}

Fungsi hash untuk Vertex diimplementasikan dengan menentukan templat


khususisasi untuk std::hash<T>. Fungsi hash adalah topik yang kompleks, tetapi
cpprefer ence.com merekomendasikan pendekatan berikut yang menggabungkan
bidang struct untuk membuat fungsi hash berkualitas baik:
1 namespace std
2 { template<> struct hash<Vertex> { size_t
3 operator()(Vertex const& vertex) const { return
4 ((hash<glm::vec3>()(vertex.pos) ^ (hash<glm::vec3> ()
5 (vertex.color) << 1)) >> 1) ^ (hash<glm::vec2>()
6 (vertex.texCoord) << 1);
7 }
8 };
9}

Kode ini harus ditempatkan di luar struktur Vertex. Fungsi hash untuk tipe GLM harus
disertakan menggunakan header berikut:

257
Machine Translated by Google

1 #define GLM_ENABLE_EXPERIMENTAL
2 #termasuk <glm/gtx/hash.hpp>

Fungsi hash didefinisikan dalam folder gtx, yang artinya secara teknis masih merupakan
ekstensi eksperimental untuk GLM. Oleh karena itu Anda perlu mendefinisikan
GLM_ENABLE_EXPERIMENTAL untuk menggunakannya. Ini berarti bahwa API dapat
berubah dengan versi GLM yang baru di masa mendatang, tetapi dalam praktiknya API tersebut sangat stabil.

Anda sekarang harus berhasil mengkompilasi dan menjalankan program Anda. Jika Anda
memeriksa ukuran simpul, maka Anda akan melihat bahwa itu menyusut dari 1.500.000
menjadi 265.645! Itu berarti bahwa setiap simpul digunakan kembali dalam jumlah rata-rata
~6 segitiga. Ini jelas menghemat banyak memori GPU.

Kode C++ / Vertex shader / Fragment shader

258
Machine Translated by Google

Menghasilkan Mipmap

pengantar

Program kami sekarang dapat memuat dan merender model 3D. Pada bab ini, kita akan
menambahkan satu fitur lagi, pembuatan mipmap. Mipmap banyak digunakan dalam
game dan perangkat lunak rendering, dan Vulkan memberi kita kendali penuh atas cara
pembuatannya.

Mipmap adalah versi gambar yang telah dihitung sebelumnya dan diturunkan skalanya.
Setiap gambar baru berukuran setengah dari lebar dan tinggi gambar sebelumnya.
Mipmap digunakan sebagai bentuk Level of Detail atau LOD. Objek yang jauh dari kamera
akan mengambil sampel teksturnya dari gambar mip yang lebih kecil. Menggunakan
gambar yang lebih kecil meningkatkan kecepatan rendering dan menghindari artefak
seperti pola Moiré. Contoh tampilan mipmap:

259
Machine Translated by Google

Pembuatan gambar
Di Vulkan, setiap gambar mip disimpan di level mip VkImage yang berbeda.
Mip level 0 adalah gambar asli, dan level mip setelah level 0 biasanya disebut sebagai rantai mip.

Jumlah level mip ditentukan saat VkImage dibuat. Hingga saat ini, kami selalu menyetel nilai ini ke
satu. Kita perlu menghitung jumlah level mip dari dimensi gambar. Pertama, tambahkan anggota
kelas untuk menyimpan nomor ini:

1 ...
2 uint32_t mipLevels;
3 VkImage teksturGambar;
4 ...

Nilai mipLevels dapat ditemukan setelah kita memuat tekstur di createTextureImage:

1 int texWidth, texHeight, texChannels; 2 stbi_uc* piksel


= stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels,
STBI_rgb_alpha);
3 ...
4 mipLevels =
static_cast<uint32_t>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;

Ini menghitung jumlah level dalam rantai mip. Fungsi max memilih dimensi terbesar. Fungsi log2
menghitung berapa kali dimensi tersebut dapat dibagi 2. Fungsi floor menangani kasus di mana
dimensi terbesar bukanlah pangkat 2. 1 ditambahkan sehingga gambar asli memiliki level mip.

Untuk menggunakan nilai ini, kita perlu mengubah fungsi createImage, createImageView, dan
transitionImageLayout untuk memungkinkan kita menentukan jumlah level mip. Tambahkan
parameter mipLevels ke fungsi:

1 void createImage (lebar uint32_t, tinggi uint32_t, uint32_t mipLevels, format


VkFormat, ubin VkImageTiling,
Penggunaan VkImageUsageFlags, properti VkMemoryPropertyFlags,
VkImage& gambar, VkDeviceMemory& imageMemory) {
2 ...
3 imageInfo.mipLevels = mipLevels;
4 ...
5}

1 VkImageView buatImageView(gambar VkImage, format VkFormat,


VkImageAspectFlags aspekFlags, uint32_t mipLevels) {
2 ...

260
Machine Translated by Google

3 viewInfo.subresourceRange.levelCount = mipLevels;
4 ...

1 batal transisiImageLayout (gambar VkImage, format VkFormat,


VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels) {

2 ...
3 barrier.subresourceRange.levelCount = mipLevels;
4 ...

Perbarui semua panggilan ke fungsi ini untuk menggunakan nilai yang benar:

1 buatGambar(swapChainExtent.width, swapChainExtent.height, 1, depthFormat,


VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);

2 ...
3 buatGambar(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
textureImage, textureImageMemory);

1 swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat,


VK_IMAGE_ASPECT_COLOR_BIT, 1);
2 ...
3 depthImageView = buatImageView(kedalamanGambar, depthFormat,
VK_IMAGE_ASPECT_DEPTH_BIT, 1);
4 ...
5 textureImageView = createImageView(textureImage,
VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels);

1 transisiImageLayout(kedalamanGambar, depthFormat,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 1);
2 ...
3 transisiImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
mipLevels);

Menghasilkan Mipmap
Gambar tekstur kita sekarang memiliki beberapa level mip, tetapi buffer staging hanya dapat
digunakan untuk mengisi level mip 0. Level lainnya masih belum ditentukan. Untuk mengisi level ini
kita perlu menghasilkan data dari satu level yang kita miliki. Kami akan

261
Machine Translated by Google

gunakan perintah vkCmdBlitImage. Perintah ini melakukan operasi penyalinan, penskalaan, dan
pemfilteran. Kami akan memanggil ini beberapa kali untuk menghapus data ke setiap level gambar
tekstur kami.

vkCmdBlitImage dianggap sebagai operasi transfer, jadi kami harus memberi tahu Vulkan bahwa
kami bermaksud menggunakan gambar tekstur sebagai sumber dan tujuan transfer. Tambahkan
VK_IMAGE_USAGE_TRANSFER_SRC_BIT ke bendera penggunaan gambar tekstur di
createTextureImage:

1 ...
2 buatGambar(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage,
textureImageMemory);
3 ...

Seperti operasi gambar lainnya, vkCmdBlitImage bergantung pada tata letak gambar yang
dioperasikannya. Kami dapat mentransisikan seluruh gambar ke VK_IMAGE_LAYOUT_GENERAL,
tetapi kemungkinan besar akan lambat. Untuk performa optimal, gambar sumber harus dalam
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL dan gambar tujuan harus dalam
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL.
Vulkan memungkinkan kita untuk mentransisikan setiap level mip dari suatu gambar secara mandiri.
Setiap blit hanya akan berurusan dengan dua level mip pada satu waktu, sehingga kita dapat
mentransisikan setiap level ke tata letak yang optimal di antara perintah blit.

transitionImageLayout hanya melakukan transisi tata letak pada seluruh gambar, jadi kita perlu
menulis beberapa perintah pipeline barrier lagi. Hapus transisi yang ada ke
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL di createTextureImage:

1 ...
2 transisiImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
mipLevels); copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth),
3 static_cast<uint32_t>(texHeight));

4 //ditransisikan ke VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL sementara


menghasilkan mipmap
5 ...

Ini akan meninggalkan setiap level gambar tekstur di VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL.


Setiap level akan dialihkan ke VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL setelah
pembacaan perintah blit selesai.

Kami sekarang akan menulis fungsi yang menghasilkan mipmaps:

262
Machine Translated by Google

1 batal menghasilkanMipmaps (gambar VkImage, int32_t texWidth, int32_t


texHeight, uint32_t mipLevels) {
2 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
3
4 Penghalang VkImageMemoryBarrier{};
5 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
6 penghalang.gambar = gambar; barrier.srcQueueFamilyIndex =
7 VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex =
8 VK_QUEUE_FAMILY_IGNORED; barrier.subresourceRange.aspectMask
9 = VK_IMAGE_ASPECT_COLOR_BIT; barrier.subresourceRange.baseArrayLayer = 0;
10 barrier.subresourceRange.layerCount = 1; barrier.subresourceRange.levelCount = 1;
11
12
13
14 endSingleTimeCommands(commandBuffer);
15 }

Kami akan melakukan beberapa transisi, jadi kami akan menggunakan kembali VkImageMemoryBarrier ini.
Kolom yang ditetapkan di atas akan tetap sama untuk semua penghalang. subresourceRange.miplevel,
oldLayout, newLayout, srcAccessMask, dan dstAccessMask akan diubah untuk setiap transisi.

1 int32_t mipWidth = texWidth; 2 int32_t


mipTinggi = texTinggi;
3
4 untuk (uint32_t i = 1; i < mipLevels; i++) {
5
6}

Loop ini akan merekam setiap perintah VkCmdBlitImage. Perhatikan bahwa variabel loop
dimulai dari 1, bukan 0.

1 barrier.subresourceRange.baseMipLevel = i - 1; 2 barrier.oldLayout =
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; 3 barrier.newLayout =
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; 4 barrier.srcAccessMask =
VK_ACCESS_TRANSFER_WRITE_BIT; 5 barrier.dstAccessMask =
VK_ACCESS_TRANSFER_READ_BIT;
6
7 vkCmdPipelineBarrier(commandBuffer,
8 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0,
9 0, nullptr, 0,
10 nullptr, 1,
11 &penghalang);

Pertama, kita transisi level i - 1 ke VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL.


Transisi ini akan menunggu level i - 1 terisi, baik dari sebelumnya

263
Machine Translated by Google

perintah blit, atau dari vkCmdCopyBufferToImage. Perintah blit saat ini akan menunggu transisi
ini.

1 VkImageBlit blit{}; 2
blit.srcOffsets[0] = { 0, 0, 0 }; 3 blit.srcOffsets[1]
= { Lebar mip, Tinggi mip, 1 }; 4 blit.srcSubresource.aspectMask =
VK_IMAGE_ASPECT_COLOR_BIT; 5 blit.srcSubresource.mipLevel = i - 1; 6
blit.srcSubresource.baseArrayLayer = 0; 7 blit.srcSubresource.layerCount = 1; 8
blit.dstOffsets[0] = { 0, 0, 0 }; 9 blit.dstOffsets[1] = { lebar mip > 1 ? mipWidth / 2 : 1,
mipHeight >

1 ? mipTinggi / 2 : 1, 1 };
10 blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; 11
blit.dstSubresource.mipLevel = i; 12 blit.dstSubresource.baseArrayLayer = 0; 13
blit.dstSubresource.layerCount = 1;

Selanjutnya, kita tentukan region yang akan digunakan dalam operasi blit. Level mip sumber
adalah i - 1 dan level mip tujuan adalah i. Dua elemen dari array srcOffsets menentukan wilayah
3D tempat data akan dihapus. dstOffsets menentukan wilayah tempat data akan dikirim. Dimensi
X dan Y dari dstOffsets[1] dibagi dua karena setiap level mip adalah setengah dari ukuran level
sebelumnya. Dimensi Z dari srcOffsets[1] dan dstOffsets[1] harus 1, karena gambar 2D memiliki
kedalaman 1.

1 vkCmdBlitImage(commandBuffer,
2 gambar, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
3 gambar, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
4 1, &blit,
5 VK_FILTER_LINEAR);

Sekarang, kami merekam perintah blit. Perhatikan bahwa textureImage digunakan untuk
parameter srcImage dan dstImage. Ini karena kita melakukan blitting di antara level yang
berbeda dari gambar yang sama. Level mip sumber baru saja dialihkan ke
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL dan level tujuan masih di
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL dari createTextureImage.

Berhati-hatilah jika Anda menggunakan antrean transfer khusus (seperti yang disarankan dalam
buffer Vertex): vkCmdBlitImage harus diserahkan ke antrean dengan kemampuan grafis.

Parameter terakhir memungkinkan kita menentukan VkFilter untuk digunakan di blit. Kami
memiliki opsi pemfilteran yang sama di sini yang kami miliki saat membuat VkSampler. Kami
menggunakan VK_FILTER_LINEAR untuk mengaktifkan interpolasi.

1 barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; 2
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

264
Machine Translated by Google

3 barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; 4
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
5
6 vkCmdPipelineBarrier(commandBuffer,
7 VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
8 0, nullptr, 0,
9 nullptr, 1,
10 &penghalang);

Penghalang ini mentransisikan mip level i - 1 ke VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.


Transisi ini menunggu perintah blit saat ini selesai. Semua operasi pengambilan sampel akan
menunggu transisi ini selesai.

1 ...
2 if (mipWidth > 1) mipWidth /= 2; if
3 (mipHeight > 1) mipHeight /= 2;
4}

Di akhir loop, kami membagi dimensi mip saat ini dengan dua. Kami memeriksa setiap dimensi
sebelum pembagian untuk memastikan bahwa dimensi tidak pernah menjadi 0.
Ini menangani kasus di mana gambar tidak persegi, karena salah satu dimensi mip akan
mencapai 1 sebelum dimensi lainnya. Ketika ini terjadi, dimensi tersebut harus tetap 1 untuk
semua level yang tersisa.

1 barrier.subresourceRange.baseMipLevel = mipLevels - 1; barrier.oldLayout


2 = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.newLayout =
3 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; barrier.srcAccessMask
4 = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask =
5 VK_ACCESS_SHADER_READ_BIT;
6
7 vkCmdPipelineBarrier(commandBuffer,
8 VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
9 0, nullptr, 0,
10 nullptr, 1,
11 &penghalang);
12
13 endSingleTimeCommands(commandBuffer);
14 }

Sebelum kami mengakhiri buffer perintah, kami memasukkan satu penghalang pipa lagi.
Penghalang ini mentransisikan level mip terakhir dari VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
ke VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. Ini tidak ditangani oleh loop,
karena level mip terakhir tidak pernah dihapus.

Terakhir, tambahkan panggilan untuk generateMipmaps di createTextureImage:

265
Machine Translated by Google

1 transisiImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
mipLevels); copyBufferToImage(stagingBuffer, textureImage,
2 static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));

3 //dialihkan ke VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL sementara


menghasilkan mipmap
4 ...
5 generateMipmaps(textureImage, texWidth, texHeight, mipLevels);

Mipmap gambar tekstur kita sekarang terisi penuh.

Dukungan penyaringan linier


Sangat mudah untuk menggunakan fungsi bawaan seperti vkCmdBlitImage untuk
menghasilkan semua level mip, tetapi sayangnya tidak dijamin akan didukung di semua
platform. Ini memerlukan format gambar tekstur yang kami gunakan untuk mendukung
pemfilteran linier, yang dapat diperiksa dengan fungsi vkGetPhysicalDeviceFormatProperties.
Kami akan menambahkan cek ke fungsi generateMipmaps untuk ini.

Pertama tambahkan parameter tambahan yang menentukan format gambar:

1 batal createTextureImage() { 2
...
3
4 generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_SRGB, texWidth,
texHeight, mipLevels);
5}
6
7 void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth,
int32_t texHeight, uint32_t mipLevels) {
8
9 ...
10 }

Dalam fungsi generateMipmaps, gunakan vkGetPhysicalDeviceFormatProperties untuk


meminta properti format gambar tekstur:

1 batal menghasilkanMipmaps (gambar VkImage, VkFormat imageFormat, int32_t


texWidth, int32_t texHeight, uint32_t mipLevels) {
2
3 // Periksa apakah format gambar mendukung blitting linier
4 VkFormatProperties formatProperties;
5 vkGetPhysicalDeviceFormatProperties(Perangkat fisik, imageFormat,
&format Properti);
6

266
Machine Translated by Google

7 ...

Struktur VkFormatProperties memiliki tiga bidang bernama linearTilingFeatures,


optimalTilingFeatures, dan bufferFeatures yang masing-masing menjelaskan bagaimana
format dapat digunakan tergantung pada cara penggunaannya. Kita membuat gambar
tekstur dengan format tiling yang optimal, jadi kita perlu memeriksa fitur optimalTiling.
Dukungan untuk fitur pemfilteran linier dapat diperiksa dengan
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT:

1 jika (!(formatProperties.optimalTilingFitur &


VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT))
2 { throw std::runtime_error(" format gambar tekstur tidak mendukung
blitting linier!");
3}

Ada dua alternatif dalam kasus ini. Anda dapat mengimplementasikan fungsi yang mencari
format gambar tekstur umum untuk yang mendukung linear blitting, atau Anda dapat
mengimplementasikan pembuatan mipmap dalam perangkat lunak dengan pustaka seperti
stb_image_resize. Setiap level mip kemudian dapat dimuat ke dalam gambar dengan cara
yang sama seperti Anda memuat gambar aslinya.

Perlu dicatat bahwa dalam praktiknya menghasilkan level mipmap pada saat runtime adalah
hal yang tidak biasa. Biasanya mereka dibuat sebelumnya dan disimpan dalam file tekstur
di samping level dasar untuk meningkatkan kecepatan pemuatan. Menerapkan pengubahan
ukuran dalam perangkat lunak dan memuat beberapa level dari file dibiarkan sebagai latihan
bagi pembaca.

Sampler
Sementara VkImage menyimpan data mipmap, VkSampler mengontrol bagaimana data
tersebut dibaca saat rendering. Vulkan memungkinkan kita menentukan minLod, maxLod,
mipLodBias, dan mipmapMode (“Lod” berarti “Level of Detail”). Saat tekstur diambil
sampelnya, sampler memilih level mip sesuai dengan pseudocode berikut:

1 lod = getLodLevelFromScreenSize(); //lebih kecil saat objek dekat, mungkin negatif


2 lod = clamp(lod + mipLodBias, minLod, maxLod); 3

4 level = clamp(floor(lod), 0, texture.mipLevels - 1); // dijepit ke jumlah level mip dalam


tekstur
5
6 if (mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST)
{ color = sample(level);
7 8 } lain {
9 warna = campuran(sampel(tingkat), sampel(tingkat + 1));
10 }

267
Machine Translated by Google

Jika samplerInfo.mipmapMode adalah VK_SAMPLER_MIPMAP_MODE_NEAREST, lod memilih


level mip untuk dijadikan sampel. Jika mode mipmap adalah VK_SAMPLER_MIPMAP_MODE_LINEAR,
lod digunakan untuk memilih dua level mip yang akan dijadikan sampel. Level tersebut diambil sampelnya
dan hasilnya dicampur secara linier.

Operasi sampel juga dipengaruhi oleh lod:

1 jika (lod <= 0) {


2 warna = readTexture(uv, magFilter);
3 } lain {
4 warna = readTexture(uv, minFilter);
5}

Jika objek dekat dengan kamera, magFilter digunakan sebagai filter. Jika objek lebih jauh
dari kamera, minFilter digunakan. Biasanya, lod adalah non-negatif, dan hanya 0 saat
menutup kamera. mipLodBias memungkinkan kita memaksa Vulkan untuk menggunakan lod
dan level yang lebih rendah daripada biasanya.

Untuk melihat hasil dari bab ini, kita perlu memilih nilai untuk textureSampler kita. Kami telah
menyetel minFilter dan magFilter untuk menggunakan VK_FILTER_LINEAR. Kita hanya perlu
memilih nilai untuk minLod, maxLod, mipLodBias, dan mipmapMode.

1 batal createTextureSampler() {
2 ...
3 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
4 samplerInfo.minLod = 0,0f; // SamplerInfo.maxLod opsional =
5 static_cast<float>(mipLevels); samplerInfo.mipLodBias = 0,0f; // Opsional
6
7 ...
8}

Untuk mengizinkan berbagai level mip digunakan, kami menetapkan minLod ke 0.0f, dan
maxLod ke jumlah level mip. Kami tidak punya alasan untuk mengubah nilai lod
, jadi kami mengatur mipLodBias ke 0.0f.

Sekarang jalankan program Anda dan Anda akan melihat yang berikut:

268
Machine Translated by Google

Ini bukan perbedaan yang dramatis, karena adegan kami sangat sederhana. Ada perbedaan
halus jika Anda melihat lebih dekat.

Perbedaan yang paling mencolok adalah tulisan di atas kertas. Dengan mipmaps, tulisan
sudah diperhalus. Tanpa mipmaps, tulisan memiliki sisi yang kasar dan

269
Machine Translated by Google

celah dari artefak Moiré.

Anda dapat bermain-main dengan pengaturan sampler untuk melihat bagaimana pengaruhnya terhadap
ping mipmap. Misalnya, dengan mengubah minLod, Anda dapat memaksa sampler untuk tidak
menggunakan level mip terendah:

1 samplerInfo.minLod = static_cast<float>(mipLevels / 2);

Pengaturan ini akan menghasilkan gambar ini:

Ini adalah bagaimana level mip yang lebih tinggi akan digunakan saat objek berada jauh dari kamera.

Kode C++ / Vertex shader / Fragment shader

270
Machine Translated by Google

Multisampling

pengantar

Program kami sekarang dapat memuat beberapa tingkat detail untuk tekstur yang
memperbaiki artefak saat merender objek jauh dari penampil. Gambar sekarang
jauh lebih halus, namun jika diamati lebih dekat, Anda akan melihat pola seperti
gergaji bergerigi di sepanjang tepi bentuk geometris yang digambar. Ini terutama
terlihat di salah satu program awal kami saat kami merender quad:

Efek yang tidak diinginkan ini disebut "aliasing" dan merupakan hasil dari jumlah
piksel terbatas yang tersedia untuk rendering. Karena tidak ada tampilan di luar sana

271
Machine Translated by Google

dengan resolusi tak terbatas, itu akan selalu terlihat sampai batas tertentu. Ada sejumlah cara
untuk memperbaikinya dan dalam bab ini kita akan fokus pada salah satu cara yang lebih populer:
Multisample anti-aliasing (MSAA).

Dalam perenderan biasa, warna piksel ditentukan berdasarkan satu titik sampel yang dalam
banyak kasus merupakan pusat piksel target di layar. Jika bagian dari garis yang ditarik melewati
piksel tertentu tetapi tidak menutupi titik sampel, piksel tersebut akan dibiarkan kosong, yang
mengarah ke efek "tangga" bergerigi.

Apa yang dilakukan MSAA adalah menggunakan beberapa titik sampel per piksel (maka dari itu
namanya) untuk menentukan warna akhirnya. Seperti yang diharapkan, lebih banyak sampel
mengarah pada hasil yang lebih baik, namun juga lebih mahal secara komputasi.

Dalam implementasi kami, kami akan fokus menggunakan sampel maksimum yang tersedia

272
Machine Translated by Google

menghitung. Tergantung pada aplikasi Anda, ini mungkin bukan pendekatan terbaik dan mungkin
lebih baik menggunakan lebih sedikit sampel demi kinerja yang lebih tinggi jika hasil akhir memenuhi
permintaan kualitas Anda.

Mendapatkan jumlah sampel yang tersedia

Mari kita mulai dengan menentukan berapa banyak sampel yang dapat digunakan perangkat keras
kita. Sebagian besar GPU modern mendukung setidaknya 8 sampel, tetapi jumlah ini tidak dijamin
sama di semua tempat. Kami akan melacaknya dengan menambahkan anggota kelas baru:

1 ...
2 VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT; 3 ...

Secara default, kami hanya akan menggunakan satu sampel per piksel yang setara dengan tidak
ada multisampling, dalam hal ini gambar akhir tidak akan berubah. Jumlah sampel maksimum yang
tepat dapat diekstraksi dari VkPhysicalDeviceProperties yang terkait dengan perangkat fisik pilihan
kami. Kami menggunakan buffer kedalaman, jadi kami harus memperhitungkan jumlah sampel untuk
warna dan kedalaman. Jumlah sampel tertinggi yang didukung oleh keduanya (&) akan menjadi
jumlah maksimum yang dapat kami dukung. Tambahkan fungsi yang akan mengambil informasi ini
untuk kita:

1 VkSampleCountFlagBits getMaxUsableSampleCount() {
2 VkPhysicalDeviceProperties physicalDeviceProperties;
3 vkGetPhysicalDeviceProperties(Perangkat fisik, &Properti Perangkat fisik);

4
5 Jumlah VkSampleCountFlags =
physicalDeviceProperties.limits.framebufferColorSampleCounts &

physicalDeviceProperties.limits.framebufferDepthSampleCounts; jika (menghitung &


6 VK_SAMPLE_COUNT_64_BIT) { kembalikan VK_SAMPLE_COUNT_64_BIT; } jika (menghitung
& VK_SAMPLE_COUNT_32_BIT) { kembalikan VK_SAMPLE_COUNT_32_BIT; } jika
7 (menghitung & VK_SAMPLE_COUNT_16_BIT) { kembalikan VK_SAMPLE_COUNT_16_BIT; }
jika (menghitung & VK_SAMPLE_COUNT_8_BIT) { kembalikan
8 VK_SAMPLE_COUNT_8_BIT; } jika (menghitung & VK_SAMPLE_COUNT_4_BIT) { kembalikan
VK_SAMPLE_COUNT_4_BIT; } jika (menghitung & VK_SAMPLE_COUNT_2_BIT)
9 { kembalikan VK_SAMPLE_COUNT_2_BIT; }

10

11

12
13 kembalikan VK_SAMPLE_COUNT_1_BIT;
14 }

273
Machine Translated by Google

Kami sekarang akan menggunakan fungsi ini untuk mengatur variabel


msaaSamples selama proses pemilihan perangkat fisik. Untuk ini, kita harus
sedikit memodifikasi fungsi pickPhysicalDevice :
1 batal pickPhysicalDevice() {
2 ...
3 for (const auto& device : devices) { if
4 (isDeviceSuitable(device)) { physicalDevice
5 = device; msaaSamples =
6 getMaxUsableSampleCount(); merusak;
7
8 }
9 }
10 ...
11 }

Menyiapkan target render


Di MSAA, setiap piksel diambil sampelnya dalam buffer di luar layar yang kemudian
ditampilkan ke layar. Buffer baru ini sedikit berbeda dari gambar biasa yang telah
kami render - gambar tersebut harus dapat menyimpan lebih dari satu sampel per
piksel. Setelah buffer multisampel dibuat, itu harus diselesaikan ke framebuffer
default (yang hanya menyimpan satu sampel per piksel). Inilah mengapa kita harus
membuat target render tambahan dan memodifikasi proses menggambar kita saat ini.
Kami hanya memerlukan satu target render karena hanya satu operasi menggambar yang
aktif pada satu waktu, seperti buffer kedalaman. Tambahkan anggota kelas berikut:
1 ...
2 VkImage colorImage; 3
VkDeviceMemory colorImageMemory; 4
VkImageView colorImageView; 5 ...

Gambar baru ini harus menyimpan jumlah sampel per piksel yang diinginkan, jadi
kami harus meneruskan nomor ini ke VkImageCreateInfo selama proses pembuatan
gambar. Ubah fungsi createImage dengan menambahkan parameter numSamples:
1 void createImage (lebar uint32_t, tinggi uint32_t, uint32_t
mipLevels, VkSampleCountFlagBits numSamples, format VkFormat,
Ubin VkImageTiling, penggunaan VkImageUsageFlags,
Properti VkMemoryPropertyFlags, VkImage& gambar,
VkDeviceMemory& imageMemory) {
2 ...
3 imageInfo.samples = numSamples;
4 ...

274
Machine Translated by Google

Untuk saat ini, perbarui semua panggilan ke fungsi ini menggunakan VK_SAMPLE_COUNT_1_BIT -
kami akan menggantinya dengan nilai yang sesuai seiring kemajuan implementasi:

1 buatGambar(swapChainExtent.width, swapChainExtent.height, 1,
VK_SAMPLE_COUNT_1_BIT, format kedalaman, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage,
depthImageMemory);
2 ...
3 buatGambar(texWidth, texHeight, mipLevels, VK_SAMPLE_COUNT_1_BIT,
VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage,
textureImageMemory);

Kami sekarang akan membuat buffer warna multisampel. Tambahkan fungsi createColorResources
dan perhatikan bahwa kami menggunakan msaaSamples di sini sebagai parameter fungsi untuk
membuat Gambar. Kami juga hanya menggunakan satu level mip, karena ini diberlakukan oleh
spesifikasi Vulkan jika ada gambar dengan lebih dari satu sampel per piksel.
Selain itu, buffer warna ini tidak memerlukan mipmap karena tidak akan digunakan sebagai tekstur:

1 batal buatColorResources() {
2 VkFormat colorFormat = swapChainImageFormat;
3
4 buatGambar(swapChainExtent.width, swapChainExtent.height, 1,
msaaSamples, format warna, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, colorImage,
colorImageMemory);
5 colorImageView = buatImageView(colorImage, colorFormat,
VK_IMAGE_ASPECT_COLOR_BIT, 1);
6}

Untuk konsistensi, panggil fungsi tepat sebelum createDepthResources:

1 batal initVulkan() {
2 ...
3 buatColorResources();
4 createDepthResources();
...
56}

Sekarang kita memiliki buffer warna multisampel di tempat saatnya untuk menjaga kedalaman. Ubah
createDepthResources dan perbarui jumlah sampel yang digunakan oleh buffer kedalaman:

275
Machine Translated by Google

1 batal createDepthResources() {
2 ...
3 buatGambar(swapChainExtent.width, swapChainExtent.height, 1,
msaaSamples, depthFormat, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage,
depthImageMemory);
4 ...
5}

Kami sekarang telah membuat beberapa sumber daya Vulkan baru, jadi jangan lupa
untuk merilisnya bila diperlukan:
1 batal pembersihanSwapChain()
2 { vkDestroyImageView(perangkat, colorImageView, nullptr);
3 vkDestroyImage(perangkat, colorImage, nullptr);
4 vkFreeMemory(perangkat, colorImageMemory, nullptr);
5 ...
6}

Dan perbarui recreateSwapChain sehingga gambar warna baru dapat dibuat


ulang dalam resolusi yang benar saat jendela diubah ukurannya:

1 batal buat ulangSwapChain()


{2 ...
3 buatGraphicsPipeline();
4 buatColorResources();
5 createDepthResources();
...
67}

Kami berhasil melewati penyiapan MSAA awal, sekarang kami harus mulai
menggunakan sumber daya baru ini di pipa grafis, framebuffer, render pass, dan lihat hasilnya!

Menambahkan lampiran baru


Mari kita urus render pass terlebih dahulu. Ubah createRenderPass dan perbarui
struktur info pembuatan lampiran warna dan kedalaman:
1 batal createRenderPass() {
2 ...
3 colorAttachment.samples = msaaSamples;
4 colorAttachment.finalLayout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
5 ...
6 depthAttachment.samples = msaaSamples;
7 ...

276
Machine Translated by Google

Anda akan melihat bahwa kami telah mengubah finalLayout dari VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
menjadi VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL. Itu karena gambar multisampling
tidak dapat disajikan secara langsung. Pertama-tama kita harus menyelesaikannya menjadi gambar biasa.
Persyaratan ini tidak berlaku untuk buffer kedalaman, karena tidak akan ditampilkan di titik mana pun.
Oleh karena itu kita harus menambahkan hanya satu lampiran baru untuk warna yang disebut lampiran
penyelesaian:

1 ...
2 VkAttachmentDescription colorAttachmentResolve{};
3 colorAttachmentResolve.format = swapChainImageFormat;
4 colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT;
5 colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
6 colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
7 colorAttachmentResolve.stencilLoadOp =
VK_ATTACHMENT_LOAD_OP_DONT_CARE;
8 colorAttachmentResolve.stencilStoreOp =
VK_ATTACHMENT_STORE_OP_DONT_CARE;
9 colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
10 colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

11 ...

Render pass sekarang harus diinstruksikan untuk menyelesaikan gambar warna


multisampel menjadi lampiran biasa. Buat referensi lampiran baru yang akan mengarah
ke buffer warna yang akan berfungsi sebagai target penyelesaian:
1 ...
2 VkAttachmentReference colorAttachmentResolveRef{};
3 colorAttachmentResolveRef.attachment = 2;
4 colorAttachmentResolveRef.layout =
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
5 ...

Atur anggota struct subpass pResolveAttachments untuk menunjuk ke referensi


lampiran yang baru dibuat. Ini cukup untuk membiarkan render pass menentukan
operasi penyelesaian multisampel yang memungkinkan kita merender gambar ke layar:
1 ...
2 subpass.pResolveAttachments = &colorAttachmentResolveRef;
3 ...

Sekarang perbarui render pass info struct dengan lampiran warna baru:
1 ...
2 std::array<VkAttachmentDescription, 3> lampiran =
{colorAttachment, depthAttachment, colorAttachmentResolve};
3 ...

277
Machine Translated by Google

Dengan render pass di tempat, ubah createFramebuffers dan tambahkan tampilan


gambar baru ke daftar:
1 batal createFramebuffers() {
2 ...
3 std::array<VkImageView, 3> lampiran = { colorImageView,
4 depthImageView, swapChainImageViews[i]
5
6
7 };
8 ...
9}

Terakhir, beri tahu pipeline yang baru dibuat untuk menggunakan lebih dari satu sampel dengan
memodifikasi createGraphicsPipeline:

1 batal buatGraphicsPipeline() { 2
...
3 multisampling.rasterizationSamples = msaaSamples;
4 ...
5}

Sekarang jalankan program Anda dan Anda akan melihat yang berikut:

278
Machine Translated by Google

Sama seperti mipmapping, perbedaannya mungkin tidak langsung terlihat.


Jika dilihat lebih dekat, Anda akan melihat bahwa tepinya tidak bergerigi lagi dan
keseluruhan gambar tampak sedikit lebih halus dibandingkan aslinya.

Perbedaannya lebih terlihat ketika melihat dari dekat di salah satu ujungnya:

Peningkatan kualitas
Ada batasan tertentu dari penerapan MSAA kami saat ini yang dapat memengaruhi
kualitas gambar keluaran dalam pemandangan yang lebih mendetail. Sebagai contoh,
saat ini kami tidak memecahkan potensi masalah yang disebabkan oleh shader aliasing,

279
Machine Translated by Google

yaitu MSAA hanya menghaluskan tepi geometri tetapi tidak mengisi interior.
Ini dapat menyebabkan situasi ketika Anda mendapatkan poligon halus yang dirender di layar
tetapi tekstur yang diterapkan akan tetap terlihat alias jika mengandung warna kontras tinggi.
Salah satu cara untuk mendekati masalah ini adalah dengan mengaktifkan Sample Shading
yang akan meningkatkan kualitas gambar lebih jauh lagi, meskipun dengan biaya kinerja
tambahan:

1 batal createLogicalDevice() {
2 ...
3 deviceFeatures.sampleRateShading = VK_TRUE; // aktifkan sampel
fitur naungan untuk perangkat
4 ...
5}
6
7 membatalkan createGraphicsPipeline()
{8 ...
9 multisampling.sampleShadingEnable = VK_TRUE; // aktifkan bayangan sampel
dalam pipa multisampling.minSampleShading = .2f; // fraksi min untuk
10 sampel
naungan; mendekati satu lebih halus
11 ...
12 }

Dalam contoh ini kami akan membiarkan bayangan sampel dinonaktifkan tetapi dalam
skenario tertentu peningkatan kualitas mungkin terlihat:

Kesimpulan

Butuh banyak usaha untuk sampai ke titik ini, tetapi sekarang Anda akhirnya memiliki
dasar yang bagus untuk program Vulkan. Pengetahuan tentang prinsip dasar Vulkan

280
Machine Translated by Google

yang Anda miliki sekarang sudah cukup untuk mulai menjelajahi lebih banyak fitur,
seperti:

• Konstanta push •
Render instan • Seragam
dinamis • Gambar
terpisah dan deskriptor sampler • Cache
pipeline • Pembuatan buffer perintah multi-utas
• Beberapa subpass • Menghitung shader

Program saat ini dapat diperluas dengan banyak cara, seperti menambahkan
pencahayaan Blinn-Phong, efek pasca-pemrosesan, dan pemetaan bayangan. Anda
seharusnya dapat mempelajari cara kerja efek ini dari tutorial untuk API lain, karena
terlepas dari ketegasan Vulkan, banyak konsep yang masih berfungsi sama.

Kode C++ / Vertex shader / Fragment shader

281
Machine Translated by Google

FAQ
Halaman ini mencantumkan solusi untuk masalah umum yang mungkin Anda temui saat
mengembangkan aplikasi Vulkan.

• Saya mendapatkan kesalahan pelanggaran akses di lapisan validasi inti:


Pastikan MSI Afterburner / RivaTuner Statistics Server tidak berjalan, karena ada
beberapa masalah kompatibilitas dengan Vulkan.

• Saya tidak melihat pesan apa pun dari lapisan validasi / Lapisan validasi tidak
tersedia: Pertama, pastikan lapisan validasi mendapatkan kesempatan untuk
mencetak kesalahan dengan membiarkan terminal tetap terbuka setelah program
Anda keluar. Anda dapat melakukan ini dari Visual Studio dengan menjalankan
program Anda dengan Ctrl-F5 alih-alih F5, dan di Linux dengan menjalankan
program Anda dari jendela terminal. Jika masih tidak ada pesan dan Anda yakin
bahwa lapisan validasi diaktifkan, maka Anda harus memastikan bahwa SDK
Vulkan Anda terinstal dengan benar dengan mengikuti petunjuk “Verifikasi
Instalasi” di halaman ini. Pastikan juga bahwa versi SDK Anda setidaknya 1.1.106.0
untuk mendukung lapisan VK_LAYER_KHRONOS_validation.

• vkCreateSwapchainKHR memicu kesalahan di SteamOver layVulkanLayer64.dll:


Tampaknya ini adalah masalah kompatibilitas di klien Steam beta. Ada beberapa
kemungkinan solusi:

– Keluar dari program beta Steam.


– Atur lingkungan DISABLE_VK_LAYER_VALVE_steam_overlay_1
variabel ke 1
– Hapus entri lapisan Steam overlay Vulkan di registri di bawah
HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers

Contoh:

282
Machine Translated by Google

283
Machine Translated by Google

Kebijakan pribadi

Umum
Kebijakan privasi ini berlaku untuk informasi yang dikumpulkan saat Anda menggunakan
vulkan-tutorial.com atau subdomainnya. Ini menjelaskan bagaimana pemilik situs web ini,
Alexander Overvoorde, mengumpulkan, menggunakan, dan membagikan informasi tentang Anda.

Analitik
Situs web ini mengumpulkan analitik tentang pengunjung menggunakan instance Matomo
yang dihosting sendiri (https://matomo.org/), sebelumnya dikenal sebagai Piwik. Itu mencatat
halaman mana yang Anda kunjungi, jenis perangkat dan browser apa yang Anda gunakan,
berapa lama Anda melihat halaman tertentu dan dari mana Anda berasal. Informasi ini
dianonimkan dengan hanya merekam dua byte pertama dari alamat IP Anda (misalnya
123.123.xxx.xxx). Log anonim ini disimpan untuk waktu yang tidak terbatas.

Analitik ini digunakan untuk tujuan melacak bagaimana konten di situs web dikonsumsi,
berapa banyak orang yang mengunjungi situs web secara umum, dan situs web lain mana
yang ditautkan ke sini. Hal ini memudahkan untuk terlibat dengan komunitas dan menentukan
area situs web mana yang harus ditingkatkan, misalnya jika waktu ekstra harus digunakan
untuk memfasilitasi pembacaan seluler.

Data ini tidak dibagikan dengan pihak ketiga.

Iklan
Situs web ini menggunakan server iklan pihak ketiga yang mungkin menggunakan cookie
untuk melacak aktivitas di situs web guna mengukur keterlibatan dengan iklan.

Komentar
Setiap bab menyertakan bagian komentar di bagian akhir yang disediakan oleh layanan
Disqus pihak ketiga. Layanan ini mengumpulkan data identitas untuk memudahkan pembacaan

284
Machine Translated by Google

dan pengajuan komentar, dan informasi penggunaan agregat untuk meningkatkan layanan
mereka.

Kebijakan privasi lengkap dari layanan pihak ketiga ini dapat ditemukan di https://help.
disqus.com/terms-and-policies/disqus-privacy-policy.

285

Anda mungkin juga menyukai