diff --git a/.gitignore b/.gitignore index a5b9e5b..25801c8 100644 --- a/.gitignore +++ b/.gitignore @@ -52,8 +52,8 @@ Thumbs.db .codex CLAUDE.md AGENTS.md -omc -omx +.omc +.omx build-win build-linux diff --git a/README.md b/README.md index ea46274..a20c158 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,36 @@ |-------------------|-----------------|---------------------------------| | Windows (x86/x64) | SDL2 窗口 | 验证渲染逻辑、算法正确性、快速迭代调试 | | Linux x86_64 | SDL2 窗口 | 验证 Linux 兼容性、CMake 构建、SDL2 系统依赖 | -| ARM Linux (交叉编译) | `/dev/fb0` 直接写屏 | 最终在 IMX6U 开发板上运行的版本 | +| ARM Linux (交叉编译) | SDL2 或 `/dev/fb0` | 最终在 IMX6U 开发板上运行的版本;优先目标以后续实际部署后端为准 | **为什么分三套?** - **Windows 编译**:开发主力机通常是 Windows,有 IDE 调试、有图形窗口,渲染算法对不对一眼就能看到。这个阶段完全不关心嵌入式细节。 - **Linux x86 编译**:验证代码在 GCC/Clang 下有无警告、CMake 配置是否跨平台、系统 SDL2 依赖是否正确。很多嵌入式工具链的问题在 x86 Linux 上就能提前暴露。 -- **ARM 交叉编译**:最终在 IMX6U 上跑。这个版本去掉 SDL2,直接 mmap `/dev/fb0` 写屏,减少依赖和内存开销。 +- **ARM 交叉编译**:最终在 IMX6U 上跑。若目标板使用 SDL2,则 SDL2 仅作为显示/输入/计时适配层,核心渲染仍按 CPU framebuffer + 一次性提交设计;如需极简依赖,也保留 `/dev/fb0` 后端作为对照。 + + +## 开发规范与性能红线 + +IMX6U 运行时性能预算较紧,后续开发必须遵守 `docs/DEVELOPMENT_GUIDELINES.md`。如果目标板使用 SDL2,仍然要把 SDL2 限制在平台适配层,核心逻辑和渲染热路径不直接依赖 SDL: + +- 核心逻辑和热路径默认不新增 `float` / `double`,需要小数时使用项目统一定点数;只在显示、调试、导入导出等边界层转换成浮点。 +- 主循环、每对象、每三角形/顶点/像素级代码中不得频繁创建 `std::vector` / `std::string` 等动态分配容器,应复用缓冲或使用固定容量结构。 +- PC/SDL 版本用于调试验证,不能把调试便利写法扩散到 ARM release 核心路径。 +- SDL2 后端只做显示、输入、计时和最终 framebuffer 提交,不在核心渲染/游戏逻辑中直接调用 SDL API。 +- 新增核心代码前按规范文档中的检查清单自查。 + +## 应用层与图形库拆分 + +项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md`: + +- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、输入/时间源、基础绘制能力,例如点、线、矩形、四边形、sprite、tile。 +- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。 +- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。 +- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。 +- 依赖方向必须保持 `Apps -> Shared -> Gfx -> Platform`,底层图形库不能反向依赖具体游戏。 + +初期推荐单进程多应用模式:Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。 ## 构建说明 @@ -70,46 +93,65 @@ cmake --build build-linux sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf ``` -构建(启用 Framebuffer 后端): +本项目 ARM 端保留两类后端: + +- **SDL2 后端**:目标是后续游戏主路径;SDL2 负责显示、输入、计时和最终 framebuffer 提交。 +- **Framebuffer 后端**:作为极简依赖和显示通路对照测试。 + +构建(Framebuffer 对照后端): ```bash -cmake -B build-arm \ +cmake -B build-arm-fb \ -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \ -DUSE_FRAMEBUFFER=ON . -cmake --build build-arm +cmake --build build-arm-fb +``` + +构建(SDL2 后端,要求工具链/sysroot 可找到目标板 SDL2 开发库): +```bash +cmake -B build-arm-sdl \ + -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \ + -DUSE_FRAMEBUFFER=OFF . +cmake --build build-arm-sdl ``` 部署到开发板: ```bash -scp build-arm/IMX6U-Game root@imx6u:/tmp/ +scp build-arm-sdl/IMX6U-Game root@imx6u:/tmp/ +# 或部署 framebuffer 对照版本: +scp build-arm-fb/IMX6U-Game root@imx6u:/tmp/ ``` -板子上运行(需要 root 权限访问 `/dev/fb0`): +板子上运行: ```bash -sudo /tmp/IMX6U-Game +/tmp/IMX6U-Game ``` -ARM 版本启动后会打印 framebuffer 参数(分辨率、bpp、像素格式)。按 `q` 键或 `Ctrl+C` 退出。 +Framebuffer 对照版本可能需要 root 权限访问 `/dev/fb0`。SDL2 版本是否需要额外环境变量或权限,取决于目标板 SDL2 视频驱动和显示栈配置。 ## 显示后端架构 -显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦: +显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层: ``` -┌──────────────────────────────────────┐ -│ 游戏/渲染逻辑(Camera、Rasterizer 等) │ -├──────────────────────────────────────┤ -│ Core::FrameBuffer(CPU 颜色缓冲区) │ -├──────────────────────────────────────┤ -│ Platform::IDisplay │ -│ - SDLDisplay : SDL2 窗口(PC) │ -│ - FBDisplay : /dev/fb0(ARM) │ -└──────────────────────────────────────┘ +┌──────────────────────────────────────────────┐ +│ Apps:Launcher / GameA / GameB │ +├──────────────────────────────────────────────┤ +│ Shared:UI、存档、配置、资源索引 │ +├──────────────────────────────────────────────┤ +│ Gfx:DrawContext、FrameBuffer、Draw2D、Math │ +├──────────────────────────────────────────────┤ +│ Platform::IDisplay │ +│ - SDLDisplay : SDL2 显示/输入/计时适配 │ +│ - FBDisplay : /dev/fb0 对照后端 │ +└──────────────────────────────────────────────┘ ``` -切换后端不需要修改任何渲染代码,只需要在 CMake 配置时开关 `USE_FRAMEBUFFER`。 +切换显示后端不应影响应用层和核心绘制逻辑;当前 CMake 通过 `USE_FRAMEBUFFER` 在 SDL2 与 framebuffer 后端间切换。 ## 目录结构 +当前源码仍处于早期验证布局: + ```text IMX6U-Game/ ├─ cmake/ @@ -119,38 +161,25 @@ IMX6U-Game/ │ ├─ SDL2/ # Windows 用 SDL2 库(头文件 + lib + DLL) │ └─ SDL_image/ # SDL2_image 库(头文件 + lib + DLL) ├─ src/ -│ ├─ Platform/ -│ │ ├─ Display.h # 显示抽象接口 -│ │ ├─ SDLDisplay.cpp # SDL2 窗口后端 -│ │ └─ FBDisplay.cpp # Linux Framebuffer 后端 -│ ├─ Core/ -│ │ ├─ FrameBuffer.h/.cpp # CPU 颜色缓冲 -│ │ ├─ DepthBuffer.h/.cpp # 深度缓冲 -│ │ └─ Renderer.h/.cpp # 渲染调度(预留) -│ ├─ Math/ -│ │ ├─ Vector2.h / Vector3.h / Vector4.h -│ │ ├─ Matrix4x4.h -│ │ └─ MathUtil.h -│ ├─ Rasterizer/ -│ │ ├─ Rasterizer.h/.cpp # 线段光栅化 -│ │ └─ TriangleRasterizer.h/.cpp # 三角形填充 -│ ├─ RenderData/ -│ │ ├─ Color.h -│ │ ├─ BoundingBox.h -│ │ └─ Triangle.h -│ ├─ Scene/ -│ │ ├─ Camera.h/.cpp -│ │ ├─ Vertex.h / Mesh.h / Model.h / Transform.h -│ ├─ Asset/ -│ │ └─ ObjLoader.h/.cpp # OBJ 模型加载(预留) -│ ├─ Shading/ -│ │ └─ BlinnPhongShader.h/.cpp # 着色(预留) +│ ├─ Platform/ # 当前显示后端适配,后续归入 Gfx/Platform +│ ├─ Core/ # FrameBuffer、DepthBuffer 等,后续归入 Gfx/Core +│ ├─ Math/ # 当前 float 数学类型,后续补定点数并归入 Gfx/Math +│ ├─ Rasterizer/ # 当前软光栅化代码,后续归入 Gfx/Draw2D 或 Gfx/Rasterizer +│ ├─ RenderData/ # Color、Triangle 等基础渲染数据 +│ ├─ Scene/ # 当前 3D demo 场景数据 +│ ├─ Asset/ # 资源加载预留 +│ ├─ Shading/ # 着色预留 │ ├─ test_fb.cpp # 独立 fb 测试(最小示例) -│ └─ main.cpp +│ └─ main.cpp # 当前 demo 入口,后续拆成 App 主循环 +├─ docs/ +│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线 +│ └─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层 ├─ CMakeLists.txt └─ README.md ``` +目标布局见 `docs/APP_AND_GFX_ARCHITECTURE.md`:后续会收敛为 `src/Gfx`、`src/Apps/Launcher`、`src/Apps/GameA`、`src/Apps/GameB`、`src/Shared`。 + ## 模块说明 ### Core @@ -167,8 +196,8 @@ IMX6U-Game/ ### Platform - **IDisplay**:显示后端抽象,解耦渲染与输出 -- **SDLDisplay**:PC 端用 SDL2 窗口实时查看效果 -- **FBDisplay**:ARM 端直接写 `/dev/fb0` +- **SDLDisplay**:SDL2 后端,PC 调试和 IMX6U SDL2 目标路径共用这一类适配思想 +- **FBDisplay**:`/dev/fb0` 对照后端,用于极简显示通路验证 ## 当前状态与后续 @@ -180,13 +209,13 @@ IMX6U-Game/ **待完成(按优先级):** 1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON) -2. 输入抽象(键盘/触摸屏 evdev) -3. 纹理贴图 -4. 2D 游戏层封装(Sprite、Tilemap) -5. OBJ 模型加载与完整光照 +2. 应用层拆分(Launcher / GameA / GameB / Shared)和统一 `IApp` 主循环 +3. SDL2 输入抽象(键盘/触摸/按键状态快照) +4. Gfx 基础 2D 绘制接口(矩形、四边形、Sprite、Tilemap) +5. 纹理贴图、OBJ 模型加载与完整光照 ## 说明 - 代码标准:**C++11**,确保兼容嵌入式老工具链 - `test_fb.cpp` 是一个完全独立的 Linux framebuffer 最小测试,不依赖项目其他代码,用于快速验证板子显示通路 -- Windows 和 Linux x86 版本共享 `SDLDisplay`,ARM 版本单独使用 `FBDisplay` +- Windows、Linux x86 和目标板 SDL2 路径共享 `SDLDisplay` 适配思想;`FBDisplay` 保留为 framebuffer 对照后端 diff --git a/docs/APP_AND_GFX_ARCHITECTURE.md b/docs/APP_AND_GFX_ARCHITECTURE.md new file mode 100644 index 0000000..1a23264 --- /dev/null +++ b/docs/APP_AND_GFX_ARCHITECTURE.md @@ -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 绘制层不要过度抽象。 diff --git a/src/CONVENTIONS.md b/docs/CONVENTIONS.md similarity index 93% rename from src/CONVENTIONS.md rename to docs/CONVENTIONS.md index 0779eaf..670769b 100644 --- a/src/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -1,7 +1,9 @@ -# CPU 软件渲染器项目约定 +# CPU 软件渲染器项目约定 本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。 +> 文档分工:坐标、矩阵、深度等数学语义记录在本文档;IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_GFX_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。 + ## 1. 通用约定 - 项目使用右手坐标系。 @@ -155,4 +157,4 @@ - `DepthBuffer` 不能替代投影合法性检查或背面剔除 - `DepthBuffer` 负责的是像素级遮挡,而不是顶点级或三角形级是否进入渲染流程 -后续如果项目要加入更严格的裁剪、剔除、透视校正插值或隐藏线规则,应当以代码实现为准,并同步更新本文档。 +后续如果项目要加入更严格的裁剪、剔除、透视校正插值或隐藏线规则,应当以代码实现为准,并同步更新本文档。 diff --git a/docs/DEVELOPMENT_GUIDELINES.md b/docs/DEVELOPMENT_GUIDELINES.md new file mode 100644 index 0000000..a74bf27 --- /dev/null +++ b/docs/DEVELOPMENT_GUIDELINES.md @@ -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 中不得逐帧大量打印。