216 lines
12 KiB
Markdown
216 lines
12 KiB
Markdown
# 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. 资源转换与运行时资源规范
|
||
|
||
- 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。
|
||
- 当前工具约定:
|
||
- `tools/png_to_header.py`:PNG -> `uint32_t` RGBA 头文件。
|
||
- `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. 新代码提交前检查清单
|
||
|
||
新增或修改核心代码前,至少检查:
|
||
|
||
- [ ] 是否在热路径新增了 `float` / `double`?如果是,是否能改成整数/定点?
|
||
- [ ] 是否在每帧或内层循环创建了 `std::vector` / `std::string` / 其他堆分配对象?
|
||
- [ ] 容器是否提前 `reserve()`,或由上层复用?
|
||
- [ ] 是否有隐藏的临时大对象拷贝?
|
||
- [ ] 是否把平台/显示层 API 类型泄漏进核心逻辑?
|
||
- [ ] 是否能在 PC 调试版和 ARM release 版分别关闭调试开销?
|
||
- [ ] 新增图片/字体资源是否已离线转换,且源资源、转换脚本、生成头文件一起提交?
|
||
- [ ] 是否保留 C++11 兼容?
|
||
- [ ] 是否需要同步更新 `docs/CONVENTIONS.md` 中的坐标/矩阵/深度等约定?
|
||
|
||
## 10. 推荐的代码结构方向
|
||
|
||
后续如果继续推进性能优化,优先建立这些基础设施:
|
||
|
||
1. 统一定点数类型与转换工具,集中放在 `src/Gfx/Math/`。
|
||
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
|
||
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
|
||
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
|
||
5. 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。
|
||
|
||
## 11. 与现有代码的关系
|
||
|
||
当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:
|
||
|
||
- 新功能不要继续扩大浮点和临时分配的使用范围。
|
||
- 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
|
||
- 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。
|
||
|
||
## 12. IMX6U + SDL2 运行后端规范
|
||
|
||
如果最终版本在 IMX6U 上使用 SDL2,而不是直接写 framebuffer,需要额外注意:SDL2 只是显示、输入、计时和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
|
||
|
||
### 12.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 秒数。
|
||
|
||
### 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/显示层整数倍放大。
|
||
- 目标建议至少维护两个档位:开发调试档、IMX6U 性能档。
|
||
- 性能档应限制:最大三角形数、最大 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 中不得逐帧大量打印。
|