Optymalizuj z mniejszą precyzją

Format numeryczny danych graficznych i obliczenia dotyczące cieniowania mogą mieć znaczny wpływ na wydajność gry.

Optymalne formaty zapewniają takie korzyści:

  • Zwiększanie wydajności używania pamięci podręcznej GPU
  • zmniejszenie zużycia przepustowości pamięci, co pozwala oszczędzać energię i zwiększać wydajność;
  • Maksymalizacja przepustowości obliczeniowej w programach cieniowania
  • Minimalizowanie wykorzystania pamięci RAM przez grę

Formaty liczb zmiennoprzecinkowych

Większość obliczeń i danych w nowoczesnych grafikach 3D wykorzystuje liczby zmiennoprzecinkowe. Vulkan na Androidzie używa liczb zmiennoprzecinkowych o rozmaju 32 lub 16 bitów. 32-bitową liczbę zmiennoprzecinkową określa się zwykle jako pojedynczą precyzję lub pełną precyzję; 16-bitową liczbę zmiennoprzecinkową o połowie dokładności.

Vulkan definiuje 64-bitowy typ zmiennoprzecinkowy, ale nie jest on powszechnie obsługiwany przez urządzenia Vulkan na Androidzie i nie zalecamy jego używania. 64-bitowa liczba zmiennoprzecinkowa jest powszechnie nazywana liczbą zmiennoprzecinkową podwójnej precyzji.

Formaty liczb całkowitych

Znakowane i bez znaku liczby całkowite są też używane do danych i obliczeń. Rozmiar standardowej liczby całkowitej to 32 bity. Obsługa innych rozmiarów bitów zależy od urządzenia. Urządzenia Vulkan z Androidem zwykle obsługują liczby całkowite 16- i 8-bitowe. Vulkan definiuje 64-bitowy typ całkowity, ale nie jest on powszechnie obsługiwany przez urządzenia Vulkan na Androidzie i nie zalecamy jego używania.

Nieoptymalne działanie w trybie półprecyzyjnym

Nowoczesne architektury GPU łączą 2 wartości 16-bitowe w parę 32-bitową i implementują instrukcje, które działają na tej parze. Aby uzyskać optymalną wydajność, unikaj stosowania skalarnych zmiennych typu float 16-bitowego. Zamiast tego wektoryzuj dane w postaci wektorów dwu- lub czteroelementowych. Kompilator shadera może używać wartości skalarnych w operacjach wektorowych. Jeśli jednak używasz kompilatora do optymalizowania skalarów, sprawdź dane wyjściowe kompilatora, aby zweryfikować wektoryzację.

Konwersja na 32- i 16-bitową precyzję zmiennoprzecinkową wiąże się z kosztem obliczeniowym. Zmniejsz koszty, minimalizując dokładność konwersji w kodze.

Porównaj różnice w wydajności Twoich algorytmów 16-bitowych i 32-bitowych. Półpełna dokładność nie zawsze powoduje poprawę wydajności, zwłaszcza w przypadku skomplikowanych obliczeń. Algorytmy, które intensywnie korzystają z instrukcji scalonego dodawania wielokrotnego dodawania danych (FMA) w przypadku danych wektorowych, są dobrym rozwiązaniem dla poprawy wydajności o połowę dokładności.

Obsługa formatu liczbowego

Wszystkie urządzenia Vulkan na Androidzie obsługują liczby zmiennoprzecinkowe 32-bitowe o pojedynczej precyzji i liczby całkowite 32-bitowe w obliczeniach danych i shaderów. Nie gwarantujemy obsługi innych formatów, a nawet jeśli są one obsługiwane, nie gwarantujemy obsługi wszystkich przypadków użycia.

Vulkan obsługuje 2 kategorie opcjonalnych formatów liczbowych: arytmetyczne i pamięciowe. Zanim użyjesz określonego formatu, sprawdź, czy urządzenie obsługuje go w obu kategoriach.

Obsługa funkcji arytmetycznych

Aby można było używać urządzenia z obsługą interfejsu Vulkan w programach do cieniowania, musi ono zadeklarować obsługę formatu arytmetycznego. Urządzenia Vulkan na Androidzie zwykle obsługują te formaty działań arytmetycznych:

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

Aby określić, czy urządzenie Vulkan obsługuje 16-bitowe liczby całkowite do wykonywania działań arytmetycznych, pobierz funkcje urządzenia, wywołując funkcję vkGetPhysicalDeviceFeatures2(), i sprawdź, czy pole shaderInt16 w strukturze wyników VkPhysicalDeviceFeatures2 ma wartość true.

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

  1. Sprawdź, czy urządzenie obsługuje rozszerzenie Vulkana VK_KHR_shader_float16_int8. Rozszerzenie jest wymagane do obsługi 16-bitowych liczb zmiennoprzecinkowych i 8-bitowych liczb całkowitych.
  2. Jeśli VK_KHR_shader_float16_int8 jest obsługiwane, dodaj wskaźnik struktury VkPhysicalDeviceShaderFloat16Int8Features do łańcucha VkPhysicalDeviceFeatures2.pNext.
  3. Po wywołaniu vkGetPhysicalDeviceFeatures2() sprawdź pola shaderFloat16shaderInt8 w strukturze wyników VkPhysicalDeviceShaderFloat16Int8Features. Jeśli wartość pola to true, format jest obsługiwany w przypadku działań arytmetycznych w programie cieniowania.

Chociaż nie jest to wymagane w Vulkan 1.1 ani w profilu bazowym Androida z 2022 roku, obsługa rozszerzenia VK_KHR_shader_float16_int8 jest bardzo powszechna na urządzeniach z Androidem.

Pomoc dotycząca miejsca na dane

Urządzenie Vulkan musi deklarować obsługę opcjonalnego formatu liczbowego w przypadku określonych typów pamięci. Rozszerzenie VK_KHR_16bit_storage deklaruje obsługę 16-bitowych liczb całkowitych i 16-bitowych formatów zmiennoprzecinkowych. Rozszerzenie definiuje 4 typy magazynowania. Urządzenie może obsługiwać liczby 16-bitowe w przypadku żadnego, niektórych lub wszystkich typów pamięci.

Dostępne typy pamięci masowej:

  • Obiekty bufora pamięci masowej
  • Jednolite obiekty bufora
  • Bloki stałych wartości
  • Interfejsy wejścia i wyjścia shadera

Większość urządzeń z Vulkan 1.1 z Androidem obsługuje formaty 16-bitowe w obiektach bufora pamięci masowej. Nie zakładaj, że obsługa jest dostępna na podstawie modelu GPU. Urządzenia ze starszymi sterownikami dla danego procesora GPU mogą nie obsługiwać obiektów bufora pamięci masowej, podczas gdy urządzenia z nowszymi sterownikami mogą to robić.

Obsługa formatów 16-bitowych w jednolitych buforach, wypychania bloków stałych i interfejsów wejścia/wyjścia do cieniowania zwykle zależy od producenta GPU. Na Androidzie GPU zwykle obsługuje wszystkie 3 te typy lub żaden z nich.

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

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 półprecyzyjnej reprezentacji może reprezentować mniejszy zakres wartości przy niższej dokładności niż liczba zmiennoprzecinkowa o pełnej precyzji. Precyzja połowi bitu jest często prostszym i mniej obciążającym percepcję wyborem niż precyzja pojedynczego bitu. Jednak w niektórych przypadkach większa precyzja może nie być potrzebna. W przypadku niektórych rodzajów danych zmniejszony zakres i precyzja mogą skutkować pojawieniem się artefaktów graficznych lub nieprawidłowego renderowania.

Typy danych, które nadają się do reprezentacji w postaci liczby zmiennoprzecinkowej o półprecyzji, to m.in.:

  • dane o pozycji w współrzędnych przestrzeni lokalnej,
  • UV tekstury dla mniejszych tekstur z ograniczonym owijaniem UV, które można ograniczyć do zakresu współrzędnych od -1,0 do 1,0
  • Dane o normalnej, stycznej i binormalnej
  • Dane o kolorze wierzchołka
  • 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, to m.in.:

  • dane o pozycji w globalnych współrzędnych świata;
  • UV tekstury do zastosowań wymagających dużej precyzji, takich jak współrzędne elementów interfejsu użytkownika w arkuszu atlasu

Dokładność w kodzie shadera

Języki programowania OpenGL Shading Language (GLSL) i High-level Shader Language (HLSL) obsługują specyfikację zrelaksowanej precyzji lub jawnej precyzji typów liczbowych. Zrelaksowana dokładność jest traktowana jako rekomendacja dla kompilatora shadera. Wyraźna precyzja wymaga precyzyjnej dokładności. Urządzenia z obsługą interfejsu Vulkan na Androidzie zwykle korzystają z formatów 16-bitowych, jeśli sugeruje to ich większą precyzję. Inne urządzenia Vulkan, zwłaszcza komputery stacjonarne z kartą graficzną, która nie obsługuje formatów 16-bitowych, mogą zignorować zrelaksowaną precyzję i nadal używać formatów 32-bitowych.

Rozszerzenia pamięci w GLSL

Aby umożliwić obsługę 16- i 18-bitowych formatów liczbowych w strukturach pamięci i buforów jednolitych, należy zdefiniować odpowiednie rozszerzenia GLSL. Odpowiednie deklaracje rozszerzeń to:

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

Zmniejszona dokładność w GLSL

Przed typem liczby zmiennoprzecinkowej stosuj kwalifikator highp, aby zaproponować liczbę zmiennoprzecinkową o pojedynczej precyzji, i kwalifikator mediump dla liczby zmiennoprzecinkowej o połowie dokładności. Kompilatory GLSL dla Vulkana interpretują stary ogranicznik 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żona precyzja w GLSL

Aby umożliwić używanie 16-bitowych typów zmiennoprzecinkowych, umieść w kodzie GLSL rozszerzenie GL_EXT_shader_explicit_arithmetic_types_float16:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

Zadeklaruj w GLSL te 16-bitowe typy liczb skalarnych, wektorowych i matrycowych, używając tych słów kluczowych:

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

W GLSL zadeklaruj 16-bitowe typy skalarne i wektory liczb całkowitych, używając tych kluczowych słów:

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. Słowo kluczowe typu minimalna precyzja określa minimalną dokładność, ale kompilator może zastąpić ją większą dokładnością, jeśli jest ona lepsza dla docelowego sprzętu. Słowo kluczowe min16float określa minimalną dokładność 16-bitowej liczby zmiennoprzecinkowej. Minimalna precyzja w postaci 16-bitowych liczb całkowitych z podpisem i bez znaku jest określana odpowiednio przez słowa kluczowe min16int i min16uint. Dodatkowe przykłady deklaracji minimalnej precyzji:

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

Dokładność w HLSL

Zmiennoprzecinkowa o połowie dokładności jest określana przez słowa kluczowe half lub float16_t. 16-bitowe liczby całkowite z podpisem i bez znaku są określane przez słowa kluczowe int16_t i uint16_t. Dodatkowe przykłady deklaracji dokładności:

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