提高精確度

無論是圖像資料的數值格式,還是著色器的運算方式,都可能會對遊戲效能產生重大影響。

最佳化格式具有以下特點:

  • 提高 GPU 快取使用效率
  • 降低記憶體頻寬用量,不僅省電還可提升效能
  • 盡可能增加著色器程式中的運算處理量
  • 盡可能減少遊戲的 CPU RAM 使用率

浮點格式

現代 3D 圖像中的運算和資料多半都會採用浮點數。Android 上的 Vulkan 可使用大小為 32 或 16 位元的浮點數。32 位元浮點數通稱為單精度或完整精確度浮點數;16 位元浮點數則稱半精度浮點數。

Vulkan 定義了 64 位元浮點類型,但 Android 上的 Vulkan 裝置一般不支援該類型,因此不建議使用。64 位元浮點數通常稱為雙精度浮點數。

整數格式

整數 (不論是否帶正負號) 也可用於資料和運算。標準整數大小為 32 位元。其他位元大小的支援情況則因裝置而異。搭載 Android 的 Vulkan 裝置通常支援 16 位元和 8 位元整數。Vulkan 定義了 64 位元整數類型,但 Android 上的 Vulkan 裝置一般不支援該類型,因此不建議使用。

非理想的半精度行為

新型 GPU 架構會將兩個 16 位元值合成 32 位元組合,並實作在該組合上執行的指令。為獲得最佳效能,請避免使用純量 16 位元浮點變數;而是要將資料向量化,做為二元素或四元素向量。著色器編譯器可能會在向量運算中使用純量值。不過,如果您在將純量最佳化時使用編譯器,請檢查編譯器輸出內容,藉此驗證向量。

在 32 位元和 16 位元精度浮點間轉換,會產生運算開銷。如要減少開銷,請盡可能降低程式碼中的精確度轉換次數。

對於 16 位元和 32 位元版本演算法的效能差異,您可以執行基準測試。半精度有時可能無法提升效能,尤其是在複雜運算時更是如此。如想提升半精度效能,最好的方式是採用演算法,對向量化資料大量使用積和熔加運算 (FMA) 指令。

數值格式支援

只要是搭載 Android 的 Vulkan 裝置,都可在資料和著色器的運算作業中,支援單精度、32 位元浮點數和 32 位元整數。但我們不保證能支援其他格式,即使可支援,也不保證適用於所有用途。

選用數值格式時,Vulkan 支援兩種類別:算術和儲存體。在您使用特定格式前,請先確認裝置在這兩個類別中都支援該格式。

算術支援

Vulkan 裝置必須宣告對數值格式的算術支援,才能在著色器程式中使用該格式。Android 上的 Vulkan 裝置一般支援以下算術格式:

  • 32 位元整數 (必要)
  • 32 位元浮點 (必要)
  • 8 位元整數 (選用)
  • 16 位元整數 (選用)
  • 16 位元半精度浮點 (選用)

如要判斷 Vulkan 裝置是否支援 16 位元整數來進行算術, 呼叫 vkGetPhysicalDeviceFeatures2() 函式並檢查是否 VkPhysicalDeviceFeatures2 中的 shaderInt16 欄位 結果結構為 true

如要判斷 Vulkan 裝置是否支援 16 位元浮點值或 8 位元整數,請執行以下步驟:

  1. 檢查裝置是否支援 VK_KHR_shader_float16_int8 Vulkan 擴充功能。如要支援 16 位元浮點值和 8 位元整數,就必須用到這項擴充功能。
  2. 如果支援 VK_KHR_shader_float16_int8,請將 VkPhysicalDeviceShaderFloat16Int8Features 結構指標附加至 VkPhysicalDeviceFeatures2.pNext 鏈結。
  3. 呼叫 vkGetPhysicalDeviceFeatures2() 後,請檢查 VkPhysicalDeviceShaderFloat16Int8Features 結果結構的 shaderFloat16shaderInt8 欄位。如果欄位值為 true,代表著色器程式算術支援該格式。

雖然在 Vulkan 1.1 或 2022 年版 Android 基準設定檔中不一定要支援 VK_KHR_shader_float16_int8 擴充功能,但這項支援在 Android 裝置上十分常見。

儲存體支援

Vulkan 裝置必須宣告能夠支援特定儲存體類型的選用數值格式。VK_KHR_16bit_storage 擴充功能會宣告支援 16 位元整數和 16 位元浮點格式。這個擴充功能定義了四種儲存體類型。對於無儲存體、部分儲存體,或所有儲存體類型,裝置都可以支援 16 位元數值。

儲存體類型分為:

  • 儲存體緩衝區物件
  • 統一緩衝區物件
  • 推送常數區塊
  • 著色器輸入和輸出介面

大多數 (但非所有) 搭載 Android 的 Vulkan 1.1 裝置都可在儲存體緩衝區物件中支援 16 位元格式。請不要根據 GPU 型號假設能否提供支援。對於特定 GPU,搭載舊版驅動程式的裝置可能不支援儲存體緩衝區物件,而搭載新版驅動程式的裝置則可能支援。

至於能否在統一緩衝區、推送常數區塊和著色器輸入/輸出介面中支援 16 位元格式,一般需視 GPU 製造商而定。通常在 Android 上,GPU 若不是可支援這三種類型,就是完全不支援。

以下範例函式可測試能否支援 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;
  }
}

資料的精確程度

與單精度浮點數相比,半精度浮點數能以較低的精確度,代表較小範圍的值。相較於單精度,半精度通常是簡單且明顯的無損選擇。然而,半精度實際上可能不適用於所有用途。對於某類資料縮小範圍和精確度,可能會導致不自然痕跡或算繪出錯。

適合以半精度浮點表示的資料類型包括:

  • 以局部空間座標表示的位置資料
  • 適用於小型紋理的紋理 UV,UV 裝設限制在 -1.0 至 1.0 的座標範圍之間
  • 法向量、切線向量和雙切線向量資料
  • 頂點顏色資料
  • 以 0.0 為中心的低精度要求資料

「不」建議以半精度浮點值表示的資料類型包括:

  • 以全球座標表示的位置資料
  • 用於高精度用途的紋理 UV,例如圖集工作表中的 UI 元素座標

著色器程式碼的精確度

OpenGL 著色語言 (GLSL)高階著色器語言 (HLSL) 都是著色器程式設計語言,支援的規格包括數值類型的寬鬆精確度/明確精確度。我們建議著色器編譯器採用寬鬆精確度。在已指定精確度的情況下,則必須採用明確精確度。搭載 Android 的 Vulkan 裝置通常會採用寬鬆精確度建議的 16 位元格式。其他 Vulkan 裝置 (特別是使用不支援 16 位元格式圖像硬體的電腦) 可能會忽略寬鬆精確度,仍然使用 32 位元格式。

GLSL 中的儲存體擴充功能

您必須定義適當的 GLSL 擴充功能,以便在儲存體和統一緩衝區結構中支援 16 位元或 8 位元數值格式。相關的擴充功能宣告如下:

// 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

這些是 GLSL 特定的擴充功能,在 HLSL 中沒有對等項目。

GLSL 中的寬鬆精確度

在浮點類型之前使用 highp 限定詞,即可建議單精度浮點值,而 mediump 限定詞則適用於半精度浮點值。Vulkan 的 GLSL 編譯器會將舊版 lowp 限定詞解讀為 mediump。以下是寬鬆精確度的幾個範例:

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

GLSL 中的明確精確度

在 GLSL 程式碼中加入 GL_EXT_shader_explicit_arithmetic_types_float16 擴充功能,即可啟用 16 位元浮點類型:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

請使用以下關鍵字,在 GLSL 中宣告 16 位元浮點純量、向量和矩陣類型:

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

請使用以下關鍵字,在 GLSL 中宣告 16 位元整數純量和向量類型:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

HLSL 中的寬鬆精確度

HLSL 所用術語是「最低精確度」,而非寬鬆精確度。最小精確度類型關鍵字可指定最低精確度,但如果目標硬體更適合採用較高精確度,編譯器可能會替換較高的精確度。關鍵字 min16float 可用於指定最低精確度為 16 位元的浮點值。關鍵字 min16intmin16uint 則分別指定最低精確度為帶正負號和不帶正負號的 16 位元整數。以下是關於最低精確度宣告的其他範例:

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

HLSL 中的明確精確度

關鍵字 halffloat16_t用於指定半精度浮點。關鍵字 int16_tuint16_t 則分別指定帶正負號和不帶正負號的 16 位元整數。以下是關於明確精確度宣告的其他範例:

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