IMX6U-Game/docs/DEVELOPMENT_GUIDELINES.md

203 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# IMX6U 开发规范与性能红线
本文档用于约束后续代码设计,目标是在 IMX6UARM Cortex-A7、无 GPU 或弱 GPU、内存与带宽有限上避免性能问题扩散减少后期大面积返工。
这些规则优先约束**核心逻辑、渲染管线、每帧循环、像素/顶点级热路径**。PC/SDL 调试层可以适度放宽,但不能让调试层的便利写法泄漏到 ARM 运行时核心代码中。
## 1. 总原则
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Core`、`src/Math`、`src/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 兼容?
- [ ] 是否需要同步更新 `src/CONVENTIONS.md` 中的坐标/矩阵/深度等约定?
## 9. 推荐的代码结构方向
后续如果继续推进性能优化,优先建立这些基础设施:
1. 统一定点数类型与转换工具,集中放在 `src/Math/`
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
5. 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。
## 10. 与现有代码的关系
当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:
- 新功能不要继续扩大浮点和临时分配的使用范围。
- 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
- 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。
## 11. IMX6U + SDL2 运行后端规范
如果最终版本在 IMX6U 上使用 SDL2而不是直接写 framebuffer需要额外注意SDL2 只是显示、输入、计时和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
### 11.1 SDL2 边界
- SDL2 类型和调用只允许出现在 `src/Platform/` 以及明确的平台适配层中。
- `src/Core`、`src/Math`、`src/Rasterizer`、`src/RenderData`、`src/Scene` 不应直接包含 `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 中不得逐帧大量打印。