Konten di situs ini telah diterjemahkan menggunakan kecerdasan buatan (AI) atau teknologi penerjemahan mesin, dan mungkin terdapat kesalahan.

Skip to content

Membuat Cache Pipeline yang Tangguh dengan Vulkan

Ada banyak konsep baru yang perlu dipelajari saat membangun renderer Vulkan. Beberapa di antaranya lebih mudah ditangani daripada yang lain, dan salah satu penambahan yang lebih sederhana adalah cache pipeline. Untuk memastikan pembuatan pipeline seefisien mungkin, Anda perlu membuat cache pipeline dan menggunakannya setiap kali Anda perlu membuat pipeline baru. Untuk memastikan aplikasi Anda tidak perlu menghabiskan waktu untuk mengkompilasi mikrokode shader berulang kali pada eksekusi berikutnya, Anda perlu menyimpan data cache pipeline ke dalam file. Kemudian memuatnya saat aplikasi Anda dimulai kembali. Seberapa sulitkah itu?

Ternyata cukup sulit.

Apa saja yang ada di cache pipeline?

Data cache pipeline adalah blob yang (sebagian besar) tidak transparan; Anda membuat objek VkPipelineCache, mungkin dengan memberikan blob awal sebagai titik awal, dan kemudian pada suatu saat Anda dapat mengambil blob data dari objek ini.

Meskipun kita tidak tahu banyak tentang isi blob tersebut kecuali dengan membaca kode sumber driver grafis,1 data cache pipeline dijamin dimulai dengan struktur yang mengidentifikasi perangkat dan terlihat seperti ini:

struct VkPipelineCacheHeaderOne<br>
{
 uint32_t length; // == sizeof(VkPipelineCacheHeaderOne)
 uint32_t version; // == VK_PIPELINE_CACHE_HEADER_VERSION_ONE
 uint32_t vendorID;
 uint32_t deviceID;
 uint8_t uuid[VK_UUID_SIZE];
}; 

Header tersebut diikuti oleh informasi khusus driver yang biasanya berisi potongan-potongan mikrokode shader (formatnya tergantung pada GPU) dan data tambahan yang mungkin berisi struktur yang ditentukan oleh driver secara arbitrer. Beberapa driver memperlakukan blob ini sebagai aliran file terstruktur dan membaca data darinya, sementara driver lain menyimpan struktur mentah yang didefinisikan dalam kode sumber driver di dalam blob tersebut dan menggunakan `memcpy` atau konversi penunjuk untuk menavigasi data; tentu saja, pembaruan driver dapat membuat cara penyimpanan data menjadi tidak valid.

Secara teori, aplikasi hanya perlu menggunakan `vkGetPipelineCacheData` untuk mengambil blob data setelah aplikasi mencapai keadaan stabil (misalnya sebelum aplikasi keluar…), menyimpan blob tersebut ke file, dan kemudian meneruskan blob ini menggunakan `VkPipelineCacheCreateInfo::pInitialData` saat membuat cache pipa pada eksekusi berikutnya. Jika isi blob tidak berfungsi untuk versi driver saat ini — mungkin driver telah diperbarui, atau mungkin pengguna beralih ke GPU yang berbeda — driver seharusnya mengabaikan data awal dan membuat cache pipeline yang kosong.

Namun, teori dan praktiknya sedikit berbeda. Aturan praktisnya adalah bahwa driver hanya akan dapat menangani blob yang persis sama dengan yang diberikan driver yang sama kepada aplikasi Anda sebelumnya, dan di sinilah masalahnya dimulai.2

Apakah drivernya sama?

Spesifikasi mengasumsikan bahwa cache tidak kompatibel antar perangkat (itulah mengapa vendorID dan deviceID ada di header), dan bergantung pada driver untuk menetapkan UUID pipa (yang merupakan GUID 16-byte) yang secara akurat mengidentifikasi seluruh faktor yang memungkinkan interpretasi blob cache pipa — Anda dapat memikirkan ini sebagai nomor versi format cache pipa. Selama pembaruan driver, misalnya, mungkin saja format cache pipeline tidak diperbarui, dalam hal ini UUID biasanya tidak akan berubah dan aplikasi tidak perlu mengkompilasi ulang shader dari awal.

Namun, driver yang beredar di pasaran cenderung menunjukkan dua jenis masalah.

Beberapa (driver yang lebih lama) mengabaikan verifikasi UUID dengan benar. Akibatnya, selama pembaruan driver, aplikasi mungkin mencoba memberikan blob dengan UUID yang sudah kadaluwarsa ke driver; driver akan mencoba menginterpretasikan ini sebagai data terbaru, dan akibatnyvkCreatePipelineCacheasi mungkin mengalami crash. Perlu dicatat bahwa secara umum, vkCreatePipelineCache tidak menjamin bahwa ia menerima data sembarangan dan dapat mengelolanya dengan baik.

Beberapa driver, termasuk yang cukup baru, mungkin mengabaikan pembaruan UUID dalam pembaruan driver yang sebenarnya merusak kompatibilitas biner pipa shader. Hal ini dapat terjadi selama pembaruan versi driver (meskipun jarang), atau (sesuatu yang terjadi dengan mudah pada driver saat ini dari setidaknya satu vendor besar) antara biner driver yang dibangun dari versi yang sama untuk ABI yang berbeda. Jika driver 32-bit dan driver 64-bit yang disertakan pada sistem yang sama memiliki UUID pipeline yang sama, maka menyimpan cache dari versi 32-bit aplikasi dan memuatnya dari versi 64-bit dapat menyebabkan driver crash — yang persis terjadi saat Anda merilis versi 32-bit aplikasi Anda dan kemudian memperbaruinya ke 64-bit sesuai pedoman Google.

Apakah datanya sama?

Sekarang setelah kita tahu apa yang menanti kita dalam hal validasi header, langkah selanjutnya adalah memvalidasi data. Setelah memanggil `vkGetPipelineCacheData`, aplikasi menyimpan blob tersebut, dan memuat blob yang persis sama pada eksekusi berikutnya.

Ternyata, menyimpan data ke file pada dasarnya sulit dilakukan dengan baik. Masalah sistem file serta stabilitas proses dapat menyebabkan file yang ditulis sebagian, memiliki bagian akhir yang diisi dengan nol (atau bahkan data acak), atau (sebagai kasus khusus) dibuat tetapi tetap berukuran nol. Di perangkat mobile, hal ini dapat diperumit oleh fakta bahwa aplikasi kemungkinan besar akan dihentikan secara tiba-tiba pada titik waktu yang tidak terduga oleh pengguna atau sistem operasi, sesuatu yang terjadi lebih jarang di desktop. Di Android, penggunaan aplikasi multi-proses (multi-aktivitas) juga umum, dan jika kode cache pipeline Anda berjalan di kedua proses dan berbagi file output yang sama, tantangan ini menjadi semakin sulit untuk diselesaikan.

Alasan mengapa file berukuran nol sangat menarik adalah karena setidaknya ada satu versi driver yang kami temui di mana pengiriman `pInitialData` dan `initialDataSize == 0` yang tidak NULL akan menghasilkan kesalahan selama pembuatan cache pipeline. Hal ini membawa kita ke peringatan terakhir.

Penanganan kesalahan itu sulit

Meskipun spesifikasi menyatakan bahwa `vkCreatePipelineCache` seharusnya selalu berhasil kecuali kehabisan memori, pernyataan semacam itu dalam spesifikasi jarang akurat. Saat membuat cache pipa, driver seharusnya mengabaikan data awal jika tidak kompatibel. Hal ini dapat terjadi jika berukuran nol, jika UUID yang disimpan tidak sesuai dengan UUID yang diharapkan, atau jika deserialisasi gagal karena alasan lain. Beberapa driver malah gagal membuat cache pipa.

Pengguna jelas tidak bersalah dalam hal ini, jadi menghentikan aplikasi bukanlah tindakan yang sopan. Meskipun umumnya dimungkinkan untuk melanjutkan tanpa cache pipeline, itu biasanya ide yang buruk karena berarti setiap pipeline harus dikompilasi ulang dari awal. Artinya, cache pipeline tetap berguna meskipun tidak diserialisasi ke disk karena memungkinkan driver untuk menyimpan hasil kompilasi di antara objek pipeline dalam memori.

Semua ini secara alami mengarah ke…

Ini bukan paranoia jika mereka benar-benar ingin menjatuhkan Anda

… solusinya. Saat menyimpan data cache pipeline ke file, kami menggunakan header yang diisi dengan informasi cukup untuk memvalidasi data, diikuti langsung oleh data cache pipeline:

  struct PipelineCachePrefixHeader<br> 
{<br> 
  uint32_t magic; // an arbitrary magic header to make sure this is actually our file<br> 
  uint32_t dataSize; // equal to *pDataSize returned by vkGetPipelineCacheData<br> 
  uint64_t dataHash; // a hash of pipeline cache data, including the header<br> 
  uint32_t vendorID; // equal to VkPhysicalDeviceProperties::vendorID<br> 
  uint32_t deviceID; // equal to VkPhysicalDeviceProperties::deviceID<br> 
  uint32_t driverVersion; // equal to VkPhysicalDeviceProperties::driverVersion<br> 
  uint32_t driverABI; // equal to sizeof(void*)<br> 
  uint8_t uuid[VK_UUID_SIZE]; // equal to VkPhysicalDeviceProperties::pipelineCacheUUID<br> 
};

Hash dari data cache pipeline akan memungkinkan kita memvalidasi integritas data. Untuk mengurangi kemungkinan kesalahan I/O yang sebenarnya menyebabkan masalah integritas, kita membuat file sementara dan menulis header ini ke file diikuti oleh data cache pipeline, lalu memindahkan file ke lokasi tujuan menggunakan perintah rename.3

Saat memuat cache pipa, kami membaca header, membaca data, memvalidasi data yang dibaca menggunakan dataSize dan dataHash, lalu memvalidasi bahwa data dapat diteruskan dengan aman ke driver dengan membandingkan bidang-bidang yang tersisa dengan properti perangkat.4

Jika data valid, `vkCreatePipelineCache` dipanggil dengan data awal yang benar. Yang penting, jika panggilan ini gagal, hal ini menunjukkan bahwa driver menerapkan pemeriksaan tambahan yang tidak terdeteksi oleh logika kami sendiri. Jadi, alih-alih melanjutkan tanpa cache pipa, kami membuat cache pipa kosong dalam kasus ini dengan memanggil `vkCreatePipelineCache` lagi tanpa data awal.

Kami juga membuat cache pipeline kosong jika file cache pipeline tidak ditemukan atau logika validasi kami mengklasifikasikan data sebagai tidak dapat digunakan.

Catatan: karena kami memasukkan driverVersion ke dalam header, setiap pembaruan driver akan menyebabkan cache pipa dibangun ulang; kami menyertakan pemeriksaan ini karena hal ini sepenuhnya menghilangkan masalah di mana UUID cache pipa tidak diperbarui meskipun seharusnya — biasanya driverVersion diperbarui sebagai bagian dari proses pembangunan, sedangkan pembaruan UUID lebih bersifat manual. Untuk aplikasi yang secara eksklusif ditujukan untuk desktop, hal ini mungkin terlalu agresif — secara umum, driver desktop cenderung lebih baik dalam menangani validitas cache pipeline sehingga tidak semua saran ini berlaku.

Kesimpulan

Sayangnya, driver Vulkan tidak selalu benar dan tidak selalu mengikuti spesifikasi secara ketat. Data cache pipeline merupakan bagian yang sangat rentan dari renderer Vulkan karena I/O sulit untuk diimplementasikan dengan benar, dan seringkali tidak ada atau hanya sedikit pemeriksaan integritas di dalam driver. Namun, dengan validasi yang cukup di sisi aplikasi, Anda dapat menghilangkan masalah stabilitas yang berasal dari penanganan cache pipeline dalam praktiknya — hanya saja membutuhkan usaha.

  1. Hal ini tentu saja dapat dilakukan saat ini! Misalnya, berikut adalah implementasi vkGetPipelineCacheData untuk radv. ↩
  2. Sisa artikel ini didasarkan pada pengalaman dalam terus menerus merilis klien Roblox di Android dengan dukungan Vulkan dan bertahan melalui berbagai pembaruan OS Android, pembaruan driver, serta secara umum menangani driver Vulkan baik yang masih dalam tahap awal maupun yang sudah ada saat ini dari semua vendor utama. ↩
  3. Secara teori, penggantian nama seharusnya bersifat atomik, tetapi dalam praktiknya, semantik dan jaminan yang tepat bervariasi tergantung pada sistem berkas; hash berguna sebagai cara untuk melakukan perbandingan yang andal. ↩
  4. Tergantung pada aplikasinya, Anda mungkin juga ingin menggunakan nama file yang berbeda berdasarkan, misalnya, vendorID atau driverABI; hal ini lebih menarik di desktop dan kurang menarik di perangkat seluler. ↩

Diterbitkan pertama kali di: https://zeux.io/2019/07/17/serializing-pipeline-cache/

Arseny Kapoulkine telah bekerja di bidang teknologi game selama satu dekade terakhir. Setelah bekerja di bidang rendering, simulasi fisika, runtime bahasa, multithreading, dan banyak bidang lainnya, ia masih menemukan masalah menarik dalam pengembangan game yang memerlukan pemikiran tingkat rendah. Setelah membantu merilis banyak judul di PS3 termasuk beberapa game FIFA, ia bergabung dengan Roblox pada tahun 2012 dan telah bekerja pada mesin internal sejak saat itu, membantu pengembang game muda mewujudkan impian mereka.

Baik Roblox Corporation maupun blog ini tidak mendukung atau mengendorse perusahaan atau layanan apa pun. Selain itu, tidak ada jaminan atau janji yang diberikan terkait keakuratan, keandalan, atau kelengkapan informasi yang terkandung dalam blog ini.

©2021 Roblox Corporation. Roblox, logo Roblox, dan Powering Imagination termasuk di antara merek dagang terdaftar dan tidak terdaftar kami di AS dan negara-negara lain.