Ottimizza con precisione ridotta

Il formato numerico dei dati grafici e dei calcoli degli shader può avere un impatto significativo sulle prestazioni del gioco.

I formati ottimali:

  • Aumenta l'efficienza dell'utilizzo della cache della GPU
  • Ridurre il consumo di larghezza di banda della memoria, risparmiando energia e aumentando le prestazioni
  • Massimizza il throughput di calcolo nei programmi shader
  • Riduci al minimo l'utilizzo della RAM della CPU del gioco

Formati in virgola mobile

La maggior parte dei calcoli e dei dati nella grafica 3D moderna utilizza numeri in virgola mobile. Vulkan su Android utilizza numeri con rappresentazione in virgola mobile di 32 o 16 bit. Un numero in virgola mobile a 32 bit è comunemente definito a precisione singola o precisione completa; un numero in virgola mobile a 16 bit è a precisione dimezzata.

Vulkan definisce un tipo di rappresentazione in virgola mobile a 64 bit, ma questo tipo non è comunemente supportato dai dispositivi Vulkan su Android e il suo utilizzo non è consigliato. Un numero con rappresentazione in virgola mobile a 64 bit viene comunemente definito a precisione doppia.

Formati di numeri interi

Per i dati e i calcoli vengono utilizzati anche numeri interi con e senza segno. La dimensione standard di un numero intero è 32 bit. Il supporto per altre dimensioni in bit dipende dal dispositivo. I dispositivi Vulkan con Android supportano comunemente numeri interi a 16 e 8 bit. Vulkan definisce un tipo di numero intero a 64 bit, ma questo tipo non è comunemente supportato dai dispositivi Vulkan su Android e il suo utilizzo non è consigliato.

Comportamento non ottimale della precisione dimezzata

Le moderne architetture GPU combinano due valori a 16 bit in una coppia a 32 bit e implementano istruzioni che operano sulla coppia. Per prestazioni ottimali, evita di utilizzare variabili float scalari a 16 bit; vettorizza i dati in vettori a due o quattro elementi. Il compilatore di shader potrebbe essere in grado di utilizzare valori scalari nelle operazioni vettoriali. Tuttavia, se ti affidi al compilatore per ottimizzare gli scalari, controlla l'output del compilatore per verificare la vettorizzazione.

La conversione da e verso la rappresentazione in virgola mobile con precisione a 32 bit e 16 bit ha un costo computazionale. Riduci l'overhead riducendo al minimo le conversioni di precisione nel codice.

Confronta le differenze di prestazioni tra le versioni a 16 bit e a 32 bit dei tuoi algoritmi. La precisione dimezzata non sempre comporta un miglioramento delle prestazioni, soprattutto per i calcoli complessi. Gli algoritmi che fanno un uso intensivo di istruzioni fused multiply-add (FMA) su dati vettorizzati sono buoni candidati per migliorare le prestazioni con precisione dimezzata.

Supporto del formato numerico

Tutti i dispositivi Vulkan su Android supportano numeri in virgola mobile a precisione singola a 32 bit e numeri interi a 32 bit nei calcoli di dati e shader. La disponibilità del supporto per altri formati non è garantita e, se disponibile, non è garantita per tutti i casi d'uso.

Vulkan offre due categorie di supporto per i formati numerici facoltativi: aritmetica e archiviazione. Prima di utilizzare un formato specifico, assicurati che un dispositivo lo supporti in entrambe le categorie.

Supporto per l'aritmetica

Un dispositivo Vulkan deve dichiarare il supporto aritmetico per un formato numerico affinché possa essere utilizzato nei programmi shader. I dispositivi Vulkan su Android supportano comunemente i seguenti formati per l'aritmetica:

  • Numero intero a 32 bit (obbligatorio)
  • Virgola mobile a 32 bit (obbligatorio)
  • Intero a 8 bit (facoltativo)
  • Numero intero a 16 bit (facoltativo)
  • Rappresentazione in virgola mobile a precisione dimezzata a 16 bit (facoltativo)

Per determinare se un dispositivo Vulkan supporta numeri interi a 16 bit per l'aritmetica, recupera le funzionalità del dispositivo chiamando la funzione vkGetPhysicalDeviceFeatures2() e controllando se il campo shaderInt16 nella struttura dei risultati VkPhysicalDeviceFeatures2 è true.

Per determinare se un dispositivo Vulkan supporta i float a 16 bit o gli interi a 8 bit, segui questi passaggi:

  1. Controlla se il dispositivo supporta l'estensione Vulkan VK_KHR_shader_float16_int8. L'estensione è necessaria per il supporto di numeri in virgola mobile a 16 bit e numeri interi a 8 bit.
  2. Se VK_KHR_shader_float16_int8 è supportato, aggiungi un puntatore alla struttura VkPhysicalDeviceShaderFloat16Int8Features a una catena VkPhysicalDeviceFeatures2.pNext.
  3. Controlla i campi shaderFloat16 e shaderInt8 della struttura dei risultati VkPhysicalDeviceShaderFloat16Int8Features dopo aver chiamato vkGetPhysicalDeviceFeatures2(). Se il valore del campo è true, il formato è supportato per l'aritmetica del programma shader.

Sebbene non sia un requisito di Vulkan 1.1 o del profilo di riferimento Android 2022, il supporto dell'estensione VK_KHR_shader_float16_int8 è molto comune sui dispositivi Android.

Supporto dello spazio di archiviazione

Un dispositivo Vulkan deve dichiarare il supporto di un formato numerico facoltativo per tipi di archiviazione specifici. L'estensione VK_KHR_16bit_storage dichiara il supporto per i formati di numeri interi a 16 bit e rappresentazione in virgola mobile a 16 bit. L'estensione definisce quattro tipi di spazio di archiviazione. Un dispositivo può supportare numeri a 16 bit per nessuno, alcuni o tutti i tipi di spazio di archiviazione.

I tipi di archiviazione sono:

  • Oggetti buffer di archiviazione
  • Uniform buffer object
  • Push constant blocks
  • Interfacce di input e output dello shader

La maggior parte, ma non tutti, dei dispositivi Vulkan 1.1 su Android supporta i formati a 16 bit negli oggetti buffer di archiviazione. Non dare per scontato il supporto in base al modello di GPU. I dispositivi con driver meno recenti per una determinata GPU potrebbero non supportare gli oggetti buffer di archiviazione, mentre i dispositivi con driver più recenti sì.

Il supporto dei formati a 16 bit nei buffer uniformi, nei blocchi di costanti push e nelle interfacce di input/output degli shader dipende in genere dal produttore della GPU. Su Android, una GPU in genere supporta tutti e tre questi tipi o nessuno.

Ecco un esempio di funzione che verifica il supporto dei formati di archiviazione e aritmetici di 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;
  }
}

Livello di precisione dei dati

Un numero in virgola mobile a mezza precisione può rappresentare un intervallo di valori più piccolo con una precisione inferiore rispetto a un numero in virgola mobile a precisione singola. La mezza precisione è spesso una scelta semplice e percettivamente senza perdita di dati rispetto alla precisione singola. Tuttavia, la mezza precisione potrebbe non essere pratica in tutti i casi d'uso. Per alcuni tipi di dati, l'intervallo e la precisione ridotti possono causare artefatti grafici o un rendering errato.

I tipi di dati che sono buoni candidati per la rappresentazione in virgola mobile a mezza precisione includono:

  • Dati sulla posizione nelle coordinate dello spazio locale
  • UV di texture per texture più piccole con wrapping UV limitato che possono essere vincolate a un intervallo di coordinate compreso tra -1,0 e 1,0
  • Dati normali, tangenti e bitangenti
  • Dati sul colore dei vertici
  • Dati con requisiti di precisione ridotti incentrati su 0.0

I tipi di dati che non sono consigliati per la rappresentazione in float a mezza precisione includono:

  • Dati sulla posizione nelle coordinate globali del mondo
  • Texture UV per casi d'uso ad alta precisione come le coordinate degli elementi UI in un foglio di atlas

Precisione nel codice dello shader

I linguaggi di programmazione degli shader OpenGL Shading Language (GLSL) e High-level Shader Language (HLSL) supportano la specifica di precisione ridotta o precisione esplicita per i tipi numerici. La precisione ridotta viene considerata un suggerimento per il compilatore di shader. La precisione esplicita è un requisito della precisione specificata. I dispositivi Vulkan su Android in genere utilizzano formati a 16 bit quando suggerito dalla precisione ridotta. Altri dispositivi Vulkan, soprattutto sui computer desktop che utilizzano hardware grafico privo di supporto per i formati a 16 bit, potrebbero ignorare la precisione ridotta e continuare a utilizzare i formati a 32 bit.

Estensioni di archiviazione in GLSL

Le estensioni GLSL appropriate devono essere definite per abilitare il supporto dei formati numerici a 16 o 8 bit nelle strutture di archiviazione e buffer uniformi. Le dichiarazioni di estensione pertinenti sono:

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

Queste estensioni sono specifiche di GLSL e non hanno un equivalente in HLSL.

Precisione ridotta in GLSL

Utilizza il qualificatore highp prima di un tipo in virgola mobile per suggerire un float a precisione singola e il qualificatore mediump per un float a precisione dimezzata. I compilatori GLSL per Vulkan interpretano il qualificatore legacy lowp come mediump. Ecco alcuni esempi di precisione ridotta:

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

Precisione esplicita in GLSL

Includi l'estensione GL_EXT_shader_explicit_arithmetic_types_float16 nel codice GLSL per consentire l'utilizzo di tipi in virgola mobile a 16 bit:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Dichiara i tipi scalari, vettoriali e di matrice in virgola mobile a 16 bit in GLSL utilizzando le seguenti parole chiave:

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

Dichiara i tipi scalari e vettoriali di numeri interi a 16 bit in GLSL utilizzando le seguenti parole chiave:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Precisione ridotta in HLSL

HLSL utilizza il termine precisione minima anziché precisione ridotta. Una parola chiave di tipo di precisione minima specifica la precisione minima, ma il compilatore potrebbe sostituirla con una precisione più elevata se questa è una scelta migliore per l'hardware di destinazione. Un float a 16 bit con precisione minima è specificato dalla parola chiave min16float. I numeri interi con e senza segno a 16 bit con precisione minima sono specificati rispettivamente dalle parole chiave min16int e min16uint. Ulteriori esempi di dichiarazioni di precisione minima includono:

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

Precisione esplicita in HLSL

La rappresentazione in virgola mobile a mezza precisione è specificata dalle parole chiave half o float16_t. I numeri interi con e senza segno a 16 bit sono specificati rispettivamente dalle parole chiave int16_t e uint16_t. Ecco altri esempi di dichiarazioni di precisione esplicite:

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