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
  • Speicherbandbreitenverbrauch reduzieren, um Strom zu sparen und die Leistung zu steigern
  • Maximalen Rechendurchsatz in Shaderprogrammen
  • CPU-RAM-Nutzung Ihres Spiels minimieren

Gleitkommaformate

Bei den meisten Berechnungen und Daten in modernen 3D-Grafiken werden Gleitkommazahlen verwendet. Vulkan auf Android verwendet Gleitkommazahlen mit 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‑ und 8‑Bit-Ganzzahlen. Vulkan definiert einen 64‑Bit-Ganzzahltyp. Dieser Typ wird jedoch von Vulkan-Geräten unter Android in der Regel 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 Skalarwerte möglicherweise in Vektoroperationen verwenden. Wenn Sie jedoch den Compiler zur Optimierung von Skalaren verwenden, prüfen Sie die Compilerausgabe, um die Vektorisierung zu überprüfen.

Die Umwandlung von und zu 32‑Bit- und 16‑Bit-Gleitkommazahlen ist mit Rechenkosten verbunden. Reduzieren Sie den Overhead, indem Sie die Anzahl der präzisen Conversions in Ihrem Code minimieren.

Benchmark-Leistungsunterschiede zwischen 16-Bit- und 32-Bit-Versionen Ihrer Algorithmen. Die Halbpräzision führt nicht immer zu einer Leistungssteigerung, insbesondere bei komplizierten Berechnungen. Algorithmen, die häufig FMA-Anweisungen (Fused Multiply-Add) auf vektorisierten Daten verwenden, sind gute Kandidaten für eine verbesserte Leistung bei halber Genauigkeit.

Unterstützung für numerisches Format

Alle Vulkan-Geräte unter 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 die Arithmetik 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üfe die Felder shaderFloat16 und shaderInt8 der VkPhysicalDeviceShaderFloat16Int8Features-Ergebnisstruktur nach dem Aufruf von vkGetPhysicalDeviceFeatures2(). 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 Buffers, Push-Konstantenblöcken und Shader-Eingabe-/Ausgabeschnittstellen hängt in der Regel vom GPU-Hersteller ab. Unter Android unterstützt eine GPU in der Regel entweder alle drei oder keinen dieser Typen.

Beispielfunktion, die 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 halbierter Genauigkeit 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.

Datentypen, die sich gut für die Darstellung in Gleitkommazahlen mit halber Genauigkeit eignen, sind:

  • Positionsdaten in lokalen Raumkoordinaten
  • Textur-UVs für kleinere Texturen mit begrenzter UV-Verpackung, 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, die auf 0,0 ausgerichtet sind

Für die Darstellung als Gleitkommazahl mit Halbgenauigkeit werden folgende Datentypen nicht empfohlen:

  • Standortdaten in globalen Weltkoordinaten
  • Textur-UVs für Anwendungsfälle mit hoher Präzision wie UI-Elementkoordinaten in einer Atlastabelle

Genauigkeit im Shadercode

Die Shader-Programmiersprachen OpenGL Shading Language (GLSL) und High-Level Shader Language (HLSL) unterstützen die Angabe einer entspannten oder expliziten 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 Desktop-Computer mit Grafikhardware, die keine Unterstützung für 16‑Bit-Formate bietet, ignorieren möglicherweise die reduzierte Genauigkeit und verwenden weiterhin 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.

Gelockerte Genauigkeit 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. Beispiele für eine geringere 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. Mit einem Schlüsselwort für den Typ „minimale Genauigkeit“ wird die Mindestgenauigkeit angegeben. Der Compiler kann jedoch eine höhere Genauigkeit verwenden, wenn dies für die Zielhardware besser geeignet ist. Mit dem Keyword min16float wird ein 16‑Bit-Float mit minimaler Genauigkeit angegeben. Vorzeichenbehaftete und vorzeichenlose 16‑Bit-Ganzzahlen mit minimaler Genauigkeit werden durch die Keywords 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 Keywords 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;