Mit geringerer Präzision optimieren

Das numerische Format von Grafikdaten und Shaderberechnungen kann sich erheblich auf die Leistung Ihres Spiels auswirken.

Optimale Formate bieten folgende Vorteile:

  • Effizienz der GPU-Cache-Nutzung erhöhen
  • Reduzieren Sie den Verbrauch der Arbeitsspeicherbandbreite, sparen Sie Strom und erhöhen die Leistung
  • Maximalen Rechendurchsatz in Shaderprogrammen
  • CPU-RAM-Nutzung Ihres Spiels minimieren

Gleitkommaformate

Die meisten Berechnungen und Daten in modernen 3D-Grafiken verwenden Gleitkommazahlen. Vulkan auf Android verwendet Gleitkommazahlen mit einer Größe von 32 oder 16 Bit. Eine 32‑Bit-Gleitkommazahl wird allgemein als Gleitkommazahl mit einfacher Genauigkeit oder voller Genauigkeit bezeichnet, eine 16‑Bit-Gleitkommazahl als Gleitkommazahl mit halber Genauigkeit.

Vulkan definiert einen 64‑Bit-Gleitkommatyp. Dieser Typ wird jedoch von Vulkan-Geräten unter Android in der Regel nicht unterstützt und seine Verwendung wird nicht empfohlen. Eine 64‑Bit-Gleitkommazahl wird häufig als doppelte Genauigkeit bezeichnet.

Ganzzahlformate

Ganzzahlen mit Vorzeichen und ohne Vorzeichen werden auch für Daten und Berechnungen verwendet. Die Standardgröße für Ganzzahlen beträgt 32 Bit. Andere Bitgrößen werden je nach Gerät unterstützt. Vulkan-Geräte mit Android unterstützen in der Regel 16-Bit- und 8-Bit-Ganzzahlen. Vulkan definiert einen 64-Bit-Ganzzahltyp. Dieser Typ wird jedoch von Vulkan-Geräten unter Android im Allgemeinen nicht unterstützt und seine Verwendung wird nicht empfohlen.

Suboptimales Verhalten bei halber Genauigkeit

Moderne GPU-Architekturen kombinieren zwei 16‑Bit-Werte zu einem 32‑Bit-Paar und implementieren Anweisungen, die auf dem Paar ausgeführt werden. Verwenden Sie für eine optimale Leistung keine skalaren 16‑Bit-Float-Variablen. Vektorisieren Sie die Daten in Vektoren mit zwei oder vier Elementen. Der Shader-Compiler kann möglicherweise skalare Werte in Vektorvorgängen verwenden. Wenn Sie jedoch zum Optimieren der Skalare vom Compiler auf den Compiler angewiesen sind, prüfen Sie die Compilerausgabe, um die Vektorisierung zu verifizieren.

Die Konvertierung in und von 32-Bit- und 16-Bit-Gleitkommazahlen ist mit Rechenkosten verbunden. Reduzieren Sie den Aufwand, indem Sie die Precision-Conversions in Ihrem Code minimieren.

Benchmark-Leistungsunterschiede zwischen 16-Bit- und 32-Bit-Versionen Ihrer Algorithmen. Eine halbe Genauigkeit führt nicht immer zu einer Leistungsverbesserung, insbesondere bei komplizierten Berechnungen. Algorithmen, die FMA-Anweisungen (Fused Multiply Add) für vektorisierte Daten in großem Umfang nutzen, sind gute Kandidaten für eine verbesserte Leistung bei halber Genauigkeit.

Unterstützung für numerisches Format

Alle Vulkan-Geräte auf Android unterstützen 32-Bit-Gleitkommazahlen mit einfacher Genauigkeit und 32-Bit-Ganzzahlen bei Daten- und Shaderberechnungen. Die Unterstützung anderer Formate ist nicht garantiert und wenn verfügbar, nicht für alle Anwendungsfälle.

Vulkan unterstützt optionale numerische Formate in zwei Kategorien: Arithmetik und Speicherung. Bevor Sie ein bestimmtes Format verwenden, prüfen Sie, ob es von einem Gerät in beiden Kategorien unterstützt wird.

Arithmetische Unterstützung

Ein Vulkan-Gerät muss die arithmetische Unterstützung für ein numerisches Format deklarieren, damit es in Shaderprogrammen verwendet werden kann. Vulkan-Geräte unter Android unterstützen in der Regel die folgenden Formate für die Arithmetik:

  • 32-Bit-Ganzzahl (erforderlich)
  • 32-Bit-Gleitkomma (erforderlich)
  • 8-Bit-Ganzzahl (optional)
  • 16-Bit-Ganzzahl (optional)
  • 16-Bit-Gleitkommazahl mit halber Genauigkeit (optional)

Wenn Sie feststellen möchten, ob ein Vulkan-Gerät 16‑Bit-Ganzzahlen für arithmetische Operationen unterstützt, rufen Sie die Funktionen des Geräts ab, indem Sie die Funktion vkGetPhysicalDeviceFeatures2() aufrufen und prüfen, ob das Feld shaderInt16 in der Ergebnisstruktur VkPhysicalDeviceFeatures2 den Wert „wahr“ hat.

So ermitteln Sie, ob ein Vulkan-Gerät 16‑Bit-Floats oder 8‑Bit-Ganzzahlen unterstützt:

  1. Prüfen Sie, ob das Gerät die Vulkan-Erweiterung VK_KHR_shader_float16_int8 unterstützt. Die Erweiterung ist für die Unterstützung von 16-Bit-Float- und 8-Bit-Ganzzahlen erforderlich.
  2. Wenn VK_KHR_shader_float16_int8 unterstützt wird, hängen Sie einen Strukturzeiger vom Typ VkPhysicalDeviceShaderFloat16Int8Features an eine VkPhysicalDeviceFeatures2.pNext-Kette an.
  3. Prüfen Sie nach dem Aufrufen von vkGetPhysicalDeviceFeatures2() die Felder shaderFloat16 und shaderInt8 der Ergebnisstruktur von VkPhysicalDeviceShaderFloat16Int8Features. Wenn der Feldwert true ist, wird das Format für die Shaderprogrammarithmetik unterstützt.

Die Unterstützung der VK_KHR_shader_float16_int8-Erweiterung ist zwar keine Anforderung in Vulkan 1.1 oder dem Android-Baseline-Profil 2022, aber auf Android-Geräten sehr verbreitet.

Speicherunterstützung

Ein Vulkan-Gerät muss die Unterstützung eines optionalen numerischen Formats für bestimmte Speichertypen angeben. Die Erweiterung VK_KHR_16bit_storage deklariert die Unterstützung von 16-Bit-Ganzzahl- und 16-Bit-Gleitkommaformaten. Die Erweiterung definiert vier Speichertypen. Ein Gerät kann 16-Bit-Zahlen für keinen, einige oder alle Speichertypen unterstützen.

Die Speichertypen sind:

  • Speicherpufferobjekte
  • Einheitliche Pufferobjekte
  • Konstante Blöcke pushen
  • Shader-Eingabe- und ‑Ausgabeschnittstellen

Die meisten, aber nicht alle Vulkan 1.1-Geräte unter Android unterstützen 16‑Bit-Formate in Speicherbufferobjekten. Gehen Sie nicht davon aus, dass die Unterstützung auf dem GPU-Modell basiert. Geräte mit älteren Treibern für eine bestimmte GPU unterstützen möglicherweise keine Speicherpufferobjekte, während Geräte mit neueren Treibern dies tun.

Die Unterstützung von 16-Bit-Formaten in einheitlichen Zwischenspeichern, konstanten Push-Blöcken und Shader-Eingabe-/-Ausgabeschnittstellen hängt im Allgemeinen vom GPU-Hersteller ab. Unter Android unterstützt eine GPU in der Regel entweder alle drei oder keinen dieser Typen.

Eine Beispielfunktion, die auf die Unterstützung von Vulkan-Arithmetik und Speicherformaten prüft:

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;
  }
}

Genauigkeitsgrad für Daten

Eine Gleitkommazahl mit Halbpräzision kann einen kleineren Wertebereich mit geringerer Genauigkeit als eine Gleitkommazahl mit einfacher Genauigkeit darstellen. Die Halbpräzision ist oft eine einfache und wahrnehmungsmäßig verlustfreie Alternative zur Single-Precision. Die Halbpräzision ist jedoch nicht für alle Anwendungsfälle geeignet. Bei einigen Datentypen kann der reduzierte Bereich und die reduzierte Genauigkeit zu Grafikartefakten oder falschem Rendering führen.

Die folgenden Datentypen eignen sich gut für die Darstellung als Gleitkommazahl mit halber Genauigkeit:

  • Positionsdaten in lokalen Raumkoordinaten
  • Textur-UVs für kleinere Texturen mit begrenzter UV-Umhüllung, die auf einen Koordinatenbereich von -1,0 bis 1,0 beschränkt werden kann
  • Normal-, Tangenten- und Bitangentendaten
  • Vertex-Farbdaten
  • Daten mit geringen Anforderungen an die Genauigkeit, zentriert auf 0,0

Zu den Datentypen, die für die Darstellung in Gleitkommazahlen mit halber Genauigkeit nicht empfohlen werden, gehören:

  • Standortdaten in globalen Weltkoordinaten
  • UV-Texturen für Anwendungsfälle mit hoher Genauigkeit wie Koordinaten von UI-Elementen in einem Atlas-Blatt

Genauigkeit im Shadercode

Die Shading-Programmiersprachen OpenGL Shading Language (GLSL) und High-level Shader Language (HLSL) unterstützen die Spezifikation von lockerer oder expliziter Genauigkeit für numerische Typen. Die reduzierte Genauigkeit wird als Empfehlung für den Shader-Compiler behandelt. Die explizite Genauigkeit ist eine Anforderung der angegebenen Genauigkeit. Vulkan-Geräte unter Android verwenden in der Regel 16‑Bit-Formate, wenn dies aufgrund der reduzierten Genauigkeit vorgeschlagen wird. Andere Vulkan-Geräte, insbesondere auf Desktop-Computern mit Grafikhardware, die keine 16-Bit-Formate unterstützt, ignorieren möglicherweise die geringere Genauigkeit und verwenden trotzdem 32-Bit-Formate.

Speichererweiterungen in GLSL

Die entsprechenden GLSL-Erweiterungen müssen definiert werden, um die Unterstützung von 16‑Bit- oder 8‑Bit-Zahlenformaten in Speicher- und einheitlichen Pufferstrukturen zu ermöglichen. Die relevanten Erweiterungserklärungen sind:

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

Diese Erweiterungen sind spezifisch für GLSL und haben kein Äquivalent in HLSL.

Text "Relaxed Precision" in GLSL

Verwenden Sie den Qualifier highp vor einem Gleitkommatyp, um einen Gleitkommatyp mit einfacher Genauigkeit vorzuschlagen, und den Qualifier mediump für einen Gleitkommatyp mit doppelter Genauigkeit. GLSL-Compiler für Vulkan interpretieren den alten Qualifier lowp als mediump. Einige Beispiele für gelockerte Genauigkeit:

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

Explizite Genauigkeit in GLSL

Fügen Sie die Erweiterung GL_EXT_shader_explicit_arithmetic_types_float16 in Ihren GLSL-Code ein, um die Verwendung von 16‑Bit-Gleitkommatypen zu aktivieren:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Deklarieren Sie 16-Bit-Gleitkomma-Skalarnoten, -Vektoren und -Matrizen in GLSL mit den folgenden Schlüsselwörtern:

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

Deklarieren Sie 16‑Bit-Ganzzahl-Skalar- und -Vektortypen in GLSL mit den folgenden Schlüsselwörtern:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Gelockerte Genauigkeit in HLSL

In HLSL wird anstelle von „Relaxed Precision“ der Begriff Minimal Precision verwendet. Ein Schlüsselwort vom Typ „Minimale Genauigkeit“ gibt die minimale Genauigkeit an. Der Compiler kann jedoch eine höhere Genauigkeit ersetzen, wenn eine höhere Genauigkeit für die Zielhardware besser geeignet ist. Eine 16-Bit-Gleitkommazahl mit minimaler Genauigkeit wird durch das Schlüsselwort min16float angegeben. Vorzeichenbehaftete und vorzeichenlose 16-Bit-Ganzzahlen mit minimaler Genauigkeit werden durch die Schlüsselwörter min16int und min16uint angegeben. Weitere Beispiele für Erklärungen zur minimalen Genauigkeit:

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

Explizite Genauigkeit in HLSL

Gleitkommazahlen mit halber Genauigkeit werden durch die Schlüsselwörter half oder float16_t angegeben. Ganzzahlen mit und ohne Vorzeichen werden durch die Keywords int16_t und uint16_t angegeben. Weitere Beispiele für Deklarationen mit expliziter Genauigkeit:

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