IMX6U-Game/README.md

452 lines
22 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.

# IMX6U-Game
一个从 CPU 软光栅化渲染器向嵌入式游戏图形库迁移的项目。
核心目标是在 IMX6UARM Cortex-A7无 GPU 或极弱 GPU上运行纯 CPU 渲染的 2D/3D 图形。为此,代码保持 C++11 兼容显示后端可切换SDL2 窗口 vs Linux Framebuffer以支持跨平台开发和嵌入式部署。
## 编译目标与验证目的
本项目支持三套编译目标,分别对应开发流程的不同阶段:
| 目标平台 | 显示后端 | 验证目的 |
|-------------------|-------------------|-------------------------------------|
| Windows (x86/x64) | SDL2 窗口 | 验证渲染逻辑、算法正确性、快速迭代调试 |
| Linux x86_64 | SDL2 窗口 | 验证 Linux 兼容性、CMake 构建、SDL2 系统依赖 |
| ARM Linux (交叉编译) | SDL2 或 `/dev/fb0` | 最终在 IMX6U 开发板上运行的版本;优先目标以后续实际部署后端为准 |
**为什么分三套?**
- **Windows 编译**:开发主力机通常是 Windows有 IDE 调试、有图形窗口,渲染算法对不对一眼就能看到。这个阶段完全不关心嵌入式细节。
- **Linux x86 编译**:验证代码在 GCC/Clang 下有无警告、CMake 配置是否跨平台、系统 SDL2 依赖是否正确。很多嵌入式工具链的问题在 x86 Linux 上就能提前暴露。
- **ARM 交叉编译**:最终在 IMX6U 上跑。若目标板使用 SDL2则 SDL2 仅作为显示/输入适配层,时间由独立 `Platform::ITimeSource` 提供,核心渲染仍按 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_CORE_ARCHITECTURE.md`
- `Core` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
- 依赖方向必须保持 `Apps -> Shared -> Core -> Platform`,底层图形库不能反向依赖具体游戏。
初期推荐单进程多应用模式Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
## 构建说明
### 通用前置
```bash
# 克隆仓库后,确保子目录完整
cd IMX6U-Game
```
### 构建类型与调试开关
项目有两根独立的条件编译轴,组合出常用的构建形态:
| 轴 | CMake 参数 | 编译宏 | 默认 |
|---|---|---|---|
| 平台软件栈 | `-DTARGET_IMX=ON/OFF` | `TARGET_IMX``TARGET_PC`(互斥、双正向) | `OFF``TARGET_PC` |
| 构建类型 | `-DCMAKE_BUILD_TYPE=Debug/Release` | `IMX6U_DEBUG`(仅 Debug 配置注入) | `Release` |
`IMX6U_DEBUG` 控制开发期辅助内容是否参与构建。当前 LightGame 中下列内容只在 Debug 构建中存在Release 构建会被完全剔除(不参与编译,不进入二进制):
- 关卡编辑器(`LevelEditor` + ImGui`F1` 切换)
- 碰撞框可视化(`LevelRenderer::draw_debug` 与 `LightGameApp::debug_mode_`
- `--debug` 命令行开关
- `[INFO] LightGame started …` 启动日志
硬件初始化失败的 `[WARN]` 提示无论 Debug/Release 都保留,作为现场诊断信息。
> 多配置生成器Visual Studio下 `CMAKE_BUILD_TYPE` 在 configure 阶段被忽略,应在构建阶段用 `--config Debug|Release` 选择单配置生成器Makefile/Ninja使用 `-DCMAKE_BUILD_TYPE=...` 在 configure 阶段决定。
### WindowsVisual Studio / MSVC
仓库已自带 SDL2 开发库(`libs/Win/SDL2`),无需额外安装。
```bash
cmake -B build-win .
cmake --build build-win --config Release
```
只构建某个 App
```bash
cmake --build build-win --config Release --target IMX6U-Game
cmake --build build-win --config Release --target IMX6U-Demo
```
启用 LightGame 关卡编辑器和调试显示,使用 Debug 构建:
```bash
cmake --build build-win --config Debug --target IMX6U-LightGame
./build-win/Debug/IMX6U-LightGame.exe
```
构建 `IMX6U-Game` 时会自动重新生成 Tom 的 atlas 头文件;也可以单独执行:
```bash
cmake --build build-win --config Release --target GenerateTomAtlasHeader
```
运行:
```bash
./build-win/Release/IMX6U-Game.exe
```
可选帧率档位:
```bash
./build-win/Release/IMX6U-Game.exe --fps 30
./build-win/Release/IMX6U-Game.exe --fps 45
./build-win/Release/IMX6U-Game.exe --fps 60
```
当前只接受 `30`、`45`、`60` 三档。主循环从 `Platform::ITimeSource` 读取单调整数毫秒时间,由 `Core::Timer` 生成固定步长 tick并通过 `33/33/34`、`22/22/22/22/23`、`16/17/17` 这类整数节奏逼近对应目标帧率,避免核心时间源依赖 `float` 秒。
### Linux x86_64Ubuntu / WSL2
需要系统 SDL2
```bash
sudo apt-get install libsdl2-dev libsdl2-image-dev cmake g++
```
构建:
```bash
cmake -B build-linux .
cmake --build build-linux
```
只构建某个 App
```bash
cmake --build build-linux --target IMX6U-Game
cmake --build build-linux --target IMX6U-Demo
```
构建 `IMX6U-Game` 时会自动重新生成 Tom 的 atlas 头文件;也可以单独执行:
```bash
cmake --build build-linux --target GenerateTomAtlasHeader
```
运行:
```bash
./build-linux/IMX6U-Game
```
### ARM 交叉编译IMX6U
需要交叉编译工具链:
```bash
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
```
本项目 ARM 端保留两类后端:
- **SDL2 后端**目标是后续游戏主路径SDL2 负责显示、输入和最终 framebuffer 提交,时间源使用独立的 `Platform::ITimeSource`
- **Framebuffer 后端**:作为极简依赖和显示通路对照测试。
> CMake 选项 `TARGET_IMX` 控制的不是"是否交叉编译",而是"使用哪一套软件栈"
> - `TARGET_IMX=ON` —— framebuffer (`/dev/fb0`) + Alsa 音频 + evdev 输入 + AP3216C 光敏,对应 IMX6U 板载硬件。
> - `TARGET_IMX=OFF` —— SDL2 显示/音频/输入/光敏PC 调试和 ARM-SDL 对照路径共用这一栈。
>
> 因此 ARM 交叉编译 + SDL2 后端使用 `-DTARGET_IMX=OFF`,与"运行在 PC"无关。
构建Framebuffer 对照后端):
```bash
cmake -B build-arm-fb \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \
-DTARGET_IMX=ON .
cmake --build build-arm-fb
```
说明单配置生成器Makefile/Ninja默认使用 `Release` 构建ARM / framebuffer 性能测试必须确认 `CMAKE_BUILD_TYPE=Release`,否则逐像素绘制和 `/dev/fb0` 提交会因未优化构建出现数量级偏差。
注意Tom 的 atlas 头文件生成工具是主机侧离线工具,依赖 PC/Linux 主机上的 SDL2_image主机构建 `IMX6U-Game` 时会自动执行ARM 交叉编译过程不会执行它。如果修改了 `src/Apps/Game/assets/raw/` 里的 PNG必须先在 Windows 或 Linux x86 构建目录执行 `GenerateTomAtlasHeader` 或构建一次 `IMX6U-Game`,再进行 ARM 交叉编译。ARM 构建只消费已经生成好的 `src/Apps/Game/generated/tom_atlas.h`
构建SDL2 后端,要求工具链/sysroot 可找到目标板 SDL2 开发库):
```bash
cmake -B build-arm-sdl \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \
-DTARGET_IMX=OFF .
cmake --build build-arm-sdl
```
部署到开发板:
```bash
scp build-arm-sdl/IMX6U-Game root@imx6u:/tmp/
# 或部署 framebuffer 对照版本:
scp build-arm-fb/IMX6U-Game root@imx6u:/tmp/
```
板子上运行:
```bash
/tmp/IMX6U-Game
```
Framebuffer 对照版本可能需要 root 权限访问 `/dev/fb0`。SDL2 版本是否需要额外环境变量或权限,取决于目标板 SDL2 视频驱动和显示栈配置。
## 资源转换工具
项目运行时不依赖 PNG/TTF 解码库。图片和字体资源在离线阶段转换成 C++ 头文件,但运行时格式按资源类型区分:
- sprite / atlas`uint16_t` `RGBA5551`5-bit R/G/B + 1-bit A
- bitmap font`uint8_t` 1-bit mask8 个像素打包为 1 个字节)
sprite 运行时像素格式统一为 `RGBA5551`16-bit内部 backbuffer 统一为 `RGB565`;透明仅支持 1-bit alpha test`A=0` 跳过,`A=1` 覆写)。
当前转换工具位于 `tools/`,需要 Python 和 Pillow
```bash
pip install pillow
```
### Sprite 转换
普通 PNG sprite 使用 `tools/png_to_header.py` 转换:
```bash
python tools/png_to_header.py assets/sprite/test_sprite.png assets/sprite/test_sprite.h test_sprite
```
生成的头文件会包含:
```cpp
test_sprite_width
test_sprite_height
test_sprite_pixels // uint16_t RGBA5551 数组
```
透明像素转换为 RGBA5551 后alpha 仅 1-bit`A=0` 透明,`A=1` 不透明)。当前 demo 通过 `RenderData::Image` 的 color_key 机制跳过指定颜色值。
Tom 游戏资源使用 `SpriteAssetTool` 从固定目录 `src/Apps/Game/assets/raw/` 读取原始 PNG一步生成 atlas 头文件。主机侧构建 `IMX6U-Game` 时 CMake 会自动执行 `GenerateTomAtlasHeader`;也可以单独执行:
```bash
cmake --build build-win --config Release --target GenerateTomAtlasHeader
```
生成结果位于:
```text
src/Apps/Game/generated/tom_atlas.h
```
该头文件包含 `tom_atlas_pixels` 和每张图的 `RenderData::Sprite`,因此板端运行 Tom 游戏时不需要额外部署图片资源文件。尺寸规则、region 名称和 PNG 文件名统一记录在 `src/Apps/Game/tools/asset_pipeline/SpriteAssetTool.cpp` 顶部的 `Sources` 表里CMake 不再重复维护每张 PNG 的路径。
`assets/sprite/` 用于存放测试用 PNG sprite 源文件及转换后的头文件Tom 主游戏不依赖它。Tom 游戏的 atlas 资源由 `src/Apps/Game/tools/asset_pipeline/SpriteAssetTool.cpp``src/Apps/Game/assets/raw/` 读取原始 PNG 生成。
### LightGame Tile Atlas
LightGame 的 tile atlas 配置位于 `assets/tile/tile_atlas.json`,生成结果位于 `src/Apps/LightGame/generated/tile_atlas.h`
```bash
python tools/pack_tile_atlas.py assets/tile/tile_atlas.json src/Apps/LightGame/generated/tile_atlas.h
```
当前 tile id 约定集中在 `src/Apps/LightGame/src/engine/TileIds.h``0` 是空透明 tile`1..4` 是背景装饰;`5..11` 是可碰撞地形;`12`/`22` 是伤害尖刺;`13..18` 是光照平台和门的状态贴图;`19..21` 是金币、旗帜和 checkpoint`23` 是藤蔓装饰。LightGame 关卡支持可选 `background_tiles` 视觉层,该层先于主 tilemap 渲染,不参与碰撞、死亡、触发器或光照 tile 规则。
### Bitmap Font 转换
像素字体图集使用 `tools/gen_font_atlas.py` 生成:
```bash
python tools/gen_font_atlas.py assets/font
```
脚本默认优先读取:
```text
assets/font/ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf
```
输出:
```text
assets/font/font_atlas.png
assets/font/font_atlas.h
```
字体范围为 ASCII 32~126按 16 列排列。生成端会先把字体 alpha 阈值化为 0/255再按整张 atlas 逐行打包成 **row-major、MSB-first** 的 1-bit mask
- `font_atlas_mask[]` 中每个 `uint8_t` 表示连续 8 个像素
- bit=1 表示该像素需要绘制
- bit=0 表示跳过
运行时 `DrawContext::draw_text` 不再读取字体 atlas 的 RGB/A 颜色,而是直接把 mask 中 bit=1 的像素写成调用方传入文字颜色对应的 `RGB565`
## 显示后端架构
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Core` 的平台适配层:
```
┌──────────────────────────────────────────────┐
│ AppsLauncher / GameA / GameB │
├──────────────────────────────────────────────┤
│ SharedUI、存档、配置、资源索引 │
├──────────────────────────────────────────────┤
│ CoreDrawContext、FrameBuffer、Draw2D、Math │
├──────────────────────────────────────────────┤
│ Platform::IDisplay │
│ - SDLDisplay : SDL2 显示/输入适配 │
│ - ITimeSource: 单调整数毫秒时间源 │
│ - FBDisplay : /dev/fb0 对照后端 │
└──────────────────────────────────────────────┘
```
切换显示后端不应影响应用层和核心绘制逻辑;当前 CMake 通过 `TARGET_IMX` 在 framebuffer (IMX6U) 与 SDL2 (PC) 后端间切换,对应代码层的 `TARGET_IMX` / `TARGET_PC` 双正向宏。
## 目录结构
```text
IMX6U-Game/
├─ cmake/
│ └─ toolchain-arm-linux-gnueabihf.cmake # ARM 交叉编译工具链
├─ libs/
│ └─ Win/
│ ├─ SDL2/ # Windows 用 SDL2 库(头文件 + lib + DLL
│ └─ SDL_image/ # SDL2_image 库(头文件 + lib + DLL
├─ assets/
│ ├─ font/ # 像素字体源文件、font_atlas.png、font_atlas.h
│ └─ sprite/ # PNG sprite 源文件及转换后的头文件
├─ tools/
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
│ └─ png_to_header.py # PNG -> uint16_t RGBA5551 header
├─ src/
│ ├─ Core/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口
│ │ ├─ Core/ # FrameBuffer、DepthBuffer、Renderer、Timer
│ │ ├─ Math/ # 向量、矩阵、数学工具
│ │ ├─ Rasterizer/ # 线段、三角形光栅化
│ │ ├─ RenderData/ # Color、Image、Sprite、Tilemap、BitmapFont 等数据结构
│ │ ├─ Scene/ # Camera、Transform、Mesh、Model
│ │ ├─ Shading/ # BlinnPhongShader 等着色器
│ │ ├─ Platform/ # IDisplay、SDLDisplay、FBDisplay、ITimeSource
│ │ │ # IAudioInput/Output、IButtonInput、IPointerInput
│ │ │ # ALSA / evdev / SDL2 后端、DefaultHardware
│ │ └─ Asset/ # ObjLoader、SpriteAssetLoader
│ ├─ Apps/
│ │ ├─ Demo/ # 2D sprite/tilemap 性能测试入口
│ │ └─ Game/ # Tom 游戏
│ │ ├─ Main.cpp # 游戏入口
│ │ ├─ generated/tom_atlas.h # 自动生成的 atlas 头文件
│ │ ├─ tools/asset_pipeline/ # SpriteAssetTool离线资源转换
│ │ └─ src/
│ │ ├─ app/ # TomGameApp
│ │ ├─ audio/ # VoiceEffect、VoicePlayer、VoiceRecorder
│ │ ├─ components/ # SpriteAnimator
│ │ ├─ scenes/ # TomScene
│ │ └─ systems/ # AnimationSystem
│ └─ test_fb.cpp # 独立 fb 测试(最小示例)
├─ docs/
│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
│ ├─ APP_AND_CORE_ARCHITECTURE.md # 应用层与图形库分层
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
├─ CMakeLists.txt
└─ README.md
```
## 模块说明
### Draw2D
- **DrawContext**:统一绘制入口,封装 FrameBuffer、DepthBuffer、Rasterizer、TriangleRasterizer对外提供 `clear`、`clear_color`、`clear_depth`、`draw_line`、`draw_triangle`、`draw_sprite`、`draw_sprite_ex`、`draw_text`、`draw_tilemap`、`fill_rect`、`present` 接口
### RenderData
- **Image**:通用图像数据结构,持有 `const void* pixels``PixelFormat`(当前统一 `RGBA5551`),支持 color_key 透明跳过
- **Sprite**:描述 atlas 中的子区域,通过 `const Image* atlas` 引用源图,是对外 sprite 绘制单位
- **Tilemap**:使用 `uint16_t` tile id 引用 atlas 中的固定大小 tile`EmptyTile` (`0xFFFF`) 表示空 tile
- **BitmapFont**bitmap 字体数据,持有 `uint8_t* mask_bits`row-major、MSB-first 1-bit mask支持 ASCII 范围字符绘制
- **Color**RGBA8888 颜色值用于绘制接口参数和调试sprite 运行时像素格式仍为 RGBA5551
### Core
- **FrameBuffer**CPU 侧 RGB565 颜色缓冲,渲染结果先写在这里
- **DepthBuffer**:深度测试用 Z-buffer
- **Renderer**:渲染器辅助工具
- **Timer**:整数毫秒固定步长 tick 生成器,支持 30/45/60 FPS 档位和每帧剩余时间计算
### Math
- 通用数学类型:`Vector2/3/4`、`Matrix4x4`
- 纯头文件实现,无动态分配
### Rasterizer
- **Rasterizer**Bresenham 线段光栅化,入口做快速全屏可见性检查,屏幕内走 `set_pixel_unsafe` 快路径,屏幕外走 Cohen-Sutherland 裁剪
- **TriangleRasterizer**:扫描线三角形填充 + 定点深度插值(增量式整数边缘函数,内层循环无 float 运算)
### Platform
- **IDisplay**:显示后端抽象,解耦渲染与输出
- **SDLDisplay**SDL2 后端PC 调试和 IMX6U SDL2 目标路径共用这一类适配思想
- **FBDisplay**`/dev/fb0` 对照后端,用于极简显示通路验证
- **ITimeSource / SteadyTimeSource**独立时间源接口与单调时钟实现Linux/IMX6U 使用 `clock_gettime(CLOCK_MONOTONIC)`Windows 使用 `std::chrono::steady_clock`Display 不再承担计时职责
- **IAudioInput / IAudioOutput**:音频输入/输出抽象接口SDL2 和 ALSA 两套后端
- **IButtonInput**按键输入抽象接口SDL2 键盘和 Linux evdev 两套后端
- **IPointerInput**:触摸/指针输入抽象接口SDL2 和 Linux evdev 两套后端
- **DefaultHardware**:根据编译配置自动选择默认音频、按键、指针后端的 typedef
### Framebuffer 性能说明
`FBDisplay` 是直接写 `/dev/fb0` 的对照后端。当前实现会从 CPU 侧 `RGB565 FrameBuffer` 提交到系统 framebuffer并针对常见像素格式提供快速路径
- RGB565直接行拷贝
- ARGB8888 / XRGB8888 类 32bpp使用专用 `RGB565 -> 32-bit` 转换;
- 其他格式:走统一 display conversion。
板端性能测试必须使用 Release 构建。一次测试中,未优化 ARM 构建曾导致 `Frame:81ms / Present:69ms`,开启 Release 后同一轻量 2D demo 可达到约 76 FPS。该结果说明 `/dev/fb0` 整屏提交仍是关键热点,但构建类型会极大影响结论。后续优化优先级:
1. 对 32-bit fb0 增加更快的 `RGB565 -> 目标格式` 批量转换;
2. 2D 场景使用 dirty rect / 局部提交,避免每帧整屏写入;
3. 避免无 3D 内容时清理 depth buffer
4. 对 tile/sprite 增加不透明行拷贝、预转换资源或专用批处理路径。
## 当前状态与后续
**已完成:**
- 可旋转立方体的 3D 渲染MVP 变换、背面剔除、扫描线填充、深度测试)
- 双平台显示后端SDL2 / Framebuffer
- 离线资源转换工具PNG sprite -> RGBA5551 C++ 头文件,像素字体 -> bitmap atlas/header
- 基础 2D sprite、atlas 子图 sprite、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
- Core 目录规范化,代码收敛到 `src/Core/`
- `Core::DrawContext` 统一绘制入口,封装现有绘制能力
- C++11 兼容代码
- CMake 跨平台构建
**待完成(按优先级):**
1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON
2. 应用层拆分Launcher / GameA / GameB / Shared和统一 `IApp` 主循环
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
4. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
5. 纹理贴图、OBJ 模型加载与完整光照
## 说明
- 代码标准:**C++11**,确保兼容嵌入式老工具链
- `test_fb.cpp` 是一个完全独立的 Linux framebuffer 最小测试,不依赖项目其他代码,用于快速验证板子显示通路
- Windows、Linux x86 和目标板 SDL2 路径共享 `SDLDisplay` 适配思想;`FBDisplay` 保留为 framebuffer 对照后端