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:

  • Aumentare l'efficienza dell'utilizzo della cache della GPU
  • Ridurre il consumo di larghezza di banda della memoria, risparmiando energia e aumentando le prestazioni
  • Massimizzare la velocità effettiva di calcolo nei programmi shader
  • Ridurre al minimo l'utilizzo di 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 in virgola mobile di dimensioni pari a 32 o 16 bit. Un numero in virgola mobile a 32 bit è comunemente indicato come precisione singola o precisione completa; un numero in virgola mobile a 16 bit, precisione dimezzata.

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

Formati interi

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

Comportamento di precisione dimezzata non ottimale

Le architetture GPU moderne 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 scalari a virgola mobile 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, esamina l'output del compilatore per verificare la vettorizzazione.

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

Differenze di rendimento del benchmark tra le versioni a 16 bit e a 32 bit dei tuoi algoritmi. La mezza precisione non sempre comporta un miglioramento delle prestazioni, soprattutto per i calcoli complessi. Gli algoritmi che utilizzano molto le istruzioni moltiplicazione e addizione combinate (FMA) su dati vettoriali sono buoni candidati per un miglioramento delle prestazioni a metà precisione.

Supporto del formato numerico

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

Vulkan ha 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 aritmetico

Un dispositivo Vulkan deve dichiarare il supporto aritmetico per un formato numerico per poter essere utilizzato nei programmi shader. I dispositivi Vulkan su Android supportano in genere 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)
  • Virgola mobile a mezza precisione 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 numeri in virgola mobile a 16 bit o numeri interi a 8 bit, svolgi i seguenti passaggi:

  1. Controlla se il dispositivo supporta l'estensione Vulkan VK_KHR_shader_float16_int8. L'estensione è necessaria per il supporto di float 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 in Vulkan 1.1 o nel profilo di base Android del 2022, il supporto dell'estensione VK_KHR_shader_float16_int8 è molto comune sui dispositivi Android.

Supporto per l'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 in virgola mobile a 16 bit. L'estensione definisce quattro tipi di archiviazione. Un dispositivo può supportare numeri a 16 bit per nessuno, alcuni o tutti i tipi di archiviazione.

I tipi di spazio di archiviazione sono:

  • Oggetti buffer di archiviazione
  • Oggetti buffer uniformi
  • Inserisci blocchi costanti
  • Interfacce di input e output degli shader

La maggior parte 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 lo fanno.

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 di loro.

Una funzione di esempio che verifica il supporto del formato aritmetico e di archiviazione 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 per i 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 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 rendering errati.

I tipi di dati adatti alla rappresentazione in virgola mobile a mezza precisione includono:

  • Dati di posizione nelle coordinate dello spazio locale
  • UV delle texture per texture più piccole con wrapping UV limitato che può essere vincolato a un intervallo di coordinate da -1,0 a 1,0
  • Dati normali, tangenti e bitangenti
  • Dati del colore dei vertici
  • Dati con requisiti di bassa precisione centrati su 0,0

I tipi di dati non consigliati per la rappresentazione in virgola mobile a mezza precisione includono:

  • Dati di posizione in coordinate mondiali globali
  • UV delle texture per casi d'uso ad alta precisione, come le coordinate degli elementi dell'interfaccia utente in un foglio atlas

Precisione nel codice shader

I linguaggi di programmazione shader OpenGL Shading Language (GLSL) e High-Level Shader Language (HLSL) supportano la specifica di precisione rilassata o esplicita per i tipi numerici. La precisione rilassata viene trattata come 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 suggeriti dalla precisione rilassata. Altri dispositivi Vulkan, in particolare i computer desktop che utilizzano hardware grafico che non supporta i formati a 16 bit, potrebbero ignorare la precisione rilassata e utilizzare comunque i formati a 32 bit.

Estensioni di archiviazione in GLSL

Per abilitare il supporto dei formati numerici a 16 bit o 8 bit nelle strutture di archiviazione e nei buffer uniformi, devono essere definite le estensioni GLSL appropriate. Le dichiarazioni delle estensioni 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 rilassata in GLSL

Utilizza il qualificatore highp prima di un tipo di virgola mobile per suggerire un valore float a precisione singola e il qualificatore mediump per un valore float a mezza precisione. I compilatori GLSL per Vulkan interpretano il qualificatore legacy lowp come mediump. Alcuni esempi di precisione rilassata:

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 abilitare l'utilizzo di tipi di dati in virgola mobile a 16 bit:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Dichiara i tipi scalari, vettoriali e di matrici 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 rilassata in HLSL

HLSL utilizza il termine precisione minima anziché precisione rilassata. Una parola chiave di tipo precisione minima specifica la precisione minima, ma il compilatore può sostituire una precisione maggiore se è una scelta migliore per l'hardware di destinazione. Un float a 16 bit con precisione minima è specificato dalla parola chiave min16float. I numeri interi a 16 bit con segno e senza segno con precisione minima sono specificati rispettivamente dalle parole chiave min16int e min16uint. Altri 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

Il punto mobile a mezza precisione è specificato dalle parole chiave half o float16_t. Gli interi a 16 bit con segno e senza segno sono specificati rispettivamente dalle parole chiave int16_t e uint16_t. Altri esempi di dichiarazioni di precisione esplicita includono:

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