Числовой формат графических данных и вычислений шейдеров может существенно повлиять на производительность вашей игры.
Оптимальные форматы выполняют следующие функции:
- Повышение эффективности использования кэша графического процессора.
- Снижение потребления пропускной способности памяти, экономия энергии, повышение производительности.
- Максимизация вычислительной мощности в шейдерных программах
- Сведите к минимуму использование оперативной памяти процессором вашей игры.
Форматы чисел с плавающей запятой
В большинстве вычислений и обработки данных в современной 3D-графике используются числа с плавающей запятой. Vulkan на Android использует числа с плавающей запятой размером 32 или 16 бит. 32-битное число с плавающей запятой обычно называют числом одинарной точности или полной точности; 16-битное число с плавающей запятой — числом половинной точности.
Vulkan определяет 64-битный тип чисел с плавающей запятой, но этот тип обычно не поддерживается устройствами Vulkan на Android, и его использование не рекомендуется. 64-битное число с плавающей запятой обычно называют числом двойной точности.
Целочисленные форматы
Для данных и вычислений также используются знаковые и беззнаковые целые числа. Стандартный размер целого числа составляет 32 бита. Поддержка других размеров битов зависит от устройства. Устройства Vulkan под управлением Android обычно поддерживают 16-битные и 8-битные целые числа. Vulkan определяет 64-битный тип целого числа, но этот тип обычно не поддерживается устройствами Vulkan на Android, и его использование не рекомендуется.
Неоптимальное поведение при половинной точности
Современные архитектуры графических процессоров объединяют два 16-битных значения в 32-битную пару и реализуют инструкции, работающие с этой парой. Для оптимальной производительности избегайте использования скалярных 16-битных переменных с плавающей запятой; векторизуйте данные в двух- или четырехэлементные векторы. Компилятор шейдеров может использовать скалярные значения в векторных операциях. Однако, если вы полагаетесь на компилятор для оптимизации скалярных значений, проверьте вывод компилятора, чтобы убедиться в векторизации.
Преобразование чисел с плавающей запятой 32-битной и 16-битной точности и обратно сопряжено с вычислительными затратами. Уменьшите накладные расходы, минимизируя преобразования точности в вашем коде.
Сравните производительность 16-битных и 32-битных версий ваших алгоритмов. Использование половинной точности не всегда приводит к повышению производительности, особенно для сложных вычислений. Алгоритмы, активно использующие инструкции умножения-сложения (FMA) для векторизованных данных, являются хорошими кандидатами на повышение производительности при половинной точности.
Поддержка числового формата
Все устройства Android с поддержкой Vulkan поддерживают 32-битные числа с плавающей запятой одинарной точности и 32-битные целые числа в вычислениях данных и шейдеров. Поддержка других форматов не гарантируется, и если она доступна, то не гарантируется для всех вариантов использования.
Vulkan поддерживает два типа необязательных числовых форматов: арифметические и форматы для хранения данных. Перед использованием определенного формата убедитесь, что устройство поддерживает его в обеих категориях.
Поддержка арифметики
Для использования в шейдерных программах устройство Vulkan должно заявлять о поддержке арифметических операций для определенного числового формата. Устройства Vulkan на Android обычно поддерживают следующие форматы арифметических операций:
- 32-битное целое число (обязательно)
- 32-битная система с плавающей запятой (обязательно)
- 8-битное целое число (необязательно)
- 16-битное целое число (необязательно)
- 16-битная плавающая точка половинной точности (опционально)
Чтобы определить, поддерживает ли устройство Vulkan 16-битные целые числа для арифметических операций, получите характеристики устройства, вызвав функцию vkGetPhysicalDeviceFeatures2() и проверив, имеет ли поле shaderInt16 в результирующей структуре VkPhysicalDeviceFeatures2 значение true.
Чтобы определить, поддерживает ли устройство Vulkan 16-битные числа с плавающей запятой или 8-битные целые числа, выполните следующие действия:
- Проверьте, поддерживает ли устройство расширение Vulkan VK_KHR_shader_float16_int8 . Это расширение необходимо для поддержки 16-битных чисел с плавающей запятой и 8-битных целых чисел.
- Если поддерживается
VK_KHR_shader_float16_int8, добавьте указатель на структуру VkPhysicalDeviceShaderFloat16Int8Features в цепочкуVkPhysicalDeviceFeatures2.pNext. - Проверьте поля
shaderFloat16иshaderInt8структуры результатаVkPhysicalDeviceShaderFloat16Int8Featuresпосле вызова функцииvkGetPhysicalDeviceFeatures2(). Если значение поля равноtrue, формат поддерживается для арифметических операций шейдерной программы.
Хотя это и не является обязательным требованием в Vulkan 1.1 или профиле Android Baseline 2022, поддержка расширения VK_KHR_shader_float16_int8 очень распространена на устройствах Android.
Поддержка хранилища
Устройство Vulkan должно объявить о поддержке необязательного числового формата для определенных типов хранения данных. Расширение VK_KHR_16bit_storage объявляет о поддержке 16-битных целых чисел и 16-битных чисел с плавающей запятой. Расширение определяет четыре типа хранения данных. Устройство может поддерживать 16-битные числа для всех, некоторых или ни для одного типа хранения данных.
Типы хранения данных:
- объекты буфера хранения
- Однородные буферные объекты
- Блоки констант для отправки
- Интерфейсы ввода и вывода шейдеров
Большинство, но не все устройства Android с поддержкой Vulkan 1.1 поддерживают 16-битные форматы в объектах буфера хранения. Не следует предполагать поддержку, основываясь на модели графического процессора. Устройства со старыми драйверами для данного графического процессора могут не поддерживать объекты буфера хранения, в то время как устройства с более новыми драйверами поддерживают.
Поддержка 16-битных форматов в буферах униформ, блоках констант и интерфейсах ввода/вывода шейдеров, как правило, зависит от производителя графического процессора. На Android графический процессор обычно либо поддерживает все три этих типа, либо ни один из них.
Пример функции, проверяющей поддержку арифметики и формата хранения данных в 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;
}
}
Уровень точности данных
Число с плавающей запятой половинной точности может представлять меньший диапазон значений с меньшей точностью, чем число с плавающей запятой одинарной точности. Половинная точность часто является простым и визуально не создающим потерь вариантом по сравнению с одинарной точностью. Однако половинная точность может быть непрактичной во всех случаях использования. Для некоторых типов данных уменьшенный диапазон и точность могут привести к графическим артефактам или некорректному отображению.
К типам данных, которые хорошо подходят для представления в формате чисел с плавающей запятой половинной точности, относятся:
- Данные о местоположении в локальных пространственных координатах
- UV-развертка текстур для небольших текстур с ограниченным диапазоном UV-обертывания, который может быть ограничен диапазоном координат от -1,0 до 1,0.
- Данные нормали, касательной и бикасательной
- данные о цвете вершины
- Данные с низкими требованиями к точности, центрированные относительно 0,0.
К типам данных, которые не рекомендуется представлять в формате чисел с плавающей запятой половинной точности, относятся:
- Данные о местоположении в глобальных мировых координатах
- UV-развертка текстур для высокоточных сценариев использования, таких как координаты элементов пользовательского интерфейса в атласе.
Точность в шейдерном коде
Языки программирования шейдеров OpenGL (GLSL) и High-level Shader Language (HLSL) поддерживают указание либо ослабленной, либо явной точности для числовых типов. Ослабленная точность рассматривается как рекомендация для компилятора шейдеров. Явная точность является обязательным требованием указанной точности. Устройства Vulkan на Android обычно используют 16-битные форматы, если это рекомендуется ослабленной точностью. Другие устройства Vulkan, особенно настольные компьютеры с графическим оборудованием, не поддерживающим 16-битные форматы, могут игнорировать ослабленную точность и по-прежнему использовать 32-битные форматы.
Расширения хранилища в GLSL
Для обеспечения поддержки 16-битных или 8-битных числовых форматов в структурах хранения и буферов униформ необходимо определить соответствующие расширения GLSL. Соответствующие объявления расширений следующие:
// 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
Эти расширения специфичны для GLSL и не имеют аналогов в HLSL.
Ослабленная точность в GLSL
Используйте квалификатор highp перед типом данных с плавающей запятой, чтобы указать тип данных с одинарной точностью, и квалификатор mediump для типа данных с половинной точностью. Компиляторы GLSL для Vulkan интерпретируют устаревший квалификатор lowp как mediump . Некоторые примеры использования пониженной точности:
mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix; // Suggest 32-bit single precision
Явная точность в GLSL
Для использования 16-битных типов данных с плавающей запятой добавьте в свой GLSL-код расширение GL_EXT_shader_explicit_arithmetic_types_float16 :
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
Объявляйте 16-битные скалярные, векторные и матричные типы с плавающей запятой в GLSL, используя следующие ключевые слова:
float16_t f16vec2 f16vec3 f16vec4
f16mat2 f16mat3 f16mat4
f16mat2x2 f16mat2x3 f16mat2x4
f16mat3x2 f16mat3x3 f16mat3x4
f16mat4x2 f16mat4x3 f16mat4x4
Объявляйте 16-битные целочисленные скалярные и векторные типы в GLSL, используя следующие ключевые слова:
int16_t i16vec2 i16vec3 i16vec4
uint16_t u16vec2 u16vec3 u16vec4
Пониженная точность в HLSL
В HLSL используется термин «минимальная точность» вместо «ослабленная точность». Ключевое слово типа «минимальная точность» указывает минимальную точность, но компилятор может заменить её на более высокую, если более высокая точность является лучшим выбором для целевого оборудования. Минимальная точность для 16-битных чисел с плавающей запятой указывается ключевым словом min16float . Минимальная точность для знаковых и беззнаковых 16-битных целых чисел указывается ключевыми словами min16int и min16uint соответственно. Дополнительные примеры объявления минимальной точности включают следующее:
// Four element vector and four-by-four matrix types
min16float4 my_vector4;
min16float4x4 my_matrix4x4;
Явная точность в HLSL
Числа с плавающей запятой половинной точности задаются ключевыми словами half или float16_t . Знаковые и беззнаковые 16-битные целые числа задаются ключевыми словами int16_t и uint16_t соответственно. Дополнительные примеры явного указания точности включают следующее:
// Four element vector and four-by-four matrix types
half4 my_vector4;
half4x4 my_matrix4x4;