22 KiB
IMX6U-Game
一个从 CPU 软光栅化渲染器向嵌入式游戏图形库迁移的项目。
核心目标是在 IMX6U(ARM 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 接口切换当前应用。
构建说明
通用前置
# 克隆仓库后,确保子目录完整
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 阶段决定。
Windows(Visual Studio / MSVC)
仓库已自带 SDL2 开发库(libs/Win/SDL2),无需额外安装。
cmake -B build-win .
cmake --build build-win --config Release
只构建某个 App:
cmake --build build-win --config Release --target IMX6U-Game
cmake --build build-win --config Release --target IMX6U-Demo
启用 LightGame 关卡编辑器和调试显示,使用 Debug 构建:
cmake --build build-win --config Debug --target IMX6U-LightGame
./build-win/Debug/IMX6U-LightGame.exe
构建 IMX6U-Game 时会自动重新生成 Tom 的 atlas 头文件;也可以单独执行:
cmake --build build-win --config Release --target GenerateTomAtlasHeader
运行:
./build-win/Release/IMX6U-Game.exe
可选帧率档位:
./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_64(Ubuntu / WSL2)
需要系统 SDL2:
sudo apt-get install libsdl2-dev libsdl2-image-dev cmake g++
构建:
cmake -B build-linux .
cmake --build build-linux
只构建某个 App:
cmake --build build-linux --target IMX6U-Game
cmake --build build-linux --target IMX6U-Demo
构建 IMX6U-Game 时会自动重新生成 Tom 的 atlas 头文件;也可以单独执行:
cmake --build build-linux --target GenerateTomAtlasHeader
运行:
./build-linux/IMX6U-Game
ARM 交叉编译(IMX6U)
需要交叉编译工具链:
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 对照后端):
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 开发库):
cmake -B build-arm-sdl \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \
-DTARGET_IMX=OFF .
cmake --build build-arm-sdl
部署到开发板:
scp build-arm-sdl/IMX6U-Game root@imx6u:/tmp/
# 或部署 framebuffer 对照版本:
scp build-arm-fb/IMX6U-Game root@imx6u:/tmp/
板子上运行:
/tmp/IMX6U-Game
Framebuffer 对照版本可能需要 root 权限访问 /dev/fb0。SDL2 版本是否需要额外环境变量或权限,取决于目标板 SDL2 视频驱动和显示栈配置。
资源转换工具
项目运行时不依赖 PNG/TTF 解码库。图片和字体资源在离线阶段转换成 C++ 头文件,但运行时格式按资源类型区分:
- sprite / atlas:
uint16_tRGBA5551(5-bit R/G/B + 1-bit A) - bitmap font:
uint8_t1-bit mask(8 个像素打包为 1 个字节)
sprite 运行时像素格式统一为 RGBA5551(16-bit),内部 backbuffer 统一为 RGB565;透明仅支持 1-bit alpha test(A=0 跳过,A=1 覆写)。
当前转换工具位于 tools/,需要 Python 和 Pillow:
pip install pillow
Sprite 转换
普通 PNG sprite 使用 tools/png_to_header.py 转换:
python tools/png_to_header.py assets/sprite/test_sprite.png assets/sprite/test_sprite.h test_sprite
生成的头文件会包含:
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;也可以单独执行:
cmake --build build-win --config Release --target GenerateTomAtlasHeader
生成结果位于:
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:
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 生成:
python tools/gen_font_atlas.py assets/font
脚本默认优先读取:
assets/font/ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf
输出:
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 的平台适配层:
┌──────────────────────────────────────────────┐
│ Apps:Launcher / GameA / GameB │
├──────────────────────────────────────────────┤
│ Shared:UI、存档、配置、资源索引 │
├──────────────────────────────────────────────┤
│ Core:DrawContext、FrameBuffer、Draw2D、Math │
├──────────────────────────────────────────────┤
│ Platform::IDisplay │
│ - SDLDisplay : SDL2 显示/输入适配 │
│ - ITimeSource: 单调整数毫秒时间源 │
│ - FBDisplay : /dev/fb0 对照后端 │
└──────────────────────────────────────────────┘
切换显示后端不应影响应用层和核心绘制逻辑;当前 CMake 通过 TARGET_IMX 在 framebuffer (IMX6U) 与 SDL2 (PC) 后端间切换,对应代码层的 TARGET_IMX / TARGET_PC 双正向宏。
目录结构
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_ttile 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 整屏提交仍是关键热点,但构建类型会极大影响结论。后续优化优先级:
- 对 32-bit fb0 增加更快的
RGB565 -> 目标格式批量转换; - 2D 场景使用 dirty rect / 局部提交,避免每帧整屏写入;
- 避免无 3D 内容时清理 depth buffer;
- 对 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 跨平台构建
待完成(按优先级):
- FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON)
- 应用层拆分(Launcher / GameA / GameB / Shared)和统一
IApp主循环 - SDL2 输入抽象(键盘/触摸/按键状态快照)
- Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
- 纹理贴图、OBJ 模型加载与完整光照
说明
- 代码标准:C++11,确保兼容嵌入式老工具链
test_fb.cpp是一个完全独立的 Linux framebuffer 最小测试,不依赖项目其他代码,用于快速验证板子显示通路- Windows、Linux x86 和目标板 SDL2 路径共享
SDLDisplay适配思想;FBDisplay保留为 framebuffer 对照后端