Format liczbowy danych graficznych i obliczeń cieniowania może mieć znaczący wpływ na wydajność gry.
Optymalne formaty:
- Zwiększanie wydajności wykorzystania 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 cieniujących
- Zminimalizuj wykorzystanie pamięci RAM procesora przez grę
Formaty zmiennoprzecinkowe
Większość obliczeń i danych w nowoczesnej grafice 3D wykorzystuje liczby zmiennoprzecinkowe. Vulkan na Androidzie używa liczb zmiennoprzecinkowych o rozmiarze 32 lub 16 bitów. 32-bitowa liczba zmiennoprzecinkowa jest zwykle określana jako liczba o pojedynczej lub pełnej precyzji, a 16-bitowa liczba zmiennoprzecinkowa jako liczba o połowicznej precyzji.
Vulkan definiuje 64-bitowy typ zmiennoprzecinkowy, ale nie jest on powszechnie obsługiwany przez urządzenia z Vulkanem na Androidzie i nie zaleca się jego używania. 64-bitowa liczba zmiennoprzecinkowa jest często określana jako liczba podwójnej precyzji.
Formaty liczb całkowitych
Liczby całkowite ze znakiem i bez znaku są też używane do danych i obliczeń. Standardowy rozmiar 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ą 16- i 8-bitowe liczby całkowite. Vulkan definiuje 64-bitowy typ liczby całkowitej, ale nie jest on powszechnie obsługiwany przez urządzenia z Vulkanem na Androidzie i nie zaleca się jego używania.
Nieoptymalne działanie w przypadku połowy precyzji
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 używania skalarnych 16-bitowych zmiennych zmiennoprzecinkowych. Wektoryzuj dane do wektorów 2- lub 4-elementowych. Kompilator cieniowania może używać wartości skalarnych w operacjach wektorowych. Jeśli jednak polegasz na kompilatorze w celu optymalizacji skalarów, sprawdź dane wyjściowe kompilatora, aby zweryfikować wektoryzację.
Konwersja między 32-bitową a 16-bitową reprezentacją zmiennoprzecinkową wiąże się z kosztami obliczeniowymi. Zmniejsz koszty ogólne, minimalizując konwersje precyzji w kodzie.
Porównaj różnice w wydajności między 16- i 32-bitowymi wersjami algorytmów. Połowa precyzji nie zawsze skutkuje poprawą wydajności, zwłaszcza w przypadku skomplikowanych obliczeń. Algorytmy, które intensywnie wykorzystują instrukcje FMA (fused multiply-add) na danych wektoryzowanych, są dobrymi kandydatami do poprawy wydajności przy połowie precyzji.
Obsługa formatu liczbowego
Wszystkie urządzenia Vulkan na Androidzie obsługują 32-bitowe liczby zmiennoprzecinkowe o pojedynczej precyzji i 32-bitowe liczby całkowite w obliczeniach danych i programów do cieniowania. Nie gwarantujemy obsługi innych formatów, a jeśli są one obsługiwane, nie gwarantujemy, że będą działać we wszystkich przypadkach użycia.
Vulkan obsługuje 2 kategorie opcjonalnych formatów liczbowych: arytmetyczne i pamięci masowej. Zanim użyjesz konkretnego formatu, upewnij się, że urządzenie obsługuje go w obu kategoriach.
Obsługa arytmetyki
Urządzenie Vulkan musi deklarować obsługę arytmetyczną formatu liczbowego, aby można było go używać w programach cieniujących. Urządzenia Vulkan na Androidzie zwykle obsługują te formaty arytmetyczne:
- 32-bitowa liczba całkowita (wymagane)
- 32-bitowa reprezentacja zmiennoprzecinkowa (obowiązkowa)
- 8-bitowa liczba całkowita (opcjonalnie)
- 16-bitowa liczba całkowita (opcjonalnie)
- 16-bitowa reprezentacja zmiennoprzecinkowa o połowicznej precyzji (opcjonalnie)
Aby określić, czy urządzenie Vulkan obsługuje 16-bitowe liczby całkowite do obliczeń arytmetycznych, pobierz funkcje urządzenia, wywołując funkcję vkGetPhysicalDeviceFeatures2() i sprawdzając, czy pole shaderInt16 w strukturze wyniku 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:
- Sprawdź, czy urządzenie obsługuje rozszerzenie Vulkan VK_KHR_shader_float16_int8. Rozszerzenie jest wymagane do obsługi 16-bitowych liczb zmiennoprzecinkowych i 8-bitowych liczb całkowitych.
- Jeśli
VK_KHR_shader_float16_int8jest obsługiwane, dołącz wskaźnik struktury VkPhysicalDeviceShaderFloat16Int8Features do łańcuchaVkPhysicalDeviceFeatures2.pNext. - Po wywołaniu
vkGetPhysicalDeviceFeatures2()sprawdź polashaderFloat16ishaderInt8w strukturze wynikuVkPhysicalDeviceShaderFloat16Int8Features. Jeśli wartość pola totrue, format jest obsługiwany w arytmetyce programu cieniującego.
Chociaż nie jest to wymagane w Vulkanie 1.1 ani w profilu podstawowym Androida z 2022 r., obsługa rozszerzenia VK_KHR_shader_float16_int8 jest bardzo powszechna na urządzeniach z Androidem.
Obsługa pamięci masowej
Urządzenie Vulkan musi deklarować obsługę opcjonalnego formatu liczbowego dla określonych typów pamięci masowej. Rozszerzenie VK_KHR_16bit_storage deklaruje obsługę 16-bitowych formatów liczb całkowitych i 16-bitowych reprezentacji zmiennoprzecinkowych. Rozszerzenie definiuje 4 typy pamięci masowej. Urządzenie może obsługiwać 16-bitowe liczby w przypadku żadnego, niektórych lub wszystkich typów pamięci.
Typy pamięci masowej:
- Obiekty bufora pamięci masowej
- Obiekty bufora jednolitego
- Bloki stałych push
- Interfejsy wejściowe i wyjściowe cieniowania
Większość, ale nie wszystkie urządzenia z Vulkanem 1.1 na Androidzie obsługują 16-bitowe formaty w obiektach bufora pamięci. Nie zakładaj, że obsługa jest dostępna, na podstawie modelu GPU. Urządzenia ze starszymi sterownikami do danego GPU mogą nie obsługiwać obiektów bufora pamięci, podczas gdy urządzenia z nowszymi sterownikami obsługują je.
Obsługa formatów 16-bitowych w buforach jednolitych, blokach stałych push i interfejsach wejścia/wyjścia cieniowania zależy ogólnie od producenta GPU. Na Androidzie GPU zwykle obsługuje wszystkie 3 typy lub żaden z nich.
Przykładowa funkcja, która sprawdza obsługę arytmetyki i formatu pamięci masowej 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 połowicznej precyzji może reprezentować mniejszy zakres wartości z mniejszą precyzją niż liczba zmiennoprzecinkowa o pojedynczej precyzji. Połowa precyzji jest często prostym i postrzeganym jako bezstratny wyborem w porównaniu z pojedynczą precyzją. Jednak połowa precyzji może nie być praktyczna we wszystkich przypadkach użycia. W przypadku niektórych typów danych zmniejszony zakres i precyzja mogą powodować artefakty graficzne lub nieprawidłowe renderowanie.
Typy danych, które dobrze nadają się do reprezentacji w postaci liczb zmiennoprzecinkowych o połowicznej precyzji, to m.in.:
- Dane o pozycji we współrzędnych przestrzeni lokalnej
- Współrzędne UV mniejszych tekstur z ograniczonym zawijaniem UV, które można ograniczyć do zakresu współrzędnych od -1,0 do 1,0.
- Dane dotyczące wektorów normalnych, stycznych i bistycznych
- Dane o kolorze wierzchołka
- Dane o niskich wymaganiach dotyczących precyzji, skupione wokół wartości 0,0
Typy danych, których nie zaleca się reprezentować w formacie zmiennoprzecinkowym o połowicznej precyzji, to m.in.:
- Dane o pozycji we współrzędnych globalnych
- Współrzędne UV tekstury w przypadku zastosowań wymagających dużej precyzji, takich jak współrzędne elementów interfejsu w arkuszu atlasu
Precyzja w kodzie cieniowania
Języki programowania cieniowania OpenGL Shading Language (GLSL) i High-level Shader Language (HLSL) obsługują określanie mniejszej lub jawnej precyzji dla typów liczbowych. Zmniejszona precyzja jest traktowana jako zalecenie dla kompilatora cieniowania. Jawna precyzja jest wymagana w przypadku określonej precyzji. Urządzenia Vulkan na Androidzie zwykle używają formatów 16-bitowych, gdy sugeruje to mniejsza precyzja. Inne urządzenia Vulkan, zwłaszcza komputery stacjonarne ze sprzętem graficznym, który nie obsługuje formatów 16-bitowych, mogą ignorować mniejszą precyzję i nadal używać formatów 32-bitowych.
Rozszerzenia pamięci w GLSL
Aby włączyć obsługę 16-bitowych lub 8-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ą specyficzne dla GLSL i nie mają odpowiednika w HLSL.
Zmniejszona precyzja w GLSL
Użyj kwalifikatora highp przed typem zmiennoprzecinkowym, aby zasugerować liczbę zmiennoprzecinkową o pojedynczej precyzji, i kwalifikatora mediump, aby zasugerować liczbę zmiennoprzecinkową o połowicznej precyzji.
Kompilatory GLSL dla Vulkana interpretują starszy kwalifikator lowp jako mediump.
Przykłady mniejszej precyzji:
mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix; // Suggest 32-bit single precision
Jawna precyzja w GLSL
Aby umożliwić używanie 16-bitowych typów zmiennoprzecinkowych, dodaj do kodu GLSL rozszerzenie GL_EXT_shader_explicit_arithmetic_types_float16:
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
Zadeklaruj 16-bitowe zmiennoprzecinkowe typy skalarne, wektorowe i macierzowe w GLSL, używając tych słów kluczowych:
float16_t f16vec2 f16vec3 f16vec4
f16mat2 f16mat3 f16mat4
f16mat2x2 f16mat2x3 f16mat2x4
f16mat3x2 f16mat3x3 f16mat3x4
f16mat4x2 f16mat4x3 f16mat4x4
Zadeklaruj 16-bitowe typy skalarne i wektorowe liczb całkowitych w GLSL, używając tych słów kluczowych:
int16_t i16vec2 i16vec3 i16vec4
uint16_t u16vec2 u16vec3 u16vec4
Zmniejszona precyzja w HLSL
W HLSL zamiast terminu „zmniejszona precyzja” używa się terminu minimalna precyzja. Słowo kluczowe typu minimalnej precyzji określa minimalną precyzję, ale kompilator może zastąpić ją wyższą, jeśli jest ona lepszym wyborem dla sprzętu docelowego. 16-bitowa liczba zmiennoprzecinkowa o minimalnej precyzji jest określana przez słowo kluczowe min16float. Minimalna precyzja 16-bitowych liczb całkowitych ze znakiem i bez znaku jest określana odpowiednio przez słowa kluczowe min16int i min16uint. Inne przykłady deklaracji o minimalnej precyzji:
// Four element vector and four-by-four matrix types
min16float4 my_vector4;
min16float4x4 my_matrix4x4;
Jawna precyzja w HLSL
Reprezentacja zmiennoprzecinkowa o połowicznej precyzji jest określana za pomocą słów kluczowych half lub float16_t. 16-bitowe liczby całkowite ze znakiem i bez znaku są określane odpowiednio przez słowa kluczowe int16_t i uint16_t. Oto dodatkowe przykłady deklaracji jawnej precyzji:
// Four element vector and four-by-four matrix types
half4 my_vector4;
half4x4 my_matrix4x4;