移动平台性能优化
移动设备的硬件约束与桌面系统存在显著差异。本节将探讨实现移动平台高性能表现所需的核心性能优化策略。
注:本章聚焦通用移动平台性能优化。针对基于瓦片的渲染器(Tile-Based Rendering,TBR)GPU 的专属优化实践,请参考Rendering Approaches: Tile-Based Rendering.。
纹理优化
本节聚焦移动平台专属的纹理优化决策。关于通用的 Vulkan 图像创建、暂存上传和描述符配置,请参考引擎系列前文章节 ------《资源管理》《渲染管线》,或 Vulkan 官方指南(https://docs.vulkan.org/guide/latest/)。
纹理通常是图形应用中最大的内存消耗项,优化纹理对移动和桌面平台的性能都至关重要。
高效纹理格式
选择合适的纹理格式对所有平台都很关键,不同之处在于特定设备 / 驱动原生支持的格式类型:
压缩格式
尽可能使用硬件支持的压缩格式:
- ASTC(Adaptive Scalable Texture Compression):现代移动 GPU 广泛支持,桌面平台支持度也在提升;具备灵活的块尺寸,画质与文件体积比表现优异。
- ETC2/EAC:OpenGL ES 3.0 强制要求支持,大多数 Android 设备均兼容;Vulkan 环境下也普遍可用。
- PVRTC:主要支持搭载 PowerVR GPU 的 iOS 设备。
- BC(Block Compression,又称 DXT/BCn):桌面平台通用;部分移动 GPU 支持。
基于内容与兼容性的格式选择
根据纹理类型和设备支持情况选择格式:
- 高细节纹理(法线、粗糙度):支持时优先选择 ASTC 4x4 或 6x6;桌面平台常用 BC5/BC7 作为替代。
- 基础色纹理(albedo/basecolor):可用时 ASTC 6x6--8x8 表现良好;桌面平台典型选择为 BC1/BC7。
- 单通道数据:考虑使用 R8 格式,或可用的单通道压缩替代格式。
提示:此优化建议并非仅适用于移动平台 ------ 块压缩可降低所有平台的内存占用和带宽消耗。移动平台章节重点强调这一点,是因为手机 / 平板的带宽、功耗和散热预算更为紧张。桌面平台同样能获得收益,核心差异在于主流支持的格式类型(如桌面端的 BC 格式、多数移动设备的 ASTC/ETC2 格式)。
以下是 Vulkan 中检查并使用压缩格式的实现示例:
cpp
bool is_format_supported(vk::PhysicalDevice physical_device, vk::Format format, vk::ImageTiling tiling,
vk::FormatFeatureFlags features) {
vk::FormatProperties props = physical_device.getFormatProperties(format);
if (tiling == vk::ImageTiling::eLinear) {
return (props.linearTilingFeatures & features) == features;
} else if (tiling == vk::ImageTiling::eOptimal) {
return (props.optimalTilingFeatures & features) == features;
}
return false;
}
vk::Format find_supported_format(vk::PhysicalDevice physical_device,
const std::vector<vk::Format>& candidates,
vk::ImageTiling tiling,
vk::FormatFeatureFlags features) {
for (vk::Format format : candidates) {
if (is_format_supported(physical_device, format, tiling, features)) {
return format;
}
}
throw std::runtime_error("Failed to find supported format");
}
内存优化
内存是所有平台的宝贵资源,而在移动平台上,由于带宽、功耗和散热预算更紧张,内存优化对性能的影响尤为关键。以下是核心优化策略:
最小化内存分配
- 内存池化:使用内存池减少频繁分配 / 释放的开销。
- 基于大内存块的子分配:避免创建大量小尺寸 Vulkan 内存分配,改为分配大内存块并从中进行子分配:
cpp
class VulkanMemoryPool {
public:
VulkanMemoryPool(vk::Device device, vk::PhysicalDevice physical_device,
vk::DeviceSize block_size, uint32_t memory_type_index)
: device(device), block_size(block_size), memory_type_index(memory_type_index) {
allocate_new_block();
}
~VulkanMemoryPool() {
for (auto& block : memory_blocks) {
device.freeMemory(block.memory);
}
}
struct Allocation {
vk::DeviceMemory memory;
vk::DeviceSize offset;
vk::DeviceSize size;
};
Allocation allocate(vk::DeviceSize size, vk::DeviceSize alignment) {
// 查找有足够空间的内存块
for (auto& block : memory_blocks) {
vk::DeviceSize aligned_offset = align(block.next_offset, alignment);
if (aligned_offset + size <= block_size) {
Allocation alloc;
alloc.memory = block.memory;
alloc.offset = aligned_offset;
alloc.size = size;
block.next_offset = aligned_offset + size;
return alloc;
}
}
// 无足够空间的内存块,分配新块
allocate_new_block();
return allocate(size, alignment); // 使用新块重试分配
}
private:
struct MemoryBlock {
vk::DeviceMemory memory;
vk::DeviceSize next_offset = 0;
};
void allocate_new_block() {
vk::MemoryAllocateInfo alloc_info;
alloc_info.setAllocationSize(block_size);
alloc_info.setMemoryTypeIndex(memory_type_index);
MemoryBlock block;
block.memory = device.allocateMemory(alloc_info);
block.next_offset = 0;
memory_blocks.push_back(block);
}
vk::DeviceSize align(vk::DeviceSize offset, vk::DeviceSize alignment) {
return (offset + alignment - 1) & ~(alignment - 1);
}
vk::Device device;
vk::DeviceSize block_size;
uint32_t memory_type_index;
std::vector<MemoryBlock> memory_blocks;
};
降低带宽占用
- 最小化状态变更:按材质分组绘制调用(draw calls)以减少状态切换。
- 使用更小的数据类型:合理使用 16 位索引和半精度浮点数。
- 优化顶点格式:使用紧凑的顶点格式降低内存带宽:
cpp
// 传统顶点格式(每个顶点48字节)
struct Vertex {
glm::vec3 position; // 12字节
glm::vec3 normal; // 12字节
glm::vec2 texCoord; // 8字节
glm::vec4 color; // 16字节
};
// 优化后的顶点格式(每个顶点16字节)
struct OptimizedVertex {
// 位置:3个分量,各16位浮点数
uint16_t position[3]; // 6字节
// 法线:2个分量(可重构Z轴),8位有符号归一化值
int8_t normal[2]; // 2字节
// 纹理坐标:2个分量,各16位浮点数
uint16_t texCoord[2]; // 4字节
// 颜色:4个分量,8位无符号归一化值
uint8_t color[4]; // 4字节
};
若面向基于瓦片的 GPU(TBR)开发,附件的加载 / 存储行为和瓦片刷新会严重影响带宽。具体优化指南请参考Rendering Approaches 章节中的 "瓦片渲染器的附件加载 / 存储操作" 和 "瓦片渲染器的流水线:子通道依赖与 BY_REGION" 部分。
绘制调用优化
移动 GPU 对绘制调用开销尤为敏感:
- 实例化渲染:使用实例化(instancing)减少重复对象的绘制调用。
- 批处理:尽可能将多个对象合并为单个网格。
- 细节层次(LOD):实现 LOD 系统,降低远处物体的几何复杂度。
对于基于瓦片的 GPU,降低 CPU 开销固然重要,但通过合理的流水线设计和子通道(subpasses)将计算任务和数据保留在片上(on-chip),往往能带来更大的性能提升。具体的内存屏障 / 子通道设计模式请参考《渲染策略》章节的 "瓦片渲染器的流水线:子通道依赖与 BY_REGION" 部分;避免外部内存访问的 loadOp/storeOp 配置建议请参考 "瓦片渲染器的附件加载 / 存储操作" 部分。
厂商专属优化
不同移动 GPU 厂商的架构特性不同,针对性优化可获得额外收益:
厂商专属 GPU 优化
-
内存管理:多数移动 SoC 采用统一内存架构(Unified Memory Architecture):
- 尽可能使用
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT类型的内存 - 利用统一内存架构中高速的 CPU-GPU 内存传输特性
- 尽可能使用
-
纹理压缩:不同设备支持不同的纹理压缩格式:
cpp
// 检查纹理压缩格式支持性
bool supports_texture_format(vk::PhysicalDevice physical_device, vk::Format format) {
vk::FormatProperties props = physical_device.getFormatProperties(format);
return (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage);
}
// 根据设备能力获取最优纹理格式
vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) {
vk::PhysicalDeviceProperties props = physical_device.getProperties();
vk::PhysicalDeviceFeatures features = physical_device.getFeatures();
// 检查ASTC支持性(现代移动GPU广泛支持)
// 多数游戏会基于资源压缩格式进行开发,因此标准做法是仅确保所需格式受支持即可
if (features.textureCompressionASTC_LDR) {
return vk::Format::eAstc8x8SrgbBlock;
}
// 可继续添加其他格式的检查逻辑
// ...
}
- 性能监控:多数厂商提供性能监控工具,可定位其硬件特有的性能瓶颈。
移动平台性能最佳实践
- 在目标设备上做性能分析:不同移动设备的性能特征差异显著,需在不同厂商、不同 GPU 架构的硬件上完成测试。
- 监控设备温度:移动设备过热时会触发性能降频,需设计能适配热节流的引擎架构。
- 平衡画质与性能:提供图形设置选项,让用户根据设备能力平衡画质与性能。
- 实现自适应分辨率:基于性能指标动态调整渲染分辨率。
在下一节中,我们将探讨移动 GPU 的不同渲染策略,重点分析基于瓦片的渲染(TBR)与立即模式渲染(IMR)的差异。
关键点回顾
- 移动平台纹理优化优先选择硬件原生支持的压缩格式(ASTC/ETC2/PVRTC),需通过 Vulkan 特性检测动态适配;
- 内存优化核心是池化分配、大块内存子分配,以及使用紧凑数据格式降低带宽;
- 移动 GPU 对绘制调用开销敏感,需结合实例化、批处理、LOD 等手段优化,TBR 架构还需重点优化附件操作和流水线设计。