Mit geringerer Präzision optimieren

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

Optimale Formate bieten folgende Vorteile:

  • Effizienz der GPU-Cache-Nutzung erhöhen
  • Verbrauch der Speicherbandbreite reduzieren, um Energie zu sparen und die Leistung zu steigern
  • Maximierung des Rechen-Durchsatzes in Shader-Programmen
  • CPU-RAM-Nutzung Ihres Spiels minimieren

Gleitkommaformate

Die meisten Berechnungen und Daten in modernen 3D-Grafiken verwenden Gleitkommazahlen. Vulkan unter Android verwendet Gleitkommazahlen mit einer Größe von 32 oder 16 Bit. Eine 32‑Bit-Gleitkommazahl wird häufig als „einfache Genauigkeit“ oder „volle Genauigkeit“ bezeichnet, eine 16‑Bit-Gleitkommazahl als „halbe Genauigkeit“.

Vulkan definiert einen 64-Bit-Gleitkommatyp, der jedoch von Vulkan-Geräten unter Android nicht häufig unterstützt wird. Die Verwendung dieses Typs wird nicht empfohlen. Eine 64‑Bit-Gleitkommazahl wird in der Regel als „doppelte Genauigkeit“ bezeichnet.

Ganzzahlformate

Für Daten und Berechnungen werden auch vorzeichenbehaftete und vorzeichenlose Ganzzahlen verwendet. Die Standardgröße für Ganzzahlen beträgt 32 Bit. Die Unterstützung anderer Bitgrößen ist geräteabhängig. Vulkan-Geräte mit Android unterstützen in der Regel 16-Bit- und 8-Bit-Ganzzahlen. Vulkan definiert einen 64-Bit-Ganzzahltyp, der jedoch von Vulkan-Geräten unter Android nicht häufig unterstützt wird. Die Verwendung dieses Typs 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 für das Paar ausgeführt werden. Um eine optimale Leistung zu erzielen, sollten Sie keine skalaren 16‑Bit-Gleitkomma-Variablen verwenden, sondern Daten in Zwei- oder Vier-Element-Vektoren vektorisieren. Der Shader-Compiler kann möglicherweise Skalarwerte in Vektoroperationen verwenden. Wenn Sie sich jedoch darauf verlassen, dass der Compiler Skalare optimiert, sollten Sie die Compiler-Ausgabe prüfen, um die Vektorisierung zu bestätigen.

Die Konvertierung zwischen 32-Bit- und 16-Bit-Gleitkommazahlen ist mit einem Rechenaufwand verbunden. Reduzieren Sie den Aufwand, indem Sie die Anzahl der Präzisionskonvertierungen in Ihrem Code minimieren.

Benchmark-Leistungsunterschiede zwischen 16-Bit- und 32-Bit-Versionen Ihrer Algorithmen. Halbpräzision führt nicht immer zu einer Leistungssteigerung, insbesondere bei komplizierten Berechnungen. Algorithmen, bei denen FMA-Befehle (Fused Multiply-Add) für vektorisierte Daten intensiv verwendet werden, sind gute Kandidaten für eine verbesserte Leistung bei halber Genauigkeit.

Unterstützung numerischer Formate

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

Vulkan bietet zwei Kategorien der Unterstützung für optionale numerische Formate: Arithmetik und Speicher. Prüfen Sie vor der Verwendung eines bestimmten Formats, ob ein Gerät es in beiden Kategorien unterstützt.

Arithmetische Unterstützung

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

  • 32-Bit-Ganzzahl (erforderlich)
  • 32-Bit-Gleitkommazahl (erforderlich)
  • 8-Bit-Ganzzahl (optional)
  • 16-Bit-Ganzzahl (optional)
  • 16-Bit-Gleitkommawert mit halber Precision (optional)

Um festzustellen, ob ein Vulkan-Gerät 16-Bit-Ganzzahlen für arithmetische Operationen unterstützt, rufen Sie die Gerätefunktionen mit der Funktion vkGetPhysicalDeviceFeatures2() ab und prüfen Sie, ob das Feld shaderInt16 in der Ergebnisstruktur VkPhysicalDeviceFeatures2 „true“ ist.

So ermitteln Sie, ob ein Vulkan-Gerät 16‑Bit-Gleitkommazahlen 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-Gleitkomma- und 8‑Bit-Ganzzahlformaten erforderlich.
  2. Wenn VK_KHR_shader_float16_int8 unterstützt wird, hängen Sie einen Zeiger auf die Struktur VkPhysicalDeviceShaderFloat16Int8Features an eine VkPhysicalDeviceFeatures2.pNext-Kette an.
  3. Prüfen Sie die Felder shaderFloat16 und shaderInt8 der Ergebnisstruktur VkPhysicalDeviceShaderFloat16Int8Features nach dem Aufrufen von vkGetPhysicalDeviceFeatures2(). Wenn der Feldwert true ist, wird das Format für die Arithmetik von Shader-Programmen unterstützt.

Die Unterstützung der VK_KHR_shader_float16_int8-Erweiterung ist auf Android-Geräten sehr verbreitet, obwohl sie in Vulkan 1.1 oder im Android-Baseline-Profil von 2022 nicht erforderlich ist.

Speicherunterstützung

Ein Vulkan-Gerät muss die Unterstützung für ein optionales numerisches Format für bestimmte Speichertypen deklarieren. Die Erweiterung VK_KHR_16bit_storage deklariert die Unterstützung für 16-Bit-Ganzzahl- und 16-Bit-Gleitkommaformate. Die Erweiterung definiert vier Speichertypen. Ein Gerät kann 16-Bit-Zahlen für keinen, einige oder alle Speichertypen unterstützen.

Folgende Speichertypen sind verfügbar:

  • Speicherpufferobjekte
  • Uniform-Pufferobjekte
  • Konstantenblöcke pushen
  • Shader-Ein- und ‑Ausgabeschnittstellen

Die meisten, aber nicht alle Vulkan 1.1-Geräte unter Android unterstützen 16‑Bit-Formate in Speicherpufferobjekten. Gehen Sie nicht davon aus, dass ein bestimmtes GPU-Modell unterstützt wird. 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 Puffern, Push-Konstantenblöcken und Shader-Ein-/Ausgabe-Schnittstellen hängt im Allgemeinen vom GPU-Hersteller ab. Auf Android unterstützt eine GPU in der Regel entweder alle drei Typen oder keinen von ihnen.

Beispielfunktion, die die Unterstützung von Vulkan-Arithmetik und ‑Speicherformaten testet:

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 halber Genauigkeit kann einen kleineren Wertebereich mit geringerer Genauigkeit als eine Gleitkommazahl mit einfacher Genauigkeit darstellen. Die halbe Genauigkeit ist oft eine einfache und wahrnehmungslose Wahl gegenüber der einfachen Genauigkeit. Die halbe Genauigkeit ist jedoch möglicherweise nicht für alle Anwendungsfälle geeignet. Bei einigen Datentypen kann der reduzierte Bereich und die geringere Genauigkeit zu grafischen Artefakten oder einer falschen Darstellung führen.

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

  • Positionsdaten in lokalen Raumkoordinaten
  • Textur-UVs für kleinere Texturen mit begrenztem UV-Wrapping, die auf einen Koordinatenbereich von -1,0 bis 1,0 beschränkt werden können
  • Normalen-, Tangenten- und Bitangentendaten
  • Vertex-Farbdaten
  • Daten mit geringen Anforderungen an die Genauigkeit, die um 0,0 zentriert sind

Datentypen, die nicht für die Darstellung in Gleitkommazahlen mit halber Genauigkeit empfohlen werden:

  • Positionsdaten in globalen Weltkoordinaten
  • Textur-UVs für Anwendungsfälle mit hoher Präzision wie UI-Elementkoordinaten in einem Atlas-Sheet

Präzision im Shader-Code

Die Shader-Programmiersprachen OpenGL Shading Language (GLSL) und High-Level Shader Language (HLSL) unterstützen die Angabe von gelockerter 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 eine geringere Präzision vorgeschlagen wird. Auf anderen Vulkan-Geräten, insbesondere auf Desktopcomputern mit Grafikkarten, die keine 16‑Bit-Formate unterstützen, wird die lockere Genauigkeit möglicherweise ignoriert und es werden weiterhin 32‑Bit-Formate verwendet.

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 Uniform-Pufferstrukturen zu ermöglichen. Die relevanten Erweiterungsdeklarationen 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.

Geringere Genauigkeit in GLSL

Verwenden Sie den Qualifizierer highp vor einem Gleitkommatyp, um einen Gleitkommatyp mit einfacher Genauigkeit anzugeben, und den Qualifizierer mediump für einen Gleitkommatyp mit halber Genauigkeit. GLSL-Compiler für Vulkan interpretieren den Legacy-Qualifier lowp als mediump. Beispiele für die 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 GL_EXT_shader_explicit_arithmetic_types_float16-Erweiterung in Ihren GLSL-Code ein, um die Verwendung von 16-Bit-Gleitkommatypen zu ermöglichen:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Deklarieren Sie 16-Bit-Gleitkomma-Skalar-, ‑Vektor- und ‑Matrixtypen in GLSL mit den folgenden Keywords:

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

Geringere Genauigkeit in HLSL

In HLSL wird der Begriff minimale Genauigkeit anstelle von „gelockerte Genauigkeit“ verwendet. Ein Keyword für einen minimalen Genauigkeitstyp gibt die Mindestgenauigkeit an. Der Compiler kann jedoch eine höhere Genauigkeit verwenden, wenn dies für die Zielhardware besser ist. Ein 16-Bit-Gleitkommawert mit minimaler Genauigkeit wird durch das Keyword min16float angegeben. Die minimale Genauigkeit für vorzeichenbehaftete und vorzeichenlose 16‑Bit-Ganzzahlen wird durch die Keywords min16int bzw. 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 Schlüsselwörter int16_t bzw. uint16_t angegeben. Weitere Beispiele für explizite Präzisionsangaben:

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