Otimizar com precisão reduzida

O formato numérico dos dados gráficos e dos cálculos de shader pode ter um impacto significativo no desempenho do jogo.

Formatos ideais fazem o seguinte:

  • Aumentam a eficiência do uso do cache da GPU
  • Reduzem o consumo de largura de banda da memória, economizam energia e aumentam o desempenho
  • Maximizam a capacidade de processamento computacional em programas de shader
  • Minimizam o uso de RAM da CPU do jogo

Formatos de ponto flutuante

A maioria dos cálculos e dados em gráficos 3D modernos usa números de ponto flutuante. O Vulkan no Android usa números de ponto flutuante com 32 ou 16 bits de tamanho. Um número de ponto flutuante de 32 bits é chamado de precisão única ou precisão total; um número de ponto flutuante de 16 bits é conhecido como meia precisão.

O Vulkan define um tipo de ponto flutuante de 64 bits, mas esse tipo não costuma ter suporte em dispositivos Vulkan no Android, e o uso dele não é recomendado. Um número de ponto flutuante de 64 bits é chamado de precisão dupla.

Formatos de números inteiros

Números inteiros assinados e não assinados também são usados para dados e cálculos. O tamanho padrão para números inteiros é de 32 bits. O suporte a outros tamanhos de bit depende do dispositivo. Os dispositivos Vulkan com o Android geralmente oferecem suporte a números inteiros de 16 e 8 bits. O Vulkan define um tipo inteiro de 64 bits, mas esse tipo não costuma ter suporte em dispositivos Vulkan no Android, e o uso dele não é recomendado.

Comportamento de meia precisão abaixo do ideal

As arquiteturas modernas de GPU combinam dois valores de 16 bits em um par de 32 bits e implementam instruções que operam no par. Para o desempenho ideal, evite usar variáveis flutuantes escalares de 16 bits. Vetorize os dados em vetores de dois ou quatro elementos. O compilador de shader pode usar valores escalares em operações vetoriais. No entanto, se você depende do compilador para otimizar os escalares, inspecione a saída do compilador para verificar a vetorização.

A conversão de e para um ponto flutuante de precisão de 32 e 16 bits tem um custo computacional. Reduza a sobrecarga minimizando as conversões de precisão no código.

Compare as diferenças de desempenho entre as versões de 16 e 32 bits dos algoritmos. A meia-precisão nem sempre resulta em uma melhoria de desempenho, especialmente para cálculos complicados. Algoritmos que fazem uso intenso de instruções de multiplicação combinada (FMA, na sigla em inglês) em dados vetorizados são bons candidatos para melhorar o desempenho usando a meia precisão.

Suporte ao formato numérico

Todos os dispositivos Vulkan no Android oferecem suporte a números de ponto flutuante de 32 bits de precisão única e números inteiros de 32 bits em cálculos de dados e de shader. Não há garantia de que o suporte para outros formatos estará disponível e, se disponível, não há garantia de que ele será oferecido para todos os casos de uso.

O Vulkan tem duas categorias de suporte para formatos numéricos opcionais: aritmético e armazenamento. Antes de usar um formato específico, verifique se um dispositivo oferece suporte às duas categorias.

Suporte aritmético

Um dispositivo Vulkan precisa declarar suporte aritmético para um formato numérico para ser utilizável em programas de shader. Os dispositivos Vulkan no Android geralmente oferecem suporte aos formatos abaixo para aritmética:

  • Número inteiro de 32 bits (obrigatório)
  • Ponto flutuante de 32 bits (obrigatório)
  • Número inteiro de 8 bits (opcional)
  • Número inteiro de 16 bits (opcional)
  • Ponto flutuante de meia precisão de 16 bits (opcional)

Para determinar se um dispositivo Vulkan oferece suporte a números inteiros de 16 bits para aritmética, recuperar os recursos do dispositivo chamando o vkGetPhysicalDeviceFeatures2() e verificando se o campo shaderInt16 no VkPhysicalDeviceFeatures2 estrutura de resultados é verdadeira.

Para determinar se um dispositivo Vulkan oferece suporte a pontos flutuantes de 16 bits ou inteiros de 8 bits, siga estas etapas:

  1. Verifique se o dispositivo oferece suporte à extensão do Vulkan VK_KHR_shader_float16_int8 (link em inglês). A extensão é obrigatória para suporte a pontos flutuantes de 16 bits e inteiros de 8 bits.
  2. Se VK_KHR_shader_float16_int8 tiver suporte, anexe um ponteiro de estrutura VkPhysicalDeviceShaderFloat16Int8Features (link em inglês) a uma cadeia de VkPhysicalDeviceFeatures2.pNext.
  3. Verifique os campos shaderFloat16 e shaderInt8 da estrutura de resultados VkPhysicalDeviceShaderFloat16Int8Features depois de chamar vkGetPhysicalDeviceFeatures2(). Se o valor do campo for true, o formato tem suporte à aritmética do programa de shader.

Embora não seja um requisito no Vulkan 1.1 ou no perfil de referência do Android de 2022, o suporte à extensão VK_KHR_shader_float16_int8 é muito comum em dispositivos Android.

Suporte de armazenamento

Um dispositivo Vulkan precisa declarar suporte a um formato numérico opcional para tipos de armazenamento específicos. A extensão VK_KHR_16bit_storage (link em inglês) declara suporte a formatos inteiros de 16 bits e de ponto flutuante de 16 bits. Quatro tipos de armazenamento são definidos pela extensão. Um dispositivo oferece suporte a números de 16 bits para nenhum, alguns ou todos os tipos de armazenamento.

Os tipos de armazenamento são:

  • Objetos do buffer de armazenamento
  • Objetos de buffer uniformes
  • Blocos de constantes de push
  • Interfaces de entrada e saída do shader

A maioria dos dispositivos Vulkan 1.1 no Android, mas não todos, oferece suporte a formatos de 16 bits em objetos de buffer de armazenamento. Não presuma o suporte com base no modelo da GPU. Dispositivos com drivers mais antigos para uma determinada GPU podem não oferecer suporte a objetos de buffer de armazenamento, enquanto os dispositivos com drivers mais recentes são.

O suporte a formatos de 16 bits em buffers uniformes, em blocos de constantes de push e em interfaces de entrada/saída do shader geralmente depende do fabricante da GPU. No Android, uma GPU geralmente oferece suporte aos três tipos desses tipos ou a nenhum deles.

Um exemplo de função que testa o suporte ao formato aritmético e de armazenamento do 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;
  }
}

Nível de precisão dos dados

Um número de ponto flutuante de meia precisão pode representar um intervalo menor de valores com uma precisão menor do que um número de ponto flutuante de precisão única. A meia-precisão é geralmente uma escolha simples e sem perdas em comparação à precisão única. No entanto, a meia-precisão pode não ser prática em todos os casos de uso. Para alguns tipos de dados, o intervalo e a precisão reduzidos podem resultar em artefatos gráficos ou renderização incorreta.

Os tipos de dados que são bons candidatos para representação em ponto flutuante de meia precisão incluem:

  • Posicionar dados em coordenadas de espaço local
  • UVs para texturas menores com quebra de UV limitada que pode ser restrita a um intervalo de coordenadas de -1,0 a 1,0
  • Dados normais, tangentes e bitangentes
  • Dados de cor do vértice
  • Dados com requisitos de baixa precisão centralizados em 0,0

Os tipos de dados que não são recomendados para representação em ponto flutuante de meia precisão incluem:

  • Posicionamento de dados em coordenadas mundiais
  • UVs de textura para casos de uso de alta precisão, como coordenadas de elementos da interface em uma planilha de atlas

Precisão no código do shader

As linguagens de programação de shader OpenGL Shading Language (GLSL) e High-level Shader Language (HLSL) oferecem suporte à especificação de precisão reduzida ou precisão explícita para tipos numéricos (links em inglês). A precisão reduzida é tratada como uma recomendação para o compilador de shader. A precisão explícita é um requisito da precisão especificada. Dispositivos Vulkan no Android geralmente usam formatos de 16 bits quando sugerido pela precisão reduzida. Outros dispositivos Vulkan, especialmente em computadores desktop que usam hardware gráfico sem suporte a formatos de 16 bits, podem ignorar a precisão reduzida e ainda usar formatos de 32 bits.

Extensões de armazenamento em GLSL

As extensões GLSL apropriadas precisam ser definidas para ativar o suporte a formatos numéricos de 16 ou 8 bits em armazenamento e estruturas uniformes de buffer. As declarações de extensão relevantes são:

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

Essas extensões são específicas à GLSL e não têm um equivalente em HLSL.

Precisão reduzida em GLSL

Use o qualificador highp antes de um tipo de ponto flutuante para sugerir um ponto flutuante de precisão única e o qualificador mediump para um ponto flutuante de meia precisão. Os compiladores GLSL para Vulkan interpretam o qualificador lowp legado como mediump. Alguns exemplos de precisão reduzida:

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

Precisão explícita em GLSL

Inclua a extensão GL_EXT_shader_explicit_arithmetic_types_float16 no código GLSL para permitir o uso de tipos de ponto flutuante de 16 bits:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Declare os tipos de matriz, vetor e escala de ponto flutuante de 16 bits em GLSL usando estas palavras-chave:

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

Declare tipos inteiros e vetoriais de 16 bits em GLSL usando estas palavras-chave:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Precisão reduzida em HLSL

A HLSL usa o termo precisão mínima em vez de precisão reduzida. Uma palavra-chave de tipo de precisão mínima especifica a precisão mínima, mas o compilador pode substituir uma precisão maior se uma precisão maior for uma escolha melhor para o hardware de destino. Um ponto flutuante de 16 bits de precisão mínima é especificado pela palavra-chave min16float. Os números inteiros de 16 bits com e sem assinatura com precisão mínima são especificados pelas palavras-chave min16int e min16uint, respectivamente. Outros exemplos de declarações de precisão mínima incluem o seguinte:

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

Precisão explícita em HLSL

O ponto flutuante de meia precisão é especificado pelas palavras-chave half ou float16_t. Os inteiros de 16 bits, assinados e não assinados, são especificados pelas palavras-chave int16_t e uint16_t, respectivamente. Outros exemplos de declarações de precisão explícitas incluem:

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