Mengoptimalkan dengan presisi yang lebih rendah

Format numerik data grafis dan penghitungan shader dapat berdampak signifikan pada performa game.

Format yang optimal dapat melakukan hal berikut:

  • Meningkatkan efisiensi penggunaan cache GPU
  • Mengurangi penggunaan bandwidth memori, menghemat daya, dan meningkatkan performa
  • Memaksimalkan throughput komputasi dalam program shader
  • Meminimalkan penggunaan RAM CPU untuk game

Format floating point

Sebagian besar penghitungan dan data dalam grafis 3D modern menggunakan bilangan floating point. Vulkan di Android menggunakan bilangan floating point yang berukuran 32 atau 16 bit. Bilangan floating point 32 bit biasanya disebut sebagai presisi tunggal atau presisi penuh; bilangan floating point 16-bit disebut presisi setengah.

Vulkan menentukan jenis floating point 64-bit, tetapi jenis ini biasanya tidak didukung oleh perangkat Vulkan di Android, dan penggunaannya tidak direkomendasikan. Bilangan floating point 64-bit biasanya disebut sebagai presisi ganda.

Format bilangan bulat

Bilangan bulat yang ditandatangani dan tidak ditandatangani juga digunakan untuk data dan penghitungan. Ukuran bilangan bulat standar adalah 32 bit. Dukungan untuk ukuran bit lain bergantung pada perangkat. Perangkat Vulkan yang menjalankan Android biasanya mendukung bilangan bulat 16-bit dan 8-bit. Vulkan menentukan jenis bilangan bulat 64-bit, tetapi jenis ini biasanya tidak didukung oleh perangkat Vulkan di Android, dan penggunaannya tidak direkomendasikan.

Perilaku presisi setengah yang kurang optimal

Arsitektur GPU modern menggabungkan dua nilai 16-bit bersama-sama dalam pasangan 32-bit dan menerapkan petunjuk yang beroperasi pada pasangan tersebut. Untuk performa yang optimal, hindari penggunaan variabel float 16-bit skalar; vektorisasi data menjadi vektor dua atau empat elemen. Compiler shader mungkin dapat menggunakan nilai skalar dalam operasi vektor. Namun, jika Anda mengandalkan compiler untuk mengoptimalkan skalar, periksa output compiler untuk memverifikasi vektorisasi.

Konversi ke dan dari floating point presisi 32-bit dan 16-bit memiliki biaya komputasi. Kurangi overhead dengan meminimalkan konversi presisi dalam kode Anda.

Tentukan tolok ukur perbedaan performa antara algoritma versi 16-bit dan 32-bit Anda. Presisi setengah tidak selalu menghasilkan peningkatan performa, terutama untuk penghitungan yang rumit. Algoritma yang banyak menggunakan petunjuk fused multiply-add (FMA) pada data vektor adalah kandidat yang baik untuk meningkatkan performa pada presisi setengah.

Dukungan format numerik

Semua perangkat Vulkan di Android mendukung bilangan floating point 32-bit presisi tunggal dan angka bilangan bulat 32-bit dalam penghitungan data dan shader. Dukungan untuk format lain tidak dijamin akan tersedia, dan jika tersedia, tidak dijamin untuk semua kasus penggunaan.

Vulkan memiliki dua kategori dukungan untuk format numerik opsional: aritmetika dan penyimpanan. Sebelum menggunakan format tertentu, pastikan perangkat mendukungnya dalam kedua kategori tersebut.

Dukungan aritmetika

Perangkat Vulkan harus mendeklarasikan dukungan aritmetika untuk format numerik agar dapat digunakan dalam program shader. Perangkat Vulkan di Android biasanya mendukung format berikut untuk aritmetika:

  • Bilangan bulat 32-bit (wajib)
  • Floating point 32 bit (wajib)
  • Bilangan bulat 8-bit (opsional)
  • Bilangan bulat 16-bit (opsional)
  • Floating point presisi setengah 16-bit (opsional)

Untuk menentukan apakah perangkat Vulkan mendukung bilangan bulat 16-bit untuk aritmetika, mengambil fitur perangkat dengan memanggil metode vkGetPhysicalDeviceFeatures2() dan memeriksa apakah kolom shaderInt16 di VkPhysicalDeviceFeatures2 struktur hasil yang benar.

Untuk menentukan apakah perangkat Vulkan mendukung float 16-bit atau bilangan bulat 8-bit, lakukan langkah-langkah berikut:

  1. Periksa apakah perangkat mendukung ekstensi Vulkan VK_KHR_shader_float16_int8. Ekstensi ini diperlukan untuk dukungan float 16-bit dan bilangan bulat 8-bit.
  2. Jika VK_KHR_shader_float16_int8 didukung, tambahkan pointer struktur VkPhysicalDeviceShaderFloat16Int8Features ke rantai VkPhysicalDeviceFeatures2.pNext.
  3. Periksa kolom shaderFloat16 dan shaderInt8 dari struktur hasil VkPhysicalDeviceShaderFloat16Int8Features setelah memanggil vkGetPhysicalDeviceFeatures2(). Jika nilai kolom adalah true, format ini didukung untuk aritmetika program shader.

Meskipun bukan persyaratan di Vulkan 1.1 atau profil Dasar Pengukuran Android 2022, dukungan untuk ekstensi VK_KHR_shader_float16_int8 biasanya ditemukan di perangkat Android.

Dukungan penyimpanan

Perangkat Vulkan harus mendeklarasikan dukungan terhadap format numerik opsional untuk jenis penyimpanan tertentu. Ekstensi VK_KHR_16bit_storage mendeklarasikan dukungan untuk format bilangan bulat 16-bit dan floating-point 16-bit. Empat jenis penyimpanan ditentukan oleh ekstensi ini. Perangkat dapat mendukung bilangan 16-bit untuk tidak satu pun, beberapa, atau semua jenis penyimpanan.

Jenis penyimpanannya adalah:

  • Objek buffer penyimpanan
  • Objek buffer seragam
  • Blok konstanta push
  • Antarmuka input dan output shader

Sebagian besar, tetapi tidak semua, perangkat Vulkan 1.1 di Android mendukung format 16-bit dalam objek buffer penyimpanan. Jangan mengasumsikan dukungan berdasarkan model GPU. Perangkat dengan driver yang lebih lama untuk GPU tertentu mungkin tidak mendukung objek buffer penyimpanan, sedangkan perangkat dengan driver yang lebih baru mendukungnya.

Dukungan untuk format 16-bit dalam buffer seragam, blok konstanta push, dan antarmuka input/output shader umumnya bergantung pada produsen GPU. Di Android, GPU biasanya mendukung ketiga jenis ini atau tidak mendukung ketiganya.

Contoh fungsi yang menguji dukungan format penyimpanan dan aritmetika Vulkan:

struct ReducedPrecisionSupportInfo {
  // Arithmetic support
  bool has_8_bit_int_ = false;
  bool has_16_bit_int_ = false;
  bool has_16_bit_float_ = false;
  // Storage support
  bool has_16_bit_SSBO_ = false;
  bool has_16_bit_UBO_ = false;
  bool has_16_bit_push_ = false;
  bool has_16_bit_input_output_ = false;
  // Use 16-bit floats if we have arithmetic
  // support and at least SSBO storage support.
  bool use_16bit_floats_ = false;
};

void CheckFormatSupport(VkPhysicalDevice physical_device,
    ReducedPrecisionSupportInfo &info) {

  // Retrieve the device extension list so we
  // can check for our desired extensions.
  uint32_t device_extension_count;
  vkEnumerateDeviceExtensionProperties(physical_device, nullptr,
      &device_extension_count, nullptr);
  std::vector<VkExtensionProperties> device_extensions(device_extension_count);
  vkEnumerateDeviceExtensionProperties(physical_device, nullptr,
      &device_extension_count, device_extensions.data());

  bool has_16_8_extension = HasDeviceExtension("VK_KHR_shader_float16_int8",
      device_extensions);

  // Initialize the device features structure and
  // chain the storage features structure and 8/16-bit
  // support structure if applicable.
  VkPhysicalDeviceFeatures2 device_features;
  memset(&device_features, 0, sizeof(device_features));
  device_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;

  VkPhysicalDeviceShaderFloat16Int8Features f16_int8_features;
  memset(&f16_int8_features, 0, sizeof(f16_int8_features));
  f16_int8_features.sType =
      VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR;

  VkPhysicalDevice16BitStorageFeatures storage_features;
  memset(&storage_features, 0, sizeof(storage_features));
  storage_features.sType =
      VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES;
  device_features.pNext = &storage_features;

  if (has_16_8_extension) {
    storage_features.pNext = &f16_int8_features;
  }

  vkGetPhysicalDeviceFeatures2(physical_device, &device_features);

  // Parse the storage features and determine
  // what kinds of 16-bit storage access are available.
  if (storage_features.storageBuffer16BitAccess ||
      storage_features.uniformAndStorageBuffer16BitAccess) {
    info.has_16_bit_SSBO_ = true;
  }
  info.has_16_bit_UBO_ = storage_features.uniformAndStorageBuffer16BitAccess;
  info.has_16_bit_push_ = storage_features.storagePushConstant16;
  info.has_16_bit_input_output_ = storage_features.storageInputOutput16;

  info.has_16_bit_int_ = device_features.features.shaderInt16;
  if (has_16_8_extension) {
    info.has_16_bit_float_ = f16_int8_features.shaderFloat16;
    info.has_8_bit_int_ = f16_int8_features.shaderInt8;
  }

  // Get arithmetic and at least some form of storage
  // support before enabling 16-bit float usage.
  if (info.has_16_bit_float_ && info.has_16_bit_SSBO_) {
    info.use_16bit_floats_ = true;
  }
}

Tingkat presisi data

Bilangan floating point presisi setengah dapat mewakili rentang nilai yang lebih kecil dengan presisi yang lebih rendah daripada bilangan floating point presisi tunggal. Presisi setengah sering kali merupakan pilihan yang mudah dan tanpa penurunan secara persepsi dibandingkan presisi tunggal. Namun, presisi setengah mungkin tidak praktis untuk semua kasus penggunaan. Untuk beberapa jenis data, rentang dan presisi yang lebih rendah dapat menyebabkan artefak grafis atau rendering yang salah.

Jenis data yang merupakan kandidat baik untuk representasi dalam floating point presisi setengah meliputi:

  • Data posisi dalam koordinat ruang lokal
  • UV tekstur untuk tekstur yang lebih kecil dengan gabungan UV terbatas yang dapat dibatasi pada rentang koordinat -1,0 sampai 1,0
  • Data normal, tangen, dan bitangen
  • Data warna vertex
  • Data dengan persyaratan presisi rendah yang berpusat pada 0,0

Jenis data yang tidak direkomendasikan untuk representasi dalam float presisi setengah meliputi:

  • Data posisi dalam koordinat dunia global
  • UV tekstur untuk kasus penggunaan presisi tinggi seperti koordinat elemen UI dalam sheet atlas

Presisi dalam kode shader

Bahasa pemrograman shader OpenGL Shading Language (GLSL) dan High-level Shader Language (HLSL) mendukung spesifikasi presisi longgar atau presisi eksplisit untuk jenis numerik. Presisi longgar diperlakukan sebagai rekomendasi untuk compiler shader. Presisi eksplisit adalah persyaratan dari presisi yang ditentukan. Perangkat Vulkan di Android umumnya menggunakan format 16-bit jika disarankan oleh presisi longgar. Perangkat Vulkan lainnya, terutama di komputer desktop yang menggunakan hardware grafis yang tidak mendukung format 16-bit, mungkin mengabaikan presisi longgar dan tetap menggunakan format 32-bit.

Ekstensi penyimpanan dalam GLSL

Ekstensi GLSL yang sesuai harus ditentukan guna mengaktifkan dukungan untuk format numerik 16-bit atau 8-bit dalam struktur buffer seragam dan penyimpanan. Deklarasi ekstensi yang relevan adalah:

// Enable 16-bit formats in storage and uniform buffers.
#extension GL_EXT_shader_16bit_storage : require
// Enable 8-bit formats in storage and uniform buffers.
#extension GL_EXT_shader_8bit_storage : require

Ekstensi ini khusus untuk GLSL dan tidak memiliki padanan di HLSL.

Presisi longgar dalam GLSL

Gunakan penentu highp sebelum jenis floating point untuk menyarankan float presisi tunggal dan penentu mediump untuk float presisi setengah. Compiler GLSL untuk Vulkan menafsirkan penentu lowp lama sebagai mediump. Beberapa contoh presisi longgar:

mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix;   // Suggest 32-bit single precision

Presisi eksplisit dalam GLSL

Sertakan ekstensi GL_EXT_shader_explicit_arithmetic_types_float16 dalam kode GLSL Anda untuk mengaktifkan penggunaan jenis floating point 16 bit:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Deklarasikan jenis skalar, vektor, dan matriks floating point 16-bit dalam GLSL menggunakan kata kunci berikut:

float16_t   f16vec2     f16vec3    f16vec4
f16mat2     f16mat3     f16mat4
f16mat2x2   f16mat2x3   f16mat2x4
f16mat3x2   f16mat3x3   f16mat3x4
f16mat4x2   f16mat4x3   f16mat4x4

Deklarasikan jenis skalar dan vektor bilangan bulat 16-bit dalam GLSL menggunakan kata kunci berikut:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Presisi longgar dalam HLSL

HLSL menggunakan istilah presisi minimal, bukan presisi longgar. Kata kunci jenis presisi minimal menentukan presisi minimum, tetapi compiler dapat mengganti dengan presisi yang lebih tinggi jika presisi yang lebih tinggi adalah pilihan yang lebih baik untuk hardware target. Float 16-bit presisi minimal ditentukan oleh kata kunci min16float. Bilangan bulat 16-bit presisi minimal yang ditandatangani dan tidak ditandatangani, masing-masing ditentukan oleh kata kunci min16int dan min16uint. Contoh tambahan dari deklarasi presisi minimal mencakup hal berikut:

// Four element vector and four-by-four matrix types
min16float4 my_vector4;
min16float4x4 my_matrix4x4;

Presisi eksplisit dalam HLSL

Floating point presisi setengah ditentukan oleh kata kunci half atau float16_t. Bilangan bulat 16-bit yang ditandatangani dan tidak ditandatangani, masing-masing ditentukan oleh kata kunci int16_t dan uint16_t. Contoh tambahan dari deklarasi presisi eksplisit mencakup hal berikut:

// Four element vector and four-by-four matrix types
half4 my_vector4;
half4x4 my_matrix4x4;