Optymalizuj z mniejszą precyzją

Format numeryczny danych graficznych i obliczeń do cieniowania może mieć na wydajność gry.

Optymalne formaty zapewniają takie korzyści:

  • Zwiększ wydajność pamięci podręcznej GPU
  • Zmniejsz wykorzystanie przepustowości pamięci, oszczędzając energię i zwiększając wydajność
  • Maksymalizacja przepustowości obliczeniowej w programach do cieniowania
  • Minimalizuj wykorzystanie pamięci RAM przez CPU przez grę
.

Formaty zmiennoprzecinkowe

W większości obliczeń i danych we współczesnej grafice 3D wykorzystuje się funkcję zmiennoprzecinkową. liczby. Interfejs Vulkan na Androidzie używa liczb zmiennoprzecinkowych, czyli 32 lub Rozmiar 16 bitów. 32-bitowa liczba zmiennoprzecinkowa jest zwykle nazywana pojedynczą precyzję lub pełną precyzję; 16-bitowa liczba zmiennoprzecinkowa, połowa precyzję.

Interfejs Vulkan definiuje 64-bitowy typ zmiennoprzecinkowy, ale rzadko obsługiwane przez urządzenia Vulkan z Androidem i nie zalecamy korzystania z niej. A 64-bitowy liczba zmiennoprzecinkowa jest powszechnie nazywana podwójną precyzją.

Formaty liczb całkowitych

Liczby całkowite ze znakiem i nieoznaczonym są używane także do obliczeń. to standardowa liczba całkowita ma 32 bity. Obsługa innych rozmiarów bitów jest uzależniona od urządzenia. zależne. Urządzenia z obsługą interfejsu Vulkan z Androidem zwykle obsługują wersję 16- i 8-bitową. liczb całkowitych. Interfejs Vulkan definiuje 64-bitową liczbę całkowitą, ale ten typ nie jest typowy obsługiwane przez urządzenia Vulkan z Androidem i nie zalecamy korzystania z niej.

Nieoptymalne zachowanie niedokładności

Nowoczesne architektury GPU łączą dwie 16-bitowe wartości w parze 32-bitowej i wdrażać instrukcje działające na tej parze. Aby uzyskać optymalną skuteczność, unikaj: przy użyciu skalarnych 16-bitowych zmiennych zmiennoprzecinkowych; przekształcanie danych w wektoryzację na dwa lub cztery elementy wektory dystrybucyjne. Kompilator cieniowania może używać wartości skalarnych w wektorach operacji. Jeśli jednak korzystasz z kompilatora do optymalizowania skalary, sprawdź na dane wyjściowe kompilatora, aby zweryfikować wektoryzację.

Konwersja 32-bitowej i 16-bitowej dokładności zmiennoprzecinkowej ma miejsce kosztów obliczeniowych. Zmniejsz koszty, minimalizując precyzyjne konwersje w kodzie.

Porównanie różnic w wydajności między 16- i 32-bitową wersją systemu za pomocą algorytmów. Połowa precyzji nie zawsze prowadzi do poprawy wydajności, zwłaszcza w przypadku skomplikowanych obliczeń. Algorytmy, które intensywnie wykorzystują uśrednione dane instrukcji mnożenia dodawania (FMA) dla danych wektorowych dobrze nadają się do poprawiono wydajność przy o połowę precyzji.

Obsługa formatu liczbowego

Wszystkie urządzenia z interfejsem Vulkan z Androidem obsługują 32-bitową zmiennoprzecinkową pojedynczą dokładność liczb i 32-bitowych liczb całkowitych w obliczeniach danych i cieniowania. Pomoc: nie możemy zagwarantować, że inne formaty będą dostępne. do wszystkich zastosowań.

Vulkan obsługuje 2 kategorie obsługi opcjonalnych formatów liczbowych: arytmetyczne i przechowywanie danych. Zanim użyjesz określonego formatu, upewnij się, że urządzenie obsługuje go w obu tych formatach kategorii zainteresowań.

Obsługa arytmetyczna

Aby urządzenie z obsługą interfejsu Vulkan mogło korzystać z formatu liczbowego, musi zadeklarować obsługę arytmetyki których można używać w programach do cieniowania. Urządzenia z obsługą interfejsu Vulkan na Androidzie zwykle obsługują następujące formaty arytmetyki:

  • 32-bitowa liczba całkowita (obowiązkowa)
  • 32-bitowa liczba zmiennoprzecinkowa (obowiązkowa)
  • 8-bitowa liczba całkowita (opcjonalnie)
  • 16-bitowa liczba całkowita (opcjonalnie)
  • 16-bitowa zmiennoprzecinkowa o połowie precyzji (opcjonalnie)

Aby określić, czy urządzenie z Vulkan obsługuje 16-bitowe liczby całkowite dla arytmetyki, możesz pobrać funkcje urządzenia, wywołując metodę vkGetPhysicalDeviceFeatures2() i sprawdzanie, czy polu shaderInt16 w obiekcie VkPhysicalDeviceFeatures2. czy struktura wyników ma wartość prawda.

Aby określić, czy urządzenie z Vulkan obsługuje 16-bitowe liczby zmiennoprzecinkowe czy 8-bitowe liczby całkowite, wykonaj te czynności:

  1. Sprawdź, czy urządzenie obsługuje VK_KHR_shader_float16_int8 Rozszerzenie Vulkan. Rozszerzenie to wymagane do obsługi 16-bitowej liczby zmiennoprzecinkowej i 8-bitowej liczby całkowitej.
  2. Jeśli VK_KHR_shader_float16_int8 jest obsługiwany, dołącz Wskaźnik struktury VkPhysicalDeviceShaderFloat16Int8Features do łańcucha VkPhysicalDeviceFeatures2.pNext.
  3. Sprawdź pola shaderFloat16 i shaderInt8 w VkPhysicalDeviceShaderFloat16Int8Features struktura wyników po wywołaniu vkGetPhysicalDeviceFeatures2() Jeśli wartością pola jest true, format to obsługiwane przez funkcje arytmetyczne programu do cieniowania.
.

Nie jest to wymagane w przypadku interfejsu Vulkan 1.1 ani wersji z 2022 r. Profil Baseline Android, obsługa VK_KHR_shader_float16_int8 jest bardzo popularne na urządzeniach z Androidem.

Obsługa miejsca na dane

Urządzenie z obsługą interfejsu Vulkan musi zadeklarować obsługę opcjonalnego formatu liczbowego dla określonych typów pamięci masowej. Rozszerzenie VK_KHR_16bit_storage deklaruje obsługę 16-bitowych liczb całkowitych i 16-bitowych formatów zmiennoprzecinkowych. Cztery typy pamięci masowej są definiowane przez rozszerzenie. Urządzenie obsługuje liczby 16-bitowe dla braku, niektórych lub wszystkich typów pamięci.

Dostępne typy pamięci masowej:

  • Obiekty bufora pamięci masowej
  • Jednolite obiekty bufora
  • Wypychanie bloków stałych
  • Interfejsy danych wejściowych i wyjściowych shadera
.

Większość urządzeń z Vulkan 1.1 z Androidem obsługuje formaty 16-bitowe obiektów bufora pamięci masowej. Nie zakładaj obsługi na podstawie modelu GPU. Urządzenia ze starszymi sterownikami dla danego GPU mogą nie obsługiwać obiektów bufora pamięci masowej, ale urządzenia z nowszymi sterownikami już tak.

Obsługa formatów 16-bitowych w jednolitych buforach, wypychanie bloków stałych i cieniowanie interfejs wejścia/wyjścia zależy zazwyczaj od producenta GPU. Wł. na Androidzie, GPU zazwyczaj obsługuje wszystkie 3 typy tych typów albo żaden .

Przykładowa funkcja, która sprawdza obsługę arytmetyki Vulkan i formatu pamięci masowej:

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

Poziom dokładności danych

Liczba zmiennoprzecinkowa o połowie dokładności może reprezentować mniejszy zakres wartości z mniejszą precyzją niż liczba zmiennoprzecinkowa o pojedynczej precyzji. Połowa precyzji to często prosta i niezauważalna stratna zmiana tylko jedną precyzję. Jednak w niektórych przypadkach półprecyzja może być niepraktyczna. W przypadku niektórych rodzajów danych zmniejszony zakres i dokładność mogą spowodować artefaktów lub nieprawidłowego renderowania.

Typy danych, które najlepiej nadają się do reprezentowania z połową dokładności zmiennoprzecinkowy, m.in.:

  • Dane o położeniu we współrzędnych w przestrzeni lokalnej
  • promieniowanie UV tekstur w mniejszych teksturach z ograniczonym owijaniem UV, ograniczone do zakresu współrzędnych od -1,0 do 1,0
  • Dane normalne, tangens i bitangent
  • Dane koloru Vertex
  • Dane o małej dokładności wyśrodkowane na 0,0

Typy danych, które nie są zalecane do przedstawiania w formie zmiennoprzecinkowej o połowie dokładności uwzględnij:

  • Dane o pozycji na globalnych współrzędnych świata
  • Wartości UV tekstur w precyzyjnych przypadkach użycia, takich jak współrzędne elementów interfejsu arkusz z atlasem

Dokładność kodu cieniowania

Język cieniowania OpenGL (GLSL) Kod cieniowania wysokiego poziomu (HLSL) języki programowania obsługują specyfikację spokojnej precyzja lub wyraźna precyzja dla typów liczbowych. Uwzględniana jest precyzja co zaleca kompilator do cieniowania. Wyraźna precyzja to wymagania o określonej dokładności. Urządzenia z obsługą interfejsu Vulkan na Androidzie zwykle używają Formaty 16-bitowe, jeśli są sugerowane przez większą precyzję. inne urządzenia Vulkan, zwłaszcza na komputerach ze sprzętem graficznym bez obsługi dla formatów 16-bitowych może ignorować swobodną precyzję i nadal używać formatów 32-bitowych.

Rozszerzenia miejsca na dane w GLSL

Aby włączyć obsługę 16-bitowych lub 16-bitowych, należy zdefiniować odpowiednie rozszerzenia GLSL 8-bitowe formaty liczbowe w pamięci i jednolitych strukturach buforów. Odpowiedni deklaracje dotyczące rozszerzeń:

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

Te rozszerzenia są charakterystyczne dla GLSL i nie mają odpowiednika w HLSL.

Zrelaksowana precyzja w GLSL

Użyj kwalifikatora highp przed typem zmiennoprzecinkowym, aby zasugerować liczba zmiennoprzecinkowa o pojedynczej precyzji i kwalifikator mediump dla pływającej o połowie dokładności. Kompilatory GLSL dla języka Vulkan interpretują starszy kwalifikator lowp jako mediump. Oto kilka przykładów miłej precyzji:

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

Wyraźna precyzja w GLSL

Umieść rozszerzenie GL_EXT_shader_explicit_arithmetic_types_float16 w: Kod GLSL umożliwiający korzystanie z 16-bitowych typów zmiennoprzecinkowych:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Zadeklaruj w GLSL 16-bitowe typy zmiennoprzecinkowych, wektorowych i macierzy zmiennoprzecinkowych za pomocą następujące słowa kluczowe:

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

Zadeklaruj w GLSL 16-bitowe liczby całkowite i typy wektorów słowa kluczowe:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

Zmniejszona precyzja w HLSL

W HLSL zamiast niejasnej precyzji używany jest termin minimalna precyzja. Minimalna Słowo kluczowe typu precyzji określa minimalną dokładność, ale kompilator może możesz zastąpić większą precyzję, jeśli większa precyzja jest lepszym wyborem dla na określony sprzęt. Minimalna precyzja 16-bitowa liczba zmiennoprzecinkowa jest określana przez min16float słowo kluczowe. 16-bitowe liczby całkowite o minimalnej dokładności ze znakiem i bez znaku określone przez słowa kluczowe min16int i min16uint. Dodatkowe informacje Oto przykłady deklaracji minimalnej precyzji:

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

Wyraźna precyzja w HLSL

Liczbę zmiennoprzecinkową o połowie dokładności określa się za pomocą funkcji half lub float16_t słów kluczowych. 16-bitowe liczby całkowite z podpisem i bez znaku są określane przez funkcję int16_t i uint16_t. Dodatkowe przykłady jawnej precyzji zawierają następujące deklaracje:

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