Compare commits

..

No commits in common. "964a11a21592fb1f7d5797396644548e11608a63" and "d92b890528049b621b64bda5faf69e3ca27e18ee" have entirely different histories.

136 changed files with 131 additions and 4777 deletions

View File

@ -10,50 +10,49 @@ endif()
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF) option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
set(CORE_SOURCES set(SOURCES
src/Core/Asset/ObjLoader.cpp src/Apps/Demo/main.cpp
src/Core/Asset/SpriteAssetLoader.cpp src/Gfx/Asset/ObjLoader.cpp
src/Core/Core/DepthBuffer.cpp src/Gfx/Core/DepthBuffer.cpp
src/Core/Core/FrameBuffer.cpp src/Gfx/Core/FrameBuffer.cpp
src/Core/Core/Renderer.cpp src/Gfx/Core/Renderer.cpp
src/Core/Draw2D/DrawContext.cpp src/Gfx/Draw2D/DrawContext.cpp
src/Core/Rasterizer/Rasterizer.cpp src/Gfx/Rasterizer/Rasterizer.cpp
src/Core/Rasterizer/TriangleRasterizer.cpp src/Gfx/Rasterizer/TriangleRasterizer.cpp
src/Core/Scene/Camera.cpp src/Gfx/Scene/Camera.cpp
src/Core/Shading/BlinnPhongShader.cpp src/Gfx/Shading/BlinnPhongShader.cpp
) )
set(CORE_INCLUDE_DIRS if(USE_FRAMEBUFFER)
src/Core/Platform list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp)
src/Core/Asset else()
src/Core/Core list(APPEND SOURCES src/Gfx/Platform/SDLDisplay.cpp)
src/Core/Draw2D endif()
src/Core/Math
src/Core/Rasterizer add_executable(IMX6U-Game ${SOURCES})
src/Core/RenderData
src/Core/Scene target_include_directories(IMX6U-Game PRIVATE
src/Core/Shading src/Gfx/Platform
src/Gfx/Asset
src/Gfx/Core
src/Gfx/Draw2D
src/Gfx/Math
src/Gfx/Rasterizer
src/Gfx/RenderData
src/Gfx/Scene
src/Gfx/Shading
assets/font assets/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")
@ -67,18 +66,21 @@ 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_include_directories(IMX6U-Game PRIVATE libs/Win/SDL2/include) target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR})
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR}) target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2 SDL2_image)
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()
@ -87,56 +89,3 @@ 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_CORE_ARCHITECTURE.md` 项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md`
- `Core` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。 - `Gfx` / 底层图形库:只提供 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 -> Core -> Platform`,底层图形库不能反向依赖具体游戏。 - 依赖方向必须保持 `Apps -> Shared -> Gfx -> 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` 会收敛到 `Core` 的平台适配层: 显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层:
``` ```
┌──────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
@ -204,7 +204,7 @@ assets/font/font_atlas.h
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ SharedUI、存档、配置、资源索引 │ │ SharedUI、存档、配置、资源索引 │
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
CoreDrawContext、FrameBuffer、Draw2D、Math │ GfxDrawContext、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/
│ ├─ Core/ # 底层图形库:可复用、无具体游戏规则 │ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ 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_CORE_ARCHITECTURE.md # 应用层与图形库分层 │ ├─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定 │ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
├─ CMakeLists.txt ├─ CMakeLists.txt
└─ README.md └─ README.md
@ -301,8 +301,10 @@ 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 跨平台构建
@ -310,7 +312,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. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径) 4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
5. 纹理贴图、OBJ 模型加载与完整光照 5. 纹理贴图、OBJ 模型加载与完整光照
## 说明 ## 说明

View File

@ -1,89 +0,0 @@
# 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

@ -1,165 +0,0 @@
# 应用层与 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_CORE_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。 > 文档分工坐标、矩阵、深度等数学语义记录在本文档IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_GFX_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/Core/Core`、`src/Core/Math`、`src/Core/RenderData` 中是否已有可复用能力。 - **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/RenderData` 中是否已有可复用能力。
- **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。 - **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
## 2. 数值计算规范 ## 2. 数值计算规范
@ -164,7 +164,7 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
后续如果继续推进性能优化,优先建立这些基础设施: 后续如果继续推进性能优化,优先建立这些基础设施:
1. 统一定点数类型与转换工具,集中放在 `src/Core/Math/`。 1. 统一定点数类型与转换工具,集中放在 `src/Gfx/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/Core/Platform/` 以及明确的平台适配层中。 - SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
- `src/Core/Core`、`src/Core/Math`、`src/Core/Rasterizer`、`src/Core/RenderData`、`src/Core/Scene`、`src/Core/Draw2D` 不应直接包含 `SDL.h` - `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/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,22 +1,19 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <iostream> #include <iostream>
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#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 "Image.h"
#include "TimeSource.h"
#include "Timer.h"
#include "font_atlas.h"
#include "test_sprite.h" #include "test_sprite.h"
#include "font_atlas.h"
#include "Display.h"
#include "TimeSource.h"
#ifdef USE_FRAMEBUFFER #ifdef USE_FRAMEBUFFER
#include "FBDisplay.h" #include "FBDisplay.h"
#else #else
@ -33,8 +30,8 @@ static void PrintUsage(const char *program_name)
<< " " << program_name << " [--fps=30|45|60]\n"; << " " << program_name << " [--fps=30|45|60]\n";
} }
struct ProgramOptions struct ProgramOptions
{ {
uint32_t target_fps; uint32_t target_fps;
bool show_help; bool show_help;
@ -43,32 +40,16 @@ static void PrintUsage(const char *program_name)
show_help(false) show_help(false)
{ {
} }
}; };
static Platform::IDisplay* CreateDisplay() static ProgramOptions ParseProgramOptions(int argc, char *argv[])
{ {
#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; ProgramOptions options;
for (int i = 1; i < argc; ++i) for (int i = 1; i < argc; ++i)
{ {
const char* arg = argv[i]; const char *arg = argv[i];
const char* fps_value = nullptr; const char *fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0) if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{ {
@ -107,19 +88,18 @@ static void PrintUsage(const char *program_name)
} }
return options; return options;
} }
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source) 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()); const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
if (sleep_ms > 0u) if (sleep_ms > 0u)
{ {
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[])
{ {
const ProgramOptions options = ParseProgramOptions(argc, argv); const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help) if (options.show_help)
@ -127,17 +107,23 @@ 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;
Platform::IDisplay* display = CreateDisplay(); #ifdef USE_FRAMEBUFFER
if (!display->init(DemoWidth, DemoHeight)) Platform::IDisplay *display = new Platform::FBDisplay();
#else
Platform::IDisplay *display = new Platform::SDLDisplay();
#endif
if (!display->init(width, height))
{ {
delete display; delete display;
return -1; return -1;
} }
Core::DrawContext ctx(DemoWidth, DemoHeight); Gfx::DrawContext ctx(width, height);
Core::Timer timer(options.target_fps); std::cout << "Target FPS: " << timer.target_fps() << std::endl;
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);
@ -163,7 +149,6 @@ 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;
@ -188,13 +173,12 @@ 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 >= 1000u) if (now - last_fps_time >= 1000)
{ {
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);

View File

@ -1,301 +0,0 @@
#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

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1,280 +0,0 @@
#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

@ -1,120 +0,0 @@
#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

@ -1,145 +0,0 @@
#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

@ -1,33 +0,0 @@
#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

@ -1,154 +0,0 @@
#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

@ -1,44 +0,0 @@
#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

@ -1,170 +0,0 @@
#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

@ -1,58 +0,0 @@
#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

@ -1 +0,0 @@

View File

@ -1,101 +0,0 @@
#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

@ -1,37 +0,0 @@
#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

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1,98 +0,0 @@
#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

@ -1,50 +0,0 @@
#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(); }
};
}

View File

@ -1 +0,0 @@

View File

@ -1,234 +0,0 @@
#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

@ -1 +0,0 @@

View File

@ -1,601 +0,0 @@
#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

@ -1,180 +0,0 @@
#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

@ -1,14 +0,0 @@
#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

@ -1,182 +0,0 @@
#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

@ -1,17 +0,0 @@
#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

@ -1,141 +0,0 @@
#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

@ -1,39 +0,0 @@
#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

@ -1,204 +0,0 @@
#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

@ -1,39 +0,0 @@
#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

@ -1,26 +0,0 @@
#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

@ -1,26 +0,0 @@
#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

@ -1,23 +0,0 @@
#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

@ -1,24 +0,0 @@
#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

@ -1,123 +0,0 @@
#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;
}
}

View File

@ -1,35 +0,0 @@
#pragma once
#include "ButtonInput.h"
namespace Platform
{
class EvdevButtonInput : public IButtonInput
{
private:
std::string device_path_;
int key_code_;
bool opened_;
bool down_;
bool pressed_;
bool released_;
#if defined(__linux__)
int fd_;
#endif
public:
EvdevButtonInput();
~EvdevButtonInput();
bool init(const std::string& device_path = "/dev/input/event0", int key_code = 28) override;
void update() override;
void shutdown() override;
bool is_open() const override { return opened_; }
bool is_down() const override { return down_; }
bool was_pressed() const override { return pressed_; }
bool was_released() const override { return released_; }
const std::string& get_device_path() const override { return device_path_; }
int get_key_code() const override { return key_code_; }
};
}

View File

@ -1,153 +0,0 @@
#include "SdlAudioInput.h"
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>
namespace
{
static bool EnsureSdlAudio()
{
if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0)
{
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return false;
}
if ((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) == 0 &&
SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
{
std::cerr << "SDL audio init failed: " << SDL_GetError() << std::endl;
return false;
}
return true;
}
}
namespace Platform
{
SdlAudioInput::SdlAudioInput()
: device_name_("default"),
sample_rate_(16000),
channels_(1),
opened_(false),
device_id_(0)
{
}
SdlAudioInput::~SdlAudioInput()
{
shutdown();
}
bool SdlAudioInput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
{
shutdown();
if (sample_rate == 0 || channels == 0)
{
std::cerr << "SdlAudioInput invalid params." << std::endl;
return false;
}
if (!EnsureSdlAudio())
{
return false;
}
SDL_AudioSpec desired;
SDL_AudioSpec obtained;
SDL_zero(desired);
SDL_zero(obtained);
desired.freq = static_cast<int>(sample_rate);
desired.format = AUDIO_S16SYS;
desired.channels = static_cast<Uint8>(channels);
desired.samples = 512;
desired.callback = nullptr;
const char* sdl_device_name = nullptr;
if (!device_name.empty() && device_name != "default")
{
sdl_device_name = device_name.c_str();
}
device_id_ = SDL_OpenAudioDevice(
sdl_device_name,
1,
&desired,
&obtained,
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
if (device_id_ == 0)
{
std::cerr << "SdlAudioInput open failed: " << SDL_GetError() << std::endl;
return false;
}
if (obtained.format != AUDIO_S16SYS)
{
std::cerr << "SdlAudioInput unsupported sample format." << std::endl;
shutdown();
return false;
}
device_name_ = device_name.empty() ? "default" : device_name;
sample_rate_ = static_cast<uint32_t>(obtained.freq);
channels_ = static_cast<uint32_t>(obtained.channels);
opened_ = true;
SDL_PauseAudioDevice(device_id_, 0);
return true;
}
int SdlAudioInput::read_samples(int16_t* buffer, int sample_count)
{
if (buffer == nullptr || sample_count <= 0 || !opened_)
{
return 0;
}
const Uint32 requested_bytes = static_cast<Uint32>(sample_count * static_cast<int>(sizeof(int16_t)));
const Uint32 available_bytes = SDL_GetQueuedAudioSize(device_id_);
const Uint32 read_bytes = std::min(requested_bytes, available_bytes);
if (read_bytes == 0)
{
return 0;
}
const Uint32 dequeued = SDL_DequeueAudio(device_id_, buffer, read_bytes);
return static_cast<int>(dequeued / sizeof(int16_t));
}
float SdlAudioInput::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 SdlAudioInput::shutdown()
{
if (device_id_ != 0)
{
SDL_CloseAudioDevice(device_id_);
device_id_ = 0;
}
opened_ = false;
}
}

View File

@ -1,34 +0,0 @@
#pragma once
#include "AudioInput.h"
#include <SDL.h>
namespace Platform
{
class SdlAudioInput : public IAudioInput
{
private:
std::string device_name_;
uint32_t sample_rate_;
uint32_t channels_;
bool opened_;
SDL_AudioDeviceID device_id_;
public:
SdlAudioInput();
~SdlAudioInput();
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

@ -1,209 +0,0 @@
#include "SdlAudioOutput.h"
#include <algorithm>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
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 EnsureSdlAudio()
{
if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0)
{
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return false;
}
if ((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) == 0 &&
SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
{
std::cerr << "SDL audio init failed: " << SDL_GetError() << std::endl;
return false;
}
return true;
}
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
{
SdlAudioOutput::SdlAudioOutput()
: device_name_("default"),
sample_rate_(16000),
channels_(1),
opened_(false),
device_id_(0),
max_queued_samples_(0)
{
}
SdlAudioOutput::~SdlAudioOutput()
{
shutdown();
}
bool SdlAudioOutput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
{
shutdown();
if (sample_rate == 0 || channels == 0)
{
std::cerr << "SdlAudioOutput invalid params." << std::endl;
return false;
}
if (!EnsureSdlAudio())
{
return false;
}
SDL_AudioSpec desired;
SDL_AudioSpec obtained;
SDL_zero(desired);
SDL_zero(obtained);
desired.freq = static_cast<int>(sample_rate);
desired.format = AUDIO_S16SYS;
desired.channels = static_cast<Uint8>(channels);
desired.samples = 512;
desired.callback = nullptr;
const char* sdl_device_name = nullptr;
if (!device_name.empty() && device_name != "default")
{
sdl_device_name = device_name.c_str();
}
device_id_ = SDL_OpenAudioDevice(
sdl_device_name,
0,
&desired,
&obtained,
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
if (device_id_ == 0)
{
std::cerr << "SdlAudioOutput open failed: " << SDL_GetError() << std::endl;
return false;
}
if (obtained.format != AUDIO_S16SYS)
{
std::cerr << "SdlAudioOutput unsupported sample format." << std::endl;
shutdown();
return false;
}
device_name_ = device_name.empty() ? "default" : device_name;
sample_rate_ = static_cast<uint32_t>(obtained.freq);
channels_ = static_cast<uint32_t>(obtained.channels);
max_queued_samples_ = std::max<uint32_t>(channels_, sample_rate_ * channels_ / 8);
opened_ = true;
SDL_PauseAudioDevice(device_id_, 0);
return true;
}
int SdlAudioOutput::write_samples(const int16_t* samples, int sample_count)
{
if (samples == nullptr || sample_count <= 0 || !opened_)
{
return 0;
}
const Uint32 queued_samples = SDL_GetQueuedAudioSize(device_id_) / sizeof(int16_t);
if (queued_samples >= max_queued_samples_)
{
return 0;
}
const uint32_t available_samples = max_queued_samples_ - queued_samples;
const int write_count = static_cast<int>(std::min<uint32_t>(
static_cast<uint32_t>(sample_count),
available_samples));
if (write_count <= 0)
{
return 0;
}
const Uint32 bytes = static_cast<Uint32>(write_count * static_cast<int>(sizeof(int16_t)));
if (SDL_QueueAudio(device_id_, samples, bytes) != 0)
{
std::cerr << "SdlAudioOutput queue failed: " << SDL_GetError() << std::endl;
return 0;
}
return write_count;
}
bool SdlAudioOutput::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;
}
return SDL_QueueAudio(device_id_, samples.data(), header.data_size) == 0;
}
void SdlAudioOutput::shutdown()
{
if (device_id_ != 0)
{
SDL_ClearQueuedAudio(device_id_);
SDL_CloseAudioDevice(device_id_);
device_id_ = 0;
}
opened_ = false;
}
}

View File

@ -1,35 +0,0 @@
#pragma once
#include "AudioOutput.h"
#include <SDL.h>
namespace Platform
{
class SdlAudioOutput : public IAudioOutput
{
private:
std::string device_name_;
uint32_t sample_rate_;
uint32_t channels_;
bool opened_;
SDL_AudioDeviceID device_id_;
uint32_t max_queued_samples_;
public:
SdlAudioOutput();
~SdlAudioOutput();
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

@ -1,99 +0,0 @@
#include "SdlKeyboardButtonInput.h"
#include <iostream>
namespace
{
static bool EnsureSdlEvents()
{
if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0)
{
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return false;
}
if ((SDL_WasInit(SDL_INIT_EVENTS) & SDL_INIT_EVENTS) == 0 &&
SDL_InitSubSystem(SDL_INIT_EVENTS) < 0)
{
std::cerr << "SDL events init failed: " << SDL_GetError() << std::endl;
return false;
}
return true;
}
static int NormalizeScancode(const std::string& device_path, int key_code)
{
const bool looks_like_default_evdev = device_path.empty() || device_path == "keyboard" || device_path == "/dev/input/event0";
if ((key_code == 0 || key_code == 28) && looks_like_default_evdev)
{
return SDL_SCANCODE_SPACE;
}
if (key_code < 0 || key_code >= SDL_NUM_SCANCODES)
{
return SDL_SCANCODE_SPACE;
}
return key_code;
}
}
namespace Platform
{
SdlKeyboardButtonInput::SdlKeyboardButtonInput()
: device_path_("keyboard"),
key_code_(SDL_SCANCODE_SPACE),
opened_(false),
down_(false),
pressed_(false),
released_(false)
{
}
SdlKeyboardButtonInput::~SdlKeyboardButtonInput()
{
shutdown();
}
bool SdlKeyboardButtonInput::init(const std::string& device_path, int key_code)
{
if (!EnsureSdlEvents())
{
return false;
}
device_path_ = device_path.empty() ? "keyboard" : device_path;
key_code_ = NormalizeScancode(device_path_, key_code);
opened_ = true;
down_ = false;
pressed_ = false;
released_ = false;
return true;
}
void SdlKeyboardButtonInput::update()
{
pressed_ = false;
released_ = false;
if (!opened_)
{
return;
}
SDL_PumpEvents();
const Uint8* keyboard_state = SDL_GetKeyboardState(nullptr);
const bool current_down = keyboard_state != nullptr && keyboard_state[key_code_] != 0;
pressed_ = current_down && !down_;
released_ = !current_down && down_;
down_ = current_down;
}
void SdlKeyboardButtonInput::shutdown()
{
opened_ = false;
down_ = false;
pressed_ = false;
released_ = false;
}
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "ButtonInput.h"
#include <SDL.h>
namespace Platform
{
class SdlKeyboardButtonInput : public IButtonInput
{
private:
std::string device_path_;
int key_code_;
bool opened_;
bool down_;
bool pressed_;
bool released_;
public:
SdlKeyboardButtonInput();
~SdlKeyboardButtonInput();
bool init(const std::string& device_path = "keyboard", int key_code = SDL_SCANCODE_SPACE) override;
void update() override;
void shutdown() override;
bool is_open() const override { return opened_; }
bool is_down() const override { return down_; }
bool was_pressed() const override { return pressed_; }
bool was_released() const override { return released_; }
const std::string& get_device_path() const override { return device_path_; }
int get_key_code() const override { return key_code_; }
};
}

View File

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

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