Mit geringerer Präzision optimieren

Das numerische Format von Grafikdaten und Shader-Berechnungen kann einen erheblichen Einfluss auf die Leistung Ihres Spiels haben.

Optimale Formate bieten folgende Vorteile:

  • Effizienz der Nutzung des GPU-Cache erhöhen
  • Reduzieren Sie den Verbrauch der Arbeitsspeicherbandbreite, sparen Sie Strom und steigern die Leistung
  • Rechendurchsatz in Shader-Programmen maximieren
  • CPU-RAM-Nutzung deines Spiels minimieren

Gleitkommaformate

Für die meisten Berechnungen und Daten in modernen 3D-Grafiken werden Gleitkommazahlen verwendet. Vulkan verwendet unter Android Gleitkommazahlen mit 32 oder 16 Bit. Eine 32-Bit-Gleitkommazahl wird im Allgemeinen als einfache oder vollständige Genauigkeit bezeichnet; eine 16-Bit-Gleitkommazahl mit halber Genauigkeit.

Vulkan definiert einen 64-Bit-Gleitkommatyp, der jedoch von Vulkan-Geräten unter Android normalerweise nicht unterstützt wird und nicht empfohlen wird. Eine 64-Bit-Gleitkommazahl wird im Allgemeinen als doppelte Genauigkeit bezeichnet.

Ganzzahlformate

Vorzeichenbehaftete und vorzeichenlose Ganzzahlen werden ebenfalls für Daten und Berechnungen verwendet. Die standardmäßige Ganzzahlgröße 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. Dieser Typ wird jedoch von Vulkan-Geräten unter Android üblicherweise nicht unterstützt und die Verwendung wird nicht empfohlen.

Suboptimales Verhalten mit halber Genauigkeit

Moderne GPU-Architekturen kombinieren zwei 16-Bit-Werte zu einem 32-Bit-Paar und implementieren Anleitungen für das Paar. Vermeiden Sie für optimale Leistung die Verwendung von skalaren 16-Bit-Gleitkommazahlen-Variablen. Vektoren Sie Daten in Vektoren mit zwei oder vier Elementen. Der Shader-Compiler kann möglicherweise skalare Werte in Vektorvorgängen verwenden. Wenn Sie sich jedoch darauf verlassen, dass der Compiler Skalare optimiert, prüfen Sie die Compiler-Ausgabe, um die Vektorisierung zu verifizieren.

Die Konvertierung von und in 32-Bit- und 16-Bit-Gleitkommazahl verursacht zusätzliche Rechenkosten. Reduzieren Sie den Aufwand, indem Sie Precision-Conversions im Code minimieren.

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

Unterstützung numerischer Formate

Alle Vulkan-Geräte auf Android-Geräten unterstützen 32-Bit-Gleitkommazahlen mit einfacher Genauigkeit sowie ganzzahlige 32-Bit-Zahlen in Daten- und Shader-Berechnungen. Eine Unterstützung für andere Formate kann nicht garantiert werden. Außerdem wird, falls verfügbar, nicht für alle Anwendungsfälle garantiert.

Vulkan unterstützt zwei Kategorien für optionale numerische Formate: Arithmetik und Speicher. Bevor Sie ein bestimmtes Format verwenden, prüfen Sie, ob das Gerät es in beiden Kategorien unterstützt.

Arithmetische Unterstützung

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

  • 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 herausfinden möchten, ob ein Vulkan-Gerät 16-Bit-Ganzzahlen für die Arithmetik unterstützt, rufen Sie die Funktionen des Geräts ab. Rufen Sie dazu die Funktion vkGetPhysicalDeviceFeatures2() auf und prüfen Sie, ob das Feld shaderInt16 in der Ergebnisstruktur VkPhysicalDeviceFeatures2 wahr ist.

Mit den folgenden Schritten können Sie feststellen, 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 zur Unterstützung von 16-Bit-Gleitkommazahlen 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 VkPhysicalDeviceShaderFloat16Int8Features-Ergebnisstruktur. Wenn der Feldwert true ist, wird das Format für die Arithmetik des Shader-Programms unterstützt.

Zwar ist dies in Vulkan 1.1 oder dem Android Baseline-Profil von 2022 nicht erforderlich, aber die Erweiterung VK_KHR_shader_float16_int8 wird auf Android-Geräten sehr häufig unterstützt.

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 unterstützt 16-Bit-Ganzzahlen und 16-Bit-Gleitkommaformate. Die Erweiterung definiert vier Speichertypen. Ein Gerät kann 16-Bit-Nummern für keinen, einige oder alle Speichertypen unterstützen.

Die Speichertypen sind:

  • Speicherpufferobjekte
  • Einheitliche Pufferobjekte
  • Konstante Blöcke verschieben
  • Eingabe- und Ausgabeschnittstellen für Shader

Die meisten, jedoch nicht alle Vulkan 1.1-Geräte unter Android unterstützen 16-Bit-Formate in Zwischenspeicherobjekten. Gehen Sie nicht davon aus, dass die Unterstützung vom GPU-Modell abhängt. Geräte mit älteren Treibern für eine bestimmte GPU unterstützen möglicherweise keine Zwischenspeicherobjekte für Speicher, Geräte mit neueren Treibern hingegen schon.

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

Eine Beispielfunktion, mit der die Unterstützung von Vulkanarithmetik und Speicherformaten getestet wird:

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 einer geringeren Genauigkeit darstellen als eine Gleitkommazahl mit einfacher Genauigkeit. Halbe Genauigkeit ist oft eine einfache und wahrnehmungsverlustfreie Wahl gegenüber einfacher Genauigkeit. Eine halbe Genauigkeit ist jedoch möglicherweise nicht in allen Anwendungsfällen praktikabel. Bei einigen Datentypen kann der verringerte Bereich und die geringere Genauigkeit zu Grafikartefakten oder einem falschen Rendering führen.

Zu den Datentypen, die sich gut für die Darstellung in Gleitkommazahlen mit halber Genauigkeit eignen, gehören:

  • Positionsdaten in lokalen Raumkoordinaten
  • UV-Texturen für kleinere Texturen mit begrenztem UV-Wrapping, die auf einen Koordinatenbereich von -1,0 bis 1,0 beschränkt werden können
  • Normale, Tangens- und Bitgentendaten
  • Vertex-Farbdaten
  • Daten mit geringen Anforderungen an die Precision, die auf 0,0 zentriert sind

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

  • Daten in globalen Weltkoordinaten positionieren
  • UV-Texturen für hochpräzise Anwendungsfälle wie UI-Elementkoordinaten in einem Atlasblatt

Genauigkeit im Shader-Code

Die Programmiersprachen OpenGL Shading Language (GLSL) und High-Level Shader Language (HLSL)-Shaders unterstützen die Spezifikation von lockerer oder expliziter Genauigkeit für numerische Typen. Eine gelockerte Genauigkeit wird als Empfehlung für den Shader-Compiler behandelt. Explizite Genauigkeit ist eine Anforderung an die angegebene Genauigkeit. Vulkan-Geräte unter Android verwenden in der Regel 16-Bit-Formate, wenn dies mit lockerer Präzision vorgeschlagen wird. Bei anderen Vulkan-Geräten, insbesondere auf Desktop-Computern mit Grafikhardware, die keine 16-Bit-Formate unterstützen, ignorieren sie möglicherweise die entspannte Präzision und verwenden trotzdem 32-Bit-Formate.

Speichererweiterungen in GLSL

Die entsprechenden GLSL-Erweiterungen müssen definiert sein, damit numerische 16- oder 8-Bit-Formate im Speicher und in einheitlichen Zwischenspeicherstrukturen unterstützt werden. 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 für GLSL spezifisch und es gibt kein Äquivalent in HLSL.

Gelockerte Präzision in GLSL

Verwenden Sie den Qualifizierer highp vor einem Gleitkommatyp, um eine Gleitkommazahl mit einfacher Genauigkeit vorzuschlagen, und den Qualifizierer mediump für eine Gleitkommazahl mit halber Genauigkeit. GLSL-Compiler für Vulkan interpretieren den alten lowp-Qualifier als mediump. Hier einige Beispiele für entspannte Präzision:

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 ermöglichen:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Sie können 16-Bit-Gleitkommaskalar-, Vektor- und Matrixtypen in GLSL mit den folgenden Schlüsselwörtern deklarieren:

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

Geben Sie die 16-Bit-Ganzzahlskalar- und -Vektortypen in GLSL an. Verwenden Sie dazu die folgenden Schlüsselwörter:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Gelockerte Precision in HLSL

HLSL verwendet den Begriff minimale Precision anstelle von lockerer Precision. Ein Schlüsselwort vom Typ „Minimale Precision“ gibt die minimale Precision an. Der Compiler kann diese jedoch durch eine höhere Genauigkeit ersetzen, wenn eine höhere Genauigkeit für die Zielhardware besser ist. Durch das Schlüsselwort min16float wird eine 16-Bit-Gleitkommazahl mit minimaler Genauigkeit angegeben. Vorzeichenbehaftete und unsignierte 16-Bit-Ganzzahlen mit minimaler Genauigkeit werden durch die Schlüsselwörter min16int bzw. min16uint angegeben. Weitere Beispiele für Deklarationen mit minimaler 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. Vorzeichenbehaftete und vorzeichenlose 16-Bit-Ganzzahlen werden durch die Schlüsselwörter int16_t und uint16_t angegeben. Weitere Beispiele für explizite Genauigkeitsdeklarationen sind:

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