Числовой формат графических данных и шейдерных вычислений может оказать существенное влияние на производительность вашей игры.
Оптимальные форматы выполняют следующие функции:
- Повышение эффективности использования кэша графического процессора
- Снижение потребления полосы пропускания памяти, экономия энергии и повышение производительности
- Увеличьте производительность вычислений в шейдерных программах
- Минимизируйте использование оперативной памяти процессора вашей игрой
Форматы с плавающей точкой
Большинство вычислений и данных в современной 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) на векторизованных данных, являются хорошими кандидатами на улучшение производительности при половинной точности.
Поддержка числовых форматов
Все устройства Vulkan на Android поддерживают 32-битные числа с плавающей точкой одинарной точности и 32-битные целые числа в вычислениях данных и шейдеров. Поддержка других форматов не гарантируется, а если и доступна, то не гарантируется для всех вариантов использования.
Vulkan имеет две категории поддержки дополнительных числовых форматов: арифметика и хранение. Перед использованием определенного формата убедитесь, что устройство поддерживает его в обеих категориях.
Арифметическая поддержка
Устройство Vulkan должно декларировать арифметическую поддержку числового формата, чтобы его можно было использовать в шейдерных программах. Устройства Vulkan на Android обычно поддерживают следующие форматы для арифметики:
- 32-битное целое число (обязательно)
- 32-бит с плавающей точкой (обязательно)
- 8-битное целое число (необязательно)
- 16-битное целое число (необязательно)
- 16-битное число с плавающей точкой половинной точности (опционально)
Чтобы определить, поддерживает ли устройство Vulkan 16-битные целые числа для арифметики, получите характеристики устройства, вызвав функцию vkGetPhysicalDeviceFeatures2() и проверив, является ли поле shaderInt16
в структуре результата VkPhysicalDeviceFeatures2 истинным.
Чтобы определить, поддерживает ли устройство Vulkan 16-битные числа с плавающей точкой или 8-битные целые числа, выполните следующие действия:
- Проверьте, поддерживает ли устройство расширение VK_KHR_shader_float16_int8 Vulkan. Расширение необходимо для поддержки 16-битных float и 8-битных integer.
- Если поддерживается
VK_KHR_shader_float16_int8
, добавьте указатель на структуру VkPhysicalDeviceShaderFloat16Int8Features к цепочкеVkPhysicalDeviceFeatures2.pNext
. - Проверьте поля
shaderFloat16
иshaderInt8
структуры результатаVkPhysicalDeviceShaderFloat16Int8Features
после вызоваvkGetPhysicalDeviceFeatures2()
. Если значение поля равноtrue
, формат поддерживается для арифметики программы шейдера.
Хотя поддержка расширения VK_KHR_shader_float16_int8
не является обязательным требованием в Vulkan 1.1 или профиле Android Baseline 2022, она очень распространена на устройствах Android.
Поддержка хранения
Устройство Vulkan должно декларировать поддержку необязательного числового формата для определенных типов хранилищ. Расширение VK_KHR_16bit_storage декларирует поддержку 16-битных целых и 16-битных форматов с плавающей точкой. Расширением определены четыре типа хранилищ. Устройство может поддерживать 16-битные числа ни для одного, для некоторых или для всех типов хранилищ.
Типы хранения:
- Объекты буфера хранения
- Объекты однородного буфера
- Выталкивать постоянные блоки
- Интерфейсы ввода и вывода шейдеров
Большинство, но не все, устройств Vulkan 1.1 на Android поддерживают 16-битные форматы в объектах буфера хранения. Не предполагайте поддержку на основе модели GPU. Устройства со старыми драйверами для данного GPU могут не поддерживать объекты буфера хранения, в то время как устройства с более новыми драйверами поддерживают.
Поддержка 16-битных форматов в унифицированных буферах, push-блоках констант и интерфейсах ввода-вывода шейдеров обычно зависит от производителя GPU. На Android GPU обычно поддерживает либо все три типа, либо ни один из них.
Пример функции, которая проверяет поддержку арифметики и формата хранения 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
Типы данных, которые не рекомендуется представлять в формате float половинной точности, включают:
- Данные о местоположении в глобальных мировых координатах
- Текстурные UV-развертки для высокоточных вариантов использования, таких как координаты элементов пользовательского интерфейса в листе атласа
Точность в коде шейдера
Языки программирования шейдеров OpenGL Shading Language (GLSL) и High-level Shader Language (HLSL) поддерживают спецификацию ослабленной точности или явной точности для числовых типов. Ослабленная точность рассматривается как рекомендация для компилятора шейдера. Явная точность является требованием указанной точности. Устройства Vulkan на Android обычно используют 16-битные форматы, когда это предлагается ослабленной точностью. Другие устройства Vulkan, особенно на настольных компьютерах, использующих графическое оборудование, не поддерживающее 16-битные форматы, могут игнорировать ослабленную точность и по-прежнему использовать 32-битные форматы.
Расширения хранилища в GLSL
Соответствующие расширения GLSL должны быть определены для поддержки 16-битных или 8-битных числовых форматов в хранилищах и структурах унифицированных буферов. Соответствующие объявления расширений:
// 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
перед типом с плавающей точкой, чтобы предложить float одинарной точности, и квалификатор mediump
для float половинной точности. Компиляторы GLSL для Vulkan интерпретируют устаревший квалификатор lowp
как mediump
. Некоторые примеры ослабленной точности:
mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix; // Suggest 32-bit single precision
Явная точность в GLSL
Включите расширение GL_EXT_shader_explicit_arithmetic_types_float16
в свой код GLSL, чтобы разрешить использование 16-битных типов с плавающей точкой:
#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;