Tối ưu hoá khi độ chính xác giảm

Định dạng số của dữ liệu đồ hoạ và các phép tính trong chương trình đổ bóng có thể có tác động đáng kể đến hiệu suất của trò chơi.

Định dạng tối ưu mang lại những lợi ích sau:

  • Tăng hiệu quả sử dụng bộ nhớ đệm của GPU
  • Giảm mức tiêu tốn băng thông bộ nhớ, tiết kiệm điện năng và tăng hiệu suất
  • Tối đa hoá công suất tính toán trong các chương trình đổ bóng
  • Giảm thiểu mức sử dụng RAM của CPU trong trò chơi của bạn

Định dạng dấu phẩy động

Phần lớn các phép tính và dữ liệu trong đồ hoạ 3D hiện đại đều sử dụng số có dấu phẩy động. Vulkan trên Android sử dụng các số có dấu phẩy động với kích thước 32 hoặc 16 bit. Số có dấu phẩy động 32 bit thường được coi là có độ chính xác đơn hoặc độ chính xác đầy đủ; số có dấu phẩy động 16 bit được coi là có độ bán chính xác.

Vulkan xác định kiểu dấu phẩy động 64 bit, nhưng kiểu này thường không được các thiết bị Vulkan trên Android hỗ trợ. Do vậy, bạn không nên sử dụng kiểu này. Số có dấu phẩy động 64 bit thường được coi là có độ chính xác kép.

Định dạng số nguyên

Các số nguyên đã ký và chưa ký cũng được dùng cho dữ liệu và hoạt động tính toán. Kích thước tiêu chuẩn của số nguyên là 32 bit. Khả năng hỗ trợ các kích thước khác tính theo bit phụ thuộc vào thiết bị. Thiết bị Vulkan chạy Android thường hỗ trợ các số nguyên 16 bit và 8 bit. Vulkan xác định kiểu số nguyên 64 bit, nhưng kiểu này thường không được các thiết bị Vulkan trên Android hỗ trợ. Do vậy, bạn không nên sử dụng kiểu này.

Hành vi bán chính xác ở dưới mức tối ưu

Các kiến trúc GPU hiện đại sẽ kết hợp 2 giá trị 16 bit với nhau thành một cặp 32 bit và triển khai các hướng dẫn áp dụng cho cặp. Để có hiệu suất tối ưu, hãy tránh sử dụng các biến số thực có độ chính xác đơn 16 bit vô hướng; vectơ hoá dữ liệu thành các vectơ có 2 hoặc 4 thành phần. Trình biên dịch chương trình đổ bóng có thể sử dụng giá trị vô hướng trong các phép toán vectơ. Tuy nhiên, nếu bạn dựa vào trình biên dịch để tối ưu hoá các đại lượng vô hướng, hãy kiểm tra đầu ra của trình biên dịch để xác minh quá trình vectơ hoá.

Việc chuyển đổi sang và từ dấu phẩy động có độ chính xác 32 bit và 16 bit sẽ gây tốn chi phí tính toán. Giảm mức hao tổn bằng cách giảm thiểu các lượt chuyển đổi độ chính xác trong mã của bạn.

Sự khác biệt về hiệu suất điểm chuẩn giữa các thuật toán phiên bản 16 bit và 32 bit. Độ bán chính xác không phải lúc nào cũng giúp cải thiện hiệu suất, đặc biệt là đối với các phép tính phức tạp. Với độ bán chính xác, các thuật toán sử dụng nhiều lệnh kết hợp của phép nhân và phép cộng (FMA) đối với dữ liệu được vectơ hoá giúp cải thiện hiệu suất hiệu quả.

Hỗ trợ định dạng số

Tất cả thiết bị Vulkan trên Android đều hỗ trợ số nguyên 32 bit và số có dấu phẩy động 32 bit với độ chính xác đơn trong các phép tính của chương trình đổ bóng và dữ liệu. Chúng tôi không đảm bảo sẽ hỗ trợ các định dạng khác, và nếu có hỗ trợ thì cũng không dùng cho mọi trường hợp sử dụng.

Vulkan có 2 danh mục hỗ trợ cho các định dạng số không bắt buộc là: số học và bộ nhớ. Trước khi sử dụng một định dạng cụ thể, hãy đảm bảo thiết bị hỗ trợ định dạng đó ở cả hai danh mục.

Hỗ trợ số học

Thiết bị Vulkan phải khai báo chế độ hỗ trợ số học cho một định dạng số để có thể sử dụng định dạng đó trong các chương trình đổ bóng. Thiết bị Vulkan trên Android thường hỗ trợ các định dạng số học sau đây:

  • Số nguyên 32 bit (bắt buộc)
  • Dấu phẩy động 32 bit (bắt buộc)
  • Số nguyên 8 bit (không bắt buộc)
  • Số nguyên 16 bit (không bắt buộc)
  • Dấu phẩy động có độ bán chính xác 16 bit (không bắt buộc)

Để xác định xem thiết bị Vulkan có hỗ trợ số nguyên 16 bit cho số học hay không, truy xuất các tính năng của thiết bị bằng cách gọi hàm vkGetPhysicalDeviceFeatures2() và kiểm tra xem trường shaderInt16 trong VkPhysicalDeviceFeatures2 cấu trúc kết quả là đúng.

Để xác định xem thiết bị Vulkan có hỗ trợ số thực có độ chính xác đơn 16 bit hay số nguyên 8 bit hay không, hãy thực hiện các bước sau:

  1. Kiểm tra xem thiết bị có hỗ trợ tiện ích Vulkan VK_KHR_shader_float16_int8 hay không. Cần có tiện ích này để hỗ trợ độ chính xác đơn 16 bit và số nguyên 8 bit.
  2. Nếu VK_KHR_shader_float16_int8 được hỗ trợ, hãy thêm con trỏ cấu trúc VkPhysicalDeviceShaderFloat16Int8Features vào chuỗi VkPhysicalDeviceFeatures2.pNext.
  3. Kiểm tra các trường shaderFloat16shaderInt8 của cấu trúc kết quả VkPhysicalDeviceShaderFloat16Int8Features sau khi gọi vkGetPhysicalDeviceFeatures2(). Nếu giá trị trường là true, thì định dạng này sẽ được hỗ trợ cho giá trị số học trong chương trình đổ bóng.

Mặc dù không nhất thiết phải hỗ trợ Vulkan 1.1 hoặc Hồ sơ cơ sở Android 2022, nhưng thường thì chế độ hỗ trợ tiện ích VK_KHR_shader_float16_int8 sẽ có trên các thiết bị Android.

Hỗ trợ lưu trữ

Thiết bị Vulkan phải khai báo khả năng hỗ trợ một định dạng số không bắt buộc cho các loại hình lưu trữ cụ thể. Tiện ích VK_KHR_16bit_storage khai báo khả năng hỗ trợ các định dạng dấu phẩy động 16 bit và số nguyên 16 bit. Tiện ích xác định 4 loại hình lưu trữ. Một thiết bị có thể hỗ trợ các số 16 bit không thuộc loại nào, một số hoặc tất cả các loại hình lưu trữ.

Một số loại hình lưu trữ:

  • Đối tượng thuộc vùng đệm lưu trữ
  • Đối tượng thuộc vùng đệm đồng nhất
  • Các khối hằng số đẩy
  • Giao diện đầu vào và đầu ra của chương trình đổ bóng

Hầu hết (nhưng không phải tất cả) các thiết bị Vulkan 1.1 trên Android đều hỗ trợ các định dạng 16 bit trong các đối tượng thuộc vùng đệm lưu trữ. Không được giả định khả năng hỗ trợ dựa trên mô hình GPU. Các thiết bị sử dụng trình điều khiển cũ cho một GPU nhất định có thể không hỗ trợ các đối tượng trong vùng đệm lưu trữ, trong khi các thiết bị dùng trình điều khiển mới thì lại hỗ trợ.

Thông thường, khả năng hỗ trợ các định dạng 16 bit trong vùng đệm đồng nhất, các khối hằng số đẩy và giao diện đầu vào/đầu ra của chương trình đổ bóng sẽ tuỳ thuộc vào nhà sản xuất GPU. Trên Android, GPU thường hỗ trợ cả 3 loại này hoặc không hỗ trợ loại nào trong số này.

Một hàm ví dụ giúp kiểm thử khả năng hỗ trợ định dạng số học và định dạng lưu trữ trong 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;
  }
}

Độ chính xác của dữ liệu

Số có dấu phẩy động với độ bán chính xác có thể biểu thị một khoảng giá trị nhỏ hơn với độ chính xác thấp hơn so với số có dấu phẩy động với độ chính xác đơn. Độ bán chính xác thường là một lựa chọn đơn giản và có cảm giác là không gây tổn hao so với độ chính xác đơn. Tuy nhiên, độ bán chính xác không phải trường hợp sử dụng nào cũng áp dụng được. Đối với một số loại dữ liệu, việc giảm phạm vi và độ chính xác có thể tạo ra các cấu phần phần mềm đồ hoạ hoặc hiển thị không chính xác.

Các loại dữ liệu phù hợp để hiển thị dưới dạng dấu phẩy động có độ bán chính xác là:

  • Dữ liệu vị trí trong toạ độ không gian cục bộ
  • UV hoạ tiết cho các hoạ tiết nhỏ hơn có tính năng gói UV giới hạn có thể bị hạn chế trong phạm vi toạ độ từ -1 đến 1
  • Dữ liệu thông thường, đường tiếp tuyến và song tiếp tuyến
  • Dữ liệu màu đỉnh
  • Dữ liệu có yêu cầu thấp về độ chính xác được đặt trọng tâm vào 0.0

Các kiểu dữ liệu không được đề xuất để hiển thị trong số thực có độ bán chính xác bao gồm:

  • Dữ liệu vị trí trên toạ độ chung trên toàn cầu
  • UV hoạ tiết cho các trường hợp sử dụng có độ chính xác cao như toạ độ phần tử trên giao diện người dùng trên tờ atlas

Độ chính xác của mã trong chương trình đổ bóng

Ngôn ngữ tô bóng OpenGL (GLSL) và các ngôn ngữ lập trình trong chương trình đổ bóng của Ngôn ngữ cấp cao cho chương trình đổ bóng (HLSL) hỗ trợ thông số kỹ thuật của độ chính xác tương đối hoặc độ chính xác rõ ràng cho các kiểu số. Độ chính xác tương đối được coi là một đề xuất cho trình biên dịch chương trình đổ bóng. Độ chính xác rõ ràng là yêu cầu của độ chính xác đã chỉ định. Các thiết bị Vulkan trên Android thường sử dụng các định dạng 16 bit khi được đề xuất bởi độ chính xác tương đối. Các thiết bị Vulkan khác (đặc biệt là trên máy tính sử dụng phần cứng đồ hoạ không hỗ trợ định dạng 16 bit) có thể bỏ qua độ chính xác tương đối và vẫn sử dụng các định dạng 32 bit.

Tiện ích lưu trữ trong GLSL

Bạn phải xác định các tiện ích GLSL thích hợp để cho phép hỗ trợ các định dạng số 16 bit hoặc 8 bit trong cấu trúc lưu trữ và cấu trúc vùng đệm đồng nhất. Sau đây là nội dung khai báo tiện ích có liên quan:

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

Những tiện ích này dành riêng cho GLSL và không có tiện ích tương đương trong HLSL.

Độ chính xác tương đối trong GLSL

Sử dụng bộ hạn định highp trước một loại dấu phẩy động để đề xuất số thực có độ chính xác đơn và bộ hạn định mediump cho số thực có độ bán chính xác. Trình biên dịch GLSL cho Vulkan diễn giải bộ hạn định lowp cũ là mediump. Một số ví dụ về độ chính xác tương đối:

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

Độ chính xác rõ ràng trong GLSL

Bao gồm tiện ích GL_EXT_shader_explicit_arithmetic_types_float16 trong mã GLSL để cho phép sử dụng các kiểu dấu phẩy động 16 bit:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Khai báo các kiểu đại lượng vô hướng, vectơ và ma trận của dấu phẩy động 16 bit trong GLSL bằng các từ khoá sau:

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

Khai báo các kiểu vectơ và đại lượng vô hướng của số nguyên 16 bit trong GLSL bằng các từ khoá sau:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Độ chính xác tương đối trong HLSL

HLSL sử dụng thuật ngữ độ chính xác tối thiểu thay vì độ chính xác tương đối. Từ khoá có kiểu độ chính xác tối thiểu chỉ định độ chính xác tối thiểu, nhưng trình biên dịch có thể thay thế độ chính xác cao hơn nếu độ chính xác cao hơn đó phù hợp hơn với phần cứng mục tiêu. Số thực 16 bit có độ chính xác tối thiểu được chỉ định bằng từ khoá min16float. Số nguyên 16 bit đã ký và chưa ký có độ chính xác tối thiểu lần lượt được chỉ định bằng các từ khoá min16intmin16uint. Sau đây là một số ví dụ khác về nội dung khai báo có độ chính xác tối thiểu:

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

Độ chính xác rõ ràng trong HLSL

Dấu phẩy động có độ bán chính xác được chỉ định bằng các từ khoá half hoặc float16_t. Số nguyên 16 bit đã ký và chưa ký được chỉ định lần lượt bằng các từ khoá int16_tuint16_t. Sau đây là một số ví dụ khác về nội dung khai báo có độ chính xác rõ ràng:

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