Optimiser les performances avec une précision réduite

Le format numérique des données graphiques et des calculs du nuanceur peut avoir un impact important sur les performances de votre jeu.

Les formats optimaux offrent les avantages suivants :

  • Une utilisation plus efficace du cache du GPU
  • Une consommation réduite de la bande passante de la mémoire pour économiser de l'énergie et améliorer les performances
  • Un débit optimisé des calculs dans les programmes du nuanceur
  • Une utilisation réduite de la RAM par le processeur de votre jeu

Formats à virgule flottante

La majorité des calculs et des données des graphiques 3D modernes utilisent des nombres à virgule flottante. Vulkan sur Android utilise des nombres à virgule flottante de 32 ou 16 bits. Un nombre à virgule flottante de 32 bits est communément appelé "précision simple" ou "précision complète". Un nombre à virgule flottante de 16 bits est, alors, appelé "demi-précision".

Vulkan définit un type à virgule flottante de 64 bits, mais il n'est généralement pas compatible avec les appareils Vulkan sur Android, et son utilisation n'est pas recommandée. Un nombre à virgule flottante de 64 bits est communément appelé "double précision".

Formats d'entiers

Les nombres entiers signés et non signés sont également utilisés pour les données et les calculs. La taille d'un entier standard est de 32 bits. La compatibilité avec d'autres tailles (en bits) dépend de l'appareil. Les appareils Vulkan exécutant Android acceptent généralement les entiers 16 bits et 8 bits. Vulkan définit un type d'entier de 64 bits, mais il n'est généralement pas compatible avec les appareils Vulkan sur Android, et son utilisation n'est pas recommandée.

Comportement de demi-précision non optimal

Les architectures GPU modernes combinent deux valeurs de 16 bits dans une paire de 32 bits et implémentent des instructions qui fonctionnent sur la paire. Pour des performances optimales, évitez d'utiliser des variables à virgule flottante de 16 bits scalaires. Vectorisez plutôt les données en vecteurs à deux ou quatre éléments. Le compilateur de nuanceurs peut utiliser des valeurs scalaires dans les opérations vectorielles. Toutefois, si vous utilisez le compilateur pour optimiser les scalaires, inspectez la sortie du compilateur pour vérifier la vectorisation.

La conversion en virgule flottante de précision 32 bits et 16 bits entraîne des coûts de calcul. Réduisez les frais généraux en minimisant les conversions de précision dans votre code.

Analysez les différences de performances entre les versions 16 bits et 32 bits de vos algorithmes. La demi-précision n'entraîne pas toujours une amélioration des performances, en particulier pour les calculs complexes. Les algorithmes qui utilisent de manière intensive les instructions FMA (fused multiply-add) sur des données vectorisées sont de bons candidats pour améliorer les performances avec une demi-précision.

Compatibilité avec les formats numériques

Tous les appareils Vulkan sur Android prennent en charge les nombres à virgule flottante 32 bits et les nombres entiers 32 bits à précision simple dans les calculs de données et de nuanceurs. La compatibilité avec d'autres formats n'est pas garantie. En cas de compatibilité, celle-ci n'est pas garantie pour tous les cas d'utilisation.

Vulkan propose deux catégories de compatibilité pour les formats numériques facultatifs : l'arithmétique et le stockage. Avant d'utiliser un format spécifique, assurez-vous qu'un appareil est compatible avec les deux catégories.

Compatibilité arithmétique

Un appareil Vulkan doit déclarer la compatibilité arithmétique d'un format numérique pour qu'il soit utilisable dans les programmes de nuanceurs. Les appareils Vulkan sur Android acceptent généralement les formats suivants pour l'arithmétique :

  • Entier 32 bits (obligatoire)
  • Valeur à virgule flottante 32 bits (obligatoire)
  • Entier 8 bits (facultatif)
  • Entier 16 bits (facultatif)
  • Valeur à virgule flottante 16 bits à demi-précision (facultatif)

Pour déterminer si un appareil Vulkan accepte les entiers 16 bits pour l'arithmétique, récupérez les caractéristiques de l'appareil en appelant la fonction vkGetPhysicalDeviceFeatures2() et en vérifiant si le champ shaderInt16 dans la structure de résultats VkPhysicalDeviceFeatures2 est vrai.

Pour déterminer si un appareil Vulkan est compatible avec les nombres à virgule flottante 16 bits ou les entiers 8 bits, procédez comme suit :

  1. Vérifiez si l'appareil est compatible avec l'extension Vulkan VK_KHR_shader_float16_int8. L'extension est requise pour la compatibilité avec les nombres à virgule flottante 16 bits et les entiers 8 bits.
  2. Si VK_KHR_shader_float16_int8 est compatible, ajoutez un pointeur de structure VkPhysicalDeviceShaderFloat16Int8Features à une chaîne VkPhysicalDeviceFeatures2.pNext.
  3. Vérifiez les champs shaderFloat16 et shaderInt8 de la structure de résultats VkPhysicalDeviceShaderFloat16Int8Features après avoir appelé vkGetPhysicalDeviceFeatures2(). Si la valeur du champ est true, le format est compatible avec l'arithmétique du programme de nuanceurs.

Bien que cela ne soit pas obligatoire dans Vulkan 1.1 ou dans le profil de référence Android 2022, la prise en charge de l'extension VK_KHR_shader_float16_int8 est très courante sur les appareils Android.

Compatibilité avec le stockage

Un appareil Vulkan doit déclarer la prise en charge d'un format numérique facultatif pour des types de stockage spécifiques. L'extension VK_KHR_16bit_storage déclare la compatibilité avec les formats de nombres entiers 16 bits et à virgule flottante 16 bits. Quatre types de stockage sont définis par l'extension. Un appareil est compatible avec les nombres 16 bits pour aucun, certains ou tous les types de stockage.

Voici les types de stockage disponibles :

  • Objets de tampon de stockage
  • Objets de tampon uniforme
  • Blocs de constantes Push
  • Interfaces d'entrée et de sortie du nuanceur

La plupart des appareils Vulkan 1.1 sur Android acceptent les formats 16 bits dans les objets de tampon de stockage. Ne partez pas du principe que la compatibilité dépend du modèle de GPU. Les appareils équipés de pilotes plus anciens pour un GPU donné peuvent ne pas prendre en charge les objets de tampon de stockage, contrairement aux appareils équipés de pilotes plus récents.

La prise en charge des formats 16 bits dans les tampons uniformes, les blocs de constantes Push et les interfaces d'entrée et de sortie du nuanceur dépend généralement du fabricant du GPU. Sur Android, un GPU est généralement compatible avec ces trois types ou aucun d'entre eux.

Voici un exemple de fonction qui teste la compatibilité avec les formats arithmétique et de stockage 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;
  }
}

Niveau de précision des données

Un nombre à virgule flottante à demi-précision peut représenter une plage de valeurs plus petite, avec une précision inférieure, qu'un nombre à virgule flottante à précision simple. La demi-précision est souvent un choix facile, qui ne semble pas présenter de perte par rapport à la précision simple. Cependant, la demi-précision peut ne pas être pratique dans tous les cas d'utilisation. Pour certains types de données, la réduction de la portée et de la précision peut entraîner des artefacts graphiques ou un rendu incorrect.

Voici les types de données qui se prêtent bien à une représentation avec valeurs à virgule flottante à demi-précision :

  • Données de position en coordonnées spatiales locales
  • Coordonnées UV des textures pour les textures plus petites avec un encapsulage UV limité qui peut être restreint à une plage de coordonnées comprise entre -1,0 et 1,0
  • Données normales, tangentes et bitangentes
  • Données de couleur des sommets
  • Données à faible précision centrées sur 0,0

Exemples de types de données qui ne sont pas recommandés pour la représentation avec des valeurs à virgule flottante à demi-précision :

  • Données de position en coordonnées mondiales
  • Coordonnées UV de texture pour les cas d'utilisation de haute précision, comme les coordonnées des éléments d'interface utilisateur dans une feuille d'atlas

Précision dans le code du nuanceur

Les langages de programmation de nuanceurs OpenGL Shading Language (GLSL) et High-level Shader Language (HLSL) sont compatibles avec la spécification d'une précision flexible ou explicite pour les types numériques. La précision flexible est considérée comme une recommandation pour le compilateur de nuanceurs. Une précision explicite est requise par rapport à la précision spécifiée. Les appareils Vulkan sur Android utilisent généralement des formats 16 bits lorsqu'ils sont suggérés par une précision flexible. D'autres appareils Vulkan, en particulier sur les ordinateurs de bureau utilisant du matériel graphique qui n'est pas compatible avec les formats 16 bits, peuvent ignorer la précision flexible et continuer à utiliser les formats 32 bits.

Extensions de stockage en GLSL

Les extensions GLSL appropriées doivent être définies pour permettre la compatibilité avec les formats numériques 16 bits ou 8 bits dans le stockage et les structures de tampon uniformes. Les déclarations d'extension pertinentes sont les suivantes :

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

Ces extensions sont spécifiques au GLSL et n'ont pas d'équivalent en HLSL.

Précision simplifiée en GLSL

Utilisez le qualificatif highp avant un type à virgule flottante pour suggérer un float à précision simple et le qualificatif mediump pour un float à demi-précision. Les compilateurs GLSL pour Vulkan interprètent l'ancien qualificatif lowp comme mediump. Voici quelques exemples de précision flexible :

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

Précision explicite en GLSL

Incluez l'extension GL_EXT_shader_explicit_arithmetic_types_float16 dans votre code GLSL pour permettre l'utilisation de types à virgule flottante 16 bits :

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Déclarez les types scalaires, vectoriels et matriciels à virgule flottante 16 bits en GLSL à l'aide des mots clés suivants :

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

Déclarez des types scalaires et vectoriels entiers de 16 bits en GLSL à l'aide des mots clés suivants :

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Précision flexible en HLSL

Le HLSL utilise le terme précision minimale au lieu de précision flexible. Un mot clé de type "précision minimale" indique la précision minimale requise, mais le compilateur peut en substituer une plus grande s'il est préférable d'utiliser une précision plus élevée pour le matériel cible. Un float 16 bits de précision minimale est spécifié par le mot clé min16float. Les entiers 16 bits de précision minimale signés et non signés sont spécifiés respectivement par les mots clés min16int et min16uint. Voici d'autres exemples de déclarations de précision minimale :

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

Précision explicite en HLSL

La valeur à virgule flottante à demi-précision est spécifiée par les mots clés half ou float16_t. Les entiers 16 bits signés et non signés sont spécifiés respectivement par les mots clés int16_t et uint16_t. Voici d'autres exemples de déclarations de précision explicite :

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