This commit is contained in:
HP 2026-06-07 16:42:07 +08:00
commit e150b153b9
2 changed files with 243 additions and 78 deletions

1
.gitignore vendored
View File

@ -59,6 +59,7 @@ build-win
build-linux build-linux
build-arm-fb build-arm-fb
build-arm-sdl build-arm-sdl
build-check
.idea .idea

View File

@ -1,8 +1,10 @@
# 应用层与 Core 分层设计 # 应用层与底层库分层设计
本文记录当前项目的代码分层。`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。 本文记录项目的代码分层:`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。文档同时记录当前已实现状态和目标架构。
## 目录边界 > 文档分工IMX6U 运行时性能红线记录在 `DEVELOPMENT_GUIDELINES.md`;坐标、矩阵、深度等数学语义记录在 `CONVENTIONS.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
## 1. 当前目录结构
```text ```text
src/ src/
@ -17,7 +19,7 @@ src/
Scene/ # Camera、Transform、Mesh、Model Scene/ # Camera、Transform、Mesh、Model
Shading/ # Shader 相关代码 Shading/ # Shader 相关代码
Apps/ Apps/
Demo/ # Core 能力演示 Demo/ # 2D sprite/tilemap 性能测试入口
Game/ # Tom 游戏 Game/ # Tom 游戏
``` ```
@ -26,9 +28,118 @@ src/
- `src/Core`:整个底层库。 - `src/Core`:整个底层库。
- `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。 - `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。
后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/``Buffer/`,但这次先只做旧底层库名称到 `Core` 的一致性修复 后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/``Buffer/`
## 依赖方向 ## 2. 目标架构
后续项目最终应拆成四个逻辑层:
```text
IMX6U-Game/
├─ src/
│ ├─ Core/ # 底层库:可复用、无具体游戏规则
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口(✅ 已实现)
│ │ ├─ Core/ # FrameBuffer、DepthBuffer、Timer✅ 已实现)
│ │ ├─ Math/ # 向量、矩阵、数学工具(✅ 已实现)
│ │ ├─ Rasterizer/ # 线段、三角形光栅化(✅ 已实现)
│ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现)
│ │ ├─ Scene/ # Camera、Transform✅ 已实现)
│ │ ├─ Shading/ # 着色器(预留)
│ │ ├─ Platform/ # SDL2 / fb0 显示适配与独立时间源(✅ 已实现)
│ │ └─ Asset/ # 资源加载(✅ 已实现)
│ ├─ Apps/
│ │ ├─ Demo/ # 2D sprite/tilemap 性能测试入口(✅ 已实现)
│ │ ├─ Game/ # Tom 游戏(✅ 已实现)
│ │ ├─ Launcher/ # 启动器应用(待实现)
│ │ ├─ GameA/ # 第一个游戏(待实现)
│ │ └─ GameB/ # 第二个游戏(待实现)
│ └─ Shared/ # 可选:应用层共享但不属于 Core 的东西
│ ├─ Save/ # 存档格式、配置读写
│ ├─ UI/ # 启动器和游戏共用 UI 组件
│ └─ Assets/ # 应用层资源索引、资源命名约定
├─ assets/
│ ├─ font/ # 共享像素字体源文件与生成的 font_atlas
│ ├─ sprite/ # 当前 demo/test sprite 源文件与生成头文件
│ ├─ launcher/
│ ├─ game_a/
│ ├─ game_b/
│ └─ shared/
├─ tools/
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
└─ docs/
```
## 3. 各层职责
### 3.1 Core底层库
职责:
- 管理 framebuffer、depthbuffer、渲染上下文。
- 提供基础绘制接口点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。
- 提供音频输入、音频输出和按键输入的抽象接口。
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas运行时不直接依赖 PNG/TTF 解码。
- 只关心"怎么画得快、怎么提交到屏幕",不关心具体游戏规则。
禁止:
- 不依赖 `GameA`、`GameB`、`Launcher`。
- 不包含具体关卡、角色、菜单流程、游戏状态机。
- 不直接读取某个游戏专属资源路径。
- 不在核心绘制接口中暴露 SDL2 类型。
- 不在每帧热路径中执行图片/字体解码;资源转换应在构建前或工具阶段完成。
### 3.2 Apps/GameA 与 Apps/GameB两个游戏
职责:
- 各自维护自己的游戏规则、状态机、关卡、实体、碰撞、计分、胜负逻辑。
- 通过 Core 提供的接口绘制画面。
- 通过平台输入快照读取按键/触摸状态,而不是直接调用 SDL。
- 管理自己的资源索引和场景切换。
禁止:
- 游戏之间互相 include 对方代码。
- 游戏直接操作 SDL renderer / texture / window。
- 游戏直接依赖 framebuffer 内存布局,除非是明确标注的性能特例。
### 3.3 Apps/Launcher启动器
职责:
- 展示游戏列表、图标、说明、版本信息。
- 选择并启动 GameA 或 GameB。
- 管理全局设置,例如音量、亮度、输入校准、语言等。
- 可以复用 Shared/UI但不承载具体游戏逻辑。
启动方式有两种可选方案:
1. **单进程多应用模式**Launcher、GameA、GameB 编译成一个可执行文件,内部切换当前 App。
2. **多进程模式**Launcher 是独立程序,选择后启动另一个游戏可执行文件。
对 IMX6U 初期开发,推荐先用 **单进程多应用模式**,原因:
- 构建、部署、调试更简单。
- SDL2 初始化、资源缓存、输入状态可以共用。
- 避免频繁退出/启动进程带来的黑屏、资源重载和状态恢复问题。
等项目成熟后,如果每个游戏体积较大,或者需要独立更新,再考虑多进程拆分。
### 3.4 Shared应用层共享模块
Shared 只放"应用层共享,但不属于底层库"的内容,例如:
- UI 控件:按钮、列表、九宫格面板、菜单焦点管理。
- 存档/设置:配置文件、最高分、解锁状态。
- 资源清单:多个应用共用的字体、图标、音效索引。
Shared 可以依赖 CoreCore 不能依赖 Shared。
## 4. 依赖方向
允许: 允许:
@ -37,63 +148,77 @@ Apps -> Shared -> Core -> Platform
Apps -> Core Apps -> Core
``` ```
禁止: 禁止反向依赖
```text ```text
Core -> Apps Core -> Apps 禁止
Core -> Shared Core -> Shared 禁止
GameA -> GameB GameA -> GameB 禁止
GameB -> GameA GameB -> GameA 禁止
Platform -> Game Platform -> Game 禁止
``` ```
Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。 Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。
## Core 职责 ## 5. 平台层接口
Core 只提供底层能力: 平台层采用"抽象接口 + 多套后端实现"的模式。游戏代码只能依赖这些 `I*` 接口。
- 管理 `FrameBuffer`、`DepthBuffer` 和绘制上下文。 ### 5.1 显示
- 提供基础绘制接口line、triangle、sprite、sprite region、tilemap、bitmap font。
- 提供基础数学、颜色、图片、三角形、tilemap 等数据结构。
- 封装 SDL2 / framebuffer 显示提交。
- 提供独立时间源 `Platform::ITimeSource`
- 提供音频输入、音频输出和按键输入的抽象接口。
- 使用离线转换后的运行时资源,不在热路径解码 PNG/TTF。
Core 不做: `Platform::IDisplay``src/Core/Platform/Display.h`
- 不实现具体游戏规则。
- 不直接读取某个游戏专属资源目录。
- 不在核心绘制接口暴露 SDL2 类型。
- 不在每帧热路径中执行图片、字体解码或文件 IO。
## 应用职责
`src/Apps/*` 负责具体应用流程:
- 创建具体游戏或 Demo 的主循环。
- 加载应用自己的资源。
- 调用 `Core::DrawContext` 绘制画面。
- 根据平台输入更新游戏状态。
当前主程序位于:
```text ```text
src/Apps/Game/Main.cpp Platform::IDisplay
SDLDisplay # PC / SDL2 调试后端
FBDisplay # Linux /dev/fb0 后端
``` ```
它默认启动 Tom 游戏视觉入口。 ### 5.2 音频输入
## DrawContext
`Core::DrawContext` 是当前统一绘制入口,位于:
```text ```text
src/Core/Draw2D/DrawContext.h Platform::IAudioInput
src/Core/Draw2D/DrawContext.cpp SdlAudioInput # PC / SDL2 麦克风后端
AlsaAudioInput # Linux ALSA 录音后端
``` ```
### 5.3 音频输出
```text
Platform::IAudioOutput
SdlAudioOutput # PC / SDL2 扬声器后端
AlsaAudioOutput # Linux ALSA 播放后端
```
### 5.4 按键输入
```text
Platform::IButtonInput
SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键
EvdevButtonInput # Linux evdev 按键后端
```
### 5.5 默认后端
如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput`
### 5.6 时间源
时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep。
ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。
### 5.7 CMake 后端切换
```cmake
-DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay
-DUSE_FRAMEBUFFER=ON # 使用 FBDisplay
```
## 6. DrawContext
`Core::DrawContext` 是当前统一绘制入口,位于 `src/Core/Draw2D/DrawContext.h`
它封装: 它封装:
- `Core::FrameBuffer` - `Core::FrameBuffer`
@ -111,53 +236,92 @@ ctx.draw_text(font, x, y, color, "text");
ctx.present(display); ctx.present(display);
``` ```
## 显示后端 ## 7. 应用统一接口(待实现)
平台层采用“抽象接口 + 多套后端实现”的模式。 推荐为 Launcher、GameA、GameB 提供统一应用接口,例如:
显示层通过 `Platform::IDisplay` 抽象: ```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(Core::DrawContext& ctx) = 0;
virtual AppId next_app() const = 0;
};
```
主循环只认识 `IApp`
```text ```text
Platform::IDisplay poll input -> update current app -> render current app -> present framebuffer
SDLDisplay # PC / SDL2 调试后端
FBDisplay # Linux /dev/fb0 后端
``` ```
音频输入通过 `Platform::IAudioInput` 抽象: 这样三个应用层共用同一个主循环、同一套 SDL2 初始化、framebuffer 提交流程和独立时间源。
```text 注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。
Platform::IAudioInput
SdlAudioInput # PC / SDL2 麦克风后端
AlsaAudioInput # Linux ALSA 录音后端
```
音频输出通过 `Platform::IAudioOutput` 抽象: 当前状态:`IApp` 接口尚未实现。`src/Apps/Game/Main.cpp` 直接编写主循环,未经过 `IApp` 抽象。
```text ## 8. 命名建议
Platform::IAudioOutput
SdlAudioOutput # PC / SDL2 扬声器后端
AlsaAudioOutput # Linux ALSA 播放后端
```
按键输入通过 `Platform::IButtonInput` 抽象: - 项目仓库仍叫 `IMX6U-Game`
- 底层库命名为 `Core`
- 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。
- CMake target 可以是:
- `imx6u_core` 静态库
- `imx6u_launcher`
- `imx6u_game_a`
- `imx6u_game_b`
- 或初期单可执行文件 `imx6u_suite`
```text ## 9. 推荐演进顺序
Platform::IButtonInput
SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键
EvdevButtonInput # Linux evdev 按键后端
```
游戏代码只能依赖这些 `I*` 接口。ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。 1. ~~先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。~~ 未实现,当前直接写主循环。
如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput` 2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~ **已完成**
3. ~~建立 `Core::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**`Core::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、present
4. ~~底层代码统一放在 `src/Core/`Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成**
5. 新增 Launcher app只做最小菜单和应用切换。
6. 新增 GameA/GameB 空壳,验证三应用切换。
7. 再逐步把现有 3D demo 能力恢复为独立验证入口,或把 2D 游戏逻辑迁入对应 Game 目录。
8. 最后重构 CMake`imx6u_core` + 应用 target 拆分。
CMake 通过 `USE_FRAMEBUFFER` 选择实现: ## 10. 性能注意事项
```cmake - 应用切换不应重复销毁/创建 SDL window、renderer、texture。
-DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay - 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源Display 不承担计时职责。
-DUSE_FRAMEBUFFER=ON # 使用 FBDisplay - `Core::DrawContext` 是当前统一绘制入口。
``` - 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
- Core 的绘制函数要保持小而直接,优先内联和连续内存写入。
- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
- 无 3D 内容的 2D 应用应使用只清颜色缓冲的路径,避免每帧清理 depth buffer。
- `/dev/fb0` 后端提交可能是主要瓶颈;板端性能分析应拆分 `Frame``Present` 耗时,并确认 ARM 构建为 Release。
- 直接写 framebuffer 不是原子换屏LCD 扫描会让局部动画看起来比完整帧率更连续;判断性能应以计时数据为准。
## 后续建议 ## 11. 资源转换约定
- 运行时资源优先使用离线转换后的简单数组,不在 IMX6U 运行时解码 PNG/TTF。
- `tools/png_to_header.py` 将 PNG 转为 `uint32_t` RGBA 数组,适用于 sprite、小图标、测试纹理等。
- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas并输出同名 PNG 预览和 C++ 头文件。
- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。
- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。
## 12. SpriteRegion 与 Tilemap 约定
- `RenderData::SpriteRegion` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。
- `DrawContext::draw_sprite_ex` 是底层 sprite 绘制入口负责源区域检查、目标屏幕裁剪、scale 和 flip`draw_sprite_region` 系列只是对 atlas 子区域的语义包装。
- `RenderData::Tilemap` 使用 `uint16_t` tile id 保存地图网格,`Tilemap::EmptyTile` (`0xFFFF`) 表示空 tile。
- `Tilemap` 当前只支持一个 atlas、固定 tile 宽高和固定 `atlas_columns`tile id 通过 `tile_id % atlas_columns` / `tile_id / atlas_columns` 映射到 atlas 中的源区域。
- `DrawContext::draw_tilemap` 的裁剪分两层:
- tilemap 层按 tile 计算需要尝试绘制的可见范围;
- sprite 层按像素裁剪每个 tile支持 camera 像素级滚动时显示半个 tile。
- 带 `viewport_w` / `viewport_h``draw_tilemap` 重载用于子视口绘制,`screen_x` / `screen_y` 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。
- 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。
## 13. 后续建议
1. 保持 `Core` 不依赖 `Apps` 1. 保持 `Core` 不依赖 `Apps`
2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*` 2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*`