Optimización con una precisión reducida

El formato numérico de los datos de gráficos y los cálculos del sombreador pueden impactar el rendimiento de tu juego de forma significativa.

Los formatos óptimos permiten hacer lo siguiente:

  • Aumentar la eficiencia del uso de la caché de la GPU
  • Reducir el consumo de ancho de banda de la memoria, ahorrar energía y aumentar el rendimiento
  • Maximizar la capacidad de procesamiento computacional en los programas sombreadores
  • Minimiza el uso de RAM de la CPU de tu juego

Formatos de números de punto flotante

La mayoría de los cálculos y datos de los gráficos 3D modernos usan números de punto flotante. Vulkan en Android usa números de punto flotante de 32 o 16 bits para el tamaño. Por lo general, un número de punto flotante de 32 bits se conoce como "precisión simple" o "precisión completa"; un número de punto flotante de 16 bits, "precisión media".

Vulkan define un número de punto flotante de 64 bits, pero los dispositivos Vulkan no suelen admitir ese tipo, y no se recomienda su uso. Un número de punto flotante de 64 bits se conoce, por lo general como "precisión doble".

Formatos de números enteros

Los números enteros con firma y sin esta también se usan para datos y cálculos. El tamaño estándar de los números enteros es de 32 bits. La compatibilidad con otros tamaños de bits depende del dispositivo. Por lo general, los dispositivos Vulkan que ejecutan Android admiten números enteros de 16 y 8 bits. Vulkan define un número de entero de 64 bits, pero los dispositivos Vulkan no suelen admitir ese tipo, y no se recomienda su uso.

Comportamiento subóptimo de la precisión media

Las arquitecturas modernas de las GPUs combinan dos valores de 16 bits en un par de 32 bits e implementan instrucciones que operan en el par. Para obtener un rendimiento óptimo, evita usar variables de números de punto flotante escalares de 16 bits; divide los datos en vectores de dos o cuatro elementos. Es posible que el compilador de sombreadores pueda usar valores escalares en las operaciones vectoriales. Sin embargo, si dependes del compilador para optimizar los escalares, inspecciona el resultado del compilador para verificar la vectorización.

La conversión a números de punto flotante con precisión de 32 y 16 bits o desde este tipo de números tiene un costo computacional. Reduce la sobrecarga minimizando las conversiones de precisión en tu código.

Compara las diferencias de rendimiento entre las versiones de 16 bits y 32 bits de los algoritmos. La precisión media no siempre mejora el rendimiento, en especial de los cálculos complicados. Los algoritmos que recurren en exceso a las instrucciones de multiplicación y adición combinadas (FMA) en datos vectorizados son buenos candidatos para mejorar el rendimiento con la precisión media.

Compatibilidad con formatos numéricos

Todos los dispositivos Vulkan en Android admiten números de punto flotante de 32 bits con precisión simple y números enteros de 32 bits en datos y cálculos de sombreadores. No se garantiza que la compatibilidad con otros formatos esté disponible y, si está, no se garantiza para todos los casos de uso.

Vulkan tiene dos categorías de compatibilidad para formatos numéricos opcionales: aritmética y almacenamiento. Antes de usar un formato específico, asegúrate de que un dispositivo lo admita en ambas categorías.

Compatibilidad con la aritmética

Un dispositivo Vulkan debe declarar la compatibilidad con la aritmética para un formato numérico, de modo que se pueda usar en programas sombreadores. En general, los dispositivos Vulkan en Android admiten los siguientes formatos de aritmética:

  • Número entero de 32 bits (obligatorio)
  • Número de punto flotante de 32 bits (obligatorio)
  • Número entero de 8 bits (opcional)
  • Número entero de 16 bits (opcional)
  • Número de punto flotante con precisión media de 16 bits (opcional)

Para determinar si un dispositivo Vulkan admite enteros de 16 bits para la aritmética, recupera las funciones del dispositivo llamando a la función vkGetPhysicalDeviceFeatures2() y verificando si el campo shaderInt16 en la estructura de resultados VkPhysicalDeviceFeatures2 es verdadero.

Para determinar si un dispositivo Vulkan admite números enteros de 8 bits o de punto flotante de 16 bits, sigue estos pasos:

  1. Verifica si el dispositivo es compatible con la extensión VK_KHR_shader_float16_int8 de Vulkan. La extensión es necesaria para la compatibilidad con números enteros de 8 bits y de punto flotante de 16 bits.
  2. Si se admite VK_KHR_shader_float16_int8, agrega un puntero de estructura VkPhysicalDeviceShaderFloat16Int8Features a una cadena VkPhysicalDeviceFeatures2.pNext.
  3. Verifica los campos shaderFloat16 y shaderInt8 de la estructura de resultados VkPhysicalDeviceShaderFloat16Int8Features después de llamar a vkGetPhysicalDeviceFeatures2(). Si el valor del campo es true, el formato es compatible con la aritmética del programa sombreador.

Si bien no es un requisito en Vulkan 1.1 ni en el perfil de Baseline de Android de 2022, la compatibilidad con la extensión VK_KHR_shader_float16_int8 es muy frecuente en dispositivos Android.

Compatibilidad con el almacenamiento

Un dispositivo Vulkan debe declarar la compatibilidad con un formato numérico opcional para tipos de almacenamiento específicos. La extensión VK_KHR_16bit_storage declara la compatibilidad con los formatos de número entero de 16 bits y de punto flotante de 16 bits. La extensión define cuatro tipos de almacenamiento. Un dispositivo puede admitir números de 16 bits para todos los tipos de almacenamiento, algunos o ninguno.

Los tipos de almacenamiento son los siguientes:

  • Objetos de los búferes de almacenamiento
  • Objetos de los búferes uniformes
  • Bloques de constante push
  • Interfaces de entrada y salida del sombreador

La mayoría de los dispositivos Vulkan 1.1 en Android admiten formatos de 16 bits en los objetos de los búferes de almacenamiento, pero no todos. No supongas que la compatibilidad se basa en el modelo de la GPU. Es posible que los dispositivos con controladores más antiguos para una GPU determinada no admitan objetos de los búferes de almacenamiento, mientras que los dispositivos con controladores más nuevos sí lo hacen.

Por lo general, la compatibilidad con los formatos de 16 bits en búferes uniformes, interfaces de entrada y salida del sombreador y bloques de constantes push depende del fabricante de la GPU. En Android, una GPU suele admitir estos tres tipos o ninguno.

Esta es una función de ejemplo que prueba la compatibilidad con el formato de aritmética y de almacenamiento de 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;
  }
}

Nivel de precisión de los datos

Un número de punto flotante con precisión media puede representar un rango de valores más pequeño con una precisión menor que un número de punto flotante con precisión simple. Con frecuencia, la precisión media es una elección simple y sin pérdidas en término de percepción en comparación con la precisión simple. Sin embargo, es posible que la precisión media no sea práctica en todos los casos de uso. En el caso de algunos tipos de datos, el rango reducido y la precisión pueden generar artefactos gráficos o una renderización incorrecta.

Entre los tipos de datos que son buenos candidatos para la representación en números de punto flotante con precisión media, se incluyen los siguientes:

  • Datos de posición en coordenadas de espacios locales
  • UV para texturas más pequeñas con unión limitada de UV que puede restringirse a un rango de coordenadas de -1.0 a 1.0.
  • Datos normales, tangentes y bitangentes
  • Datos del color de los vértices
  • Datos con requisitos de baja precisión centrados en 0.0

Entre los tipos de datos que no se recomiendan para la representación en números de punto flotante con precisión media, se incluyen los siguientes:

  • Datos de posición en coordenadas globales
  • UV de textura para casos de uso de alta precisión, como coordenadas de elementos de la IU en una hoja de atlas

Precisión en el código del sombreador

Los lenguajes de programación de sombreadores OpenGL Shading Language (GLSL) y High-level Shader Language (HLSL) admiten la especificación de precisión flexible o explícita para tipos numéricos. La precisión relajada se considera una recomendación para el compilador de sombreadores. La precisión explícita es un requisito de la precisión especificada. Por lo general, los dispositivos Vulkan en Android usan formatos de 16 bits cuando los sugiere la precisión relajada. Es posible que otros dispositivos con Vulkan, en especial en computadoras de escritorio con hardware de gráficos que no admiten formatos de 16 bits, ignoren la precisión relajada y todavía utilicen formatos de 32 bits.

Extensiones de almacenamiento en GLSL

Se deben definir las extensiones GLSL adecuadas para permitir la compatibilidad con formatos numéricos de 8 bits o 16 bits en almacenamiento y estructuras de búferes uniformes. Las declaraciones de extensión relevantes son las siguientes:

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

Estas extensiones son específicas para GLSL y no tienen un equivalente en HLSL.

Precisión relajada en GLSL

Usa el calificador highp antes de un tipo de número de punto flotante para sugerir un número de punto flotante con precisión simple y el calificador mediump para un número de punto flotante con precisión media. Los compiladores GLSL para Vulkan interpretan el calificador lowp heredado como mediump. Estos son algunos ejemplos de precisión relajada:

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

Precisión explícita en GLSL

Incluye la extensión GL_EXT_shader_explicit_arithmetic_types_float16 en tu código GLSL para habilitar el uso de tipos de número de punto flotante de 16 bits:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Declara los tipos escalares, vectoriales y de matrices de los números punto flotante de 16 bits en GLSL con las siguientes palabras clave:

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

Declara los tipos escalares y vectoriales de los números enteros de 16 bits en GLSL con las siguientes palabras clave:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Precisión relajada en HLSL

HLSL usa el término precisión mínima en lugar de precisión relajada. Una palabra clave para la precisión mínima especifica este tipo de precisión, pero el compilador puede sustituir una precisión más alta si esta es una mejor opción para el hardware de destino. La palabra clave min16float especifica un número de punto flotante con precisión mínima de 16 bits. Las palabras clave min16int y min16uint especifican los números enteros con precisión mínima de 16 bits con firma y sin esta. Los ejemplos adicionales de declaraciones de precisión mínima incluyen lo siguiente:

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

Precisión explícita en HLSL

El número de punto flotante con precisión media se especifica con las palabras clave half o float16_t. Los números enteros de 16 bits con firma y sin esta se especifican con las palabras clave int16_t y uint16_t, respectivamente. Entre los ejemplos adicionales de declaraciones de precisión explícitas, se incluyen los siguientes:

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