图形数据和着色器计算的数字格式会对游戏性能产生重大影响。
最佳格式具有以下特点:
- 提高 GPU 缓存使用效率
- 降低内存带宽消耗、节省电量并提高性能
- 在着色器程序中最大限度地提高计算吞吐量
- 最大限度地降低游戏的 CPU RAM 使用量
浮点格式
现代 3D 图形中的大多数计算和数据都使用浮点数。Android 上的 Vulkan 使用大小为 32 或 16 位的浮点数。32 位浮点数通常称为单精度或全精度;16 位浮点数称为半精度。
Vulkan 定义了一个 64 位浮点类型,但搭载 Android 的 Vulkan 设备通常不支持该类型,因此不建议使用该类型。64 位浮点数通常称为双精度。
整数格式
数据和计算也可以使用有符号和无符号整数。标准整数大小为 32 位。是否支持其他位大小取决于设备。搭载 Android 的 Vulkan 设备通常支持 16 位和 8 位整数。Vulkan 定义了一个 64 位整数类型,但搭载 Android 的 Vulkan 设备通常不支持该类型,因此不建议使用该类型。
不太理想的半精度行为
现代 GPU 架构会将两个 16 位值组合成一个 32 位值对,并实现针对这两个值对的指令。为了获得最佳性能,请避免使用标量 16 位浮点变量;请将数据向量化为二元素或四元素向量。着色器编译器也许能够在向量运算中使用标量值。但是,如果您依靠编译器优化标量,请检查编译器输出以验证向量化效果。
在 32 位和 16 位精度的浮点数之间转换会产生计算开销。您可以通过尽量减少代码中的精度转换次数来降低开销。
对 16 位和 32 位算法版本之间的性能差异进行基准化测试。半精度并不总是会提升性能,尤其是对于复杂的计算来说。对向量化数据大量使用融合乘法加法 (FMA) 指令的算法非常适合用于提高半精度性能。
数字格式支持
所有搭载 Android 的 Vulkan 设备在数据和着色器计算中都支持单精度 32 位浮点数和 32 位整数。我们无法保证支持其他格式;如果支持,也不能保证所有用例都适用。
Vulkan 对可选数字格式提供两类支持:算术和存储。在使用某种特定格式之前,请确保设备在这两个类别中都支持该格式。
算术支持
Vulkan 设备必须声明对数字格式的算术支持,这样才能在着色器程序中使用该格式。搭载 Android 的 Vulkan 设备通常支持以下算术格式:
- 32 位整数(必需)
- 32 位浮点数(必需)
- 8 位整数(可选)
- 16 位整数(可选)
- 16 位半精度浮点数(可选)
如需确定 Vulkan 设备是否支持使用 16 位整数进行算术,请执行以下操作:
检索设备的功能,方法是调用
vkGetPhysicalDeviceFeatures2() 函数并检查是否
VkPhysicalDeviceFeatures2 中的 shaderInt16
字段
为 true。
如需确定 Vulkan 设备是否支持 16 位浮点数或 8 位整数,请执行以下步骤:
- 检查设备是否支持 VK_KHR_shader_float16_int8 Vulkan 扩展。必须支持此扩展,才能支持 16 位浮点数和 8 位整数。
- 如果支持
VK_KHR_shader_float16_int8
,请将 VkPhysicalDeviceShaderFloat16Int8Features 结构指针附加到VkPhysicalDeviceFeatures2.pNext
链上。 - 调用
vkGetPhysicalDeviceFeatures2()
后,请检查VkPhysicalDeviceShaderFloat16Int8Features
结果结构的shaderFloat16
和shaderInt8
字段。如果该字段的值为true
,则着色器程序算术支持该格式。
虽然在 Vulkan 1.1 或 2022 Android 基准配置文件中不是必需的,但对 VK_KHR_shader_float16_int8
扩展的支持在 Android 设备上很常见。
存储空间支持
Vulkan 设备必须声明支持特定存储类型的可选数字格式。VK_KHR_16bit_storage 扩展声明支持 16 位整数和 16 位浮点格式。该扩展定义了四种存储类型。设备可能对任何存储类型都不支持 16 位数字,也可能对部分或所有存储类型支持 16 位数字。
存储类型包括:
- 存储缓冲区对象
- 统一缓冲区对象
- 推送常量块
- 着色器输入和输出接口
大多数(但不是全部)搭载 Android 的 Vulkan 1.1 设备都支持存储缓冲区对象的 16 位格式。不要基于 GPU 模型假定会支持。对于指定 GPU 来说,搭载旧版驱动程序的设备可能不支持存储缓冲区对象,而搭载新版驱动程序的设备则支持。
是否支持统一缓冲区、推送常量块和着色器输入/输出接口的 16 位格式通常取决于 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 为中心、精度要求低的数据
不建议用半精度浮点数表示的数据类型包括:
- 以全局世界坐标表示的位置数据
- 适用于高精度用例的纹理 UV,例如图集工作表中的界面元素坐标
着色器代码中的精度
OpenGL 着色语言 (GLSL) 和高级着色器语言 (HLSL) 着色器编程语言支持用放宽精度或显式精度的规范表示数字类型。建议针对着色器编译器采用放宽的精度。特定精度必须采用显式精度。如果存在放宽的精度建议,搭载 Android 的 Vulkan 设备通常采用 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
限定符可建议单精度浮点数,使用 mediump
限定符可建议半精度浮点数。Vulkan 的 GLSL 编译器会将旧版 lowp
限定符解读为 mediump
。下面是一些放宽精度的示例:
mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix; // Suggest 32-bit single precision
GLSL 中的显式精度
在 GLSL 代码中添加 GL_EXT_shader_explicit_arithmetic_types_float16
扩展可启用 16 位浮点类型:
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
您可以使用以下关键字在 GLSL 中声明 16 位浮点标量、向量和矩阵类型:
float16_t f16vec2 f16vec3 f16vec4
f16mat2 f16mat3 f16mat4
f16mat2x2 f16mat2x3 f16mat2x4
f16mat3x2 f16mat3x3 f16mat3x4
f16mat4x2 f16mat4x3 f16mat4x4
可以使用以下关键字在 GLSL 中声明 16 位整数标量和向量类型:
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;