制订规范

This commit is contained in:
SepComet
2026-06-06 17:10:47 +08:00
parent 9a2edd59fe
commit e1a9a9770b
5 changed files with 510 additions and 58 deletions
+219
View File
@@ -0,0 +1,219 @@
# 应用层与图形库分层设计
本项目后续包含三个应用层目标:两个游戏和一个启动器;同时还需要沉淀一套可复用的 IMX6U 轻量图形库。为了避免后期耦合和性能返工,必须明确区分“应用层”和“底层库”。
## 1. 推荐总体结构
推荐拆成四个逻辑层:
```text
IMX6U-Game/
├─ src/
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Core/ # FrameBuffer、DepthBuffer、RendererContext
│ │ ├─ Draw2D/ # rect、quad、line、sprite、tile 等基础绘制
│ │ ├─ Math/ # 定点数、向量、矩阵、几何工具
│ │ ├─ RenderData/ # Color、Rect、Quad、Texture、Vertex 等数据结构
│ │ └─ Platform/ # SDL2 / fb0 / input / timer 的平台适配
│ ├─ Apps/
│ │ ├─ Launcher/ # 启动器应用
│ │ ├─ GameA/ # 第一个游戏
│ │ └─ GameB/ # 第二个游戏
│ └─ Shared/ # 可选:应用层共享但不属于 Gfx 的东西
│ ├─ Save/ # 存档格式、配置读写
│ ├─ UI/ # 启动器和游戏共用 UI 组件
│ └─ Assets/ # 应用层资源索引、资源命名约定
├─ assets/
│ ├─ launcher/
│ ├─ game_a/
│ ├─ game_b/
│ └─ shared/
└─ docs/
```
当前项目已有 `Core/Math/Platform/Rasterizer/RenderData/Scene` 等目录,短期不必一次性搬迁;但新增代码应按上面的边界收敛。等功能稳定后,再把现有底层代码整体移动到 `src/Gfx/`。
## 2. 四个层级的职责
### 2.1 Gfx:底层图形库
职责:
- 管理 framebuffer、depthbuffer、渲染上下文。
- 提供基础绘制接口:点、线、矩形、四边形、三角形、sprite、tile、简单文本等。
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
- 封装 SDL2 / framebuffer 显示提交、输入轮询、时间源。
- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。
禁止:
- 不依赖 `GameA`、`GameB`、`Launcher`。
- 不包含具体关卡、角色、菜单流程、游戏状态机。
- 不直接读取某个游戏专属资源路径。
- 不在核心绘制接口中暴露 SDL2 类型。
### 2.2 Apps/GameA 与 Apps/GameB:两个游戏
职责:
- 各自维护自己的游戏规则、状态机、关卡、实体、碰撞、计分、胜负逻辑。
- 通过 Gfx 提供的接口绘制画面。
- 通过平台输入快照读取按键/触摸状态,而不是直接调用 SDL。
- 管理自己的资源索引和场景切换。
禁止:
- 游戏之间互相 include 对方代码。
- 游戏直接操作 SDL renderer / texture / window。
- 游戏直接依赖 framebuffer 内存布局,除非是明确标注的性能特例。
### 2.3 Apps/Launcher:启动器
职责:
- 展示游戏列表、图标、说明、版本信息。
- 选择并启动 GameA 或 GameB。
- 管理全局设置,例如音量、亮度、输入校准、语言等。
- 可以复用 Shared/UI,但不承载具体游戏逻辑。
启动方式有两种可选方案:
1. **单进程多应用模式**:Launcher、GameA、GameB 编译成一个可执行文件,内部切换当前 App。
2. **多进程模式**:Launcher 是独立程序,选择后启动另一个游戏可执行文件。
对 IMX6U 初期开发,推荐先用 **单进程多应用模式**,原因:
- 构建、部署、调试更简单。
- SDL2 初始化、资源缓存、输入状态可以共用。
- 避免频繁退出/启动进程带来的黑屏、资源重载和状态恢复问题。
等项目成熟后,如果每个游戏体积较大,或者需要独立更新,再考虑多进程拆分。
### 2.4 Shared:应用层共享模块
Shared 只放“应用层共享,但不属于底层图形库”的内容,例如:
- UI 控件:按钮、列表、九宫格面板、菜单焦点管理。
- 存档/设置:配置文件、最高分、解锁状态。
- 资源清单:多个应用共用的字体、图标、音效索引。
Shared 可以依赖 Gfx;Gfx 不能依赖 Shared。
## 3. 依赖方向
必须保持单向依赖:
```text
Apps/GameA ─┐
Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0)
Launcher ─┘
Apps/GameA ─┐
Apps/GameB ─┼──────────> Gfx
Launcher ─┘
```
禁止反向依赖:
```text
Gfx -> Apps 禁止
Gfx -> Shared 禁止
GameA -> GameB 禁止
GameB -> GameA 禁止
Platform -> Game 禁止
```
## 4. 应用统一接口
推荐为 Launcher、GameA、GameB 提供统一应用接口,例如:
```cpp
class IApp
{
public:
virtual ~IApp() {}
virtual void on_enter() = 0;
virtual void on_exit() = 0;
virtual void update(uint32_t fixed_delta_ms) = 0;
virtual void render(Gfx::DrawContext& ctx) = 0;
virtual AppId next_app() const = 0;
};
```
主循环只认识 `IApp`:
```text
poll input -> update current app -> render current app -> present framebuffer
```
这样三个应用层共用同一个主循环、同一套 SDL2 初始化和 framebuffer 提交流程。
注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。
## 5. 底层图形库优先提供的 API
Gfx 初期建议先做 2D 基础能力,不要一开始就做复杂引擎:
```cpp
namespace Gfx
{
struct RectI { int32_t x, y, w, h; };
struct PointI { int32_t x, y; };
struct Color32 { uint32_t rgba; };
class DrawContext
{
public:
void clear(Color32 color);
void draw_pixel(int32_t x, int32_t y, Color32 color);
void draw_line(PointI a, PointI b, Color32 color);
void draw_rect(RectI rect, Color32 color);
void fill_rect(RectI rect, Color32 color);
void draw_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
void fill_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
};
}
```
后续再扩展:
- `draw_sprite`
- `draw_sprite_region`
- `draw_tilemap`
- `draw_text_bitmap_font`
- `set_clip_rect`
- `set_camera_2d`
- `batch sprite/tile`
## 6. 命名建议
为了避免“项目名、库名、游戏名”混乱,建议:
- 项目仓库仍叫 `IMX6U-Game`。
- 底层图形库命名为 `Gfx` 或 `MiniGfx`。
- 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。
- CMake target 可以是:
- `imx6u_gfx` 静态库
- `imx6u_launcher`
- `imx6u_game_a`
- `imx6u_game_b`
- 或初期单可执行文件 `imx6u_suite`
## 7. 推荐演进顺序
1. 先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。
2. 把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。
3. 建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。
4. 新增 Launcher app,只做最小菜单和应用切换。
5. 新增 GameA/GameB 空壳,验证三应用切换。
6. 再逐步把现有 3D demo 或 2D 游戏逻辑迁入对应 Game 目录。
7. 最后重构 CMake,按 `imx6u_gfx` + 应用 target 拆分。
## 8. 性能注意事项
- 应用切换不应重复销毁/创建 SDL window、renderer、texture。
- 三个应用共用 framebuffer、输入状态和时间源。
- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。
- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
+160
View File
@@ -0,0 +1,160 @@
# CPU 软件渲染器项目约定
本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。
> 文档分工:坐标、矩阵、深度等数学语义记录在本文档;IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_GFX_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
## 1. 通用约定
- 项目使用右手坐标系。
- `Vector3::cross(a, b)` 遵循右手定则。
- 除非特别说明,向量按列向量理解。
- 变换写法按 `M * v` 解释。
## 2. 世界空间与局部空间
世界空间与物体局部空间目前使用同一套方向命名:
- `+X`:右
- `+Y`:上
- `+Z`:前
`Scene::Transform` 也遵循这套约定:
- `get_right()`:旋转后的局部右方向
- `get_up()`:旋转后的局部上方向
- `get_forward()`:旋转后的局部前方向
- `get_left()`、`get_down()`、`get_back()`:分别为对应反方向
也就是说,`Transform` 中的 `forward` 明确定义为 `+Z`。
## 3. 旋转约定
- 欧拉角使用弧度制。
- `rotation.x`:绕 `X` 轴旋转
- `rotation.y`:绕 `Y` 轴旋转
- `rotation.z`:绕 `Z` 轴旋转
- 组合旋转顺序为 `Rz * Ry * Rx`
当前项目里,方向向量的旋转方式是先构造旋转矩阵,再去变换基础方向轴。
## 4. 矩阵约定
`Math::Matrix4x4` 当前采用以下规则:
- 语义上按列向量使用,写法为 `M * v`
- 元素访问方式为 `matrix[row][col]`
- `data()` 暴露的是连续的 row-major 内存
- 平移分量存放在最后一列
因此,常见的组合变换阅读顺序是从右往左:
- `worldPosition = Translation * Rotation * Scale * localPosition`
## 5. 相机与视图空间
相机目前由 `Scene::Camera::transform` 驱动。
相机自身局部方向定义为:
- 相机右方向:`transform.get_right()`
- 相机上方向:`transform.get_up()`
- 相机前方向:`transform.get_forward()`
但进入视图空间后,当前项目采用的是常见的相机空间约定:
- 位于相机前方的点,其 view-space `z` 为负值
- 视图矩阵第三行存的是相机 backward,而不是 forward
这和当前透视投影矩阵实现是一致的,因为那里对应的是 `clip.w = -viewZ` 这套约定。
## 6. 三角形绕序与正面约定
当前项目将三角形正面统一约定为顺时针 `CW` 绕序。
这里的“顺时针”按当前渲染流程解释为:
- 三角形经过 view / projection / viewport 变换后
- 从屏幕上观察其顶点顺序时,正面三角形按顺时针排列
与这套约定配套的实现规则是:
- 背面剔除当前在 view space 中完成
- 法线方向使用 `faceNormal = (v1 - v0).cross(v2 - v0)` 计算
- 当前 demo 中,`faceNormal.dot(faceCenter) > 0` 被视为正面
这意味着:
- 所有手写或导入的三角形索引都必须保持一致绕序
- 如果未来改成逆时针 `CCW` 为正面,那么剔除判定符号也必须同步调整
- `main.cpp` 里的 `cubeTriangles` 和 `cubeFaces` 当前应继续保持与这套约定一致,不要单独翻转其中一部分
## 7. 投影与 NDC
当前透视投影相关约定如下:
- 规范化设备坐标(NDC)可见范围为 `[-1, 1]`
- `x`、`y`、`z` 都会在映射到 viewport 之前做范围检查
- 当前测试代码里,如果点超出 NDC,可能会直接判为不可见,而不是继续做线段裁剪
这意味着:
- 当前 demo 还没有实现完整的视锥裁剪
- 如果一条线段只有一部分还在屏幕内,但端点已经越出 NDC,整条线仍可能被直接丢弃
## 8. 屏幕与像素坐标
屏幕/像素坐标使用左上角为原点的约定:
- 原点在左上角
- `x` 向右增大
- `y` 向下增大
`Core::FrameBuffer` 当前行为如下:
- 有效像素范围:`x in [0, width)`
- 有效像素范围:`y in [0, height)`
- 越界写入会被直接忽略
- 像素缓冲按 row-major 排列
- 内存中的第一行对应 `y = 0`
`Camera::get_viewport_matrix()` 里也对 `Y` 做了翻转,因此 NDC 的“向上”为正,最终会映射成屏幕坐标“向下”为正。
## 9. 深度缓冲约定
当前项目已经接入 `Core::DepthBuffer`,并采用以下规则:
- `DepthBuffer` 存储类型为 `float`
- 每帧开始时必须调用 `depthBuffer->clear()`,默认清为 `INFINITY`
- 当前约定为“深度值越小,离相机越近”
- 深度测试通过后,必须同时更新 `DepthBuffer` 和 `FrameBuffer`
- `DepthBuffer` 只负责存储和读取深度,不负责决定颜色写入逻辑;是否写颜色由光栅化阶段决定
当前三角形光栅化里的深度流程为:
- 在屏幕空间遍历三角形包围盒
- 以像素中心 `x + 0.5, y + 0.5` 作为采样点
- 用屏幕空间 `x/y` 计算重心坐标
- 用同一组重心坐标判断点是否在三角形内,并插值顶点 `z`
- 若新深度更近,则写入 `DepthBuffer` 和 `FrameBuffer`
也就是说:
- 重心坐标的计算是二维问题,只使用屏幕空间 `x/y`
- 顶点 `z` 的插值使用这组重心权重完成
- 当前实现是屏幕空间线性插值,后续如果引入纹理、法线或更严格的属性插值,需要进一步考虑透视校正插值
## 10. Demo 中的可见性规则
`main.cpp` 里的旋转立方体示例,目前采用以下可见性与遮挡规则:
- 三角形是否参与光栅化,仍由投影合法性检查和背面剔除决定
- 三角形之间的遮挡,不再依赖按平均深度排序,而是由 `DepthBuffer` 逐像素决定
- 当前 demo 仍保留按面筛选后的轮廓线绘制,用于显示黑色边框
这意味着:
- `DepthBuffer` 不能替代投影合法性检查或背面剔除
- `DepthBuffer` 负责的是像素级遮挡,而不是顶点级或三角形级是否进入渲染流程
后续如果项目要加入更严格的裁剪、剔除、透视校正插值或隐藏线规则,应当以代码实现为准,并同步更新本文档。
+202
View File
@@ -0,0 +1,202 @@
# IMX6U 开发规范与性能红线
本文档用于约束后续代码设计,目标是在 IMX6U(ARM 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 中不得逐帧大量打印。