Оптимизировать с пониженной точностью

Числовой формат графических данных и вычислений шейдеров может значительно повлиять на производительность игры.

Оптимальные форматы:

  • Повысить эффективность использования кеша графического процессора
  • Снижение потребления пропускной способности памяти, экономия энергии и повышение производительности.
  • Максимальная вычислительная пропускная способность в программах шейдеров
  • Как минимизировать использование ОЗУ ЦП в игре

Форматы с плавающей запятой

Большинство вычислений и данных в современной 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 значение true.

Чтобы определить, поддерживает ли устройство Vulkan 16-битные числа с плавающей запятой или 8-битные целые числа, выполните следующие действия:

  1. Проверьте, поддерживает ли устройство расширение Vulkan VK_KHR_shader_float16_int8. Расширение требуется для поддержки 16-битных чисел с плавающей запятой и 8-битных целых чисел.
  2. Если VK_KHR_shader_float16_int8 поддерживается, добавьте указатель структуры VkPhysicalDeviceShaderFloat16Int8Features в цепочку VkPhysicalDeviceFeatures2.pNext.
  3. Проверьте поля shaderFloat16 и shaderInt8 в структуре результата VkPhysicalDeviceShaderFloat16Int8Features после вызова vkGetPhysicalDeviceFeatures2(). Если значение поля равно true, формат поддерживается для арифметических операций в шейдерных программах.

Хотя это не является обязательным требованием в Vulkan 1.1 или базовом профиле Android 2022 года, поддержка расширения VK_KHR_shader_float16_int8 очень распространена на устройствах Android.

Поддержка хранилища

Устройство Vulkan должно объявлять поддержку необязательного числового формата для определенных типов хранилищ. Расширение VK_KHR_16bit_storage объявляет поддержку 16-битных целых чисел и 16-битных форматов с плавающей запятой. Расширение определяет четыре типа хранилища. Устройство может поддерживать 16-битные числа для всех, некоторых или ни одного из типов хранилищ.

Доступны следующие типы хранилищ:

  • Объекты буфера хранилища
  • Объекты унифицированного буфера
  • Push-константы
  • Интерфейсы ввода и вывода шейдеров

Большинство, но не все устройства Vulkan 1.1 на Android поддерживают 16-битные форматы в объектах буфера хранения. Не делайте предположений о поддержке на основе модели графического процессора. Устройства со старыми драйверами для определенного графического процессора могут не поддерживать объекты буфера хранения, а устройства с более новыми драйверами – поддерживать.

Поддержка 16-битных форматов в однородных буферах, блоках push-констант и интерфейсах ввода-вывода шейдеров обычно зависит от производителя графического процессора. На устройствах 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-координаты для небольших текстур с ограниченным наложением, которые можно ограничить диапазоном от -1,0 до 1,0.
  • Данные о нормалях, касательных и бикасательных
  • Данные о цвете вершин
  • Данные с низкими требованиями к точности, центрированные по значению 0,0.

Типы данных, которые не рекомендуется представлять в виде чисел с плавающей запятой половинной точности:

  • Данные о положении в глобальных координатах.
  • UV-координаты текстуры для высокоточных сценариев использования, например координат элементов интерфейса в атласе.

Точность в коде шейдера

Языки программирования шейдеров OpenGL Shading Language (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;