11 KiB
11 KiB
IMX6U 开发规范与性能红线
本文档用于约束后续代码设计,目标是在 IMX6U(ARM Cortex-A7、无 GPU 或弱 GPU、内存与带宽有限)上避免性能问题扩散,减少后期大面积返工。
这些规则优先约束核心逻辑、渲染管线、每帧循环、像素/顶点级热路径。PC/SDL 调试层可以适度放宽,但不能让调试层的便利写法泄漏到 ARM 运行时核心代码中。
1. 总原则
- 先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。 Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
- 热路径默认禁止隐式高成本操作。 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
- 核心逻辑保持 C++11 兼容。 不引入需要新工具链或重型运行时支持的语言/库特性。
- 优先复用已有类型与缓冲。 新增抽象前先确认
src/Gfx/Core、src/Gfx/Math、src/Gfx/RenderData中是否已有可复用能力。 - 性能相关例外必须写明边界。 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
2. 数值计算规范
2.1 核心逻辑禁止直接使用浮点
核心逻辑包括但不限于:
- 物理、碰撞、动画状态推进
- 相机/物体变换的运行时更新
- 光栅化、插值、深度、裁剪、剔除
- 地图、寻路、AI、游戏规则判定
- 每帧大量执行的资源与渲染调度
规范:
- 新增核心代码默认使用整数或项目自定义定点数。
- 不在热路径中新增
float/double作为主要计算类型。 - 需要小数时,先封装为项目统一的定点类型,例如
Fixed16/Fixed32,不要在各模块散落自定义缩放因子。 - 定点数必须明确:底层整数类型、缩放位数、舍入策略、溢出策略、与整数/float 的转换边界。
- 三角函数、归一化、矩阵等高成本运算应优先考虑查表、预计算、缓存或定点实现。
2.2 float 只允许作为边界表示层
允许使用浮点的场景:
- PC 调试显示、日志、调试 UI
- 与 SDL、图片库、外部工具或离线导入流程交互
- 临时验证算法正确性的非 ARM demo 代码
- 未来尚未定点化的旧代码迁移阶段
要求:
- 浮点必须尽量集中在表示层/适配层/工具层,不要向核心数据结构扩散。
- 只有在需要展示、导入、导出或调用外部 API 时才把定点/整数转换成
float。 - 从
float转回核心类型时必须显式处理舍入和范围,不允许依赖隐式转换。 - 已存在的浮点数学代码如果继续保留,新增功能不得继续扩大其使用面;后续优化应逐步迁移到统一定点类型。
3. 内存与容器规范
3.1 热路径禁止频繁创建容器
禁止在以下位置反复创建/销毁 std::vector、std::string、std::map 等动态分配容器:
- 主循环每帧
- 每个物体更新
- 每个三角形/顶点/像素处理
- 输入事件高频处理
- framebuffer/depthbuffer 清理与提交路径
推荐做法:
- 缓冲区由上层或对象生命周期统一持有,循环内只
clear()并复用容量。 - 已知最大容量时,初始化阶段
reserve()或使用固定容量数组。 - 小型固定数据优先使用
std::array、C 数组或项目自定义固定缓冲。 - 帧级临时数据放入 frame scratch / workspace,由一帧统一 reset,而不是到处 new/delete。
- 资源加载阶段可以使用
std::vector构建数据,但进入运行时前应整理成紧凑、可顺序访问的数据结构。
3.2 分配边界
- 初始化、关卡加载、资源导入阶段允许动态分配。
- 稳态运行阶段禁止无控制的堆分配。
- 热路径中不得直接
new/delete,不得隐藏在容器增长、字符串拼接、临时对象集合中。 - 如果运行时确实需要增长容量,必须有上限、失败策略和日志,不能无限增长。
4. 数据布局与访问规范
- 优先使用连续内存和顺序访问,减少指针追踪。
- 渲染数据优先按批处理组织,避免每像素/每顶点访问分散对象树。
- 小型数学类型保持轻量、可内联、无动态分配。
- 热路径传参优先使用引用或指针,避免大对象拷贝。
- 谨慎使用虚函数:平台边界可以虚化,像素/顶点级热路径不要通过虚函数分派。
- 避免在内层循环调用带边界检查的通用接口;需要安全接口时区分 debug 检查与 release 快路径。
5. 渲染管线性能规范
- FrameBuffer / DepthBuffer 清理必须优先考虑批量填充(如
memset、std::fill、平台优化路径),不要逐像素走复杂逻辑。 - 像素写入路径应尽量减少分支和函数调用层级。
- 裁剪、剔除、包围盒收缩要尽早执行,避免把不可见数据送入像素级循环。
- 三角形属性插值、深度测试、纹理采样等未来功能必须先定义定点/整数方案,再接入热路径。
- PC 调试版可以保留更易读的检查与可视化代码,但 ARM release 路径必须能关闭这些额外成本。
6. STL 与标准库使用边界
允许:
- 初始化和加载阶段使用
std::vector、std::string等提高开发效率。 - 非热路径使用 RAII 管理资源生命周期。
std::array、轻量算法、明确不会分配的工具在核心逻辑中使用。
谨慎或禁止:
- 热路径中
std::vector自动扩容。 - 热路径中字符串拼接、格式化、日志构造。
- 使用
std::function、复杂迭代器适配器或隐藏分配的回调机制。 - 在核心模块依赖异常作为正常控制流。
7. 日志、调试与断言
- 每帧日志必须默认关闭,不能在 ARM release 中输出高频日志。
- 断言用于捕捉开发期错误,但不能替代运行时边界处理。
- Debug 检查和 Release 快路径应可区分;不要为了调试便利让最终路径长期承担检查成本。
8. 新代码提交前检查清单
新增或修改核心代码前,至少检查:
- 是否在热路径新增了
float/double?如果是,是否能改成整数/定点? - 是否在每帧或内层循环创建了
std::vector/std::string/ 其他堆分配对象? - 容器是否提前
reserve(),或由上层复用? - 是否有隐藏的临时大对象拷贝?
- 是否把平台/显示层 API 类型泄漏进核心逻辑?
- 是否能在 PC 调试版和 ARM release 版分别关闭调试开销?
- 是否保留 C++11 兼容?
- 是否需要同步更新
docs/CONVENTIONS.md中的坐标/矩阵/深度等约定?
9. 推荐的代码结构方向
后续如果继续推进性能优化,优先建立这些基础设施:
- 统一定点数类型与转换工具,集中放在
src/Gfx/Math/。 - 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
- 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
- ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
- 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。
10. 与现有代码的关系
当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:
- 新功能不要继续扩大浮点和临时分配的使用范围。
- 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
- 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。
11. IMX6U + SDL2 运行后端规范
如果最终版本在 IMX6U 上使用 SDL2,而不是直接写 framebuffer,需要额外注意:SDL2 只是显示、输入、计时和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
11.1 SDL2 边界
- SDL2 类型和调用只允许出现在
src/Gfx/Platform/以及明确的平台适配层中。 src/Gfx/Core、src/Gfx/Math、src/Gfx/Rasterizer、src/Gfx/RenderData、src/Gfx/Scene、src/Gfx/Draw2D不应直接包含SDL.h。- 游戏逻辑不直接处理
SDL_Event,应转换为项目自己的输入状态结构。 - 时间源可以来自 SDL,但核心逻辑使用整数 tick / fixed timestep,不直接依赖 float 秒数。
11.2 提交帧策略
- 每帧只提交一次最终 framebuffer,避免在一帧内多次
SDL_RenderPresent()。 - 优先使用固定尺寸 streaming texture;初始化时创建,运行时复用。
- 不在每帧创建/销毁
SDL_Texture、SDL_Surface、SDL_Renderer、窗口或字体等资源。 - 避免每帧像素格式转换;CPU framebuffer 的像素格式应尽量与 SDL texture 格式一致。
- 如果使用
SDL_UpdateTexture成为瓶颈,优先评估SDL_LockTexture直接写入 texture 缓冲,减少一次额外拷贝。 - 只更新 dirty rect 的策略可以用于 2D UI/地图类画面;但全屏软光栅 3D 通常仍是整帧提交,重点在降低 CPU 侧绘制成本。
11.3 SDL Renderer 选择
- IMX6U 上不要假设
SDL_RENDERER_ACCELERATED一定更快;实际可能走软件或受驱动限制。 - 需要在开发板上对比
SDL_RENDERER_ACCELERATED、SDL_RENDERER_SOFTWARE、默认 renderer 的帧时间和稳定性。 - 一旦确定目标板最快/最稳配置,应在代码或 CMake 选项中固定,不要依赖默认行为。
- 如果 SDL2 后端只是搬运 CPU framebuffer,重点关注 texture update、copy、present 的总耗时,而不是复杂 SDL 绘图 API。
11.4 分辨率与帧率预算
- 先确定目标分辨率和目标 FPS,再决定渲染功能;不要默认使用屏幕原生高分辨率。
- IMX6U 上优先考虑低分辨率内部渲染,再由 SDL/显示层整数倍放大。
- 目标建议至少维护两个档位:开发调试档、IMX6U 性能档。
- 性能档应限制:最大三角形数、最大 sprite 数、最大粒子数、最大动态光源数、最大纹理尺寸。
- 所有预算都应以实测帧时间为准,不能只按 PC 表现推断。
11.5 输入、音频和资源
- SDL 输入事件应每帧集中轮询一次,转换为当前帧输入快照;不要在逻辑各处直接轮询 SDL。
- 音频回调中禁止分配内存、加锁等待或做复杂逻辑。
- 图片、音频、地图等资源必须在加载阶段解码;运行中避免临时解码和格式转换。
- 大纹理/图片进入运行时前应转换成目标像素格式和目标尺寸。
11.6 必须建立的性能观测
在 IMX6U 上接入 SDL2 后,应尽快加入轻量 profiler 或计时日志,至少拆分统计:
- 输入轮询耗时
- 逻辑更新耗时
- CPU rasterize / draw 耗时
- framebuffer clear 耗时
- depth clear 耗时
- SDL texture update / lock-unlock 耗时
- SDL render copy + present 耗时
- 总帧时间、最低 FPS、峰值帧时间
性能日志默认低频输出,例如每 60 帧汇总一次;ARM release 中不得逐帧大量打印。