Compare commits

..

6 Commits

Author SHA1 Message Date
HP 964a11a215 1 2026-06-07 14:38:42 +08:00
HP 0defa04eef Merge branch 'master' of https://gitea.sepcomet.xyz/basil/IMX6U-Game 2026-06-07 14:37:58 +08:00
HP 352a1233e6 Merge branch 'master' of https://gitea.sepcomet.xyz/basil/IMX6U-Game 2026-06-07 12:34:12 +08:00
HP c177114bf3 111 2026-06-07 12:34:07 +08:00
HP b093796e61 merge remote and local changes 2026-06-07 10:39:29 +08:00
HP 3beae16917 local changes before merge 2026-06-07 10:38:34 +08:00
136 changed files with 4783 additions and 137 deletions

View File

@ -10,49 +10,50 @@ endif()
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF) option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
set(SOURCES set(CORE_SOURCES
src/Apps/Demo/main.cpp src/Core/Asset/ObjLoader.cpp
src/Gfx/Asset/ObjLoader.cpp src/Core/Asset/SpriteAssetLoader.cpp
src/Gfx/Core/DepthBuffer.cpp src/Core/Core/DepthBuffer.cpp
src/Gfx/Core/FrameBuffer.cpp src/Core/Core/FrameBuffer.cpp
src/Gfx/Core/Renderer.cpp src/Core/Core/Renderer.cpp
src/Gfx/Draw2D/DrawContext.cpp src/Core/Draw2D/DrawContext.cpp
src/Gfx/Rasterizer/Rasterizer.cpp src/Core/Rasterizer/Rasterizer.cpp
src/Gfx/Rasterizer/TriangleRasterizer.cpp src/Core/Rasterizer/TriangleRasterizer.cpp
src/Gfx/Scene/Camera.cpp src/Core/Scene/Camera.cpp
src/Gfx/Shading/BlinnPhongShader.cpp src/Core/Shading/BlinnPhongShader.cpp
) )
if(USE_FRAMEBUFFER) set(CORE_INCLUDE_DIRS
list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp) src/Core/Platform
else() src/Core/Asset
list(APPEND SOURCES src/Gfx/Platform/SDLDisplay.cpp) src/Core/Core
endif() src/Core/Draw2D
src/Core/Math
add_executable(IMX6U-Game ${SOURCES}) src/Core/Rasterizer
src/Core/RenderData
target_include_directories(IMX6U-Game PRIVATE src/Core/Scene
src/Gfx/Platform src/Core/Shading
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/font
assets/sprite assets/sprite
) )
set(SOURCES
src/Apps/Game/Main.cpp
${CORE_SOURCES}
)
if(USE_FRAMEBUFFER)
list(APPEND SOURCES src/Core/Platform/FBDisplay.cpp)
else()
list(APPEND SOURCES src/Core/Platform/SDLDisplay.cpp)
endif()
add_executable(IMX6U-Game ${SOURCES})
target_include_directories(IMX6U-Game PRIVATE ${CORE_INCLUDE_DIRS})
if(USE_FRAMEBUFFER) if(USE_FRAMEBUFFER)
target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER) target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER)
else() else()
target_include_directories(IMX6U-Game PRIVATE
libs/Win/SDL2/include
libs/Win/SDL_image/include
)
if(WIN32) if(WIN32)
if(CMAKE_SIZEOF_VOID_P EQUAL 8) if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(SDL2_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL2/lib/x64") set(SDL2_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL2/lib/x64")
@ -66,21 +67,18 @@ else()
set(SDL2_IMAGE_DLL "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL_image/lib/x86/SDL2_image.dll") set(SDL2_IMAGE_DLL "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL_image/lib/x86/SDL2_image.dll")
endif() endif()
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR}) target_include_directories(IMX6U-Game PRIVATE libs/Win/SDL2/include)
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2 SDL2_image) target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR})
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2)
add_custom_command(TARGET IMX6U-Game POST_BUILD add_custom_command(TARGET IMX6U-Game POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL}" "${SDL2_DLL}"
"$<TARGET_FILE_DIR:IMX6U-Game>" "$<TARGET_FILE_DIR:IMX6U-Game>"
) )
add_custom_command(TARGET IMX6U-Game POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_IMAGE_DLL}"
"$<TARGET_FILE_DIR:IMX6U-Game>"
)
else() else()
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(SDL2_image QUIET)
target_link_libraries(IMX6U-Game PRIVATE SDL2::SDL2) target_link_libraries(IMX6U-Game PRIVATE SDL2::SDL2)
endif() endif()
endif() endif()
@ -89,3 +87,56 @@ if(MSVC)
target_compile_options(IMX6U-Game PRIVATE /utf-8 /W3) target_compile_options(IMX6U-Game PRIVATE /utf-8 /W3)
set_property(TARGET IMX6U-Game PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) set_property(TARGET IMX6U-Game PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
endif() endif()
if(NOT USE_FRAMEBUFFER)
set(SPRITE_ASSET_TOOL_SOURCES
src/Apps/Game/tools/asset_pipeline/SpriteAssetTool.cpp
src/Core/Asset/SpriteAssetLoader.cpp
)
if(WIN32)
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
target_include_directories(SpriteAssetTool PRIVATE
src/Core/Asset
src/Core/RenderData
libs/Win/SDL2/include
libs/Win/SDL_image/include
)
target_link_directories(SpriteAssetTool PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR})
target_link_libraries(SpriteAssetTool PRIVATE SDL2main SDL2 SDL2_image)
add_custom_command(TARGET SpriteAssetTool POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL}"
"$<TARGET_FILE_DIR:SpriteAssetTool>"
)
add_custom_command(TARGET SpriteAssetTool POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_IMAGE_DLL}"
"$<TARGET_FILE_DIR:SpriteAssetTool>"
)
elseif(SDL2_image_FOUND)
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
target_include_directories(SpriteAssetTool PRIVATE
src/Core/Asset
src/Core/RenderData
)
target_link_libraries(SpriteAssetTool PRIVATE SDL2::SDL2 SDL2_image::SDL2_image)
else()
message(STATUS "SpriteAssetTool disabled: SDL2_image was not found")
endif()
if(TARGET SpriteAssetTool)
add_custom_target(ConvertTomSprites
COMMAND $<TARGET_FILE:SpriteAssetTool>
--batch
"src/Apps/Game/assets/raw"
"src/Apps/Game/assets/sprites"
--preset tom-800x480
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS SpriteAssetTool
COMMENT "Converting PNG assets to board-ready .sprite files"
VERBATIM
)
endif()
endif()

View File

@ -33,13 +33,13 @@ IMX6U 运行时性能预算较紧,后续开发必须遵守 `docs/DEVELOPMENT_G
## 应用层与图形库拆分 ## 应用层与图形库拆分
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md` 项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_CORE_ARCHITECTURE.md`
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。 - `Core` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。 - `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。 - `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。 - `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
- 依赖方向必须保持 `Apps -> Shared -> Gfx -> Platform`,底层图形库不能反向依赖具体游戏。 - 依赖方向必须保持 `Apps -> Shared -> Core -> Platform`,底层图形库不能反向依赖具体游戏。
初期推荐单进程多应用模式Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。 初期推荐单进程多应用模式Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
@ -196,7 +196,7 @@ assets/font/font_atlas.h
## 显示后端架构 ## 显示后端架构
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层: 显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Core` 的平台适配层:
``` ```
┌──────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
@ -204,7 +204,7 @@ assets/font/font_atlas.h
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ SharedUI、存档、配置、资源索引 │ │ SharedUI、存档、配置、资源索引 │
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
GfxDrawContext、FrameBuffer、Draw2D、Math │ CoreDrawContext、FrameBuffer、Draw2D、Math │
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ Platform::IDisplay │ │ Platform::IDisplay │
│ - SDLDisplay : SDL2 显示/输入适配 │ │ - SDLDisplay : SDL2 显示/输入适配 │
@ -232,7 +232,7 @@ IMX6U-Game/
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header │ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
│ └─ png_to_header.py # PNG -> uint32_t RGBA header │ └─ png_to_header.py # PNG -> uint32_t RGBA header
├─ src/ ├─ src/
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则 │ ├─ Core/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口 │ │ ├─ Draw2D/ # DrawContext 统一绘制入口
│ │ ├─ Core/ # FrameBuffer、DepthBuffer │ │ ├─ Core/ # FrameBuffer、DepthBuffer
│ │ ├─ Math/ # 向量、矩阵、数学工具 │ │ ├─ Math/ # 向量、矩阵、数学工具
@ -247,7 +247,7 @@ IMX6U-Game/
│ └─ test_fb.cpp # 独立 fb 测试(最小示例) │ └─ test_fb.cpp # 独立 fb 测试(最小示例)
├─ docs/ ├─ docs/
│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线 │ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
│ ├─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层 │ ├─ APP_AND_CORE_ARCHITECTURE.md # 应用层与图形库分层
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定 │ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
├─ CMakeLists.txt ├─ CMakeLists.txt
└─ README.md └─ README.md
@ -301,10 +301,8 @@ IMX6U-Game/
- 双平台显示后端SDL2 / Framebuffer - 双平台显示后端SDL2 / Framebuffer
- 离线资源转换工具PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header - 离线资源转换工具PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap - 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
- 当前板端性能 demo 已临时移除旋转正方体,只保留 2D sprite/tilemap/FPS 与 `Frame/Present` 耗时显示,用于测量 framebuffer 提交瓶颈
- Gfx 目录规范化,代码收敛到 `src/Gfx/` - Gfx 目录规范化,代码收敛到 `src/Gfx/`
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力 - `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
- `DrawContext::clear_color()` 支持只清颜色缓冲,避免 2D-only demo 每帧无意义清 depth buffer
- C++11 兼容代码 - C++11 兼容代码
- CMake 跨平台构建 - CMake 跨平台构建
@ -312,7 +310,7 @@ IMX6U-Game/
1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON 1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON
2. 应用层拆分Launcher / GameA / GameB / Shared和统一 `IApp` 主循环 2. 应用层拆分Launcher / GameA / GameB / Shared和统一 `IApp` 主循环
3. SDL2 输入抽象(键盘/触摸/按键状态快照) 3. SDL2 输入抽象(键盘/触摸/按键状态快照)
4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径) 4. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
5. 纹理贴图、OBJ 模型加载与完整光照 5. 纹理贴图、OBJ 模型加载与完整光照
## 说明 ## 说明

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,165 @@
# 应用层与 Core 分层设计
本文记录当前项目的代码分层。`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。
## 目录边界
```text
src/
Core/ # 底层库:渲染、数学、资源、平台适配
Asset/ # 离线资源格式加载
Core/ # FrameBuffer、DepthBuffer、Renderer、Timer
Draw2D/ # Core::DrawContext
Math/ # Vector、Matrix、MathUtil
Platform/ # 显示、时间、音频、按键等平台接口和后端
Rasterizer/ # 线段和三角形光栅化
RenderData/ # Color、Image、Triangle、Tilemap 等数据结构
Scene/ # Camera、Transform、Mesh、Model
Shading/ # Shader 相关代码
Apps/
Demo/ # Core 能力演示
Game/ # Tom 游戏
```
`src/Core/Core` 这个二级目录保留的是原底层库里的核心运行时对象。它和顶层 `src/Core` 名称重复,但含义不同:
- `src/Core`:整个底层库。
- `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。
后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/``Buffer/`,但这次先只做旧底层库名称到 `Core` 的一致性修复。
## 依赖方向
允许:
```text
Apps -> Shared -> Core -> Platform
Apps -> Core
```
禁止:
```text
Core -> Apps
Core -> Shared
GameA -> GameB
GameB -> GameA
Platform -> Game
```
Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。
## Core 职责
Core 只提供底层能力:
- 管理 `FrameBuffer`、`DepthBuffer` 和绘制上下文。
- 提供基础绘制接口line、triangle、sprite、sprite region、tilemap、bitmap font。
- 提供基础数学、颜色、图片、三角形、tilemap 等数据结构。
- 封装 SDL2 / framebuffer 显示提交。
- 提供独立时间源 `Platform::ITimeSource`
- 提供音频输入、音频输出和按键输入的抽象接口。
- 使用离线转换后的运行时资源,不在热路径解码 PNG/TTF。
Core 不做:
- 不实现具体游戏规则。
- 不直接读取某个游戏专属资源目录。
- 不在核心绘制接口暴露 SDL2 类型。
- 不在每帧热路径中执行图片、字体解码或文件 IO。
## 应用职责
`src/Apps/*` 负责具体应用流程:
- 创建具体游戏或 Demo 的主循环。
- 加载应用自己的资源。
- 调用 `Core::DrawContext` 绘制画面。
- 根据平台输入更新游戏状态。
当前主程序位于:
```text
src/Apps/Game/Main.cpp
```
它默认启动 Tom 游戏视觉入口。
## DrawContext
`Core::DrawContext` 是当前统一绘制入口,位于:
```text
src/Core/Draw2D/DrawContext.h
src/Core/Draw2D/DrawContext.cpp
```
它封装:
- `Core::FrameBuffer`
- `Core::DepthBuffer`
- `Rasterizer::Rasterizer`
- `Rasterizer::TriangleRasterizer`
对外提供:
```cpp
Core::DrawContext ctx(width, height);
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(x, y, image);
ctx.draw_text(font, x, y, color, "text");
ctx.present(display);
```
## 显示后端
平台层采用“抽象接口 + 多套后端实现”的模式。
显示层通过 `Platform::IDisplay` 抽象:
```text
Platform::IDisplay
SDLDisplay # PC / SDL2 调试后端
FBDisplay # Linux /dev/fb0 后端
```
音频输入通过 `Platform::IAudioInput` 抽象:
```text
Platform::IAudioInput
SdlAudioInput # PC / SDL2 麦克风后端
AlsaAudioInput # Linux ALSA 录音后端
```
音频输出通过 `Platform::IAudioOutput` 抽象:
```text
Platform::IAudioOutput
SdlAudioOutput # PC / SDL2 扬声器后端
AlsaAudioOutput # Linux ALSA 播放后端
```
按键输入通过 `Platform::IButtonInput` 抽象:
```text
Platform::IButtonInput
SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键
EvdevButtonInput # Linux evdev 按键后端
```
游戏代码只能依赖这些 `I*` 接口。ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。
如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput`
CMake 通过 `USE_FRAMEBUFFER` 选择实现:
```cmake
-DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay
-DUSE_FRAMEBUFFER=ON # 使用 FBDisplay
```
## 后续建议
1. 保持 `Core` 不依赖 `Apps`
2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*`
3. 新增底层绘制、数学、资源格式、平台显示能力放在 `src/Core`
4. 如果继续清理命名,优先处理 `src/Core/Core` 这个重复目录名。

View File

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

View File

@ -9,7 +9,7 @@
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。 - **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。 - **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。 - **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/RenderData` 中是否已有可复用能力。 - **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Core/Core`、`src/Core/Math`、`src/Core/RenderData` 中是否已有可复用能力。
- **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。 - **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
## 2. 数值计算规范 ## 2. 数值计算规范
@ -164,7 +164,7 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
后续如果继续推进性能优化,优先建立这些基础设施: 后续如果继续推进性能优化,优先建立这些基础设施:
1. 统一定点数类型与转换工具,集中放在 `src/Gfx/Math/`。 1. 统一定点数类型与转换工具,集中放在 `src/Core/Math/`。
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。 2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。 3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。 4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
@ -184,8 +184,8 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
### 12.1 SDL2 边界 ### 12.1 SDL2 边界
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。 - SDL2 类型和调用只允许出现在 `src/Core/Platform/` 以及明确的平台适配层中。
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h` - `src/Core/Core`、`src/Core/Math`、`src/Core/Rasterizer`、`src/Core/RenderData`、`src/Core/Scene`、`src/Core/Draw2D` 不应直接包含 `SDL.h`
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。 - 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep不直接依赖 float 秒数。 - 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep不直接依赖 float 秒数。

View File

@ -1,19 +1,22 @@
#include <iostream> #include <algorithm>
#include <array> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdlib>
#include <cstring> #include <cstring>
#include <cstdio>
#include <iostream>
#include <thread> #include <thread>
#include <chrono> #include <chrono>
#include "Color.h" #include "Color.h"
#include <cstdlib> #include <cstdlib>
#include "Timer.h" #include "Timer.h"
#include "DrawContext.h" #include "DrawContext.h"
#include "test_sprite.h" #include "Image.h"
#include "font_atlas.h"
#include "Display.h"
#include "TimeSource.h" #include "TimeSource.h"
#include "Timer.h"
#include "font_atlas.h"
#include "test_sprite.h"
#ifdef USE_FRAMEBUFFER #ifdef USE_FRAMEBUFFER
#include "FBDisplay.h" #include "FBDisplay.h"
#else #else
@ -42,6 +45,22 @@ struct ProgramOptions
} }
}; };
static Platform::IDisplay* CreateDisplay()
{
#ifdef USE_FRAMEBUFFER
return new Platform::FBDisplay();
#else
return new Platform::SDLDisplay();
#endif
}
static void PrintUsage(const char* program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
static ProgramOptions ParseProgramOptions(int argc, char* argv[]) static ProgramOptions ParseProgramOptions(int argc, char* argv[])
{ {
ProgramOptions options; ProgramOptions options;
@ -98,6 +117,7 @@ static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::IT
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
} }
} }
}
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
@ -107,23 +127,17 @@ int main(int argc, char *argv[])
PrintUsage(argv[0]); PrintUsage(argv[0]);
return 0; return 0;
} }
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
#ifdef USE_FRAMEBUFFER Platform::IDisplay* display = CreateDisplay();
Platform::IDisplay *display = new Platform::FBDisplay(); if (!display->init(DemoWidth, DemoHeight))
#else
Platform::IDisplay *display = new Platform::SDLDisplay();
#endif
if (!display->init(width, height))
{ {
delete display; delete display;
return -1; return -1;
} }
Gfx::DrawContext ctx(width, height); Core::DrawContext ctx(DemoWidth, DemoHeight);
std::cout << "Target FPS: " << timer.target_fps() << std::endl; Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
RenderData::BitmapFont font; RenderData::BitmapFont font;
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height); font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
@ -149,6 +163,7 @@ int main(int argc, char *argv[])
int32_t fps = 0; int32_t fps = 0;
int32_t frame_count = 0; int32_t frame_count = 0;
uint32_t last_fps_time = 0; uint32_t last_fps_time = 0;
uint32_t animation_time_ms = 0;
char fps_text[32]; char fps_text[32];
char perf_text[64]; char perf_text[64];
uint32_t last_frame_ms = 0; uint32_t last_frame_ms = 0;
@ -173,12 +188,13 @@ int main(int argc, char *argv[])
// FPS 计数 // FPS 计数
++frame_count; ++frame_count;
const uint32_t now = time_source.get_time_ms(); const uint32_t now = time_source.get_time_ms();
if (now - last_fps_time >= 1000) if (now - last_fps_time >= 1000u)
{ {
fps = frame_count; fps = frame_count;
frame_count = 0; frame_count = 0;
last_fps_time = now; last_fps_time = now;
} }
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps); std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text); ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
std::snprintf(perf_text, sizeof(perf_text), "Frame:%ums Present:%ums", last_frame_ms, last_present_ms); std::snprintf(perf_text, sizeof(perf_text), "Frame:%ums Present:%ums", last_frame_ms, last_present_ms);

301
src/Apps/Game/Main.cpp Normal file
View File

@ -0,0 +1,301 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include "Color.h"
#include "Display.h"
#include "DrawContext.h"
#include "Image.h"
#include "SpriteAssetLoader.h"
#include "TimeSource.h"
#include "Timer.h"
#ifdef USE_FRAMEBUFFER
#include "FBDisplay.h"
#else
#include "SDLDisplay.h"
#endif
namespace
{
const int32_t ScreenWidth = 800;
const int32_t ScreenHeight = 480;
struct ProgramOptions
{
uint32_t target_fps;
bool show_help;
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
}
};
struct SpriteImage
{
std::vector<uint32_t> pixels;
RenderData::Image image;
bool is_valid() const
{
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
};
static Platform::IDisplay* CreateDisplay()
{
#ifdef USE_FRAMEBUFFER
return new Platform::FBDisplay();
#else
return new Platform::SDLDisplay();
#endif
}
static void PrintUsage(const char* program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
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));
}
}
static std::string FindSpriteAssetPath(const std::string& file_name)
{
const char* roots[] = {
"src/Apps/Game/assets/sprites/",
"../src/Apps/Game/assets/sprites/",
"../../src/Apps/Game/assets/sprites/",
"../../../src/Apps/Game/assets/sprites/",
"/usr/local/share/imx6u-game/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
{
const std::string path = std::string(roots[i]) + file_name;
std::ifstream file(path.c_str(), std::ios::binary);
if (file.good())
{
return path;
}
}
return std::string("src/Apps/Game/assets/sprites/") + file_name;
}
static void EnableAlphaColorKey(SpriteImage& sprite)
{
for (size_t i = 0; i < sprite.pixels.size(); ++i)
{
if ((sprite.pixels[i] & 0xFFu) == 0u)
{
sprite.pixels[i] = 0x00000000u;
}
}
sprite.image = RenderData::Image(
sprite.pixels.data(),
sprite.image.width,
sprite.image.height,
0x00000000u);
}
static bool LoadSpriteAsset(const std::string& file_name, SpriteImage& sprite, bool use_alpha_key)
{
const std::string path = FindSpriteAssetPath(file_name);
if (!Asset::SpriteAssetLoader::Load(path, sprite.pixels, sprite.image))
{
std::cerr << "Load sprite asset failed: " << path << std::endl;
return false;
}
if (use_alpha_key)
{
EnableAlphaColorKey(sprite);
}
return sprite.is_valid();
}
static const RenderData::Image& SelectTomFrame(
uint32_t animation_time_ms,
const SpriteImage& stand,
const SpriteImage& listen,
const SpriteImage* const* speaking_frames,
size_t speaking_frame_count)
{
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
if (phase < 2u)
{
return stand.image;
}
if (phase < 3u)
{
return listen.image;
}
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
return speaking_frames[frame_index]->image;
}
}
int main(int argc, char* argv[])
{
const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help)
{
PrintUsage(argv[0]);
return 0;
}
Platform::IDisplay* display = CreateDisplay();
if (!display->init(ScreenWidth, ScreenHeight))
{
delete display;
return -1;
}
SpriteImage background;
SpriteImage tom_stand;
SpriteImage tom_listen;
SpriteImage tom_say1;
SpriteImage tom_say2;
SpriteImage tom_say3;
SpriteImage tom_say4;
SpriteImage record_button;
if (!LoadSpriteAsset("background.sprite", background, false) ||
!LoadSpriteAsset("Tom-stand.sprite", tom_stand, true) ||
!LoadSpriteAsset("Tom-listhen.sprite", tom_listen, true) ||
!LoadSpriteAsset("Tom-say1.sprite", tom_say1, true) ||
!LoadSpriteAsset("Tom-say2.sprite", tom_say2, true) ||
!LoadSpriteAsset("Tom-say3.sprite", tom_say3, true) ||
!LoadSpriteAsset("Tom-say4.sprite", tom_say4, true) ||
!LoadSpriteAsset("ui-record.sprite", record_button, true))
{
std::cerr << "Missing Tom sprite assets. Run the ConvertTomSprites target first.\n";
display->shutdown();
delete display;
return -1;
}
const SpriteImage* speaking_frames[] = {
&tom_say1,
&tom_say2,
&tom_say3,
&tom_say4,
&tom_say3,
&tom_say2
};
const size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
std::cout << "Tom game started. Target FPS: " << timer.target_fps() << std::endl;
bool is_running = true;
uint32_t animation_time_ms = 0;
while (is_running)
{
timer.begin_frame(time_source.get_time_ms());
animation_time_ms += timer.fixed_delta_ms();
bool should_quit = false;
display->poll_events(should_quit);
if (should_quit)
{
is_running = false;
}
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(0, 0, background.image);
const RenderData::Image& tom = SelectTomFrame(
animation_time_ms,
tom_stand,
tom_listen,
speaking_frames,
speaking_frame_count);
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
const int32_t tom_y = std::max(0, ScreenHeight - tom.height - 72);
ctx.draw_sprite(tom_x, tom_y, tom);
const int32_t button_x = (ScreenWidth - record_button.image.width) / 2;
const int32_t button_y = ScreenHeight - record_button.image.height - 16;
ctx.draw_sprite(button_x, button_y, record_button.image);
ctx.present(display);
SleepRemainingFrameTime(timer, time_source);
}
display->shutdown();
delete display;
return 0;
}

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 @@

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,280 @@
#include "TomGameApp.h"
#include "../audio/VoiceEffect.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "ButtonInput.h"
#include "FrameBuffer.h"
#include "SpriteRasterizer.h"
#include "Image.h"
namespace Game
{
TomGameApp::TomGameApp(
Core::FrameBuffer* frameBuffer,
Rasterizer::SpriteRasterizer* spriteRasterizer,
Platform::IAudioInput* audioInput,
Platform::IAudioOutput* audioOutput,
Platform::IButtonInput* 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);
}
}

View File

@ -0,0 +1,120 @@
#pragma once
#include <cstdint>
#include <vector>
#include "../audio/VoicePlayer.h"
#include "../audio/VoiceRecorder.h"
#include "../components/SpriteAnimator.h"
#include "Sprite.h"
namespace Core
{
class FrameBuffer;
}
namespace Rasterizer
{
class SpriteRasterizer;
}
namespace RenderData
{
class Image;
}
namespace Platform
{
class IAudioInput;
class IAudioOutput;
class IButtonInput;
}
namespace Game
{
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;
Platform::IAudioInput* audioInput;
Platform::IAudioOutput* audioOutput;
Platform::IButtonInput* 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,
Platform::IAudioInput* audioInput,
Platform::IAudioOutput* audioOutput,
Platform::IButtonInput* 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,154 @@
#include "VoicePlayer.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(Platform::IAudioOutput& 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,44 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "AudioOutput.h"
namespace Game
{
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(Platform::IAudioOutput& 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,170 @@
#include "VoiceRecorder.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(Platform::IAudioInput& 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,58 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "AudioInput.h"
namespace Game
{
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(Platform::IAudioInput& 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 "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 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,98 @@
#include "AnimationSystem.h"
#include "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
src/Apps/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[] = {
"src/Apps/Game/assets/sprites/",
"../src/Apps/Game/assets/sprites/",
"../../src/Apps/Game/assets/sprites/",
"../../../src/Apps/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("src/Apps/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;
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,601 @@
#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) {}
};
struct LoadedImage
{
std::vector<uint32_t> pixels;
RenderData::Image image;
void assign(int32_t width, int32_t height, const std::vector<uint32_t>& sourcePixels)
{
pixels = sourcePixels;
image = RenderData::Image(pixels.empty() ? nullptr : pixels.data(), width, height);
}
bool is_valid() const
{
return image.pixels != nullptr && image.width > 0 && image.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, LoadedImage& 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) * static_cast<size_t>(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.assign(rgbaSurface->w, rgbaSurface->h, pixels);
SDL_FreeSurface(rgbaSurface);
return image.is_valid();
}
static LoadedImage ResizeImageNearest(const LoadedImage& source, int32_t width, int32_t height)
{
if (!source.is_valid() || width <= 0 || height <= 0)
{
return LoadedImage();
}
std::vector<uint32_t> pixels(static_cast<size_t>(width) * static_cast<size_t>(height), 0);
for (int32_t y = 0; y < height; ++y)
{
const int32_t sourceY = y * source.image.height / height;
for (int32_t x = 0; x < width; ++x)
{
const int32_t sourceX = x * source.image.width / width;
pixels[static_cast<size_t>(y) * static_cast<size_t>(width) + static_cast<size_t>(x)] =
source.image.pixels[static_cast<size_t>(sourceY) * static_cast<size_t>(source.image.width) + static_cast<size_t>(sourceX)];
}
}
LoadedImage result;
result.assign(width, height, pixels);
return result;
}
static LoadedImage ResizeImageToFit(const LoadedImage& source, int32_t maxWidth, int32_t maxHeight)
{
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
{
return LoadedImage();
}
const float scaleX = static_cast<float>(maxWidth) / source.image.width;
const float scaleY = static_cast<float>(maxHeight) / source.image.height;
const float scale = std::min(scaleX, scaleY);
const int32_t width = std::max(1, static_cast<int32_t>(source.image.width * scale));
const int32_t height = std::max(1, static_cast<int32_t>(source.image.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, LoadedImage& 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)
{
LoadedImage 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.image))
{
std::cerr << "Save failed: " << outputPath << std::endl;
return false;
}
std::cout << inputPath << " -> " << outputPath
<< " (" << image.image.width << "x" << image.image.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 src/Apps/Game/assets/raw src/Apps/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

@ -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

@ -0,0 +1,182 @@
#include "SpriteAssetLoader.h"
#include <cstdint>
#include <fstream>
#include <limits>
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;
}
}
namespace Asset
{
bool SpriteAssetLoader::Load(
const std::string& path,
std::vector<uint32_t>& pixels,
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> loadedPixels(pixelCount, 0);
if (!ReadPixels(file, loadedPixels))
{
return false;
}
char trailingByte = 0;
file.read(&trailingByte, 1);
if (file.gcount() != 0)
{
return false;
}
pixels.swap(loadedPixels);
image = RenderData::Image(pixels.data(), static_cast<int32_t>(width), static_cast<int32_t>(height));
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
{
if (image.pixels == nullptr || image.width <= 0 || image.height <= 0)
{
return false;
}
size_t pixelCount = 0;
if (!CheckPixelCount(static_cast<uint32_t>(image.width), static_cast<uint32_t>(image.height), pixelCount))
{
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.width));
WriteU32LE(file, static_cast<uint32_t>(image.height));
WriteU32LE(file, SpriteFormatRgba8888);
for (size_t i = 0; i < pixelCount; ++i)
{
WriteU32LE(file, image.pixels[i]);
}
return file.good();
}
}

View File

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

View File

View File

@ -6,7 +6,7 @@
#include "Display.h" #include "Display.h"
#include <algorithm> #include <algorithm>
namespace Gfx namespace Core
{ {
DrawContext::DrawContext(int32_t width, int32_t height) DrawContext::DrawContext(int32_t width, int32_t height)
{ {

View File

@ -25,7 +25,7 @@ namespace Platform
class IDisplay; class IDisplay;
} }
namespace Gfx namespace Core
{ {
class DrawContext class DrawContext
{ {

View File

@ -0,0 +1,141 @@
#include "AlsaAudioInput.h"
#include <cmath>
#include <iostream>
#include <vector>
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
#include <alsa/asoundlib.h>
#endif
namespace Platform
{
AlsaAudioInput::AlsaAudioInput()
: device_name_("default"),
sample_rate_(16000),
channels_(1),
opened_(false)
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
,
handle_(nullptr)
#endif
{
}
AlsaAudioInput::~AlsaAudioInput()
{
shutdown();
}
bool AlsaAudioInput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
{
shutdown();
if (sample_rate == 0 || channels == 0)
{
std::cerr << "AlsaAudioInput invalid params." << std::endl;
return false;
}
device_name_ = device_name;
sample_rate_ = sample_rate;
channels_ = channels;
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
int result = snd_pcm_open(&handle_, device_name_.c_str(), SND_PCM_STREAM_CAPTURE, 0);
if (result < 0)
{
std::cerr << "AlsaAudioInput 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_,
sample_rate_,
1,
50000);
if (result < 0)
{
std::cerr << "AlsaAudioInput set params failed: " << snd_strerror(result) << std::endl;
shutdown();
return false;
}
opened_ = true;
return true;
#else
std::cerr << "AlsaAudioInput backend is unavailable. Enable ALSA development libraries." << std::endl;
return false;
#endif
}
int AlsaAudioInput::read_samples(int16_t* buffer, int sample_count)
{
if (buffer == nullptr || sample_count <= 0 || !opened_)
{
return 0;
}
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sample_count / 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 << "AlsaAudioInput read failed: " << snd_strerror(result) << std::endl;
return 0;
}
}
return result * static_cast<int>(channels_);
#else
return 0;
#endif
}
float AlsaAudioInput::read_volume(int sample_count)
{
if (sample_count <= 0)
{
return 0.0f;
}
std::vector<int16_t> samples(static_cast<size_t>(sample_count), 0);
const int count = read_samples(samples.data(), sample_count);
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 AlsaAudioInput::shutdown()
{
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
if (handle_ != nullptr)
{
snd_pcm_close(handle_);
handle_ = nullptr;
}
#endif
opened_ = false;
}
}

View File

@ -0,0 +1,39 @@
#pragma once
#include "AudioInput.h"
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
typedef struct _snd_pcm snd_pcm_t;
#endif
namespace Platform
{
class AlsaAudioInput : public IAudioInput
{
private:
std::string device_name_;
uint32_t sample_rate_;
uint32_t channels_;
bool opened_;
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
snd_pcm_t* handle_;
#endif
public:
AlsaAudioInput();
~AlsaAudioInput();
bool init(
const std::string& device_name = "default",
uint32_t sample_rate = 16000,
uint32_t channels = 1) override;
int read_samples(int16_t* buffer, int sample_count) override;
float read_volume(int sample_count = 512) override;
void shutdown() override;
bool is_open() const override { return opened_; }
uint32_t get_sample_rate() const override { return sample_rate_; }
uint32_t get_channels() const override { return channels_; }
const std::string& get_device_name() const override { return device_name_; }
};
}

View File

@ -0,0 +1,204 @@
#include "AlsaAudioOutput.h"
#include <algorithm>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
#include <alsa/asoundlib.h>
#endif
namespace
{
struct WavHeader
{
char riff[4];
uint32_t file_size;
char wave[4];
char fmt[4];
uint32_t fmt_size;
uint16_t audio_format;
uint16_t channels;
uint32_t sample_rate;
uint32_t byte_rate;
uint16_t block_align;
uint16_t bits_per_sample;
char data[4];
uint32_t data_size;
};
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.audio_format == 1 &&
header.bits_per_sample == 16 &&
header.fmt_size == 16;
}
}
namespace Platform
{
AlsaAudioOutput::AlsaAudioOutput()
: device_name_("default"),
sample_rate_(16000),
channels_(1),
opened_(false)
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
,
handle_(nullptr)
#endif
{
}
AlsaAudioOutput::~AlsaAudioOutput()
{
shutdown();
}
bool AlsaAudioOutput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
{
shutdown();
if (sample_rate == 0 || channels == 0)
{
std::cerr << "AlsaAudioOutput invalid params." << std::endl;
return false;
}
device_name_ = device_name;
sample_rate_ = sample_rate;
channels_ = channels;
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
int result = snd_pcm_open(&handle_, device_name_.c_str(), SND_PCM_STREAM_PLAYBACK, 0);
if (result < 0)
{
std::cerr << "AlsaAudioOutput 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_,
sample_rate_,
1,
50000);
if (result < 0)
{
std::cerr << "AlsaAudioOutput set params failed: " << snd_strerror(result) << std::endl;
shutdown();
return false;
}
opened_ = true;
return true;
#else
std::cerr << "AlsaAudioOutput backend is unavailable. Enable ALSA development libraries." << std::endl;
return false;
#endif
}
int AlsaAudioOutput::write_samples(const int16_t* samples, int sample_count)
{
if (samples == nullptr || sample_count <= 0 || !opened_)
{
return 0;
}
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sample_count / 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 << "AlsaAudioOutput write failed: " << snd_strerror(result) << std::endl;
return 0;
}
}
return result * static_cast<int>(channels_);
#else
return 0;
#endif
}
bool AlsaAudioOutput::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_ || sample_rate_ != header.sample_rate || channels_ != header.channels)
{
if (!init(device_name_, header.sample_rate, header.channels))
{
return false;
}
}
std::vector<int16_t> samples(header.data_size / sizeof(int16_t), 0);
file.read(reinterpret_cast<char*>(samples.data()), header.data_size);
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__) && defined(PLATFORM_HAS_ALSA)
if (handle_ != nullptr)
{
snd_pcm_drain(handle_);
}
#endif
return true;
}
void AlsaAudioOutput::shutdown()
{
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
if (handle_ != nullptr)
{
snd_pcm_close(handle_);
handle_ = nullptr;
}
#endif
opened_ = false;
}
}

View File

@ -0,0 +1,39 @@
#pragma once
#include "AudioOutput.h"
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
typedef struct _snd_pcm snd_pcm_t;
#endif
namespace Platform
{
class AlsaAudioOutput : public IAudioOutput
{
private:
std::string device_name_;
uint32_t sample_rate_;
uint32_t channels_;
bool opened_;
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
snd_pcm_t* handle_;
#endif
public:
AlsaAudioOutput();
~AlsaAudioOutput();
bool init(
const std::string& device_name = "default",
uint32_t sample_rate = 16000,
uint32_t channels = 1) override;
int write_samples(const int16_t* samples, int sample_count) override;
bool play_wav(const std::string& path) override;
void shutdown() override;
bool is_open() const override { return opened_; }
uint32_t get_sample_rate() const override { return sample_rate_; }
uint32_t get_channels() const override { return channels_; }
const std::string& get_device_name() const override { return device_name_; }
};
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <string>
namespace Platform
{
class IAudioInput
{
public:
virtual ~IAudioInput() {}
virtual bool init(
const std::string& device_name = "default",
uint32_t sample_rate = 16000,
uint32_t channels = 1) = 0;
virtual int read_samples(int16_t* buffer, int sample_count) = 0;
virtual float read_volume(int sample_count = 512) = 0;
virtual void shutdown() = 0;
virtual bool is_open() const = 0;
virtual uint32_t get_sample_rate() const = 0;
virtual uint32_t get_channels() const = 0;
virtual const std::string& get_device_name() const = 0;
};
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <string>
namespace Platform
{
class IAudioOutput
{
public:
virtual ~IAudioOutput() {}
virtual bool init(
const std::string& device_name = "default",
uint32_t sample_rate = 16000,
uint32_t channels = 1) = 0;
virtual int write_samples(const int16_t* samples, int sample_count) = 0;
virtual bool play_wav(const std::string& path) = 0;
virtual void shutdown() = 0;
virtual bool is_open() const = 0;
virtual uint32_t get_sample_rate() const = 0;
virtual uint32_t get_channels() const = 0;
virtual const std::string& get_device_name() const = 0;
};
}

View File

@ -0,0 +1,23 @@
#pragma once
#include <string>
namespace Platform
{
class IButtonInput
{
public:
virtual ~IButtonInput() {}
virtual bool init(const std::string& device_path = "", int key_code = 0) = 0;
virtual void update() = 0;
virtual void shutdown() = 0;
virtual bool is_open() const = 0;
virtual bool is_down() const = 0;
virtual bool was_pressed() const = 0;
virtual bool was_released() const = 0;
virtual const std::string& get_device_path() const = 0;
virtual int get_key_code() const = 0;
};
}

View File

@ -0,0 +1,24 @@
#pragma once
#ifdef USE_FRAMEBUFFER
#include "AlsaAudioInput.h"
#include "AlsaAudioOutput.h"
#include "EvdevButtonInput.h"
#else
#include "SdlAudioInput.h"
#include "SdlAudioOutput.h"
#include "SdlKeyboardButtonInput.h"
#endif
namespace Platform
{
#ifdef USE_FRAMEBUFFER
typedef AlsaAudioInput DefaultAudioInput;
typedef AlsaAudioOutput DefaultAudioOutput;
typedef EvdevButtonInput DefaultButtonInput;
#else
typedef SdlAudioInput DefaultAudioInput;
typedef SdlAudioOutput DefaultAudioOutput;
typedef SdlKeyboardButtonInput DefaultButtonInput;
#endif
}

View File

@ -0,0 +1,123 @@
#include "EvdevButtonInput.h"
#include <iostream>
#if defined(__linux__)
#include <errno.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#endif
namespace Platform
{
EvdevButtonInput::EvdevButtonInput()
: device_path_("/dev/input/event0"),
key_code_(28),
opened_(false),
down_(false),
pressed_(false),
released_(false)
#if defined(__linux__)
,
fd_(-1)
#endif
{
}
EvdevButtonInput::~EvdevButtonInput()
{
shutdown();
}
bool EvdevButtonInput::init(const std::string& device_path, int key_code)
{
shutdown();
device_path_ = device_path.empty() ? "/dev/input/event0" : device_path;
key_code_ = key_code == 0 ? 28 : key_code;
down_ = false;
pressed_ = false;
released_ = false;
#if defined(__linux__)
fd_ = open(device_path_.c_str(), O_RDONLY | O_NONBLOCK);
if (fd_ < 0)
{
std::cerr << "EvdevButtonInput open failed: " << device_path_ << std::endl;
return false;
}
opened_ = true;
return true;
#else
std::cerr << "EvdevButtonInput is only implemented on Linux evdev." << std::endl;
return false;
#endif
}
void EvdevButtonInput::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 != key_code_)
{
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 << "EvdevButtonInput read failed." << std::endl;
}
break;
}
}
#endif
}
void EvdevButtonInput::shutdown()
{
#if defined(__linux__)
if (fd_ >= 0)
{
close(fd_);
fd_ = -1;
}
#endif
opened_ = false;
down_ = false;
pressed_ = false;
released_ = false;
}
}

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