IMX6U-Game/docs/DEVELOPMENT_GUIDELINES.md

13 KiB
Raw Blame History

IMX6U 开发规范与性能红线

本文档用于约束后续代码设计,目标是在 IMX6UARM Cortex-A7、无 GPU 或弱 GPU、内存与带宽有限上避免性能问题扩散减少后期大面积返工。

这些规则优先约束核心逻辑、渲染管线、每帧循环、像素/顶点级热路径。PC/SDL 调试层可以适度放宽,但不能让调试层的便利写法泄漏到 ARM 运行时核心代码中。

1. 总原则

  • 先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。 Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
  • 热路径默认禁止隐式高成本操作。 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
  • 核心逻辑保持 C++11 兼容。 不引入需要新工具链或重型运行时支持的语言/库特性。
  • 优先复用已有类型与缓冲。 新增抽象前先确认 src/Core/Coresrc/Core/Mathsrc/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::vectorstd::stringstd::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 清理必须优先考虑批量填充(如 memsetstd::fill、平台优化路径),不要逐像素走复杂逻辑。
  • 像素写入路径应尽量减少分支和函数调用层级。
  • 裁剪、剔除、包围盒收缩要尽早执行,避免把不可见数据送入像素级循环。
  • 三角形属性插值、深度测试、纹理采样等未来功能必须先定义定点/整数方案,再接入热路径。
  • PC 调试版可以保留更易读的检查与可视化代码,但 ARM release 路径必须能关闭这些额外成本。

6. STL 与标准库使用边界

允许:

  • 初始化和加载阶段使用 std::vectorstd::string 等提高开发效率。
  • 非热路径使用 RAII 管理资源生命周期。
  • std::array、轻量算法、明确不会分配的工具在核心逻辑中使用。

谨慎或禁止:

  • 热路径中 std::vector 自动扩容。
  • 热路径中字符串拼接、格式化、日志构造。
  • 使用 std::function、复杂迭代器适配器或隐藏分配的回调机制。
  • 在核心模块依赖异常作为正常控制流。

7. 日志、调试与断言

  • 每帧日志必须默认关闭,不能在 ARM release 中输出高频日志。
  • 断言用于捕捉开发期错误,但不能替代运行时边界处理。
  • Debug 检查和 Release 快路径应可区分;不要为了调试便利让最终路径长期承担检查成本。

8. 资源转换与运行时资源规范

  • 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。
  • 当前工具约定:
    • tools/png_to_header.pyPNG -> 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/Core/Math/
  2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
  3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
  4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
  5. 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。

11. 与现有代码的关系

当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:

  • 新功能不要继续扩大浮点和临时分配的使用范围。
  • 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
  • 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。

12. IMX6U + SDL2 运行后端规范

如果最终版本在 IMX6U 上使用 SDL2而不是直接写 framebuffer需要额外注意SDL2 只是显示、输入和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源;时间源应通过独立 Platform::ITimeSource 提供。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。

12.1 SDL2 边界

  • SDL2 类型和调用只允许出现在 src/Core/Platform/ 以及明确的平台适配层中。
  • src/Core/Coresrc/Core/Mathsrc/Core/Rasterizersrc/Core/RenderDatasrc/Core/Scenesrc/Core/Draw2D 不应直接包含 SDL.h
  • 游戏逻辑不直接处理 SDL_Event,应转换为项目自己的输入状态结构。
  • 时间源不挂在 IDisplay 上;核心逻辑从 Platform::ITimeSource 读取单调整数毫秒,并使用 Core::Timer 生成整数 tick / fixed timestep不直接依赖 float 秒数。

12.2 提交帧策略

  • 每帧只提交一次最终 framebuffer避免在一帧内多次 SDL_RenderPresent()
  • 优先使用固定尺寸 streaming texture初始化时创建运行时复用。
  • 不在每帧创建/销毁 SDL_TextureSDL_SurfaceSDL_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_ACCELERATEDSDL_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 FPS45/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 中不得逐帧大量打印。