Publikasi ini didedikasikan untuk terjemahan bagian Menggambar segitiga, yaitu subbagian Setup, kode Dasar dan bab Instance.
Kandungan
Kode Dasar
Struktur umum
Di bab sebelumnya, kami membahas cara membuat project untuk Vulkan, mengonfigurasinya dengan benar, dan mengujinya menggunakan cuplikan kode. Dalam bab ini, kita akan mulai dengan dasar-dasarnya.
Perhatikan kode berikut:
#include <vulkan/vulkan.h>
#include <iostream>
#include <stdexcept>
#include <cstdlib>
class HelloTriangleApplication {
public:
void run() {
initVulkan();
mainLoop();
cleanup();
}
private:
void initVulkan() {
}
void mainLoop() {
}
void cleanup() {
}
};
int main() {
HelloTriangleApplication app;
try {
app.run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Pertama, kami menyertakan file header Vulkan dari LunarG SDK. File header
stdexcepts
dan
iostream
digunakan untuk penanganan kesalahan dan distribusi. File header
cstdlib
menyediakan makro
EXIT_SUCCESS
dan
EXIT_FAILURE
.
Program itu sendiri dibungkus dalam kelas HelloTriangleApplication, di mana kita akan menyimpan objek Vulkan sebagai anggota pribadi kelas. Di sana kita juga akan menambahkan fungsi untuk menginisialisasi setiap objek yang dipanggil dari fungsi tersebut
initVulkan
. Setelah itu, mari buat loop utama untuk rendering frame. Untuk melakukan ini, isi fungsi di
mainLoop
mana loop akan dijalankan hingga jendela ditutup. Setelah menutup jendela dan keluar,
mainLoop
sumber daya harus dilepaskan. Untuk melakukan ini, isi
cleanup
.
Jika kesalahan kritis terjadi selama operasi, kami akan melempar pengecualian
std::runtime_error
yang akan ditangkap dalam fungsi
main
, dan deskripsi akan ditampilkan di
std::cerr
. Salah satu kesalahan tersebut mungkin, misalnya, pesan bahwa ekstensi yang diperlukan tidak didukung. Untuk menangani banyak tipe pengecualian standar, kami menangkap yang lebih umum
std::exception
.
Hampir setiap bab berikutnya akan menambahkan fungsi baru yang dipanggil dari
initVulkan
, dan objek Vulkan baru yang perlu dirilis di
cleanup
akhir program.
Pengelolaan sumber daya
Jika objek Vulkan tidak lagi diperlukan, objek tersebut harus dihancurkan. C ++ memungkinkan Anda untuk secara otomatis membatalkan alokasi sumber daya menggunakan RAII atau petunjuk cerdas yang disediakan oleh file header
<memory>
. Namun, dalam tutorial ini kami memutuskan untuk menulis secara eksplisit kapan harus mengalokasikan dan membatalkan alokasi objek Vulkan. Bagaimanapun, ini adalah kekhasan pekerjaan Vulkan - untuk menjelaskan secara detail setiap operasi untuk menghindari kemungkinan kesalahan.
Setelah membaca tutorial, Anda dapat mengimplementasikan pengelolaan sumber daya otomatis dengan menulis kelas C ++ yang menerima objek Vulkan di konstruktor dan membebaskannya di destruktor. Anda juga dapat menerapkan penghapus Anda sendiri untuk
std::unique_ptr
atau
std::shared_ptr
, tergantung pada kebutuhan Anda. Konsep RAII direkomendasikan untuk program yang lebih besar, tetapi akan sangat membantu untuk mempelajarinya lebih lanjut.
Objek Vulkan dibuat langsung menggunakan fungsi seperti vkCreateXXX , atau dialokasikan melalui objek lain menggunakan fungsi seperti vkAllocateXXX . Setelah memastikan bahwa objek tidak sedang digunakan di mana pun, Anda harus memusnahkannya dengan vkDestroyXXX atau vkFreeXXX . Parameter untuk fitur ini biasanya bervariasi tergantung pada jenis objek, tapi ada satu parameter umum:
pAllocator
. Ini adalah parameter opsional yang memungkinkan Anda menggunakan callback untuk alokasi memori khusus. Kami tidak membutuhkannya di manual, kami akan menyebarkannya sebagai argumen
nullptr
.
Integrasi GLFW
Vulkan berfungsi dengan baik tanpa membuat jendela saat menggunakan rendering di luar layar, tetapi jauh lebih baik saat hasilnya terlihat di layar.
Pertama, ganti baris dengan yang
#include <vulkan/vulkan.h>
berikut ini:
#define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h>
Tambahkan fungsi
initWindow
dan tambahkan panggilannya dari metode
run
sebelum panggilan lain. Kami akan menggunakan
initWindow
GLFW untuk menginisialisasi dan membuat jendela.
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
private:
void initWindow() {
}
Panggilan pertama ke
initWindow
harus berupa fungsi
glfwInit()
yang menginisialisasi pustaka GLFW. GLFW awalnya dirancang untuk bekerja dengan OpenGL. Kita tidak membutuhkan konteks OpenGL, jadi tunjukkan bahwa kita tidak perlu membuatnya menggunakan panggilan berikut:
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
Nonaktifkan sementara kemampuan untuk mengubah ukuran jendela, karena menangani situasi ini memerlukan pertimbangan terpisah:
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
Tetap membuat jendela. Untuk melakukan ini, tambahkan anggota pribadi
GLFWwindow* window;
dan inisialisasi jendela dengan:
window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
Tiga parameter pertama menentukan lebar, tinggi dan judul jendela. Parameter keempat adalah opsional, ini memungkinkan Anda untuk menentukan monitor di mana jendela akan ditampilkan. Parameter terakhir khusus untuk OpenGL.
Alangkah baiknya menggunakan konstanta untuk lebar dan tinggi jendela, karena kita akan membutuhkan nilai ini di tempat lain. Tambahkan baris berikut sebelum definisi kelas
HelloTriangleApplication
:
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
dan ganti panggilan untuk membuat jendela
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
Anda harus memiliki fungsi berikut
initWindow
:
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
Mari kita gambarkan loop utama dalam metode
mainLoop
untuk menjaga aplikasi tetap berjalan hingga jendela ditutup:
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
Kode ini seharusnya tidak menimbulkan pertanyaan apa pun. Ini menangani acara seperti menekan tombol X sebelum pengguna menutup jendela. Juga dari loop ini kita akan memanggil fungsi untuk membuat frame individual.
Setelah menutup jendela, kita perlu membebaskan sumber daya dan keluar dari GLFW. Pertama, mari tambahkan
cleanup
kode berikut:
void cleanup() {
glfwDestroyWindow(window);
glfwTerminate();
}
Hasilnya, setelah memulai program, Anda akan melihat jendela dengan nama
Vulkan
yang akan ditampilkan hingga program ditutup. Sekarang kita memiliki kerangka untuk bekerja dengan Vulkan, mari kita lanjutkan untuk membuat objek Vulkan pertama kita!
Contoh
Instansiasi
Hal pertama yang perlu Anda lakukan adalah membuat instance untuk menginisialisasi perpustakaan. Instance adalah link antara program Anda dan library Vulkan, dan untuk membuatnya, Anda perlu memberikan beberapa informasi tentang program Anda kepada driver.
Tambahkan metode
createInstance
dan panggil dari fungsi
initVulkan
.
void initVulkan() { createInstance(); }
Tambahkan anggota instance ke kelas kami untuk memegang pegangan instance:
private:
VkInstance instance;
Sekarang kita perlu mengisi struktur khusus dengan informasi tentang program. Secara teknis, data bersifat opsional, namun ini memungkinkan pengemudi memperoleh informasi yang berguna untuk mengoptimalkan pekerjaan dengan program Anda. Struktur ini disebut
VkApplicationInfo
:
void createInstance() {
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
}
Seperti yang disebutkan, banyak struktur di Vulkan memerlukan definisi tipe eksplisit dalam anggota sType . Selain itu, struktur ini, seperti banyak struktur lainnya, berisi elemen
pNext
yang memungkinkan Anda memberikan informasi untuk ekstensi. Kami menggunakan inisialisasi nilai untuk mengisi struktur dengan nol.
Sebagian besar informasi di Vulkan diteruskan melalui struktur, jadi Anda perlu mengisi satu struktur lagi guna memberikan informasi yang cukup untuk membuat instance. Struktur berikut diperlukan, ini memberi tahu pengemudi ekstensi global dan lapisan validasi mana yang ingin kita gunakan. "Global" berarti ekstensi berlaku untuk seluruh program dan tidak untuk perangkat tertentu.
VkInstanceCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo;
Dua parameter pertama tidak menimbulkan pertanyaan. Dua anggota berikutnya menentukan ekstensi global yang diperlukan. Seperti yang Anda ketahui, Vulkan API sepenuhnya tidak bergantung pada platform. Ini berarti Anda memerlukan ekstensi untuk berinteraksi dengan sistem jendela. GLFW memiliki fungsi built-in praktis yang mengembalikan daftar ekstensi yang diperlukan.
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;
Dua anggota struktur terakhir menentukan lapisan validasi global mana yang akan disertakan. Kita akan membicarakannya lebih detail di bab berikutnya, jadi biarkan nilai-nilai ini kosong untuk saat ini.
createInfo.enabledLayerCount = 0;
Sekarang Anda telah melakukan semua yang diperlukan untuk membuat sebuah instance. Lakukan panggilan
vkCreateInstance
:
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
Biasanya, parameter fungsi untuk membuat objek berada dalam urutan berikut:
- Arahkan ke struktur dengan informasi yang diperlukan
- Arahkan ke pengalokasi khusus
- Arahkan ke variabel tempat deskriptor objek baru akan ditulis
Jika semuanya dilakukan dengan benar, deskriptor instance akan disimpan dalam instance . Hampir semua fungsi Vulkan mengembalikan nilai VkResult , yang bisa berupa
VK_SUCCESS
kode kesalahan atau kode kesalahan. Kami tidak perlu menyimpan hasilnya untuk memastikan instance telah dibuat. Mari gunakan pemeriksaan sederhana:
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
Sekarang jalankan program untuk memverifikasi bahwa instance telah berhasil dibuat.
Memeriksa Ekstensi yang Didukung
Jika kita melihat dokumentasi Vulkan , kita dapat menemukan bahwa salah satu kode kesalahan yang mungkin ada adalah
VK_ERROR_EXTENSION_NOT_PRESENT
. Kami cukup menentukan ekstensi yang diperlukan dan berhenti bekerja jika tidak didukung. Ini masuk akal untuk ekstensi utama seperti antarmuka sistem jendela, tetapi bagaimana jika kita ingin menguji kemampuan opsional?
Untuk mendapatkan daftar ekstensi yang didukung sebelum membuat instance, gunakan fungsi vkEnumerateInstanceExtensionProperties... Parameter pertama dari fungsi ini opsional, ini memungkinkan Anda untuk memfilter ekstensi dengan lapisan validasi tertentu, jadi kami akan membiarkannya kosong untuk saat ini. Selain itu, fungsi tersebut memerlukan penunjuk ke variabel di mana jumlah ekstensi akan ditulis dan penunjuk ke area memori tempat informasi tentang ekstensi harus ditulis.
Untuk mengalokasikan memori untuk menyimpan informasi ekstensi, Anda harus terlebih dahulu mengetahui jumlah ekstensi. Biarkan parameter terakhir kosong untuk meminta jumlah ekstensi:
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
Alokasikan array untuk menyimpan informasi ekstensi (jangan lupa tentang
include <vector>
):
std::vector<VkExtensionProperties> extensions(extensionCount);
Anda sekarang dapat meminta informasi tentang ekstensi.
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
Setiap struktur VkExtensionProperties berisi nama dan versi ekstensi. Mereka dapat dicantumkan dengan loop for sederhana (
\t
berikut adalah tab indentasi):
std::cout << "available extensions:\n";
for (const auto& extension : extensions) {
std::cout << '\t' << extension.extensionName << '\n';
}
Anda dapat menambahkan kode ini ke fungsi
createInstance
untuk informasi lebih lanjut tentang dukungan Vulkan. Anda juga dapat mencoba membuat fungsi yang akan memeriksa apakah semua ekstensi yang dikembalikan oleh fungsi
glfwGetRequiredInstanceExtensions
tersebut termasuk dalam daftar ekstensi yang didukung.
Pembersihan
VkInstance harus dihancurkan sebelum menutup program. Ini bisa dilakukan
cleanup
dengan menggunakan fungsi VkDestroyInstance :
void cleanup() {
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
Parameter untuk fungsi vkDestroyInstance sudah cukup jelas. Seperti disebutkan di bab sebelumnya, fungsi alokasi dan dealokasi di Vulkan menerima petunjuk opsional ke pengalokasi khusus yang tidak kita gunakan dan kita teruskan
nullptr
. Semua resource Vulkan lainnya harus dibersihkan sebelum instance dihancurkan.
Sebelum melanjutkan ke langkah yang lebih kompleks, kita perlu menyiapkan lapisan validasi untuk kemudahan debugging.
Kode C ++