15 KiB
IMX6U 开发规范与性能红线
本文档用于约束后续代码设计,目标是在 IMX6U(ARM Cortex-A7、无 GPU 或弱 GPU、内存与带宽有限)上避免性能问题扩散,减少后期大面积返工。
这些规则优先约束核心逻辑、渲染管线、每帧循环、像素/顶点级热路径。PC/SDL 调试层可以适度放宽,但不能让调试层的便利写法泄漏到 ARM 运行时核心代码中。
1. 总原则
- 先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。 Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
- 热路径默认禁止隐式高成本操作。 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
- 核心逻辑保持 C++11 兼容。 不引入需要新工具链或重型运行时支持的语言/库特性。
- 优先复用已有类型与缓冲。 新增抽象前先确认
src/Core/Core、src/Core/Math、src/Core/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、平台优化路径),不要逐像素走复杂逻辑。 - 2D-only 或不使用深度测试的场景应只清颜色缓冲,例如使用
DrawContext::clear_color();不要每帧无意义清理 depth buffer。 - 像素写入路径应尽量减少分支和函数调用层级。
- 像素级写入的 Release 快路径不要使用带额外边界检查的容器访问(例如
std::vector::at());边界检查应在外层完成。 - 裁剪、剔除、包围盒收缩要尽早执行,避免把不可见数据送入像素级循环。
Rasterizer::DrawLine已采用此策略:入口做快速全屏可见性检查,屏幕内直接走set_pixel_unsafe快路径;屏幕外走 Cohen-Sutherland 裁剪后再走快路径。- 三角形属性插值、深度测试、纹理采样等未来功能必须先定义定点/整数方案,再接入热路径。
TriangleRasterizer::DrawTriangle2D已采用增量式整数边缘函数(替代每像素 float 重心+除法)和定点深度插值(depth_fp += depth_fp_per_pixel,提取时>> 16),内层循环无 float 运算。- PC 调试版可以保留更易读的检查与可视化代码,但 ARM release 路径必须能关闭这些额外成本。
5.1 /dev/fb0 提交路径
Framebuffer 后端是 IMX6U 上最容易误判性能的路径。FBDisplay::present() 需要把 CPU 侧 framebuffer 写入 /dev/fb0,如果逐像素走通用格式转换,整屏 1024x600 提交会成为主瓶颈。
规范:
- ARM / framebuffer 性能测试必须使用 Release 构建;单配置生成器应确认
CMAKE_BUILD_TYPE=Release。 - 性能结论必须拆分
Frame和Present耗时;如果Present接近Frame,优先优化显示提交,而不是游戏逻辑。 - fb0 像素格式应在初始化时打印并据此走专用路径,常见格式包括 RGB565、ARGB8888/XRGB8888、RGBA8888。
- 避免每帧重复做不必要的通用 RGBA 转换;可考虑目标格式 backbuffer、预转换资源、行拷贝、dirty rect 和局部提交。
- 直接写
/dev/fb0不是原子换屏;LCD 控制器可能边扫描边显示正在写入的内存,因此肉眼流畅度不等同于完整帧率。
已观察到的板端测试结论:
- 未优化 ARM 构建下,轻量 2D demo 曾出现约
Frame:81ms / Present:69ms。 - 改为 Release 构建后,同一类 framebuffer 测试最高约 76 FPS。
- 因此板端性能测试首先检查构建类型,再判断算法或硬件瓶颈。
6. STL 与标准库使用边界
允许:
- 初始化和加载阶段使用
std::vector、std::string等提高开发效率。 - 非热路径使用 RAII 管理资源生命周期。
std::array、轻量算法、明确不会分配的工具在核心逻辑中使用。
谨慎或禁止:
- 热路径中
std::vector自动扩容。 - 热路径中字符串拼接、格式化、日志构造。
- 使用
std::function、复杂迭代器适配器或隐藏分配的回调机制。 - 在核心模块依赖异常作为正常控制流。
7. 日志、调试与断言
- 每帧日志必须默认关闭,不能在 ARM release 中输出高频日志。
- 断言用于捕捉开发期错误,但不能替代运行时边界处理。
- Debug 检查和 Release 快路径应可区分;不要为了调试便利让最终路径长期承担检查成本。
8. 资源转换与运行时资源规范
- 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。
- 当前工具约定:
tools/png_to_header.py:PNG ->uint32_tRGBA 头文件。tools/gen_font_atlas.py:像素字体 TTF -> ASCII bitmap font atlas/header。
- 生成像素格式统一为
(R << 24) | (G << 16) | (B << 8) | A,与RenderData::Color::to_rgba()和Core::FrameBuffer当前格式保持一致。 - 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。
- 运行时绘制 sprite/font/tilemap 时只做裁剪、透明判断、颜色替换、tile id 查表或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。
- Tilemap 绘制应按视口可见范围遍历 tile,不能每帧无条件扫描整张地图;视口边缘允许通过 sprite 像素裁剪显示半个 tile。
- 字体资源当前走像素风路径,生成端会把 alpha 阈值化为 0/255;如果未来要恢复抗锯齿字体,必须同步设计 framebuffer alpha blending,而不能只在绘制端把所有非 0 alpha 当作实心像素。
9. 新代码提交前检查清单
新增或修改核心代码前,至少检查:
- ARM / framebuffer 性能测试是否确认使用 Release 构建?
- 是否在热路径新增了
float/double?如果是,是否能改成整数/定点? - 是否在每帧或内层循环创建了
std::vector/std::string/ 其他堆分配对象? - 容器是否提前
reserve(),或由上层复用? - 是否有隐藏的临时大对象拷贝?
- 是否把平台/显示层 API 类型泄漏进核心逻辑?
- 是否能在 PC 调试版和 ARM release 版分别关闭调试开销?
- 新增图片/字体资源是否已离线转换,且源资源、转换脚本、生成头文件一起提交?
- 是否保留 C++11 兼容?
- 是否需要同步更新
docs/CONVENTIONS.md中的坐标/矩阵/深度等约定?
10. 推荐的代码结构方向
后续如果继续推进性能优化,优先建立这些基础设施:
- 统一定点数类型与转换工具,集中放在
src/Core/Math/。 - 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
- 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
- ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
- 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。
11. 与现有代码的关系
当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:
- 新功能不要继续扩大浮点和临时分配的使用范围。
- 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
- 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。
12. IMX6U + SDL2 运行后端规范
如果最终版本在 IMX6U 上使用 SDL2,而不是直接写 framebuffer,需要额外注意:SDL2 只是显示、输入和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源;时间源应通过独立 Platform::ITimeSource 提供。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
12.1 SDL2 边界
- SDL2 类型和调用只允许出现在
src/Core/Platform/以及明确的平台适配层中。 src/Core/Core、src/Core/Math、src/Core/Rasterizer、src/Core/RenderData、src/Core/Scene、src/Core/Draw2D不应直接包含SDL.h。- 游戏逻辑不直接处理
SDL_Event,应转换为项目自己的输入状态结构。 - 时间源不挂在
IDisplay上;核心逻辑从Platform::ITimeSource读取单调整数毫秒,并使用Core::Timer生成整数 tick / fixed timestep,不直接依赖 float 秒数。
12.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 侧绘制成本。
12.3 SDL Renderer 选择
- IMX6U 上不要假设
SDL_RENDERER_ACCELERATED一定更快;实际可能走软件或受驱动限制。 - 需要在开发板上对比
SDL_RENDERER_ACCELERATED、SDL_RENDERER_SOFTWARE、默认 renderer 的帧时间和稳定性。 - 一旦确定目标板最快/最稳配置,应在代码或 CMake 选项中固定,不要依赖默认行为。
- 如果 SDL2 后端只是搬运 CPU framebuffer,重点关注 texture update、copy、present 的总耗时,而不是复杂 SDL 绘图 API。
12.4 分辨率与帧率预算
- 先确定目标分辨率和目标 FPS,再决定渲染功能;不要默认使用屏幕原生高分辨率。
- IMX6U 上优先考虑低分辨率内部渲染,再由 SDL/显示层整数倍放大。
- 目标帧率当前维护
30/45/60 FPS三档,通过命令行--fps 30|45|60选择;IMX6U 默认优先 30 FPS,45/60 FPS 需要以实测帧时间确认。 - 固定步长 tick 使用整数毫秒余数累计,例如 30 FPS 为
33/33/34 ms节奏,45 FPS 为22/22/22/22/23 ms节奏,60 FPS 为16/17/17 ms节奏。 - 性能档应限制:最大三角形数、最大 sprite 数、最大粒子数、最大动态光源数、最大纹理尺寸。
- 所有预算都应以实测帧时间为准,不能只按 PC 表现推断。
12.5 输入、音频和资源
- SDL 输入事件应每帧集中轮询一次,转换为当前帧输入快照;不要在逻辑各处直接轮询 SDL。
- 音频回调中禁止分配内存、加锁等待或做复杂逻辑。
- 图片、音频、地图等资源必须在加载阶段解码;运行中避免临时解码和格式转换。
- 大纹理/图片进入运行时前应转换成目标像素格式和目标尺寸。
12.6 必须建立的性能观测
在 IMX6U 上接入 SDL2 后,应尽快加入轻量 profiler 或计时日志,至少拆分统计:
- 输入轮询耗时
- 逻辑更新耗时
- CPU rasterize / draw 耗时
- framebuffer clear 耗时
- depth clear 耗时
- SDL texture update / lock-unlock 耗时
- SDL render copy + present 耗时
- 总帧时间、最低 FPS、峰值帧时间
性能日志默认低频输出,例如每 60 帧汇总一次;ARM release 中不得逐帧大量打印。
Framebuffer 对照后端同样需要至少显示或记录:
Frame:从帧开始到提交完成的总耗时;Present:IDisplay::present()的耗时;- 当前 FPS。
当前 Demo 的板端测试 UI 已包含这些信息,后续正式 profiler 可替代该临时显示。