IMX6U-Game/docs/APP_AND_CORE_ARCHITECTURE.md

338 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 应用层与底层库分层设计
本文记录项目的代码分层:`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。文档同时记录当前已实现状态和目标架构。
> 文档分工IMX6U 运行时性能红线记录在 `DEVELOPMENT_GUIDELINES.md`;坐标、矩阵、深度等数学语义记录在 `CONVENTIONS.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
## 1. 当前目录结构
```text
src/
Core/ # 底层库:渲染、数学、资源、平台适配
Asset/ # 离线资源格式加载
Core/ # FrameBuffer、DepthBuffer、Renderer、Timer
Draw2D/ # Core::DrawContext
Math/ # Vector、Matrix、MathUtil
Platform/ # 显示、时间、音频、按键等平台接口和后端
Rasterizer/ # 线段和三角形光栅化
RenderData/ # Color、Image、Triangle、Tilemap 等数据结构
Scene/ # Camera、Transform、Mesh、Model
Shading/ # Shader 相关代码
Apps/
Demo/ # 2D sprite/tilemap 性能测试入口
Game/ # Tom 游戏
generated/ # 自动生成的 tom_atlas.h
tools/ # SpriteAssetTool离线资源转换
src/
app/ # TomGameApp
audio/ # VoiceEffect、VoicePlayer、VoiceRecorder
components/ # SpriteAnimator
scenes/ # TomScene
systems/ # AnimationSystem
```
`src/Core/Core` 这个二级目录保留的是原底层库里的核心运行时对象。它和顶层 `src/Core` 名称重复,但含义不同:
- `src/Core`:整个底层库。
- `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。
后续如果觉得重复命名影响阅读,可以再把 `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、atlas 子图 sprite、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. 依赖方向
允许:
```text
Apps -> Shared -> Core -> Platform
Apps -> Core
```
禁止反向依赖:
```text
Core -> Apps 禁止
Core -> Shared 禁止
GameA -> GameB 禁止
GameB -> GameA 禁止
Platform -> Game 禁止
```
Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。
## 5. 平台层接口
平台层采用"抽象接口 + 多套后端实现"的模式。游戏代码只能依赖这些 `I*` 接口。
### 5.1 显示
`Platform::IDisplay``src/Core/Platform/Display.h`
```text
Platform::IDisplay
SDLDisplay # PC / SDL2 调试后端
FBDisplay # Linux /dev/fb0 后端
```
### 5.2 音频输入
```text
Platform::IAudioInput
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::DepthBuffer`
- `Rasterizer::Rasterizer`
- `Rasterizer::TriangleRasterizer`
对外提供:
```cpp
Core::DrawContext ctx(width, height);
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(x, y, sprite);
ctx.draw_text(font, x, y, color, "text");
ctx.present(display);
```
## 7. 应用统一接口(待实现)
推荐为 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(Core::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` 这类热路径函数不要虚化。
当前状态:`IApp` 接口尚未实现。`src/Apps/Game/Main.cpp` 直接编写主循环,未经过 `IApp` 抽象。
## 8. 命名建议
- 项目仓库仍叫 `IMX6U-Game`
- 底层库命名为 `Core`
- 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。
- CMake target 可以是:
- `imx6u_core` 静态库
- `imx6u_launcher`
- `imx6u_game_a`
- `imx6u_game_b`
- 或初期单可执行文件 `imx6u_suite`
## 9. 推荐演进顺序
1. ~~先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。~~ 未实现,当前直接写主循环。
2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~ **已完成**
3. ~~建立 `Core::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**`Core::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、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 拆分。
## 10. 性能注意事项
- 应用切换不应重复销毁/创建 SDL window、renderer、texture。
- 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源Display 不承担计时职责。
- `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++ 头文件;运行时字体数据使用 `uint8_t` 1-bit maskrow-major、MSB-first
- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。
- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应继续评估 1-bit mask、RLE 或自定义资源包格式。
## 12. Sprite 与 Tilemap 约定
- `RenderData::Sprite` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。
- `DrawContext::draw_sprite` 是对外 sprite 绘制入口;`draw_sprite_ex` 只在需要 scale / flip 时使用。两者都以 `RenderData::Sprite` 为渲染单位,内部再按 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`
2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*`
3. 新增底层绘制、数学、资源格式、平台显示能力放在 `src/Core`
4. 如果继续清理命名,优先处理 `src/Core/Core` 这个重复目录名。