Building a Simple Engine -- Mobile Development -- Performance optimizations

移动平台性能优化

移动设备的硬件约束与桌面系统存在显著差异。本节将探讨实现移动平台高性能表现所需的核心性能优化策略。

注:本章聚焦通用移动平台性能优化。针对基于瓦片的渲染器(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");
}

内存优化

内存是所有平台的宝贵资源,而在移动平台上,由于带宽、功耗和散热预算更紧张,内存优化对性能的影响尤为关键。以下是核心优化策略:

最小化内存分配

  1. 内存池化:使用内存池减少频繁分配 / 释放的开销。
  2. 基于大内存块的子分配:避免创建大量小尺寸 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;
};

降低带宽占用

  1. 最小化状态变更:按材质分组绘制调用(draw calls)以减少状态切换。
  2. 使用更小的数据类型:合理使用 16 位索引和半精度浮点数。
  3. 优化顶点格式:使用紧凑的顶点格式降低内存带宽:
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 对绘制调用开销尤为敏感:

  1. 实例化渲染:使用实例化(instancing)减少重复对象的绘制调用。
  2. 批处理:尽可能将多个对象合并为单个网格。
  3. 细节层次(LOD):实现 LOD 系统,降低远处物体的几何复杂度。

对于基于瓦片的 GPU,降低 CPU 开销固然重要,但通过合理的流水线设计和子通道(subpasses)将计算任务和数据保留在片上(on-chip),往往能带来更大的性能提升。具体的内存屏障 / 子通道设计模式请参考《渲染策略》章节的 "瓦片渲染器的流水线:子通道依赖与 BY_REGION" 部分;避免外部内存访问的 loadOp/storeOp 配置建议请参考 "瓦片渲染器的附件加载 / 存储操作" 部分。

厂商专属优化

不同移动 GPU 厂商的架构特性不同,针对性优化可获得额外收益:

厂商专属 GPU 优化

  1. 内存管理:多数移动 SoC 采用统一内存架构(Unified Memory Architecture):

    • 尽可能使用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT类型的内存
    • 利用统一内存架构中高速的 CPU-GPU 内存传输特性
  2. 纹理压缩:不同设备支持不同的纹理压缩格式:

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;
    }
    // 可继续添加其他格式的检查逻辑
    // ...
}
  1. 性能监控:多数厂商提供性能监控工具,可定位其硬件特有的性能瓶颈。

移动平台性能最佳实践

  1. 在目标设备上做性能分析:不同移动设备的性能特征差异显著,需在不同厂商、不同 GPU 架构的硬件上完成测试。
  2. 监控设备温度:移动设备过热时会触发性能降频,需设计能适配热节流的引擎架构。
  3. 平衡画质与性能:提供图形设置选项,让用户根据设备能力平衡画质与性能。
  4. 实现自适应分辨率:基于性能指标动态调整渲染分辨率。

在下一节中,我们将探讨移动 GPU 的不同渲染策略,重点分析基于瓦片的渲染(TBR)与立即模式渲染(IMR)的差异。

关键点回顾

  1. 移动平台纹理优化优先选择硬件原生支持的压缩格式(ASTC/ETC2/PVRTC),需通过 Vulkan 特性检测动态适配;
  2. 内存优化核心是池化分配、大块内存子分配,以及使用紧凑数据格式降低带宽;
  3. 移动 GPU 对绘制调用开销敏感,需结合实例化、批处理、LOD 等手段优化,TBR 架构还需重点优化附件操作和流水线设计。
相关推荐
mxwin5 天前
unity shader中 ddx ddy是什么
unity·游戏引擎·shader
审判长烧鸡6 天前
Go 内存优化骚操作
go·内存优化
mxwin6 天前
Unity SetPassCall和DrawCall的区别是什么
unity·游戏引擎·shader
mxwin8 天前
在unity shader中,通过pass产生阴影,通过主pass的光照 接收阴影!那么问题来了,是先产生阴影吗?还是先接收阴影,执行顺序是啥呢
数码相机·unity·游戏引擎·shader
小贺儿开发8 天前
《唐朝诡事录之长安》——盛世马球
人工智能·unity·ai·shader·绘画·影视·互动
mxwin16 天前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader
mxwin16 天前
Unity Shader 手写基于 PBR 的 URP Lit Shader 核心光照计算
unity·游戏引擎·shader
mxwin16 天前
Unity GPU Shader 性能优化指南
unity·游戏引擎·shader
mxwin18 天前
Unity Custom Interpolators与半透明阴影的原理与实战
unity·游戏引擎·shader
小贺儿开发21 天前
【MediaPipe】Unity3D 虚拟面具互动演示
unity·人机交互·shader·摄像头·面具·互动·脸部捕捉