merge remote and local changes

This commit is contained in:
HP 2026-06-07 10:39:29 +08:00
commit b093796e61
119 changed files with 6484 additions and 105 deletions

8
.gitignore vendored
View File

@ -52,8 +52,12 @@ Thumbs.db
.codex .codex
CLAUDE.md CLAUDE.md
AGENTS.md AGENTS.md
omc .omc
omx .omx
build-win build-win
build-linux build-linux
.idea
assets/test

View File

@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF) option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
<<<<<<< HEAD
set(ENGINE_SOURCES set(ENGINE_SOURCES
src/Asset/ObjLoader.cpp src/Asset/ObjLoader.cpp
src/Asset/SpriteAssetLoader.cpp src/Asset/SpriteAssetLoader.cpp
@ -26,6 +27,19 @@ set(ENGINE_SOURCES
src/Rasterizer/TriangleRasterizer.cpp src/Rasterizer/TriangleRasterizer.cpp
src/Scene/Camera.cpp src/Scene/Camera.cpp
src/Shading/BlinnPhongShader.cpp src/Shading/BlinnPhongShader.cpp
=======
set(SOURCES
src/Apps/Demo/main.cpp
src/Gfx/Asset/ObjLoader.cpp
src/Gfx/Core/DepthBuffer.cpp
src/Gfx/Core/FrameBuffer.cpp
src/Gfx/Core/Renderer.cpp
src/Gfx/Draw2D/DrawContext.cpp
src/Gfx/Rasterizer/Rasterizer.cpp
src/Gfx/Rasterizer/TriangleRasterizer.cpp
src/Gfx/Scene/Camera.cpp
src/Gfx/Shading/BlinnPhongShader.cpp
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5
) )
set(ENGINE_INCLUDE_DIRS set(ENGINE_INCLUDE_DIRS
@ -50,14 +64,30 @@ set(SOURCES
) )
if(USE_FRAMEBUFFER) if(USE_FRAMEBUFFER)
list(APPEND SOURCES src/Platform/FBDisplay.cpp) list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp)
else() else()
list(APPEND SOURCES src/Platform/SDLDisplay.cpp) list(APPEND SOURCES src/Gfx/Platform/SDLDisplay.cpp)
endif() endif()
add_executable(IMX6U-Game ${SOURCES}) add_executable(IMX6U-Game ${SOURCES})
<<<<<<< HEAD
target_include_directories(IMX6U-Game PRIVATE ${ENGINE_INCLUDE_DIRS}) target_include_directories(IMX6U-Game PRIVATE ${ENGINE_INCLUDE_DIRS})
=======
target_include_directories(IMX6U-Game PRIVATE
src/Gfx/Platform
src/Gfx/Asset
src/Gfx/Core
src/Gfx/Draw2D
src/Gfx/Math
src/Gfx/Rasterizer
src/Gfx/RenderData
src/Gfx/Scene
src/Gfx/Shading
assets/font
assets/sprite
)
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5
if(USE_FRAMEBUFFER) if(USE_FRAMEBUFFER)
target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER) target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER)

229
README.md
View File

@ -8,17 +8,40 @@
本项目支持三套编译目标,分别对应开发流程的不同阶段: 本项目支持三套编译目标,分别对应开发流程的不同阶段:
| 目标平台 | 显示后端 | 验证目的 | | 目标平台 | 显示后端 | 验证目的 |
|---------|---------|---------| |-------------------|-------------------|-------------------------------------|
| Windows (x86/x64) | SDL2 窗口 | 验证渲染逻辑、算法正确性、快速迭代调试 | | Windows (x86/x64) | SDL2 窗口 | 验证渲染逻辑、算法正确性、快速迭代调试 |
| Linux x86_64 | SDL2 窗口 | 验证 Linux 兼容性、CMake 构建、SDL2 系统依赖 | | Linux x86_64 | SDL2 窗口 | 验证 Linux 兼容性、CMake 构建、SDL2 系统依赖 |
| ARM Linux (交叉编译) | `/dev/fb0` 直接写屏 | 最终在 IMX6U 开发板上运行的版本 | | ARM Linux (交叉编译) | SDL2 或 `/dev/fb0` | 最终在 IMX6U 开发板上运行的版本;优先目标以后续实际部署后端为准 |
**为什么分三套?** **为什么分三套?**
- **Windows 编译**:开发主力机通常是 Windows有 IDE 调试、有图形窗口,渲染算法对不对一眼就能看到。这个阶段完全不关心嵌入式细节。 - **Windows 编译**:开发主力机通常是 Windows有 IDE 调试、有图形窗口,渲染算法对不对一眼就能看到。这个阶段完全不关心嵌入式细节。
- **Linux x86 编译**:验证代码在 GCC/Clang 下有无警告、CMake 配置是否跨平台、系统 SDL2 依赖是否正确。很多嵌入式工具链的问题在 x86 Linux 上就能提前暴露。 - **Linux x86 编译**:验证代码在 GCC/Clang 下有无警告、CMake 配置是否跨平台、系统 SDL2 依赖是否正确。很多嵌入式工具链的问题在 x86 Linux 上就能提前暴露。
- **ARM 交叉编译**:最终在 IMX6U 上跑。这个版本去掉 SDL2直接 mmap `/dev/fb0` 写屏,减少依赖和内存开销。 - **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_GFX_ARCHITECTURE.md`
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
- 依赖方向必须保持 `Apps -> Shared -> Gfx -> Platform`,底层图形库不能反向依赖具体游戏。
初期推荐单进程多应用模式Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
## 构建说明 ## 构建说明
@ -43,6 +66,15 @@ cmake --build build-win --config Release
./build-win/Release/IMX6U-Game.exe ./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 ### Linux x86_64Ubuntu / WSL2
需要系统 SDL2 需要系统 SDL2
@ -70,43 +102,116 @@ cmake --build build-linux
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
``` ```
构建(启用 Framebuffer 后端): 本项目 ARM 端保留两类后端:
- **SDL2 后端**目标是后续游戏主路径SDL2 负责显示、输入和最终 framebuffer 提交,时间源使用独立的 `Platform::ITimeSource`
- **Framebuffer 后端**:作为极简依赖和显示通路对照测试。
构建Framebuffer 对照后端):
```bash ```bash
cmake -B build-arm \ cmake -B build-arm-fb \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \ -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-linux-gnueabihf.cmake \
-DUSE_FRAMEBUFFER=ON . -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 ```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 ```bash
sudo /tmp/IMX6U-Game /tmp/IMX6U-Game
``` ```
ARM 版本启动后会打印 framebuffer 参数分辨率、bpp、像素格式。按 `q` 键或 `Ctrl+C` 退出。 Framebuffer 对照版本可能需要 root 权限访问 `/dev/fb0`。SDL2 版本是否需要额外环境变量或权限,取决于目标板 SDL2 视频驱动和显示栈配置。
## 资源转换工具
项目运行时不依赖 PNG/TTF 解码库。图片和字体资源在离线阶段转换成 C++ 头文件,运行时直接以 `uint32_t` 数组访问,像素格式统一为:
```text
(R << 24) | (G << 16) | (B << 8) | A
```
当前转换工具位于 `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
```
透明像素仍保留 alpha当前 demo 通过 `RenderData::Image(..., 0x00000000)` 把全透明像素作为 color key 跳过。
### 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以匹配当前 `DrawContext::draw_text` 的像素风路径;运行时只把非透明像素替换成调用方指定颜色。
## 显示后端架构 ## 显示后端架构
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦: 显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层
``` ```
┌──────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
│ 游戏/渲染逻辑Camera、Rasterizer 等) │ │ AppsLauncher / GameA / GameB │
├──────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ Core::FrameBufferCPU 颜色缓冲区) │ │ SharedUI、存档、配置、资源索引 │
├──────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ Platform::IDisplay │ │ GfxDrawContext、FrameBuffer、Draw2D、Math │
│ - SDLDisplay : SDL2 窗口PC ├──────────────────────────────────────────────┤
│ - FBDisplay : /dev/fb0ARM │ Platform::IDisplay │
└──────────────────────────────────────┘ │ - SDLDisplay : SDL2 显示/输入适配 │
│ - ITimeSource: 单调整数毫秒时间源 │
│ - FBDisplay : /dev/fb0 对照后端 │
└──────────────────────────────────────────────┘
``` ```
切换后端不需要修改任何渲染代码,只需要在 CMake 配置时开关 `USE_FRAMEBUFFER` 切换显示后端不应影响应用层和核心绘制逻辑;当前 CMake 通过 `USE_FRAMEBUFFER` 在 SDL2 与 framebuffer 后端间切换
## 目录结构 ## 目录结构
@ -118,44 +223,45 @@ IMX6U-Game/
│ └─ Win/ │ └─ Win/
│ ├─ SDL2/ # Windows 用 SDL2 库(头文件 + lib + DLL │ ├─ SDL2/ # Windows 用 SDL2 库(头文件 + lib + DLL
│ └─ SDL_image/ # SDL2_image 库(头文件 + 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 -> uint32_t RGBA header
├─ src/ ├─ src/
│ ├─ Platform/ │ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Display.h # 显示抽象接口 │ │ ├─ Draw2D/ # DrawContext 统一绘制入口
│ │ ├─ SDLDisplay.cpp # SDL2 窗口后端 │ │ ├─ Core/ # FrameBuffer、DepthBuffer
│ │ └─ FBDisplay.cpp # Linux Framebuffer 后端 │ │ ├─ Math/ # 向量、矩阵、数学工具
│ ├─ Core/ │ │ ├─ Rasterizer/ # 线段、三角形光栅化
│ │ ├─ FrameBuffer.h/.cpp # CPU 颜色缓冲 │ │ ├─ RenderData/ # Color、Triangle 等数据结构
│ │ ├─ DepthBuffer.h/.cpp # 深度缓冲 │ │ ├─ Scene/ # Camera、Transform、Mesh
│ │ └─ Renderer.h/.cpp # 渲染调度(预留) │ │ ├─ Shading/ # 着色器(预留)
│ ├─ Math/ │ │ ├─ Platform/ # IDisplay、SDLDisplay、FBDisplay、ITimeSource
│ │ ├─ Vector2.h / Vector3.h / Vector4.h │ │ └─ Asset/ # ObjLoader 等资源加载
│ │ ├─ Matrix4x4.h │ ├─ Apps/
│ │ └─ MathUtil.h │ │ └─ Demo/ # 当前 3D 立方体 demo 入口
│ ├─ Rasterizer/ │ └─ test_fb.cpp # 独立 fb 测试(最小示例)
│ │ ├─ Rasterizer.h/.cpp # 线段光栅化 ├─ docs/
│ │ └─ TriangleRasterizer.h/.cpp # 三角形填充 │ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
│ ├─ RenderData/ │ ├─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层
│ │ ├─ Color.h │ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
│ │ ├─ 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 # 着色(预留)
│ ├─ test_fb.cpp # 独立 fb 测试(最小示例)
│ └─ main.cpp
├─ CMakeLists.txt ├─ CMakeLists.txt
└─ README.md └─ README.md
``` ```
## 模块说明 ## 模块说明
### Draw2D
- **DrawContext**:统一绘制入口,封装 FrameBuffer、DepthBuffer、Rasterizer、TriangleRasterizer对外提供 `clear`、`draw_line`、`draw_triangle`、`draw_sprite`、`draw_sprite_region`、`draw_text`、`draw_tilemap`、`present` 接口
- **SpriteRegion**:描述 atlas 中的子区域,`draw_sprite_region` / `draw_sprite_region_ex` 可直接绘制子图,底层复用 `draw_sprite_ex`
- **Tilemap**:使用 `uint16_t` tile id 引用 atlas 中的固定大小 tile`draw_tilemap` 按视口可见范围遍历 tile并在视口边缘做像素级裁剪
### Core ### Core
- **FrameBuffer**CPU 侧颜色缓冲,渲染结果先写在这里 - **FrameBuffer**CPU 侧颜色缓冲,渲染结果先写在这里
- **DepthBuffer**:深度测试用 Z-buffer - **DepthBuffer**:深度测试用 Z-buffer
- **Timer**:整数毫秒固定步长 tick 生成器,支持 30/45/60 FPS 档位和每帧剩余时间计算
### Math ### Math
- 通用数学类型:`Vector2/3/4`、`Matrix4x4` - 通用数学类型:`Vector2/3/4`、`Matrix4x4`
@ -167,26 +273,31 @@ IMX6U-Game/
### Platform ### Platform
- **IDisplay**:显示后端抽象,解耦渲染与输出 - **IDisplay**:显示后端抽象,解耦渲染与输出
- **SDLDisplay**PC 端用 SDL2 窗口实时查看效果 - **SDLDisplay**SDL2 后端PC 调试和 IMX6U SDL2 目标路径共用这一类适配思想
- **FBDisplay**ARM 端直接写 `/dev/fb0` - **FBDisplay**`/dev/fb0` 对照后端,用于极简显示通路验证
- **ITimeSource / SteadyTimeSource**独立时间源接口与单调时钟实现Linux/IMX6U 使用 `clock_gettime(CLOCK_MONOTONIC)`Windows 使用 `std::chrono::steady_clock`Display 不再承担计时职责
## 当前状态与后续 ## 当前状态与后续
**已完成:** **已完成:**
- 可旋转立方体的 3D 渲染MVP 变换、背面剔除、扫描线填充、深度测试) - 可旋转立方体的 3D 渲染MVP 变换、背面剔除、扫描线填充、深度测试)
- 双平台显示后端SDL2 / Framebuffer - 双平台显示后端SDL2 / Framebuffer
- 离线资源转换工具PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
- C++11 兼容代码 - C++11 兼容代码
- CMake 跨平台构建 - CMake 跨平台构建
**待完成(按优先级):** **待完成(按优先级):**
1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON 1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON
2. 输入抽象(键盘/触摸屏 evdev 2. 应用层拆分Launcher / GameA / GameB / Shared和统一 `IApp` 主循环
3. 纹理贴图 3. SDL2 输入抽象(键盘/触摸/按键状态快照)
4. 2D 游戏层封装Sprite、Tilemap 4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径
5. OBJ 模型加载与完整光照 5. 纹理贴图、OBJ 模型加载与完整光照
## 说明 ## 说明
- 代码标准:**C++11**,确保兼容嵌入式老工具链 - 代码标准:**C++11**,确保兼容嵌入式老工具链
- `test_fb.cpp` 是一个完全独立的 Linux framebuffer 最小测试,不依赖项目其他代码,用于快速验证板子显示通路 - `test_fb.cpp` 是一个完全独立的 Linux framebuffer 最小测试,不依赖项目其他代码,用于快速验证板子显示通路
- Windows 和 Linux x86 版本共享 `SDLDisplay`ARM 版本单独使用 `FBDisplay` - Windows、Linux x86 和目标板 SDL2 路径共享 `SDLDisplay` 适配思想;`FBDisplay` 保留为 framebuffer 对照后端

1417
assets/font/font_atlas.h Normal file

File diff suppressed because it is too large Load Diff

BIN
assets/font/font_atlas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,25 @@
// Auto-generated by tools/png_to_header.py
#pragma once
#include <cstdint>
static const int32_t test_sprite_width = 16;
static const int32_t test_sprite_height = 16;
static const uint32_t test_sprite_pixels[] = {
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

View File

@ -0,0 +1,89 @@
# This is the CMakeCache file.
# For build in directory: e:/嵌入式实验/实训/IMX6U-Game/build-check
# It was generated by CMake: C:/Program Files/CMake/bin/cmake.exe
# You can edit this file to change values found and used by cmake.
# If you do not want to change any of the values, simply exit the editor.
# If you do want to change a value, simply edit, save, and exit the editor.
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
//Semicolon separated list of supported configuration types, only
// supports Debug, Release, MinSizeRel, and RelWithDebInfo, anything
// else will be ignored.
CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo
//Value Computed by CMake.
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game/build-check/CMakeFiles/pkgRedirects
//Value Computed by CMake
CMAKE_PROJECT_COMPAT_VERSION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_DESCRIPTION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_NAME:STATIC=IMX6U-Game
//Value Computed by CMake
CMAKE_PROJECT_SPDX_LICENSE:STATIC=
//Value Computed by CMake
IMX6U-Game_BINARY_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game/build-check
//Value Computed by CMake
IMX6U-Game_IS_TOP_LEVEL:STATIC=ON
//Value Computed by CMake
IMX6U-Game_SOURCE_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game
########################
# INTERNAL cache entries
########################
//This is the directory where this CMakeCache.txt was created
CMAKE_CACHEFILE_DIR:INTERNAL=e:/嵌入式实验/实训/IMX6U-Game/build-check
//Major version of cmake used to create the current loaded cache
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=4
//Minor version of cmake used to create the current loaded cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=3
//Patch version of cmake used to create the current loaded cache
CMAKE_CACHE_PATCH_VERSION:INTERNAL=3
//Path to CMake executable.
CMAKE_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cmake.exe
//Path to cpack program executable.
CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cpack.exe
//Path to ctest program executable.
CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/CMake/bin/ctest.exe
//Name of external makefile project generator.
CMAKE_EXTRA_GENERATOR:INTERNAL=
//Name of generator.
CMAKE_GENERATOR:INTERNAL=Visual Studio 17 2022
//Generator instance identifier.
CMAKE_GENERATOR_INSTANCE:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community
//Name of generator platform.
CMAKE_GENERATOR_PLATFORM:INTERNAL=
//Name of generator toolset.
CMAKE_GENERATOR_TOOLSET:INTERNAL=
//Source directory with the top level CMakeLists.txt file for this
// project
CMAKE_HOME_DIRECTORY:INTERNAL=E:/嵌入式实验/实训/IMX6U-Game
//Name of CMakeLists files to read
CMAKE_LIST_FILE_NAME:INTERNAL=CMakeLists.txt
//number of local generators
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
//Platform information initialized
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
//Path to CMake installation.
CMAKE_ROOT:INTERNAL=C:/Program Files/CMake/share/cmake-4.3

View File

@ -0,0 +1,259 @@
# 应用层与图形库分层设计
本项目后续包含三个应用层目标:两个游戏和一个启动器;同时还需要沉淀一套可复用的 IMX6U 轻量图形库。为了避免后期耦合和性能返工,必须明确区分“应用层”和“底层库”。
## 1. 推荐总体结构
推荐拆成四个逻辑层:
```text
IMX6U-Game/
├─ src/
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口(✅ 已实现)
│ │ ├─ Core/ # FrameBuffer、DepthBuffer✅ 已实现)
│ │ ├─ Math/ # 向量、矩阵、数学工具(✅ 已实现)
│ │ ├─ Rasterizer/ # 线段、三角形光栅化(✅ 已实现)
│ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现)
│ │ ├─ Scene/ # Camera、Transform✅ 已实现)
│ │ ├─ Shading/ # 着色器(预留)
│ │ ├─ Platform/ # SDL2 / fb0 显示适配与独立时间源(✅ 已实现)
│ │ └─ Asset/ # 资源加载(✅ 已实现)
│ ├─ Apps/
│ │ ├─ Demo/ # 当前 3D 立方体 demo✅ 已实现)
│ │ ├─ Launcher/ # 启动器应用(待实现)
│ │ ├─ GameA/ # 第一个游戏(待实现)
│ │ └─ GameB/ # 第二个游戏(待实现)
│ └─ Shared/ # 可选:应用层共享但不属于 Gfx 的东西
│ ├─ 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/
```
~~当前项目已有 `Core/Math/Platform/Rasterizer/RenderData/Scene` 等目录,短期不必一次性搬迁;但新增代码应按上面的边界收敛。等功能稳定后,再把现有底层代码整体移动到 `src/Gfx/`。~~
**已完成**:底层代码已整体迁移到 `src/Gfx/`,包括 Core、Math、Rasterizer、RenderData、Scene、Shading、Platform、Asset 和新增的 Draw2D。Demo 入口位于 `src/Apps/Demo/`
## 2. 四个层级的职责
### 2.1 Gfx底层图形库
职责:
- 管理 framebuffer、depthbuffer、渲染上下文。
- 提供基础绘制接口点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas运行时不直接依赖 PNG/TTF 解码。
- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。
禁止:
- 不依赖 `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 可以依赖 GfxGfx 不能依赖 Shared。
## 3. 依赖方向
必须保持单向依赖:
```text
Apps/GameA ─┐
Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0/ITimeSource)
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);
void draw_sprite(int32_t x, int32_t y, const RenderData::Image& image);
void draw_sprite_region(int32_t x, int32_t y,
const RenderData::SpriteRegion& region);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
RenderData::Color color, const char* text);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y);
};
}
```
后续再扩展:
- `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。~~ **已完成**`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、present
4. ~~底层代码迁移到 `src/Gfx/`Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成**
5. 新增 Launcher app只做最小菜单和应用切换。
6. 新增 GameA/GameB 空壳,验证三应用切换。
7. 再逐步把现有 3D demo 或 2D 游戏逻辑迁入对应 Game 目录。
8. 最后重构 CMake`imx6u_gfx` + 应用 target 拆分。
## 8. 性能注意事项
- 应用切换不应重复销毁/创建 SDL window、renderer、texture。
- 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源Display 不承担计时职责。
- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。
- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
## 9. 资源转换约定
- 运行时资源优先使用离线转换后的简单数组,不在 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 或自定义资源包格式。
## 10. 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 快路径。

View File

@ -1,7 +1,9 @@
# CPU 软件渲染器项目约定 # CPU 软件渲染器项目约定
本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。 本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。
> 文档分工坐标、矩阵、深度等数学语义记录在本文档IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_GFX_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
## 1. 通用约定 ## 1. 通用约定
- 项目使用右手坐标系。 - 项目使用右手坐标系。

View File

@ -0,0 +1,216 @@
# IMX6U 开发规范与性能红线
本文档用于约束后续代码设计,目标是在 IMX6UARM Cortex-A7、无 GPU 或弱 GPU、内存与带宽有限上避免性能问题扩散减少后期大面积返工。
这些规则优先约束**核心逻辑、渲染管线、每帧循环、像素/顶点级热路径**。PC/SDL 调试层可以适度放宽,但不能让调试层的便利写法泄漏到 ARM 运行时核心代码中。
## 1. 总原则
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/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. 资源转换与运行时资源规范
- 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。
- 当前工具约定:
- `tools/png_to_header.py`PNG -> `uint32_t` RGBA 头文件。
- `tools/gen_font_atlas.py`:像素字体 TTF -> ASCII bitmap font atlas/header。
- 生成像素格式统一为 `(R << 24) | (G << 16) | (B << 8) | A`,与 `RenderData::Color::to_rgba()``Core::FrameBuffer` 当前格式保持一致。
- 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。
- 运行时绘制 sprite/font/tilemap 时只做裁剪、透明判断、颜色替换、tile id 查表或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。
- Tilemap 绘制应按视口可见范围遍历 tile不能每帧无条件扫描整张地图视口边缘允许通过 sprite 像素裁剪显示半个 tile。
- 字体资源当前走像素风路径,生成端会把 alpha 阈值化为 0/255如果未来要恢复抗锯齿字体必须同步设计 framebuffer alpha blending而不能只在绘制端把所有非 0 alpha 当作实心像素。
## 9. 新代码提交前检查清单
新增或修改核心代码前,至少检查:
- [ ] 是否在热路径新增了 `float` / `double`?如果是,是否能改成整数/定点?
- [ ] 是否在每帧或内层循环创建了 `std::vector` / `std::string` / 其他堆分配对象?
- [ ] 容器是否提前 `reserve()`,或由上层复用?
- [ ] 是否有隐藏的临时大对象拷贝?
- [ ] 是否把平台/显示层 API 类型泄漏进核心逻辑?
- [ ] 是否能在 PC 调试版和 ARM release 版分别关闭调试开销?
- [ ] 新增图片/字体资源是否已离线转换,且源资源、转换脚本、生成头文件一起提交?
- [ ] 是否保留 C++11 兼容?
- [ ] 是否需要同步更新 `docs/CONVENTIONS.md` 中的坐标/矩阵/深度等约定?
## 10. 推荐的代码结构方向
后续如果继续推进性能优化,优先建立这些基础设施:
1. 统一定点数类型与转换工具,集中放在 `src/Gfx/Math/`
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
5. 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。
## 11. 与现有代码的关系
当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:
- 新功能不要继续扩大浮点和临时分配的使用范围。
- 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
- 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。
## 12. IMX6U + SDL2 运行后端规范
如果最终版本在 IMX6U 上使用 SDL2而不是直接写 framebuffer需要额外注意SDL2 只是显示、输入和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源;时间源应通过独立 `Platform::ITimeSource` 提供。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
### 12.1 SDL2 边界
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h`
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep不直接依赖 float 秒数。
### 12.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 侧绘制成本。
### 12.3 SDL Renderer 选择
- IMX6U 上不要假设 `SDL_RENDERER_ACCELERATED` 一定更快;实际可能走软件或受驱动限制。
- 需要在开发板上对比 `SDL_RENDERER_ACCELERATED`、`SDL_RENDERER_SOFTWARE`、默认 renderer 的帧时间和稳定性。
- 一旦确定目标板最快/最稳配置,应在代码或 CMake 选项中固定,不要依赖默认行为。
- 如果 SDL2 后端只是搬运 CPU framebuffer重点关注 texture update、copy、present 的总耗时,而不是复杂 SDL 绘图 API。
### 12.4 分辨率与帧率预算
- 先确定目标分辨率和目标 FPS再决定渲染功能不要默认使用屏幕原生高分辨率。
- IMX6U 上优先考虑低分辨率内部渲染,再由 SDL/显示层整数倍放大。
- 目标帧率当前维护 `30/45/60 FPS` 三档,通过命令行 `--fps 30|45|60` 选择IMX6U 默认优先 30 FPS45/60 FPS 需要以实测帧时间确认。
- 固定步长 tick 使用整数毫秒余数累计,例如 30 FPS 为 `33/33/34 ms` 节奏45 FPS 为 `22/22/22/22/23 ms` 节奏60 FPS 为 `16/17/17 ms` 节奏。
- 性能档应限制:最大三角形数、最大 sprite 数、最大粒子数、最大动态光源数、最大纹理尺寸。
- 所有预算都应以实测帧时间为准,不能只按 PC 表现推断。
### 12.5 输入、音频和资源
- SDL 输入事件应每帧集中轮询一次,转换为当前帧输入快照;不要在逻辑各处直接轮询 SDL。
- 音频回调中禁止分配内存、加锁等待或做复杂逻辑。
- 图片、音频、地图等资源必须在加载阶段解码;运行中避免临时解码和格式转换。
- 大纹理/图片进入运行时前应转换成目标像素格式和目标尺寸。
### 12.6 必须建立的性能观测
在 IMX6U 上接入 SDL2 后,应尽快加入轻量 profiler 或计时日志,至少拆分统计:
- 输入轮询耗时
- 逻辑更新耗时
- CPU rasterize / draw 耗时
- framebuffer clear 耗时
- depth clear 耗时
- SDL texture update / lock-unlock 耗时
- SDL render copy + present 耗时
- 总帧时间、最低 FPS、峰值帧时间
性能日志默认低频输出,例如每 60 帧汇总一次ARM release 中不得逐帧大量打印。

116
game/README.md Normal file
View File

@ -0,0 +1,116 @@
# 游戏目录结构
这个目录用于编写新的游戏。仓库根目录下的 `src/` 继续作为软渲染器、数学库、光栅化和平台显示层;`game/` 只放游戏自己的代码、资源、脚本和数据。
## 目录结构
```text
game/
README.md
assets/
sprites/ 运行时使用的 2D 精灵图片。
textures/ 3D 材质或其他渲染用途的纹理。
tilesets/ 瓦片图片和瓦片集元数据。
fonts/ 位图字体或字体图集。
audio/
music/ 背景音乐。
sfx/ 短音效。
models/ 运行时使用的 3D 模型文件。
materials/ 模型或精灵使用的材质定义。
ui/ UI 面板、图标、光标、HUD 资源。
raw/ 原始素材文件,不建议运行时直接加载。
scripts/
gameplay/ 预留给运行时玩法脚本。
ui/ 预留给运行时 UI 流程脚本。
tools/ 游戏开发期间使用的小型辅助脚本。
src/
app/ 游戏启动、场景切换、主循环衔接代码。
scenes/ 启动、菜单、游戏、暂停、结算等场景。
systems/ 输入、渲染、碰撞、动画、AI、音频等系统。
entities/ 实体定义和实体工厂。
components/ 系统使用的小型数据组件。
ui/ HUD 和菜单代码。
data/
config/ 分辨率、控制、难度、平台选项等配置。
balance/ 速度、生命、伤害、分数等数值表。
localization/ 多语言文本表。
tools/
asset_pipeline/ 资产转换、压缩、打包相关工具。
level_editor/ 可选的关卡编辑工具。
docs/
design/ 玩法规则、操作方式、关卡设计说明。
technical/ 运行时约束和接入说明。
tests/
unit/ 纯逻辑单元测试。
fixtures/ 测试用的小型数据文件。
```
## 边界约定
- 仓库根目录的 `src/` 是渲染器和引擎基础层。
- `game/src/` 可以依赖根目录 `src/`,但根目录 `src/` 不应该反向依赖 `game/`
- `game/assets/raw/` 只放可编辑源素材,运行时应加载转换后的资源。
- `game/assets/sprites/` 放离线转换后的 `.sprite` 文件PC 测试和 IMX6U 运行时都从这里读取。
- `game/scripts/gameplay/` 预留给运行时脚本;构建、转换、检查类脚本放在 `game/scripts/tools/``game/tools/asset_pipeline/`
- 平台相关逻辑尽量集中在 `game/src/app/``game/data/config/`,不要散落到各个玩法模块里。
## 精灵资源转换
板端运行时不直接解码 PNG。开发时把 PNG 放在 `game/assets/raw/`,再用 PC 端工具转换为 `game/assets/sprites/*.sprite`
`.sprite` 是项目自定义的简单二进制格式:
```text
offset size 内容
0 4 magic: "SPRT"
4 4 version: 1, little-endian uint32
8 4 width, little-endian uint32
12 4 height, little-endian uint32
16 4 format: 1 表示 RGBA8888
20 * width * height 个 RGBA8888 像素,每像素 little-endian uint32
```
像素颜色沿用渲染器约定:`0xRRGGBBAA`,最低 8 位是 alpha。
常用命令:
```bash
# 构建 PC 端工具
cmake -B build-win .
cmake --build build-win --config Release --target SpriteAssetTool
# 批量转换当前汤姆猫素材,输出适配 800x480 的板端资源
cmake --build build-win --config Release --target ConvertTomSprites
# 单张转换,保持原尺寸
./build-win/Release/SpriteAssetTool.exe game/assets/raw/ui-record.png game/assets/sprites/ui-record.sprite
# 单张转换并等比缩放到最大范围
./build-win/Release/SpriteAssetTool.exe game/assets/raw/Tom-stand.png game/assets/sprites/Tom-stand.sprite --fit 560 360
```
Linux PC 构建工具需要 `libsdl2-dev``libsdl2-image-dev`。ARM framebuffer 构建不会编译 `SpriteAssetTool`,只编译 `.sprite` 加载器。
启动汤姆猫资源链路测试:
```bash
./build-win/Release/IMX6U-Game.exe --tom
```
部署到板端时,把可执行文件和转换后的 `game/assets/sprites/` 一起拷贝,保持相对路径即可;也可以放到 `/tmp/game/assets/sprites/``/usr/local/share/imx6u-game/sprites/`
## 建议优先创建的文件
真正开始写游戏逻辑时,建议先从这些文件开始:
```text
game/src/app/GameApp.h
game/src/app/GameApp.cpp
game/src/scenes/BootScene.h
game/src/scenes/BootScene.cpp
game/src/scenes/PlayScene.h
game/src/scenes/PlayScene.cpp
game/data/config/game.json
```
后续可以把现有的 `src/main.cpp` 改成薄启动器:只负责创建显示后端、初始化缓冲区和渲染器,然后把更新和绘制流程交给 `GameApp`

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

1
game/assets/raw/.gitkeep Normal file
View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

BIN
game/assets/raw/ui-fat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
game/assets/raw/ui-hand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
game/assets/raw/ui-i.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
game/assets/raw/ui-tom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

1
game/assets/ui/.gitkeep Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

1
game/src/app/.gitkeep Normal file
View File

@ -0,0 +1 @@

280
game/src/app/TomGameApp.cpp Normal file
View File

@ -0,0 +1,280 @@
#include "TomGameApp.h"
#include "../audio/VoiceEffect.h"
#include "../hardware/AudioInput.h"
#include "../hardware/AudioOutput.h"
#include "../hardware/ButtonInput.h"
#include "../../../src/Core/FrameBuffer.h"
#include "../../../src/Rasterizer/SpriteRasterizer.h"
#include "../../../src/RenderData/Image.h"
namespace Game
{
TomGameApp::TomGameApp(
Core::FrameBuffer* frameBuffer,
Rasterizer::SpriteRasterizer* spriteRasterizer,
AudioInput* audioInput,
AudioOutput* audioOutput,
ButtonInput* buttonInput) :
frameBuffer(frameBuffer),
spriteRasterizer(spriteRasterizer),
audioInput(audioInput),
audioOutput(audioOutput),
buttonInput(buttonInput),
state(TomGameState::Idle),
screenWidth(frameBuffer ? frameBuffer->get_width() : 0),
screenHeight(frameBuffer ? frameBuffer->get_height() : 0),
tomX(0),
tomY(0),
buttonX(0),
buttonY(0),
audioSampleRate(16000),
audioChannels(1),
pitchFactor(1.45f),
outputGain(1.15f)
{
recorder.configure(audioSampleRate, audioChannels);
}
void TomGameApp::set_assets(const TomGameAssets& assets)
{
this->assets = assets;
speakingFrames.clear();
if (assets.tomSay1 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay1));
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
if (assets.tomSay4 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay4));
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
speakingAnimator.set_frames(speakingFrames);
speakingAnimator.set_frame_time(0.08f);
speakingAnimator.set_loop(true);
speakingAnimator.stop();
calculate_layout();
}
void TomGameApp::configure_audio(uint32_t sampleRate, uint32_t channels)
{
if (sampleRate > 0)
{
audioSampleRate = sampleRate;
}
if (channels > 0)
{
audioChannels = channels;
}
recorder.configure(audioSampleRate, audioChannels);
}
void TomGameApp::update(float deltaTime, bool buttonPressed)
{
bool trigger = buttonPressed;
if (buttonInput != nullptr)
{
buttonInput->update();
trigger = trigger || buttonInput->was_pressed();
}
switch (state)
{
case TomGameState::Idle:
if (trigger)
{
start_recording();
}
break;
case TomGameState::Recording:
if (audioInput != nullptr)
{
recorder.update(*audioInput, 512);
}
if (trigger || recorder.is_finished())
{
finish_recording();
}
break;
case TomGameState::Processing:
process_voice();
break;
case TomGameState::Speaking:
speakingAnimator.update(deltaTime);
if (audioOutput == nullptr)
{
back_to_idle();
break;
}
player.update(*audioOutput, 1024);
if (player.is_finished())
{
back_to_idle();
}
break;
}
}
void TomGameApp::draw()
{
if (spriteRasterizer == nullptr)
{
return;
}
if (assets.background != nullptr)
{
spriteRasterizer->DrawImage(*assets.background, 0, 0);
}
const RenderData::Sprite tomSprite = get_current_tom_sprite();
if (tomSprite.is_valid())
{
spriteRasterizer->DrawSprite(tomSprite, tomX, tomY);
}
if (assets.button != nullptr)
{
spriteRasterizer->DrawImage(*assets.button, buttonX, buttonY);
}
}
bool TomGameApp::is_ready() const
{
return frameBuffer != nullptr &&
spriteRasterizer != nullptr &&
assets.background != nullptr &&
assets.tomStand != nullptr &&
assets.tomListen != nullptr &&
assets.button != nullptr &&
speakingFrames.size() == 6;
}
bool TomGameApp::is_button_hit(int32_t x, int32_t y) const
{
if (assets.button == nullptr)
{
return false;
}
return x >= buttonX &&
x < buttonX + assets.button->get_width() &&
y >= buttonY &&
y < buttonY + assets.button->get_height();
}
void TomGameApp::calculate_layout()
{
screenWidth = frameBuffer ? frameBuffer->get_width() : 0;
screenHeight = frameBuffer ? frameBuffer->get_height() : 0;
if (assets.button != nullptr)
{
buttonX = (screenWidth - assets.button->get_width()) / 2;
buttonY = screenHeight - assets.button->get_height() - 16;
}
const RenderData::Sprite tomSprite = get_current_tom_sprite();
if (tomSprite.is_valid())
{
tomX = (screenWidth - tomSprite.width) / 2;
tomY = screenHeight - tomSprite.height - 72;
if (tomY < 0)
{
tomY = 0;
}
}
}
void TomGameApp::start_recording()
{
if (audioInput != nullptr && !audioInput->is_open())
{
audioInput->init("default", audioSampleRate, audioChannels);
}
player.stop();
recorder.configure(audioSampleRate, audioChannels);
recorder.start();
state = TomGameState::Recording;
calculate_layout();
}
void TomGameApp::finish_recording()
{
if (recorder.is_recording())
{
recorder.stop();
}
state = TomGameState::Processing;
}
void TomGameApp::process_voice()
{
std::vector<int16_t> samples = recorder.get_samples();
samples = VoiceEffect::trim_silence(samples, 0.02f, audioChannels);
samples = VoiceEffect::pitch_up(samples, pitchFactor, audioChannels);
samples = VoiceEffect::amplify(samples, outputGain);
start_speaking(samples);
}
void TomGameApp::start_speaking(const std::vector<int16_t>& samples)
{
if (samples.empty())
{
back_to_idle();
return;
}
player.set_voice(samples, audioSampleRate, audioChannels);
player.play();
speakingAnimator.reset();
speakingAnimator.play();
state = TomGameState::Speaking;
calculate_layout();
}
void TomGameApp::back_to_idle()
{
player.stop();
speakingAnimator.stop();
state = TomGameState::Idle;
calculate_layout();
}
void TomGameApp::draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y)
{
if (spriteRasterizer == nullptr || !sprite.is_valid())
{
return;
}
spriteRasterizer->DrawSprite(sprite, centerX - sprite.width / 2, y);
}
const RenderData::Sprite TomGameApp::get_current_tom_sprite() const
{
if (state == TomGameState::Recording && assets.tomListen != nullptr)
{
return RenderData::Sprite(assets.tomListen);
}
if (state == TomGameState::Speaking)
{
const RenderData::Sprite* sprite = speakingAnimator.get_current_sprite();
if (sprite != nullptr)
{
return *sprite;
}
}
return RenderData::Sprite(assets.tomStand);
}
}

117
game/src/app/TomGameApp.h Normal file
View File

@ -0,0 +1,117 @@
#pragma once
#include <cstdint>
#include <vector>
#include "../audio/VoicePlayer.h"
#include "../audio/VoiceRecorder.h"
#include "../components/SpriteAnimator.h"
#include "../../../src/RenderData/Sprite.h"
namespace Core
{
class FrameBuffer;
}
namespace Rasterizer
{
class SpriteRasterizer;
}
namespace RenderData
{
class Image;
}
namespace Game
{
class AudioInput;
class AudioOutput;
class ButtonInput;
enum class TomGameState
{
Idle,
Recording,
Processing,
Speaking
};
struct TomGameAssets
{
const RenderData::Image* background;
const RenderData::Image* tomStand;
const RenderData::Image* tomListen;
const RenderData::Image* tomSay1;
const RenderData::Image* tomSay2;
const RenderData::Image* tomSay3;
const RenderData::Image* tomSay4;
const RenderData::Image* button;
TomGameAssets() :
background(nullptr),
tomStand(nullptr),
tomListen(nullptr),
tomSay1(nullptr),
tomSay2(nullptr),
tomSay3(nullptr),
tomSay4(nullptr),
button(nullptr)
{}
};
class TomGameApp
{
private:
Core::FrameBuffer* frameBuffer;
Rasterizer::SpriteRasterizer* spriteRasterizer;
AudioInput* audioInput;
AudioOutput* audioOutput;
ButtonInput* buttonInput;
TomGameAssets assets;
TomGameState state;
VoiceRecorder recorder;
VoicePlayer player;
SpriteAnimator speakingAnimator;
std::vector<RenderData::Sprite> speakingFrames;
int32_t screenWidth;
int32_t screenHeight;
int32_t tomX;
int32_t tomY;
int32_t buttonX;
int32_t buttonY;
uint32_t audioSampleRate;
uint32_t audioChannels;
float pitchFactor;
float outputGain;
void calculate_layout();
void start_recording();
void finish_recording();
void process_voice();
void start_speaking(const std::vector<int16_t>& samples);
void back_to_idle();
void draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y);
const RenderData::Sprite get_current_tom_sprite() const;
public:
TomGameApp(
Core::FrameBuffer* frameBuffer,
Rasterizer::SpriteRasterizer* spriteRasterizer,
AudioInput* audioInput,
AudioOutput* audioOutput,
ButtonInput* buttonInput = nullptr);
void set_assets(const TomGameAssets& assets);
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
void update(float deltaTime, bool buttonPressed = false);
void draw();
bool is_ready() const;
bool is_button_hit(int32_t x, int32_t y) const;
TomGameState get_state() const { return state; }
float get_record_volume() const { return recorder.get_last_volume(); }
float get_play_progress() const { return player.get_progress(); }
};
}

View File

@ -0,0 +1,145 @@
#include "VoiceEffect.h"
#include <algorithm>
#include <cmath>
namespace Game
{
std::vector<int16_t> VoiceEffect::pitch_up(
const std::vector<int16_t>& samples,
float factor,
uint32_t channels)
{
return change_speed(samples, factor, channels);
}
std::vector<int16_t> VoiceEffect::change_speed(
const std::vector<int16_t>& samples,
float speed,
uint32_t channels)
{
if (samples.empty() || speed <= 0.0f || channels == 0)
{
return samples;
}
const size_t frameCount = samples.size() / channels;
if (frameCount == 0)
{
return std::vector<int16_t>();
}
const size_t outputFrameCount = std::max<size_t>(1, static_cast<size_t>(frameCount / speed));
std::vector<int16_t> output(outputFrameCount * channels, 0);
for (size_t outFrame = 0; outFrame < outputFrameCount; ++outFrame)
{
const float sourceFrame = static_cast<float>(outFrame) * speed;
const size_t source0 = std::min(static_cast<size_t>(sourceFrame), frameCount - 1);
const size_t source1 = std::min(source0 + 1, frameCount - 1);
const float t = sourceFrame - static_cast<float>(source0);
for (uint32_t channel = 0; channel < channels; ++channel)
{
const int16_t a = samples[source0 * channels + channel];
const int16_t b = samples[source1 * channels + channel];
const float mixed = static_cast<float>(a) + (static_cast<float>(b) - static_cast<float>(a)) * t;
output[outFrame * channels + channel] = clamp_to_sample(mixed);
}
}
return output;
}
std::vector<int16_t> VoiceEffect::amplify(const std::vector<int16_t>& samples, float gain)
{
if (samples.empty() || gain == 1.0f)
{
return samples;
}
std::vector<int16_t> output(samples.size(), 0);
for (size_t i = 0; i < samples.size(); ++i)
{
output[i] = clamp_to_sample(static_cast<float>(samples[i]) * gain);
}
return output;
}
std::vector<int16_t> VoiceEffect::trim_silence(
const std::vector<int16_t>& samples,
float threshold,
uint32_t channels)
{
if (samples.empty() || channels == 0)
{
return samples;
}
const size_t frameCount = samples.size() / channels;
size_t startFrame = 0;
size_t endFrame = frameCount;
while (startFrame < frameCount)
{
bool loud = false;
for (uint32_t channel = 0; channel < channels; ++channel)
{
if (sample_abs(samples[startFrame * channels + channel]) >= threshold)
{
loud = true;
break;
}
}
if (loud)
{
break;
}
++startFrame;
}
while (endFrame > startFrame)
{
bool loud = false;
const size_t frame = endFrame - 1;
for (uint32_t channel = 0; channel < channels; ++channel)
{
if (sample_abs(samples[frame * channels + channel]) >= threshold)
{
loud = true;
break;
}
}
if (loud)
{
break;
}
--endFrame;
}
const size_t startSample = startFrame * channels;
const size_t endSample = endFrame * channels;
return std::vector<int16_t>(samples.begin() + startSample, samples.begin() + endSample);
}
int16_t VoiceEffect::clamp_to_sample(float value)
{
if (value > 32767.0f)
{
return 32767;
}
if (value < -32768.0f)
{
return -32768;
}
return static_cast<int16_t>(value);
}
float VoiceEffect::sample_abs(int16_t sample)
{
return std::abs(static_cast<float>(sample) / 32768.0f);
}
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <vector>
namespace Game
{
class VoiceEffect
{
public:
static std::vector<int16_t> pitch_up(
const std::vector<int16_t>& samples,
float factor = 1.45f,
uint32_t channels = 1);
static std::vector<int16_t> change_speed(
const std::vector<int16_t>& samples,
float speed,
uint32_t channels = 1);
static std::vector<int16_t> amplify(
const std::vector<int16_t>& samples,
float gain);
static std::vector<int16_t> trim_silence(
const std::vector<int16_t>& samples,
float threshold = 0.02f,
uint32_t channels = 1);
private:
static int16_t clamp_to_sample(float value);
static float sample_abs(int16_t sample);
};
}

View File

@ -0,0 +1,155 @@
#include "VoicePlayer.h"
#include "../hardware/AudioOutput.h"
#include <algorithm>
namespace Game
{
VoicePlayer::VoicePlayer() :
playPosition(0),
sampleRate(16000),
channels(1),
playing(false),
finished(false)
{}
void VoicePlayer::set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate, uint32_t channels)
{
this->samples = samples;
this->sampleRate = sampleRate > 0 ? sampleRate : 16000;
this->channels = channels > 0 ? channels : 1;
reset();
}
void VoicePlayer::play()
{
if (samples.empty())
{
playing = false;
finished = true;
return;
}
if (playPosition >= samples.size())
{
playPosition = 0;
}
playing = true;
finished = false;
}
void VoicePlayer::stop()
{
playing = false;
finished = true;
}
void VoicePlayer::reset()
{
playPosition = 0;
playing = false;
finished = samples.empty();
}
void VoicePlayer::clear()
{
samples.clear();
playPosition = 0;
playing = false;
finished = false;
}
int VoicePlayer::update(AudioOutput& audioOutput, int maxSamplesPerUpdate)
{
if (!playing || samples.empty())
{
return 0;
}
if (!audioOutput.is_open() ||
audioOutput.get_sample_rate() != sampleRate ||
audioOutput.get_channels() != channels)
{
if (!audioOutput.init(audioOutput.get_device_name(), sampleRate, channels))
{
stop();
return 0;
}
}
const size_t remaining = get_remaining_samples();
if (remaining == 0)
{
stop();
return 0;
}
int requestCount = get_aligned_sample_count(maxSamplesPerUpdate);
if (requestCount <= 0)
{
return 0;
}
requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(requestCount), remaining));
requestCount = get_aligned_sample_count(requestCount);
if (requestCount <= 0)
{
stop();
return 0;
}
const int written = audioOutput.write_samples(samples.data() + playPosition, requestCount);
if (written <= 0)
{
return 0;
}
playPosition += static_cast<size_t>(written);
if (playPosition >= samples.size())
{
stop();
}
return written;
}
size_t VoicePlayer::get_remaining_samples() const
{
if (playPosition >= samples.size())
{
return 0;
}
return samples.size() - playPosition;
}
float VoicePlayer::get_duration_seconds() const
{
if (sampleRate == 0 || channels == 0)
{
return 0.0f;
}
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
}
float VoicePlayer::get_progress() const
{
if (samples.empty())
{
return 0.0f;
}
return static_cast<float>(playPosition) / static_cast<float>(samples.size());
}
int VoicePlayer::get_aligned_sample_count(int sampleCount) const
{
if (sampleCount <= 0 || channels == 0)
{
return 0;
}
return sampleCount - (sampleCount % static_cast<int>(channels));
}
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Game
{
class AudioOutput;
class VoicePlayer
{
private:
std::vector<int16_t> samples;
size_t playPosition;
uint32_t sampleRate;
uint32_t channels;
bool playing;
bool finished;
int get_aligned_sample_count(int sampleCount) const;
public:
VoicePlayer();
void set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate = 16000, uint32_t channels = 1);
void play();
void stop();
void reset();
void clear();
int update(AudioOutput& audioOutput, int maxSamplesPerUpdate = 1024);
const std::vector<int16_t>& get_samples() const { return samples; }
size_t get_sample_count() const { return samples.size(); }
size_t get_play_position() const { return playPosition; }
size_t get_remaining_samples() const;
uint32_t get_sample_rate() const { return sampleRate; }
uint32_t get_channels() const { return channels; }
float get_duration_seconds() const;
float get_progress() const;
bool is_playing() const { return playing; }
bool is_finished() const { return finished; }
bool has_voice() const { return !samples.empty(); }
};
}

View File

@ -0,0 +1,171 @@
#include "VoiceRecorder.h"
#include "../hardware/AudioInput.h"
#include <algorithm>
#include <cmath>
#include <vector>
namespace Game
{
VoiceRecorder::VoiceRecorder() :
sampleRate(16000),
channels(1),
maxDurationSeconds(5.0f),
silenceThreshold(0.03f),
silenceDurationSeconds(0.8f),
minRecordSeconds(0.3f),
lastVolume(0.0f),
silentSampleCount(0),
recording(false),
finished(false),
finishedBySilence(false)
{}
void VoiceRecorder::configure(
uint32_t sampleRate,
uint32_t channels,
float maxDurationSeconds,
float silenceThreshold,
float silenceDurationSeconds,
float minRecordSeconds)
{
if (sampleRate > 0)
{
this->sampleRate = sampleRate;
}
if (channels > 0)
{
this->channels = channels;
}
if (maxDurationSeconds > 0.0f)
{
this->maxDurationSeconds = maxDurationSeconds;
}
if (silenceThreshold >= 0.0f)
{
this->silenceThreshold = silenceThreshold;
}
if (silenceDurationSeconds > 0.0f)
{
this->silenceDurationSeconds = silenceDurationSeconds;
}
if (minRecordSeconds >= 0.0f)
{
this->minRecordSeconds = minRecordSeconds;
}
}
void VoiceRecorder::start()
{
clear();
recording = true;
finished = false;
finishedBySilence = false;
}
void VoiceRecorder::stop()
{
recording = false;
finished = true;
}
void VoiceRecorder::clear()
{
samples.clear();
lastVolume = 0.0f;
silentSampleCount = 0;
recording = false;
finished = false;
finishedBySilence = false;
}
int VoiceRecorder::update(AudioInput& audioInput, int sampleCount)
{
if (!recording || sampleCount <= 0)
{
return 0;
}
const size_t maxSampleCount = get_max_sample_count();
if (samples.size() >= maxSampleCount)
{
stop();
return 0;
}
const size_t remaining = maxSampleCount - samples.size();
const int requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(sampleCount), remaining));
std::vector<int16_t> buffer(static_cast<size_t>(requestCount), 0);
const int readCount = audioInput.read_samples(buffer.data(), requestCount);
if (readCount <= 0)
{
return 0;
}
lastVolume = calculate_volume(buffer.data(), readCount);
samples.insert(samples.end(), buffer.begin(), buffer.begin() + readCount);
if (samples.size() >= get_min_sample_count() && lastVolume < silenceThreshold)
{
silentSampleCount += static_cast<size_t>(readCount);
}
else
{
silentSampleCount = 0;
}
if (silentSampleCount >= get_silence_sample_limit())
{
finishedBySilence = true;
stop();
}
else if (samples.size() >= maxSampleCount)
{
stop();
}
return readCount;
}
float VoiceRecorder::get_duration_seconds() const
{
if (sampleRate == 0 || channels == 0)
{
return 0.0f;
}
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
}
size_t VoiceRecorder::get_max_sample_count() const
{
return static_cast<size_t>(sampleRate * channels * maxDurationSeconds);
}
size_t VoiceRecorder::get_silence_sample_limit() const
{
return static_cast<size_t>(sampleRate * channels * silenceDurationSeconds);
}
size_t VoiceRecorder::get_min_sample_count() const
{
return static_cast<size_t>(sampleRate * channels * minRecordSeconds);
}
float VoiceRecorder::calculate_volume(const int16_t* buffer, int sampleCount) const
{
if (buffer == nullptr || sampleCount <= 0)
{
return 0.0f;
}
double sum = 0.0;
for (int i = 0; i < sampleCount; ++i)
{
const double value = static_cast<double>(buffer[i]) / 32768.0;
sum += value * value;
}
return static_cast<float>(std::sqrt(sum / sampleCount));
}
}

View File

@ -0,0 +1,59 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Game
{
class AudioInput;
class VoiceRecorder
{
private:
std::vector<int16_t> samples;
uint32_t sampleRate;
uint32_t channels;
float maxDurationSeconds;
float silenceThreshold;
float silenceDurationSeconds;
float minRecordSeconds;
float lastVolume;
size_t silentSampleCount;
bool recording;
bool finished;
bool finishedBySilence;
size_t get_max_sample_count() const;
size_t get_silence_sample_limit() const;
size_t get_min_sample_count() const;
float calculate_volume(const int16_t* buffer, int sampleCount) const;
public:
VoiceRecorder();
void configure(
uint32_t sampleRate,
uint32_t channels,
float maxDurationSeconds = 5.0f,
float silenceThreshold = 0.03f,
float silenceDurationSeconds = 0.8f,
float minRecordSeconds = 0.3f);
void start();
void stop();
void clear();
int update(AudioInput& audioInput, int sampleCount = 512);
const std::vector<int16_t>& get_samples() const { return samples; }
size_t get_sample_count() const { return samples.size(); }
uint32_t get_sample_rate() const { return sampleRate; }
uint32_t get_channels() const { return channels; }
float get_duration_seconds() const;
float get_last_volume() const { return lastVolume; }
bool is_recording() const { return recording; }
bool is_finished() const { return finished; }
bool is_finished_by_silence() const { return finishedBySilence; }
bool has_samples() const { return !samples.empty(); }
};
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,101 @@
#include "SpriteAnimator.h"
namespace Game
{
SpriteAnimator::SpriteAnimator() :
currentFrame(0),
frameTime(0.1f),
timer(0.0f),
loop(true),
playing(true)
{}
SpriteAnimator::SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop) :
frames(frames),
currentFrame(0),
frameTime(frameTime > 0.0f ? frameTime : 0.1f),
timer(0.0f),
loop(loop),
playing(!frames.empty())
{}
void SpriteAnimator::set_frames(const std::vector<RenderData::Sprite>& frames)
{
this->frames = frames;
reset();
}
void SpriteAnimator::set_frame_time(float frameTime)
{
if (frameTime > 0.0f)
{
this->frameTime = frameTime;
}
}
void SpriteAnimator::set_loop(bool loop)
{
this->loop = loop;
}
void SpriteAnimator::play()
{
playing = !frames.empty();
}
void SpriteAnimator::stop()
{
playing = false;
}
void SpriteAnimator::reset()
{
currentFrame = 0;
timer = 0.0f;
playing = !frames.empty();
}
void SpriteAnimator::update(float deltaTime)
{
if (!playing || frames.empty() || frameTime <= 0.0f)
{
return;
}
timer += deltaTime;
while (timer >= frameTime)
{
timer -= frameTime;
if (currentFrame + 1 < frames.size())
{
++currentFrame;
}
else if (loop)
{
currentFrame = 0;
}
else
{
playing = false;
currentFrame = frames.size() - 1;
break;
}
}
}
const RenderData::Sprite* SpriteAnimator::get_current_sprite() const
{
if (frames.empty() || currentFrame >= frames.size())
{
return nullptr;
}
return &frames[currentFrame];
}
bool SpriteAnimator::is_finished() const
{
return !loop && !playing && !frames.empty() && currentFrame == frames.size() - 1;
}
}

View File

@ -0,0 +1,37 @@
#pragma once
#include <cstddef>
#include <vector>
#include "../../../src/RenderData/Sprite.h"
namespace Game
{
class SpriteAnimator
{
private:
std::vector<RenderData::Sprite> frames;
size_t currentFrame;
float frameTime;
float timer;
bool loop;
bool playing;
public:
SpriteAnimator();
SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop = true);
void set_frames(const std::vector<RenderData::Sprite>& frames);
void set_frame_time(float frameTime);
void set_loop(bool loop);
void play();
void stop();
void reset();
void update(float deltaTime);
const RenderData::Sprite* get_current_sprite() const;
size_t get_current_frame() const { return currentFrame; }
size_t get_frame_count() const { return frames.size(); }
bool is_playing() const { return playing; }
bool is_finished() const;
};
}

View File

@ -0,0 +1,140 @@
#include "AudioInput.h"
#include <cmath>
#include <cstdlib>
#include <iostream>
#if defined(__linux__)
#include <alsa/asoundlib.h>
#endif
namespace Game
{
AudioInput::AudioInput() :
deviceName("default"),
sampleRate(16000),
channels(1),
opened(false)
#if defined(__linux__)
,
handle(nullptr)
#endif
{}
AudioInput::~AudioInput()
{
shutdown();
}
bool AudioInput::init(const std::string& deviceName, uint32_t sampleRate, uint32_t channels)
{
shutdown();
if (sampleRate == 0 || channels == 0)
{
std::cerr << "AudioInput invalid params." << std::endl;
return false;
}
this->deviceName = deviceName;
this->sampleRate = sampleRate;
this->channels = channels;
#if defined(__linux__)
int result = snd_pcm_open(&handle, deviceName.c_str(), SND_PCM_STREAM_CAPTURE, 0);
if (result < 0)
{
std::cerr << "AudioInput open failed: " << snd_strerror(result) << std::endl;
handle = nullptr;
return false;
}
result = snd_pcm_set_params(
handle,
SND_PCM_FORMAT_S16_LE,
SND_PCM_ACCESS_RW_INTERLEAVED,
channels,
sampleRate,
1,
50000);
if (result < 0)
{
std::cerr << "AudioInput set params failed: " << snd_strerror(result) << std::endl;
shutdown();
return false;
}
opened = true;
return true;
#else
std::cerr << "AudioInput is only implemented on Linux ALSA." << std::endl;
return false;
#endif
}
int AudioInput::read_samples(int16_t* buffer, int sampleCount)
{
if (buffer == nullptr || sampleCount <= 0 || !opened)
{
return 0;
}
#if defined(__linux__)
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sampleCount / channels);
int result = snd_pcm_readi(handle, buffer, frames);
if (result == -EPIPE)
{
snd_pcm_prepare(handle);
return 0;
}
if (result < 0)
{
result = snd_pcm_recover(handle, result, 0);
if (result < 0)
{
std::cerr << "AudioInput read failed: " << snd_strerror(result) << std::endl;
return 0;
}
}
return result * static_cast<int>(channels);
#else
return 0;
#endif
}
float AudioInput::read_volume(int sampleCount)
{
if (sampleCount <= 0)
{
return 0.0f;
}
std::vector<int16_t> samples(static_cast<size_t>(sampleCount), 0);
const int count = read_samples(samples.data(), sampleCount);
if (count <= 0)
{
return 0.0f;
}
double sum = 0.0;
for (int i = 0; i < count; ++i)
{
const double value = static_cast<double>(samples[i]) / 32768.0;
sum += value * value;
}
return static_cast<float>(std::sqrt(sum / count));
}
void AudioInput::shutdown()
{
#if defined(__linux__)
if (handle != nullptr)
{
snd_pcm_close(handle);
handle = nullptr;
}
#endif
opened = false;
}
}

View File

@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#if defined(__linux__)
typedef struct _snd_pcm snd_pcm_t;
#endif
namespace Game
{
class AudioInput
{
private:
std::string deviceName;
uint32_t sampleRate;
uint32_t channels;
bool opened;
#if defined(__linux__)
snd_pcm_t* handle;
#endif
public:
AudioInput();
~AudioInput();
bool init(
const std::string& deviceName = "default",
uint32_t sampleRate = 16000,
uint32_t channels = 1);
int read_samples(int16_t* buffer, int sampleCount);
float read_volume(int sampleCount = 512);
void shutdown();
bool is_open() const { return opened; }
uint32_t get_sample_rate() const { return sampleRate; }
uint32_t get_channels() const { return channels; }
const std::string& get_device_name() const { return deviceName; }
};
}

View File

@ -0,0 +1,203 @@
#include "AudioOutput.h"
#include <algorithm>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#if defined(__linux__)
#include <alsa/asoundlib.h>
#endif
namespace
{
struct WavHeader
{
char riff[4];
uint32_t fileSize;
char wave[4];
char fmt[4];
uint32_t fmtSize;
uint16_t audioFormat;
uint16_t channels;
uint32_t sampleRate;
uint32_t byteRate;
uint16_t blockAlign;
uint16_t bitsPerSample;
char data[4];
uint32_t dataSize;
};
static bool IsSupportedWav(const WavHeader& header)
{
return std::memcmp(header.riff, "RIFF", 4) == 0 &&
std::memcmp(header.wave, "WAVE", 4) == 0 &&
std::memcmp(header.fmt, "fmt ", 4) == 0 &&
std::memcmp(header.data, "data", 4) == 0 &&
header.audioFormat == 1 &&
header.bitsPerSample == 16 &&
header.fmtSize == 16;
}
}
namespace Game
{
AudioOutput::AudioOutput() :
deviceName("default"),
sampleRate(16000),
channels(1),
opened(false)
#if defined(__linux__)
,
handle(nullptr)
#endif
{}
AudioOutput::~AudioOutput()
{
shutdown();
}
bool AudioOutput::init(const std::string& deviceName, uint32_t sampleRate, uint32_t channels)
{
shutdown();
if (sampleRate == 0 || channels == 0)
{
std::cerr << "AudioOutput invalid params." << std::endl;
return false;
}
this->deviceName = deviceName;
this->sampleRate = sampleRate;
this->channels = channels;
#if defined(__linux__)
int result = snd_pcm_open(&handle, deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0);
if (result < 0)
{
std::cerr << "AudioOutput open failed: " << snd_strerror(result) << std::endl;
handle = nullptr;
return false;
}
result = snd_pcm_set_params(
handle,
SND_PCM_FORMAT_S16_LE,
SND_PCM_ACCESS_RW_INTERLEAVED,
channels,
sampleRate,
1,
50000);
if (result < 0)
{
std::cerr << "AudioOutput set params failed: " << snd_strerror(result) << std::endl;
shutdown();
return false;
}
opened = true;
return true;
#else
std::cerr << "AudioOutput is only implemented on Linux ALSA." << std::endl;
return false;
#endif
}
int AudioOutput::write_samples(const int16_t* samples, int sampleCount)
{
if (samples == nullptr || sampleCount <= 0 || !opened)
{
return 0;
}
#if defined(__linux__)
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sampleCount / channels);
int result = snd_pcm_writei(handle, samples, frames);
if (result == -EPIPE)
{
snd_pcm_prepare(handle);
return 0;
}
if (result < 0)
{
result = snd_pcm_recover(handle, result, 0);
if (result < 0)
{
std::cerr << "AudioOutput write failed: " << snd_strerror(result) << std::endl;
return 0;
}
}
return result * static_cast<int>(channels);
#else
return 0;
#endif
}
bool AudioOutput::play_wav(const std::string& path)
{
std::ifstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
std::cerr << "Open wav failed: " << path << std::endl;
return false;
}
WavHeader header;
file.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!file.good() || !IsSupportedWav(header))
{
std::cerr << "Unsupported wav: " << path << std::endl;
return false;
}
if (!opened || sampleRate != header.sampleRate || channels != header.channels)
{
if (!init(deviceName, header.sampleRate, header.channels))
{
return false;
}
}
std::vector<int16_t> samples(header.dataSize / sizeof(int16_t), 0);
file.read(reinterpret_cast<char*>(samples.data()), header.dataSize);
if (!file.good())
{
return false;
}
size_t offset = 0;
while (offset < samples.size())
{
const int count = static_cast<int>(std::min<size_t>(4096, samples.size() - offset));
const int written = write_samples(samples.data() + offset, count);
if (written <= 0)
{
return false;
}
offset += static_cast<size_t>(written);
}
#if defined(__linux__)
if (handle != nullptr)
{
snd_pcm_drain(handle);
}
#endif
return true;
}
void AudioOutput::shutdown()
{
#if defined(__linux__)
if (handle != nullptr)
{
snd_pcm_close(handle);
handle = nullptr;
}
#endif
opened = false;
}
}

View File

@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string>
#if defined(__linux__)
typedef struct _snd_pcm snd_pcm_t;
#endif
namespace Game
{
class AudioOutput
{
private:
std::string deviceName;
uint32_t sampleRate;
uint32_t channels;
bool opened;
#if defined(__linux__)
snd_pcm_t* handle;
#endif
public:
AudioOutput();
~AudioOutput();
bool init(
const std::string& deviceName = "default",
uint32_t sampleRate = 16000,
uint32_t channels = 1);
int write_samples(const int16_t* samples, int sampleCount);
bool play_wav(const std::string& path);
void shutdown();
bool is_open() const { return opened; }
uint32_t get_sample_rate() const { return sampleRate; }
uint32_t get_channels() const { return channels; }
const std::string& get_device_name() const { return deviceName; }
};
}

View File

@ -0,0 +1,122 @@
#include "ButtonInput.h"
#include <iostream>
#if defined(__linux__)
#include <errno.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#endif
namespace Game
{
ButtonInput::ButtonInput() :
devicePath("/dev/input/event0"),
keyCode(28),
opened(false),
down(false),
pressed(false),
released(false)
#if defined(__linux__)
,
fd(-1)
#endif
{}
ButtonInput::~ButtonInput()
{
shutdown();
}
bool ButtonInput::init(const std::string& devicePath, int keyCode)
{
shutdown();
this->devicePath = devicePath;
this->keyCode = keyCode;
down = false;
pressed = false;
released = false;
#if defined(__linux__)
fd = open(devicePath.c_str(), O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
std::cerr << "ButtonInput open failed: " << devicePath << std::endl;
return false;
}
opened = true;
return true;
#else
std::cerr << "ButtonInput is only implemented on Linux evdev." << std::endl;
return false;
#endif
}
void ButtonInput::update()
{
pressed = false;
released = false;
if (!opened)
{
return;
}
#if defined(__linux__)
input_event event;
while (true)
{
const ssize_t bytes = read(fd, &event, sizeof(event));
if (bytes == static_cast<ssize_t>(sizeof(event)))
{
if (event.type != EV_KEY || event.code != keyCode)
{
continue;
}
if (event.value == 1)
{
if (!down)
{
pressed = true;
}
down = true;
}
else if (event.value == 0)
{
if (down)
{
released = true;
}
down = false;
}
}
else
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
std::cerr << "ButtonInput read failed." << std::endl;
}
break;
}
}
#endif
}
void ButtonInput::shutdown()
{
#if defined(__linux__)
if (fd >= 0)
{
close(fd);
fd = -1;
}
#endif
opened = false;
down = false;
pressed = false;
released = false;
}
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <cstdint>
#include <string>
namespace Game
{
class ButtonInput
{
private:
std::string devicePath;
int keyCode;
bool opened;
bool down;
bool pressed;
bool released;
#if defined(__linux__)
int fd;
#endif
public:
ButtonInput();
~ButtonInput();
bool init(const std::string& devicePath = "/dev/input/event0", int keyCode = 28);
void update();
void shutdown();
bool is_open() const { return opened; }
bool is_down() const { return down; }
bool was_pressed() const { return pressed; }
bool was_released() const { return released; }
const std::string& get_device_path() const { return devicePath; }
int get_key_code() const { return keyCode; }
};
}

1
game/src/scenes/.gitkeep Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,98 @@
#include "AnimationSystem.h"
#include "../../../src/Rasterizer/SpriteRasterizer.h"
namespace Game
{
size_t AnimationSystem::add(SpriteAnimator* animator, int32_t x, int32_t y)
{
objects.push_back(AnimationObject(animator, x, y));
return objects.size() - 1;
}
void AnimationSystem::remove(size_t index)
{
if (index >= objects.size())
{
return;
}
objects.erase(objects.begin() + index);
}
void AnimationSystem::clear()
{
objects.clear();
}
void AnimationSystem::update(float deltaTime)
{
for (size_t i = 0; i < objects.size(); ++i)
{
if (objects[i].animator != nullptr)
{
objects[i].animator->update(deltaTime);
}
}
}
void AnimationSystem::draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const
{
for (size_t i = 0; i < objects.size(); ++i)
{
const AnimationObject& object = objects[i];
if (!object.visible || object.animator == nullptr)
{
continue;
}
const RenderData::Sprite* sprite = object.animator->get_current_sprite();
if (sprite != nullptr)
{
spriteRasterizer.DrawSprite(*sprite, object.x, object.y);
}
}
}
void AnimationSystem::set_position(size_t index, int32_t x, int32_t y)
{
AnimationObject* object = get_object(index);
if (object == nullptr)
{
return;
}
object->x = x;
object->y = y;
}
void AnimationSystem::set_visible(size_t index, bool visible)
{
AnimationObject* object = get_object(index);
if (object == nullptr)
{
return;
}
object->visible = visible;
}
AnimationObject* AnimationSystem::get_object(size_t index)
{
if (index >= objects.size())
{
return nullptr;
}
return &objects[index];
}
const AnimationObject* AnimationSystem::get_object(size_t index) const
{
if (index >= objects.size())
{
return nullptr;
}
return &objects[index];
}
}

View File

@ -0,0 +1,50 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "../components/SpriteAnimator.h"
namespace Rasterizer
{
class SpriteRasterizer;
}
namespace Game
{
struct AnimationObject
{
SpriteAnimator* animator;
int32_t x;
int32_t y;
bool visible;
AnimationObject() : animator(nullptr), x(0), y(0), visible(true) {}
AnimationObject(SpriteAnimator* animator, int32_t x, int32_t y) :
animator(animator),
x(x),
y(y),
visible(true)
{}
};
class AnimationSystem
{
private:
std::vector<AnimationObject> objects;
public:
size_t add(SpriteAnimator* animator, int32_t x, int32_t y);
void remove(size_t index);
void clear();
void update(float deltaTime);
void draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const;
void set_position(size_t index, int32_t x, int32_t y);
void set_visible(size_t index, bool visible);
AnimationObject* get_object(size_t index);
const AnimationObject* get_object(size_t index) const;
size_t count() const { return objects.size(); }
};
}

1
game/tests/fixtures/.gitkeep vendored Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,234 @@
#include <SDL.h>
#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "FrameBuffer.h"
#include "SDLDisplay.h"
#include "SpriteAssetLoader.h"
#include "SpriteRasterizer.h"
#include "Image.h"
#include "Sprite.h"
#include "SpriteAnimator.h"
#include "AnimationSystem.h"
static std::string FindAssetPath(const std::string& fileName)
{
const char* roots[] = {
"game/assets/sprites/",
"../game/assets/sprites/",
"../../game/assets/sprites/",
"../../../game/assets/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
{
const std::string path = std::string(roots[i]) + fileName;
std::ifstream file(path.c_str(), std::ios::binary);
if (file.good())
{
return path;
}
}
return std::string("game/assets/sprites/") + fileName;
}
static bool LoadSpriteImage(const std::string& fileName, RenderData::Image& image)
{
const std::string path = FindAssetPath(fileName);
if (!Asset::SpriteAssetLoader::Load(path, image))
{
std::cerr << "Load sprite failed: " << path << std::endl;
return false;
}
return true;
}
static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height)
{
if (!source.is_valid() || width <= 0 || height <= 0)
{
return RenderData::Image();
}
RenderData::Image result(width, height);
for (int32_t y = 0; y < height; ++y)
{
const int32_t sourceY = y * source.get_height() / height;
for (int32_t x = 0; x < width; ++x)
{
const int32_t sourceX = x * source.get_width() / width;
result.set_pixel(x, y, source.get_pixel_fast(sourceX, sourceY));
}
}
return result;
}
static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight)
{
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
{
return RenderData::Image();
}
const float scaleX = static_cast<float>(maxWidth) / source.get_width();
const float scaleY = static_cast<float>(maxHeight) / source.get_height();
const float scale = std::min(scaleX, scaleY);
const int32_t width = std::max(1, static_cast<int32_t>(source.get_width() * scale));
const int32_t height = std::max(1, static_cast<int32_t>(source.get_height() * scale));
return ResizeImageNearest(source, width, height);
}
static bool LoadFrames(
const std::vector<std::string>& fileNames,
std::vector<RenderData::Image>& images,
std::vector<RenderData::Sprite>& frames,
int32_t maxFrameWidth,
int32_t maxFrameHeight)
{
images.clear();
frames.clear();
images.reserve(fileNames.size());
for (size_t i = 0; i < fileNames.size(); ++i)
{
RenderData::Image image;
if (!LoadSpriteImage(fileNames[i], image))
{
return false;
}
images.push_back(ResizeImageToFit(image, maxFrameWidth, maxFrameHeight));
}
frames.reserve(images.size());
for (size_t i = 0; i < images.size(); ++i)
{
frames.push_back(RenderData::Sprite(&images[i]));
}
return !frames.empty();
}
int main(int argc, char* argv[])
{
(void)argc;
(void)argv;
const int32_t width = 800;
const int32_t height = 480;
Platform::SDLDisplay display;
if (!display.init(width, height))
{
return -1;
}
RenderData::Image originalBackground;
if (!LoadSpriteImage("background.sprite", originalBackground))
{
display.shutdown();
return -1;
}
RenderData::Image background = ResizeImageNearest(originalBackground, width, height);
std::vector<std::string> frameFiles;
frameFiles.push_back("Tom-stand.sprite");
frameFiles.push_back("Tom-listhen.sprite");
frameFiles.push_back("Tom-openmouse1.sprite");
frameFiles.push_back("Tom-openmouse2.sprite");
frameFiles.push_back("Tom-say1.sprite");
frameFiles.push_back("Tom-say2.sprite");
frameFiles.push_back("Tom-say3.sprite");
frameFiles.push_back("Tom-say4.sprite");
std::vector<RenderData::Image> frameImages;
std::vector<RenderData::Sprite> frames;
if (!LoadFrames(frameFiles, frameImages, frames, width * 7 / 10, height * 9 / 10))
{
display.shutdown();
return -1;
}
Core::FrameBuffer frameBuffer(width, height);
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
Game::SpriteAnimator tomAnimator(frames, 0.12f, true);
Game::AnimationSystem animationSystem;
const RenderData::Sprite* firstFrame = tomAnimator.get_current_sprite();
const int32_t tomX = firstFrame ? (width - firstFrame->width) / 2 : 0;
const int32_t tomY = firstFrame ? height - firstFrame->height - 20 : 0;
animationSystem.add(&tomAnimator, tomX, tomY);
std::cout << "Sprite animation test" << std::endl;
std::cout << "Space: play/pause, R: reset, Esc: quit" << std::endl;
std::cout << "Background: " << originalBackground.get_width() << "x" << originalBackground.get_height()
<< " -> " << background.get_width() << "x" << background.get_height() << std::endl;
std::cout << "Tom frame 0: " << frameImages[0].get_width() << "x" << frameImages[0].get_height() << std::endl;
bool shouldQuit = false;
uint32_t lastTime = display.get_time_ms();
while (!shouldQuit)
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
{
shouldQuit = true;
}
else if (event.type == SDL_KEYDOWN)
{
if (event.key.repeat != 0)
{
continue;
}
if (event.key.keysym.sym == SDLK_ESCAPE)
{
shouldQuit = true;
}
else if (event.key.keysym.sym == SDLK_SPACE)
{
if (tomAnimator.is_playing())
{
tomAnimator.stop();
}
else
{
tomAnimator.play();
}
}
else if (event.key.keysym.sym == SDLK_r)
{
tomAnimator.reset();
tomAnimator.play();
}
}
}
const uint32_t currentTime = display.get_time_ms();
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
lastTime = currentTime;
animationSystem.update(deltaTime);
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
spriteRasterizer.DrawImage(background, 0, 0);
animationSystem.draw(spriteRasterizer);
display.present(&frameBuffer);
SDL_Delay(16);
}
display.shutdown();
return 0;
}

1
game/tests/unit/.gitkeep Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,581 @@
#include <SDL.h>
#include <SDL_image.h>
#include <algorithm>
#include <cerrno>
#include <cctype>
#include <climits>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#include "Image.h"
#include "SpriteAssetLoader.h"
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#else
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#endif
namespace
{
enum class TransformMode
{
None,
Resize,
Fit,
Tom800x480Preset
};
struct TransformOptions
{
TransformMode mode;
int32_t width;
int32_t height;
TransformOptions() : mode(TransformMode::None), width(0), height(0) {}
};
static bool IsPathSeparator(char value)
{
return value == '/' || value == '\\';
}
static std::string ToLower(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(), [](char value) {
return static_cast<char>(std::tolower(static_cast<unsigned char>(value)));
});
return text;
}
static std::string JoinPath(const std::string& directory, const std::string& fileName)
{
if (directory.empty())
{
return fileName;
}
if (IsPathSeparator(directory[directory.size() - 1]))
{
return directory + fileName;
}
return directory + "/" + fileName;
}
static std::string GetFileName(const std::string& path)
{
const size_t slash = path.find_last_of("/\\");
if (slash == std::string::npos)
{
return path;
}
return path.substr(slash + 1);
}
static std::string GetParentPath(const std::string& path)
{
const size_t slash = path.find_last_of("/\\");
if (slash == std::string::npos)
{
return std::string();
}
if (slash == 0)
{
return path.substr(0, 1);
}
return path.substr(0, slash);
}
static std::string ChangeExtensionToSprite(const std::string& fileName)
{
const size_t slash = fileName.find_last_of("/\\");
const size_t dot = fileName.find_last_of('.');
if (dot != std::string::npos && (slash == std::string::npos || dot > slash))
{
return fileName.substr(0, dot) + Asset::SpriteAssetLoader::GetFileExtension();
}
return fileName + Asset::SpriteAssetLoader::GetFileExtension();
}
static bool HasPngExtension(const std::string& path)
{
const std::string lower = ToLower(path);
return lower.size() >= 4 && lower.substr(lower.size() - 4) == ".png";
}
static bool ParsePositiveInt(const char* text, int32_t& value)
{
if (text == nullptr || text[0] == '\0')
{
return false;
}
errno = 0;
char* end = nullptr;
const long parsed = std::strtol(text, &end, 10);
if (errno != 0 || end == text || *end != '\0' || parsed <= 0 || parsed > INT_MAX)
{
return false;
}
value = static_cast<int32_t>(parsed);
return true;
}
static bool DirectoryExists(const std::string& path)
{
#if defined(_WIN32)
const DWORD attrs = GetFileAttributesA(path.c_str());
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
#else
struct stat info;
return stat(path.c_str(), &info) == 0 && S_ISDIR(info.st_mode);
#endif
}
static bool CreateSingleDirectory(const std::string& path)
{
if (path.empty() || DirectoryExists(path))
{
return true;
}
#if defined(_WIN32)
if (CreateDirectoryA(path.c_str(), nullptr) != 0)
{
return true;
}
return GetLastError() == ERROR_ALREADY_EXISTS && DirectoryExists(path);
#else
if (mkdir(path.c_str(), 0755) == 0)
{
return true;
}
return errno == EEXIST && DirectoryExists(path);
#endif
}
static bool EnsureDirectoryTree(const std::string& path)
{
if (path.empty())
{
return true;
}
std::string normalized = path;
while (!normalized.empty() && IsPathSeparator(normalized[normalized.size() - 1]))
{
normalized.erase(normalized.size() - 1);
}
if (normalized.empty())
{
return true;
}
size_t start = 0;
if (normalized.size() >= 2 && normalized[1] == ':')
{
start = 2;
}
if (start < normalized.size() && IsPathSeparator(normalized[start]))
{
++start;
}
for (size_t i = start; i <= normalized.size(); ++i)
{
if (i != normalized.size() && !IsPathSeparator(normalized[i]))
{
continue;
}
const std::string part = normalized.substr(0, i);
if (part.empty() || (part.size() == 2 && part[1] == ':'))
{
continue;
}
if (!CreateSingleDirectory(part))
{
std::cerr << "Create directory failed: " << part << std::endl;
return false;
}
}
return DirectoryExists(normalized);
}
static bool EnsureParentDirectory(const std::string& path)
{
return EnsureDirectoryTree(GetParentPath(path));
}
static bool ListPngFiles(const std::string& directory, std::vector<std::string>& fileNames)
{
fileNames.clear();
#if defined(_WIN32)
const std::string searchPath = JoinPath(directory, "*");
WIN32_FIND_DATAA data;
HANDLE handle = FindFirstFileA(searchPath.c_str(), &data);
if (handle == INVALID_HANDLE_VALUE)
{
return false;
}
do
{
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
HasPngExtension(data.cFileName))
{
fileNames.push_back(data.cFileName);
}
}
while (FindNextFileA(handle, &data) != 0);
FindClose(handle);
#else
DIR* dir = opendir(directory.c_str());
if (dir == nullptr)
{
return false;
}
dirent* entry = nullptr;
while ((entry = readdir(dir)) != nullptr)
{
const std::string name = entry->d_name;
if (name == "." || name == ".." || !HasPngExtension(name))
{
continue;
}
fileNames.push_back(name);
}
closedir(dir);
#endif
std::sort(fileNames.begin(), fileNames.end(), [](const std::string& left, const std::string& right) {
return ToLower(left) < ToLower(right);
});
return true;
}
}
static bool LoadPngImage(const std::string& path, RenderData::Image& image)
{
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
if (loadedSurface == nullptr)
{
std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl;
return false;
}
SDL_Surface* rgbaSurface = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0);
SDL_FreeSurface(loadedSurface);
if (rgbaSurface == nullptr)
{
std::cerr << "SDL_ConvertSurfaceFormat failed: " << SDL_GetError() << std::endl;
return false;
}
std::vector<uint32_t> pixels(static_cast<size_t>(rgbaSurface->w) * rgbaSurface->h, 0);
if (SDL_LockSurface(rgbaSurface) != 0)
{
std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl;
SDL_FreeSurface(rgbaSurface);
return false;
}
for (int32_t y = 0; y < rgbaSurface->h; ++y)
{
const uint8_t* srcRow = static_cast<const uint8_t*>(rgbaSurface->pixels) + y * rgbaSurface->pitch;
const uint32_t* srcPixels = reinterpret_cast<const uint32_t*>(srcRow);
for (int32_t x = 0; x < rgbaSurface->w; ++x)
{
pixels[static_cast<size_t>(y) * rgbaSurface->w + x] = srcPixels[x];
}
}
SDL_UnlockSurface(rgbaSurface);
image = RenderData::Image(rgbaSurface->w, rgbaSurface->h, pixels);
SDL_FreeSurface(rgbaSurface);
return image.is_valid();
}
static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height)
{
if (!source.is_valid() || width <= 0 || height <= 0)
{
return RenderData::Image();
}
RenderData::Image result(width, height);
for (int32_t y = 0; y < height; ++y)
{
const int32_t sourceY = y * source.get_height() / height;
for (int32_t x = 0; x < width; ++x)
{
const int32_t sourceX = x * source.get_width() / width;
result.set_pixel(x, y, source.get_pixel_fast(sourceX, sourceY));
}
}
return result;
}
static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight)
{
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
{
return RenderData::Image();
}
const float scaleX = static_cast<float>(maxWidth) / source.get_width();
const float scaleY = static_cast<float>(maxHeight) / source.get_height();
const float scale = std::min(scaleX, scaleY);
const int32_t width = std::max(1, static_cast<int32_t>(source.get_width() * scale));
const int32_t height = std::max(1, static_cast<int32_t>(source.get_height() * scale));
return ResizeImageNearest(source, width, height);
}
static TransformOptions ResolveTransformForInput(
const std::string& inputPath,
const TransformOptions& requested)
{
if (requested.mode != TransformMode::Tom800x480Preset)
{
return requested;
}
TransformOptions resolved;
const std::string fileName = ToLower(GetFileName(inputPath));
if (fileName == "background.png")
{
resolved.mode = TransformMode::Resize;
resolved.width = 800;
resolved.height = 480;
}
else if (fileName.find("tom-") == 0)
{
resolved.mode = TransformMode::Fit;
resolved.width = 560;
resolved.height = 360;
}
else if (fileName.find("ui-") == 0)
{
resolved.mode = TransformMode::Fit;
resolved.width = 72;
resolved.height = 72;
}
return resolved;
}
static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, RenderData::Image& image)
{
const TransformOptions resolved = ResolveTransformForInput(inputPath, options);
switch (resolved.mode)
{
case TransformMode::None:
return image.is_valid();
case TransformMode::Resize:
image = ResizeImageNearest(image, resolved.width, resolved.height);
return image.is_valid();
case TransformMode::Fit:
image = ResizeImageToFit(image, resolved.width, resolved.height);
return image.is_valid();
case TransformMode::Tom800x480Preset:
return false;
}
return false;
}
static bool ConvertFile(
const std::string& inputPath,
const std::string& outputPath,
const TransformOptions& transformOptions)
{
RenderData::Image image;
if (!LoadPngImage(inputPath, image))
{
return false;
}
if (!ApplyTransform(inputPath, transformOptions, image))
{
std::cerr << "Image transform failed: " << inputPath << std::endl;
return false;
}
if (!EnsureParentDirectory(outputPath))
{
return false;
}
if (!Asset::SpriteAssetLoader::Save(outputPath, image))
{
std::cerr << "Save failed: " << outputPath << std::endl;
return false;
}
std::cout << inputPath << " -> " << outputPath
<< " (" << image.get_width() << "x" << image.get_height() << ")" << std::endl;
return true;
}
static bool ConvertBatch(
const std::string& inputDirectory,
const std::string& outputDirectory,
const TransformOptions& transformOptions)
{
std::vector<std::string> files;
if (!ListPngFiles(inputDirectory, files) || files.empty())
{
std::cerr << "No PNG files found: " << inputDirectory << std::endl;
return false;
}
if (!EnsureDirectoryTree(outputDirectory))
{
return false;
}
size_t successCount = 0;
for (size_t i = 0; i < files.size(); ++i)
{
const std::string inputPath = JoinPath(inputDirectory, files[i]);
const std::string outputPath = JoinPath(outputDirectory, ChangeExtensionToSprite(files[i]));
if (ConvertFile(inputPath, outputPath, transformOptions))
{
++successCount;
}
}
std::cout << "Converted " << successCount << " / " << files.size() << " PNG files." << std::endl;
return successCount == files.size();
}
static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options)
{
if (startIndex >= argc)
{
options = TransformOptions();
return true;
}
const std::string mode = argv[startIndex];
if (mode == "--resize" || mode == "--fit")
{
if (startIndex + 2 >= argc)
{
return false;
}
int32_t width = 0;
int32_t height = 0;
if (!ParsePositiveInt(argv[startIndex + 1], width) ||
!ParsePositiveInt(argv[startIndex + 2], height) ||
startIndex + 3 != argc)
{
return false;
}
options.mode = mode == "--resize" ? TransformMode::Resize : TransformMode::Fit;
options.width = width;
options.height = height;
return true;
}
if (mode == "--preset")
{
if (startIndex + 2 != argc)
{
return false;
}
const std::string presetName = argv[startIndex + 1];
if (presetName != "tom-800x480")
{
std::cerr << "Unknown preset: " << presetName << std::endl;
return false;
}
options.mode = TransformMode::Tom800x480Preset;
return true;
}
return false;
}
static void PrintUsage()
{
std::cout << "Usage:" << std::endl;
std::cout << " SpriteAssetTool input.png output.sprite [--resize width height]" << std::endl;
std::cout << " SpriteAssetTool input.png output.sprite [--fit maxWidth maxHeight]" << std::endl;
std::cout << " SpriteAssetTool --batch inputDir outputDir [--resize width height]" << std::endl;
std::cout << " SpriteAssetTool --batch inputDir outputDir [--fit maxWidth maxHeight]" << std::endl;
std::cout << " SpriteAssetTool --batch game/assets/raw game/assets/sprites --preset tom-800x480" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc < 3)
{
PrintUsage();
return 1;
}
const int imageFlags = IMG_INIT_PNG;
if ((IMG_Init(imageFlags) & imageFlags) != imageFlags)
{
std::cerr << "IMG_Init failed: " << IMG_GetError() << std::endl;
return 1;
}
TransformOptions transformOptions;
bool ok = false;
const std::string command = argv[1];
if (command == "--batch")
{
if (argc < 4 || !ParseTransformOptions(argc, argv, 4, transformOptions))
{
PrintUsage();
IMG_Quit();
return 1;
}
ok = ConvertBatch(argv[2], argv[3], transformOptions);
}
else
{
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
{
PrintUsage();
IMG_Quit();
return 1;
}
ok = ConvertFile(argv[1], argv[2], transformOptions);
}
IMG_Quit();
return ok ? 0 : 1;
}

View File

@ -1,22 +1,28 @@
#include <iostream> #include <iostream>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
<<<<<<< HEAD:src/main.cpp
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <vector> #include <vector>
=======
#include <cstdio>
#include <cstring>
#include <thread>
#include <chrono>
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#include "Vector2.h" #include "Vector2.h"
#include "Vector3.h" #include "Vector3.h"
#include "Vector4.h" #include "Vector4.h"
#include "Matrix4x4.h" #include "Matrix4x4.h"
#include "MathUtil.h" #include "MathUtil.h"
#include "Color.h" #include "Color.h"
#include "FrameBuffer.h"
#include "Rasterizer.h"
#include "TriangleRasterizer.h"
#include "Triangle.h" #include "Triangle.h"
#include "Camera.h" #include "Camera.h"
#include <cstdlib> #include <cstdlib>
#include "Timer.h"
#include "Vertex.h" #include "Vertex.h"
<<<<<<< HEAD:src/main.cpp
#include "DepthBuffer.h" #include "DepthBuffer.h"
#include "SpriteAssetLoader.h" #include "SpriteAssetLoader.h"
#include "SpriteRasterizer.h" #include "SpriteRasterizer.h"
@ -25,8 +31,14 @@
#include "AudioInput.h" #include "AudioInput.h"
#include "AudioOutput.h" #include "AudioOutput.h"
#include "ButtonInput.h" #include "ButtonInput.h"
=======
#include "DrawContext.h"
#include "test_sprite.h"
#include "font_atlas.h"
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#include "Display.h" #include "Display.h"
#include "TimeSource.h"
#ifdef USE_FRAMEBUFFER #ifdef USE_FRAMEBUFFER
#include "FBDisplay.h" #include "FBDisplay.h"
#else #else
@ -115,6 +127,7 @@ static bool IsTriangleVisible(const CubeTriangle &triangle, const std::array<Mat
return faceNormal.dot(faceCenter) > 0.0f; return faceNormal.dot(faceCenter) > 0.0f;
} }
<<<<<<< HEAD:src/main.cpp
static bool HasArg(int argc, char* argv[], const std::string& expected) static bool HasArg(int argc, char* argv[], const std::string& expected)
{ {
for (int i = 1; i < argc; ++i) for (int i = 1; i < argc; ++i)
@ -253,14 +266,101 @@ static int RunTomGame()
display->shutdown(); display->shutdown();
delete display; delete display;
return 0; return 0;
=======
static void PrintUsage(const char *program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
struct ProgramOptions
{
uint32_t target_fps;
bool show_help;
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
}
};
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
{
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char *arg = argv[i];
const char *fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::ITimeSource &time_source)
{
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
if (sleep_ms > 0u)
{
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
} }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
<<<<<<< HEAD:src/main.cpp
if (HasArg(argc, argv, "--tom")) if (HasArg(argc, argv, "--tom"))
{ {
return RunTomGame(); return RunTomGame();
} }
=======
const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help)
{
PrintUsage(argv[0]);
return 0;
}
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#ifdef USE_FRAMEBUFFER #ifdef USE_FRAMEBUFFER
Platform::IDisplay *display = new Platform::FBDisplay(); Platform::IDisplay *display = new Platform::FBDisplay();
@ -274,10 +374,25 @@ int main(int argc, char *argv[])
return -1; return -1;
} }
Core::FrameBuffer *frameBuffer = new Core::FrameBuffer(width, height); Gfx::DrawContext ctx(width, height);
Core::DepthBuffer *depthBuffer = new Core::DepthBuffer(width, height); std::cout << "Target FPS: " << timer.target_fps() << std::endl;
Rasterizer::Rasterizer rasterizer(frameBuffer, depthBuffer);
Rasterizer::TriangleRasterizer triangleRasterizer(frameBuffer, depthBuffer); RenderData::BitmapFont font;
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
font.char_w = font_char_w;
font.char_h = font_char_h;
font.columns = font_columns;
font.first_char = font_first_char;
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
const std::array<uint16_t, 8 * 4> tileIds = {
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
Scene::Camera camera; Scene::Camera camera;
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f); camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
@ -310,20 +425,31 @@ int main(int argc, char *argv[])
const RenderData::Color clearColor(18, 18, 24, 255); const RenderData::Color clearColor(18, 18, 24, 255);
const RenderData::Color cubeColor(240, 240, 240, 255); const RenderData::Color cubeColor(240, 240, 240, 255);
const RenderData::Color fpsColor(0, 255, 80, 255);
const RenderData::Color fpsBg(0, 0, 0, 200);
const float aspectRatio = static_cast<float>(width) / static_cast<float>(height); const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
bool isRuning = true; int32_t fps = 0;
while (isRuning) int32_t frame_count = 0;
uint32_t last_fps_time = 0;
char fps_text[32];
bool isRunning = true;
uint32_t animation_time_ms = 0;
while (isRunning)
{ {
display->poll_events(isRuning); timer.begin_frame(time_source.get_time_ms());
const uint32_t fixed_delta_ms = timer.fixed_delta_ms();
animation_time_ms += fixed_delta_ms;
frameBuffer->clear(clearColor); display->poll_events(isRunning);
depthBuffer->clear();
const float timeSeconds = static_cast<float>(display->get_time_ms()) * 0.001f; ctx.clear(clearColor);
const float animation_time = static_cast<float>(animation_time_ms) / 1000.0f;
const Math::Matrix4x4 model = const Math::Matrix4x4 model =
Math::MathUtil::get_rotation_matrix_y(timeSeconds) * Math::MathUtil::get_rotation_matrix_y(animation_time) *
Math::MathUtil::get_rotation_matrix_x(timeSeconds * 0.6f); Math::MathUtil::get_rotation_matrix_x(static_cast<float>(animation_time_ms * 6u) / 10000.0f);
const Math::Matrix4x4 view = camera.get_view_matrix(); const Math::Matrix4x4 view = camera.get_view_matrix();
const Math::Matrix4x4 modelView = view * model; const Math::Matrix4x4 modelView = view * model;
const Math::Matrix4x4 projection = camera.get_perspective_projection_matrix(aspectRatio); const Math::Matrix4x4 projection = camera.get_perspective_projection_matrix(aspectRatio);
@ -361,10 +487,6 @@ int main(int argc, char *argv[])
continue; continue;
} }
const Math::Vector3 &viewV0 = viewSpaceVertices[cubeTriangle.vertices[0]];
const Math::Vector3 &viewV1 = viewSpaceVertices[cubeTriangle.vertices[1]];
const Math::Vector3 &viewV2 = viewSpaceVertices[cubeTriangle.vertices[2]];
drawTriangles[drawCommandCount++] = drawTriangles[drawCommandCount++] =
RenderData::Triangle( RenderData::Triangle(
Scene::Vertex(v0.screen), Scene::Vertex(v0.screen),
@ -374,7 +496,7 @@ int main(int argc, char *argv[])
for (size_t i = 0; i < drawCommandCount; ++i) for (size_t i = 0; i < drawCommandCount; ++i)
{ {
triangleRasterizer.DrawTriangle2D(drawTriangles[i], cubeColor); ctx.draw_triangle(drawTriangles[i], cubeColor);
} }
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex) for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
@ -396,19 +518,36 @@ int main(int argc, char *argv[])
continue; continue;
} }
rasterizer.DrawLine( ctx.draw_line(
Math::Vector2(start.screen.x, start.screen.y).to_vector2Int(), Math::Vector2(start.screen.x, start.screen.y).to_vector2Int(),
Math::Vector2(end.screen.x, end.screen.y).to_vector2Int(), Math::Vector2(end.screen.x, end.screen.y).to_vector2Int(),
clearColor); clearColor);
} }
} }
display->present(frameBuffer); // sprite 测试
ctx.draw_sprite(10, 10, sprite_img);
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(animation_time_ms / 20u) % 32, 0);
// FPS 计数
++frame_count;
const uint32_t now = time_source.get_time_ms();
if (now - last_fps_time >= 1000)
{
fps = frame_count;
frame_count = 0;
last_fps_time = now;
}
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
ctx.present(display);
SleepRemainingFrameTime(timer, time_source);
} }
display->shutdown(); display->shutdown();
delete display; delete display;
delete frameBuffer;
delete depthBuffer;
return 0; return 0;
} }

View File

@ -0,0 +1,180 @@
#include "SpriteAssetLoader.h"
#include <cstdint>
#include <fstream>
#include <limits>
#include <vector>
namespace
{
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
static const size_t SpriteHeaderSize = 20;
static const uint32_t SpriteVersion = 1;
static const uint32_t SpriteFormatRgba8888 = 1;
static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
static bool ReadExact(std::ifstream& file, char* data, size_t size)
{
file.read(data, static_cast<std::streamsize>(size));
return static_cast<size_t>(file.gcount()) == size;
}
static bool ReadU32LE(const std::vector<uint8_t>& bytes, size_t offset, uint32_t& value)
{
if (offset + 4 > bytes.size())
{
return false;
}
value =
static_cast<uint32_t>(bytes[offset]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
return true;
}
static void WriteU32LE(std::ofstream& file, uint32_t value)
{
const char bytes[4] = {
static_cast<char>(value & 0xFF),
static_cast<char>((value >> 8) & 0xFF),
static_cast<char>((value >> 16) & 0xFF),
static_cast<char>((value >> 24) & 0xFF)
};
file.write(bytes, sizeof(bytes));
}
static bool CheckPixelCount(uint32_t width, uint32_t height, size_t& pixelCount)
{
if (width == 0 || height == 0)
{
return false;
}
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
if (total > MaxSpritePixels ||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t)))
{
return false;
}
pixelCount = static_cast<size_t>(total);
return true;
}
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels)
{
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0);
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
{
return false;
}
for (size_t i = 0; i < pixels.size(); ++i)
{
uint32_t value = 0;
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value))
{
return false;
}
pixels[i] = value;
}
return true;
}
static void WritePixels(std::ofstream& file, const RenderData::Image& image)
{
for (size_t i = 0; i < image.total_pixels(); ++i)
{
WriteU32LE(file, image.data()[i]);
}
}
}
namespace Asset
{
bool SpriteAssetLoader::Load(const std::string& path, RenderData::Image& image)
{
std::ifstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
return false;
}
std::vector<uint8_t> header(SpriteHeaderSize, 0);
if (!ReadExact(file, reinterpret_cast<char*>(header.data()), header.size()))
{
return false;
}
if (header[0] != SpriteMagic[0] ||
header[1] != SpriteMagic[1] ||
header[2] != SpriteMagic[2] ||
header[3] != SpriteMagic[3])
{
return false;
}
uint32_t version = 0;
uint32_t width = 0;
uint32_t height = 0;
uint32_t format = 0;
if (!ReadU32LE(header, 4, version) ||
!ReadU32LE(header, 8, width) ||
!ReadU32LE(header, 12, height) ||
!ReadU32LE(header, 16, format))
{
return false;
}
size_t pixelCount = 0;
if (version != SpriteVersion ||
format != SpriteFormatRgba8888 ||
!CheckPixelCount(width, height, pixelCount))
{
return false;
}
std::vector<uint32_t> pixels(pixelCount, 0);
if (!ReadPixels(file, pixels))
{
return false;
}
char trailingByte = 0;
file.read(&trailingByte, 1);
if (file.gcount() != 0)
{
return false;
}
image = RenderData::Image(
static_cast<int32_t>(width),
static_cast<int32_t>(height),
pixels);
return image.is_valid();
}
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
{
if (!image.is_valid())
{
return false;
}
std::ofstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
return false;
}
file.write(SpriteMagic, sizeof(SpriteMagic));
WriteU32LE(file, SpriteVersion);
WriteU32LE(file, static_cast<uint32_t>(image.get_width()));
WriteU32LE(file, static_cast<uint32_t>(image.get_height()));
WriteU32LE(file, SpriteFormatRgba8888);
WritePixels(file, image);
return file.good();
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <string>
#include "Image.h"
namespace Asset
{
class SpriteAssetLoader
{
public:
static const char* GetFileExtension() { return ".sprite"; }
static bool Load(const std::string& path, RenderData::Image& image);
static bool Save(const std::string& path, const RenderData::Image& image);
};
}

View File

71
src/Gfx/Core/Timer.h Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include <cstdint>
namespace Core
{
class Timer
{
public:
static const uint32_t DefaultFps = 30;
explicit Timer(uint32_t target_fps = DefaultFps)
: target_fps_(normalize_fps(target_fps)),
tick_remainder_(0),
frame_start_ms_(0),
fixed_delta_ms_(0)
{
}
void begin_frame(uint32_t now_ms)
{
frame_start_ms_ = now_ms;
fixed_delta_ms_ = next_tick_ms();
}
uint32_t target_fps() const
{
return target_fps_;
}
uint32_t fixed_delta_ms() const
{
return fixed_delta_ms_;
}
uint32_t frame_start_ms() const
{
return frame_start_ms_;
}
uint32_t remaining_frame_ms(uint32_t now_ms) const
{
const uint32_t elapsed_ms = now_ms - frame_start_ms_;
return elapsed_ms < fixed_delta_ms_ ? fixed_delta_ms_ - elapsed_ms : 0u;
}
static bool is_supported_fps(uint32_t fps)
{
return fps == 30u || fps == 45u || fps == 60u;
}
static uint32_t normalize_fps(uint32_t fps)
{
return is_supported_fps(fps) ? fps : DefaultFps;
}
private:
uint32_t next_tick_ms()
{
tick_remainder_ += 1000u;
const uint32_t tick_ms = tick_remainder_ / target_fps_;
tick_remainder_ %= target_fps_;
return tick_ms;
}
uint32_t target_fps_;
uint32_t tick_remainder_;
uint32_t frame_start_ms_;
uint32_t fixed_delta_ms_;
};
}

View File

@ -0,0 +1,323 @@
#include "DrawContext.h"
#include "FrameBuffer.h"
#include "DepthBuffer.h"
#include "Rasterizer.h"
#include "TriangleRasterizer.h"
#include "Display.h"
#include <algorithm>
namespace Gfx
{
DrawContext::DrawContext(int32_t width, int32_t height)
{
frameBuffer = new Core::FrameBuffer(width, height);
depthBuffer = new Core::DepthBuffer(width, height);
rasterizer = new Rasterizer::Rasterizer(frameBuffer, depthBuffer);
triangleRasterizer = new Rasterizer::TriangleRasterizer(frameBuffer, depthBuffer);
}
DrawContext::~DrawContext()
{
delete triangleRasterizer;
delete rasterizer;
delete depthBuffer;
delete frameBuffer;
}
int32_t DrawContext::get_width() const
{
return frameBuffer->get_width();
}
int32_t DrawContext::get_height() const
{
return frameBuffer->get_height();
}
void DrawContext::clear(const RenderData::Color& color)
{
frameBuffer->clear(color);
depthBuffer->clear();
}
void DrawContext::clear_depth()
{
depthBuffer->clear();
}
void DrawContext::draw_line(const Math::Vector2Int& from, const Math::Vector2Int& to, const RenderData::Color& color)
{
rasterizer->DrawLine(from, to, color);
}
void DrawContext::draw_triangle(const RenderData::Triangle& triangle, const RenderData::Color& color)
{
triangleRasterizer->DrawTriangle2D(triangle, color);
}
void DrawContext::draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::Image& img)
{
draw_sprite_region(dst_x, dst_y, img, 0, 0, img.width, img.height);
}
void DrawContext::draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region)
{
draw_sprite_region(dst_x, dst_y, region);
}
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h)
{
draw_sprite_ex(dst_x, dst_y, img, src_x, src_y, src_w, src_h, 1, false, false);
}
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region)
{
draw_sprite_region_ex(dst_x, dst_y, region, 1, false, false);
}
void DrawContext::draw_sprite_region_ex(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region,
int32_t scale, bool flip_h, bool flip_v)
{
if (!region.atlas) return;
draw_sprite_ex(
dst_x,
dst_y,
*region.atlas,
region.x,
region.y,
region.width,
region.height,
scale,
flip_h,
flip_v);
}
void DrawContext::draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
int32_t scale, bool flip_h, bool flip_v)
{
if (scale < 1 || !img.pixels || src_w <= 0 || src_h <= 0) return;
if (src_x < 0 || src_y < 0 || src_x + src_w > img.width || src_y + src_h > img.height) return;
const int32_t img_w = img.width;
const int32_t screen_w = frameBuffer->get_width();
const int32_t screen_h = frameBuffer->get_height();
const int32_t draw_w = src_w * scale;
const int32_t draw_h = src_h * scale;
if (dst_x >= screen_w || dst_y >= screen_h || dst_x + draw_w <= 0 || dst_y + draw_h <= 0) return;
int32_t start_dx = 0;
int32_t start_dy = 0;
int32_t end_dx = draw_w;
int32_t end_dy = draw_h;
if (dst_x < 0) start_dx = -dst_x;
if (dst_y < 0) start_dy = -dst_y;
if (dst_x + end_dx > screen_w) end_dx = screen_w - dst_x;
if (dst_y + end_dy > screen_h) end_dy = screen_h - dst_y;
const uint32_t* src = img.pixels;
const bool has_key = img.has_color_key;
const uint32_t key = img.color_key;
if (scale == 1)
{
for (int32_t sy = start_dy; sy < end_dy; ++sy)
{
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + sy;
for (int32_t sx = start_dx; sx < end_dx; ++sx)
{
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x];
if (has_key && pixel == key) continue;
frameBuffer->set_pixel(dst_x + sx, dst_y_abs, pixel);
}
}
return;
}
for (int32_t dy_abs = start_dy; dy_abs < end_dy; ++dy_abs)
{
const int32_t sy = dy_abs / scale;
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + dy_abs;
for (int32_t dx_abs = start_dx; dx_abs < end_dx; ++dx_abs)
{
const int32_t sx = dx_abs / scale;
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x];
if (has_key && pixel == key) continue;
frameBuffer->set_pixel(dst_x + dx_abs, dst_y_abs, pixel);
}
}
}
void DrawContext::draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t camera_x, int32_t camera_y)
{
draw_tilemap(tilemap,
screen_x,
screen_y,
frameBuffer->get_width() - screen_x,
frameBuffer->get_height() - screen_y,
camera_x,
camera_y);
}
void DrawContext::draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y)
{
if (!tilemap.tiles || !tilemap.atlas || !tilemap.atlas->pixels) return;
if (tilemap.width <= 0 || tilemap.height <= 0) return;
if (tilemap.tile_w <= 0 || tilemap.tile_h <= 0 || tilemap.atlas_columns <= 0) return;
if (viewport_w <= 0 || viewport_h <= 0) return;
const int32_t viewport_left = screen_x;
const int32_t viewport_top = screen_y;
const int32_t viewport_right = screen_x + viewport_w;
const int32_t viewport_bottom = screen_y + viewport_h;
int32_t start_tile_x = camera_x / tilemap.tile_w;
int32_t start_tile_y = camera_y / tilemap.tile_h;
int32_t offset_x = -(camera_x % tilemap.tile_w);
int32_t offset_y = -(camera_y % tilemap.tile_h);
if (camera_x < 0 && camera_x % tilemap.tile_w != 0)
{
--start_tile_x;
offset_x = -camera_x - (-start_tile_x * tilemap.tile_w);
}
if (camera_y < 0 && camera_y % tilemap.tile_h != 0)
{
--start_tile_y;
offset_y = -camera_y - (-start_tile_y * tilemap.tile_h);
}
const int32_t visible_cols = viewport_w / tilemap.tile_w + 2;
const int32_t visible_rows = viewport_h / tilemap.tile_h + 2;
for (int32_t row = 0; row < visible_rows; ++row)
{
const int32_t map_y = start_tile_y + row;
if (map_y < 0 || map_y >= tilemap.height) continue;
const int32_t dst_y = screen_y + offset_y + row * tilemap.tile_h;
for (int32_t col = 0; col < visible_cols; ++col)
{
const int32_t map_x = start_tile_x + col;
if (map_x < 0 || map_x >= tilemap.width) continue;
const uint16_t tile_id = tilemap.get_tile(map_x, map_y);
if (tile_id == RenderData::Tilemap::EmptyTile) continue;
const int32_t src_x = (tile_id % tilemap.atlas_columns) * tilemap.tile_w;
const int32_t src_y = (tile_id / tilemap.atlas_columns) * tilemap.tile_h;
const int32_t dst_x = screen_x + offset_x + col * tilemap.tile_w;
const int32_t tile_right = dst_x + tilemap.tile_w;
const int32_t tile_bottom = dst_y + tilemap.tile_h;
int32_t clipped_left = dst_x;
int32_t clipped_top = dst_y;
int32_t clipped_right = tile_right;
int32_t clipped_bottom = tile_bottom;
if (clipped_left < viewport_left) clipped_left = viewport_left;
if (clipped_top < viewport_top) clipped_top = viewport_top;
if (clipped_right > viewport_right) clipped_right = viewport_right;
if (clipped_bottom > viewport_bottom) clipped_bottom = viewport_bottom;
if (clipped_left >= clipped_right || clipped_top >= clipped_bottom) continue;
const int32_t clipped_src_x = src_x + (clipped_left - dst_x);
const int32_t clipped_src_y = src_y + (clipped_top - dst_y);
const int32_t clipped_w = clipped_right - clipped_left;
const int32_t clipped_h = clipped_bottom - clipped_top;
draw_sprite_ex(clipped_left, clipped_top, *tilemap.atlas,
clipped_src_x, clipped_src_y, clipped_w, clipped_h,
1, false, false);
}
}
}
void DrawContext::fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color)
{
const uint32_t rgba = color.to_rgba();
for (int32_t row = y; row < y + h; ++row)
{
for (int32_t col = x; col < x + w; ++col)
{
frameBuffer->set_pixel(col, row, rgba);
}
}
}
void DrawContext::draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
const RenderData::Color& color, const char* text)
{
if (!font.atlas.pixels || !text) return;
const uint32_t rgba = color.to_rgba();
const int32_t cw = font.char_w;
const int32_t ch = font.char_h;
const int32_t atlas_w = font.atlas.width;
const uint32_t* src = font.atlas.pixels;
int32_t cursor_x = x;
for (const char* p = text; *p; ++p)
{
const int32_t index = static_cast<int32_t>(*p) - font.first_char;
if (index < 0) { cursor_x += cw; continue; }
const int32_t col = index % font.columns;
const int32_t row = index / font.columns;
const int32_t src_x = col * cw;
const int32_t src_y = row * ch;
for (int32_t sy = 0; sy < ch; ++sy)
{
const uint32_t* atlas_row = src + (src_y + sy) * atlas_w;
const int32_t dst_y_abs = y + sy;
for (int32_t sx = 0; sx < cw; ++sx)
{
const uint32_t pixel = atlas_row[src_x + sx];
if ((pixel & 0xFF) == 0) continue;
frameBuffer->set_pixel(cursor_x + sx, dst_y_abs, rgba);
}
}
cursor_x += cw;
}
}
void DrawContext::draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
const RenderData::Color& color, const RenderData::Color& bg_color, const char* text)
{
if (!text) return;
int32_t len = 0;
for (const char* p = text; *p; ++p) ++len;
fill_rect(x, y, len * font.char_w, font.char_h, bg_color);
draw_text(font, x, y, color, text);
}
void DrawContext::present(Platform::IDisplay* display)
{
display->present(frameBuffer);
}
}

View File

@ -0,0 +1,80 @@
#pragma once
#include "Color.h"
#include "Vector2.h"
#include "Triangle.h"
#include "Image.h"
#include "SpriteRegion.h"
#include "Tilemap.h"
#include "BitmapFont.h"
#include <cstdint>
namespace Core
{
class FrameBuffer;
class DepthBuffer;
}
namespace Rasterizer
{
class Rasterizer;
class TriangleRasterizer;
}
namespace Platform
{
class IDisplay;
}
namespace Gfx
{
class DrawContext
{
private:
Core::FrameBuffer* frameBuffer;
Core::DepthBuffer* depthBuffer;
Rasterizer::Rasterizer* rasterizer;
Rasterizer::TriangleRasterizer* triangleRasterizer;
public:
DrawContext(int32_t width, int32_t height);
~DrawContext();
DrawContext(const DrawContext&) = delete;
DrawContext& operator=(const DrawContext&) = delete;
int32_t get_width() const;
int32_t get_height() const;
void clear(const RenderData::Color& color);
void clear_depth();
void draw_line(const Math::Vector2Int& from, const Math::Vector2Int& to, const RenderData::Color& color);
void draw_triangle(const RenderData::Triangle& triangle, const RenderData::Color& color);
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::Image& img);
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region);
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h);
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region);
void draw_sprite_region_ex(int32_t dst_x, int32_t dst_y, const RenderData::SpriteRegion& region,
int32_t scale, bool flip_h, bool flip_v);
void draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
int32_t scale, bool flip_h, bool flip_v);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t camera_x, int32_t camera_y);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y);
void fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
const RenderData::Color& color, const char* text);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
const RenderData::Color& color, const RenderData::Color& bg_color, const char* text);
void present(Platform::IDisplay* display);
};
}

View File

@ -15,7 +15,6 @@ namespace Platform
virtual bool init(int width, int height) = 0; virtual bool init(int width, int height) = 0;
virtual void present(const Core::FrameBuffer* framebuffer) = 0; virtual void present(const Core::FrameBuffer* framebuffer) = 0;
virtual void poll_events(bool& should_quit) = 0; virtual void poll_events(bool& should_quit) = 0;
virtual uint32_t get_time_ms() const = 0;
virtual void shutdown() = 0; virtual void shutdown() = 0;
}; };
} }

View File

@ -119,13 +119,6 @@ namespace Platform
} }
} }
uint32_t FBDisplay::get_time_ms() const
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint32_t>(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
void FBDisplay::shutdown() void FBDisplay::shutdown()
{ {
if (fb_mem != nullptr) if (fb_mem != nullptr)

View File

@ -21,7 +21,6 @@ namespace Platform
bool init(int w, int h) override; bool init(int w, int h) override;
void present(const Core::FrameBuffer* framebuffer) override; void present(const Core::FrameBuffer* framebuffer) override;
void poll_events(bool& should_quit) override; void poll_events(bool& should_quit) override;
uint32_t get_time_ms() const override;
void shutdown() override; void shutdown() override;
}; };
} }

View File

@ -76,11 +76,6 @@ namespace Platform
} }
} }
uint32_t SDLDisplay::get_time_ms() const
{
return SDL_GetTicks();
}
void SDLDisplay::shutdown() void SDLDisplay::shutdown()
{ {
if (texture != nullptr) if (texture != nullptr)

View File

@ -17,7 +17,6 @@ namespace Platform
bool init(int w, int h) override; bool init(int w, int h) override;
void present(const Core::FrameBuffer* framebuffer) override; void present(const Core::FrameBuffer* framebuffer) override;
void poll_events(bool& should_quit) override; void poll_events(bool& should_quit) override;
uint32_t get_time_ms() const override;
void shutdown() override; void shutdown() override;
}; };
} }

View File

@ -0,0 +1,59 @@
#pragma once
#include <cstdint>
#ifdef _WIN32
#include <chrono>
#else
#include <ctime>
#endif
namespace Platform
{
class ITimeSource
{
public:
virtual ~ITimeSource() {}
virtual uint32_t get_time_ms() const = 0;
};
class SteadyTimeSource : public ITimeSource
{
public:
SteadyTimeSource()
:
#ifdef _WIN32
start_(std::chrono::steady_clock::now())
#else
start_ms_(read_monotonic_ms())
#endif
{
}
uint32_t get_time_ms() const override
{
#ifdef _WIN32
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
const std::chrono::milliseconds elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(now - start_);
return static_cast<uint32_t>(elapsed.count());
#else
return read_monotonic_ms() - start_ms_;
#endif
}
private:
#ifdef _WIN32
std::chrono::steady_clock::time_point start_;
#else
static uint32_t read_monotonic_ms()
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint32_t>(ts.tv_sec * 1000u + ts.tv_nsec / 1000000u);
}
uint32_t start_ms_;
#endif
};
}

View File

@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include "Image.h"
namespace RenderData
{
struct BitmapFont
{
Image atlas;
int32_t char_w;
int32_t char_h;
int32_t columns;
int32_t first_char;
};
}

Some files were not shown because too many files have changed in this diff Show More