# 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。 - 当前规范固定 `FrameBuffer` 为 `RGB565`;sprite/tilemap 主路径直接从 `RGBA5551` 写入 `RGB565`,透明仅支持 1-bit alpha test(`A=0` 跳过,`A=1` 覆写)。 - 避免每帧重复做不必要的通用 RGBA 转换;优先使用 `RGB565` 行拷贝、预转换资源、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 -> `uint16_t` `RGBA5551` 头文件(sprite 主路径)。 - `tools/gen_font_atlas.py`:像素字体 TTF -> ASCII bitmap font atlas/header,其中 atlas header 输出为 `uint8_t` 1-bit mask。 - sprite 运行时输入规范统一为 `RGBA5551`,内部 backbuffer 统一为 `RGB565`;字体 atlas 运行时输入统一为 row-major、MSB-first 的 1-bit mask,不再保留 `RGBA8888` 兼容路径,也不支持 alpha blending。 - 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。 - 运行时绘制 sprite/font/tilemap 时只做裁剪、透明判断、颜色替换、tile id 查表或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。 - Tilemap 绘制应按视口可见范围遍历 tile,不能每帧无条件扫描整张地图;视口边缘允许通过 sprite 像素裁剪显示半个 tile。 - 字体资源当前走像素风路径,生成端会把 alpha 阈值化为 0/255 后再压成 1-bit mask;如果未来要恢复抗锯齿字体,必须同步设计 framebuffer alpha blending,而不能只把字体资源重新扩回多 bit alpha。 ## 9. 新代码提交前检查清单 新增或修改核心代码前,至少检查: - [ ] ARM / framebuffer 性能测试是否确认使用 Release 构建? - [ ] 是否在热路径新增了 `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/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 可替代该临时显示。