Merge branch 'master' of https://gitea.sepcomet.xyz/basil/IMX6U-Game
This commit is contained in:
commit
0defa04eef
|
|
@ -57,7 +57,11 @@ AGENTS.md
|
|||
|
||||
build-win
|
||||
build-linux
|
||||
build-arm-fb
|
||||
build-arm-sdl
|
||||
|
||||
.idea
|
||||
|
||||
assets/test
|
||||
|
||||
gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz
|
||||
|
|
@ -4,6 +4,10 @@ project(IMX6U-Game)
|
|||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||
endif()
|
||||
|
||||
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
|
||||
|
||||
set(CORE_SOURCES
|
||||
|
|
|
|||
23
README.md
23
README.md
|
|
@ -115,6 +115,8 @@ cmake -B build-arm-fb \
|
|||
cmake --build build-arm-fb
|
||||
```
|
||||
|
||||
说明:单配置生成器(Makefile/Ninja)默认使用 `Release` 构建;ARM / framebuffer 性能测试必须确认 `CMAKE_BUILD_TYPE=Release`,否则逐像素绘制和 `/dev/fb0` 提交会因未优化构建出现数量级偏差。
|
||||
|
||||
构建(SDL2 后端,要求工具链/sysroot 可找到目标板 SDL2 开发库):
|
||||
```bash
|
||||
cmake -B build-arm-sdl \
|
||||
|
|
@ -277,6 +279,21 @@ IMX6U-Game/
|
|||
- **FBDisplay**:`/dev/fb0` 对照后端,用于极简显示通路验证
|
||||
- **ITimeSource / SteadyTimeSource**:独立时间源接口与单调时钟实现;Linux/IMX6U 使用 `clock_gettime(CLOCK_MONOTONIC)`,Windows 使用 `std::chrono::steady_clock`,Display 不再承担计时职责
|
||||
|
||||
### Framebuffer 性能说明
|
||||
|
||||
`FBDisplay` 是直接写 `/dev/fb0` 的对照后端。当前实现会从 CPU 侧 `FrameBuffer` 提交到系统 framebuffer,并针对常见像素格式提供快速路径:
|
||||
|
||||
- RGB565:使用专用 RGBA -> RGB565 转换;
|
||||
- ARGB8888 / XRGB8888 类 32bpp:使用专用通道重排;
|
||||
- RGBA8888 且行宽连续时:整块 `memcpy`。
|
||||
|
||||
板端性能测试必须使用 Release 构建。一次测试中,未优化 ARM 构建曾导致 `Frame:81ms / Present:69ms`,开启 Release 后同一轻量 2D demo 可达到约 76 FPS。该结果说明 `/dev/fb0` 整屏提交仍是关键热点,但构建类型会极大影响结论。后续优化优先级:
|
||||
|
||||
1. 直接使用与目标 fb0 一致的 backbuffer 像素格式,例如 RGB565,减少提交时转换;
|
||||
2. 2D 场景使用 dirty rect / 局部提交,避免每帧整屏写入;
|
||||
3. 避免无 3D 内容时清理 depth buffer;
|
||||
4. 对 tile/sprite 增加不透明行拷贝、预转换资源或专用批处理路径。
|
||||
|
||||
## 当前状态与后续
|
||||
|
||||
**已完成:**
|
||||
|
|
@ -284,13 +301,13 @@ IMX6U-Game/
|
|||
- 双平台显示后端(SDL2 / Framebuffer)
|
||||
- 离线资源转换工具:PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
|
||||
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
|
||||
- Core 目录规范化,代码收敛到 `src/Core/`
|
||||
- `Core::DrawContext` 统一绘制入口,封装现有绘制能力
|
||||
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
|
||||
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
|
||||
- C++11 兼容代码
|
||||
- CMake 跨平台构建
|
||||
|
||||
**待完成(按优先级):**
|
||||
1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON)
|
||||
1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON)
|
||||
2. 应用层拆分(Launcher / GameA / GameB / Shared)和统一 `IApp` 主循环
|
||||
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
|
||||
4. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
# 应用层与图形库分层设计
|
||||
|
||||
本项目后续包含三个应用层目标:两个游戏和一个启动器;同时还需要沉淀一套可复用的 IMX6U 轻量图形库。为了避免后期耦合和性能返工,必须明确区分“应用层”和“底层库”。
|
||||
|
||||
## 1. 推荐总体结构
|
||||
|
||||
推荐拆成四个逻辑层:
|
||||
|
||||
```text
|
||||
IMX6U-Game/
|
||||
├─ src/
|
||||
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
|
||||
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口(✅ 已实现)
|
||||
│ │ ├─ Core/ # FrameBuffer、DepthBuffer(✅ 已实现)
|
||||
│ │ ├─ Math/ # 向量、矩阵、数学工具(✅ 已实现)
|
||||
│ │ ├─ Rasterizer/ # 线段、三角形光栅化(✅ 已实现)
|
||||
│ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现)
|
||||
│ │ ├─ Scene/ # Camera、Transform(✅ 已实现)
|
||||
│ │ ├─ Shading/ # 着色器(预留)
|
||||
│ │ ├─ Platform/ # SDL2 / fb0 显示适配与独立时间源(✅ 已实现)
|
||||
│ │ └─ Asset/ # 资源加载(✅ 已实现)
|
||||
│ ├─ Apps/
|
||||
│ │ ├─ Demo/ # 当前板端性能 demo;历史上也用于 3D 立方体验证(✅ 已实现)
|
||||
│ │ ├─ Launcher/ # 启动器应用(待实现)
|
||||
│ │ ├─ GameA/ # 第一个游戏(待实现)
|
||||
│ │ └─ GameB/ # 第二个游戏(待实现)
|
||||
│ └─ Shared/ # 可选:应用层共享但不属于 Gfx 的东西
|
||||
│ ├─ Save/ # 存档格式、配置读写
|
||||
│ ├─ UI/ # 启动器和游戏共用 UI 组件
|
||||
│ └─ Assets/ # 应用层资源索引、资源命名约定
|
||||
├─ assets/
|
||||
│ ├─ font/ # 共享像素字体源文件与生成的 font_atlas
|
||||
│ ├─ sprite/ # 当前 demo/test sprite 源文件与生成头文件
|
||||
│ ├─ launcher/
|
||||
│ ├─ game_a/
|
||||
│ ├─ game_b/
|
||||
│ └─ shared/
|
||||
├─ tools/
|
||||
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
|
||||
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
|
||||
└─ docs/
|
||||
```
|
||||
|
||||
~~当前项目已有 `Core/Math/Platform/Rasterizer/RenderData/Scene` 等目录,短期不必一次性搬迁;但新增代码应按上面的边界收敛。等功能稳定后,再把现有底层代码整体移动到 `src/Gfx/`。~~
|
||||
|
||||
**已完成**:底层代码已整体迁移到 `src/Gfx/`,包括 Core、Math、Rasterizer、RenderData、Scene、Shading、Platform、Asset 和新增的 Draw2D。Demo 入口位于 `src/Apps/Demo/`。
|
||||
|
||||
## 2. 四个层级的职责
|
||||
|
||||
### 2.1 Gfx:底层图形库
|
||||
|
||||
职责:
|
||||
|
||||
- 管理 framebuffer、depthbuffer、渲染上下文。
|
||||
- 提供基础绘制接口:点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。
|
||||
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
|
||||
- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。
|
||||
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas),运行时不直接依赖 PNG/TTF 解码。
|
||||
- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。
|
||||
|
||||
禁止:
|
||||
|
||||
- 不依赖 `GameA`、`GameB`、`Launcher`。
|
||||
- 不包含具体关卡、角色、菜单流程、游戏状态机。
|
||||
- 不直接读取某个游戏专属资源路径。
|
||||
- 不在核心绘制接口中暴露 SDL2 类型。
|
||||
- 不在每帧热路径中执行图片/字体解码;资源转换应在构建前或工具阶段完成。
|
||||
|
||||
### 2.2 Apps/GameA 与 Apps/GameB:两个游戏
|
||||
|
||||
职责:
|
||||
|
||||
- 各自维护自己的游戏规则、状态机、关卡、实体、碰撞、计分、胜负逻辑。
|
||||
- 通过 Gfx 提供的接口绘制画面。
|
||||
- 通过平台输入快照读取按键/触摸状态,而不是直接调用 SDL。
|
||||
- 管理自己的资源索引和场景切换。
|
||||
|
||||
禁止:
|
||||
|
||||
- 游戏之间互相 include 对方代码。
|
||||
- 游戏直接操作 SDL renderer / texture / window。
|
||||
- 游戏直接依赖 framebuffer 内存布局,除非是明确标注的性能特例。
|
||||
|
||||
### 2.3 Apps/Launcher:启动器
|
||||
|
||||
职责:
|
||||
|
||||
- 展示游戏列表、图标、说明、版本信息。
|
||||
- 选择并启动 GameA 或 GameB。
|
||||
- 管理全局设置,例如音量、亮度、输入校准、语言等。
|
||||
- 可以复用 Shared/UI,但不承载具体游戏逻辑。
|
||||
|
||||
启动方式有两种可选方案:
|
||||
|
||||
1. **单进程多应用模式**:Launcher、GameA、GameB 编译成一个可执行文件,内部切换当前 App。
|
||||
2. **多进程模式**:Launcher 是独立程序,选择后启动另一个游戏可执行文件。
|
||||
|
||||
对 IMX6U 初期开发,推荐先用 **单进程多应用模式**,原因:
|
||||
|
||||
- 构建、部署、调试更简单。
|
||||
- SDL2 初始化、资源缓存、输入状态可以共用。
|
||||
- 避免频繁退出/启动进程带来的黑屏、资源重载和状态恢复问题。
|
||||
|
||||
等项目成熟后,如果每个游戏体积较大,或者需要独立更新,再考虑多进程拆分。
|
||||
|
||||
### 2.4 Shared:应用层共享模块
|
||||
|
||||
Shared 只放“应用层共享,但不属于底层图形库”的内容,例如:
|
||||
|
||||
- UI 控件:按钮、列表、九宫格面板、菜单焦点管理。
|
||||
- 存档/设置:配置文件、最高分、解锁状态。
|
||||
- 资源清单:多个应用共用的字体、图标、音效索引。
|
||||
|
||||
Shared 可以依赖 Gfx;Gfx 不能依赖 Shared。
|
||||
|
||||
## 3. 依赖方向
|
||||
|
||||
必须保持单向依赖:
|
||||
|
||||
```text
|
||||
Apps/GameA ─┐
|
||||
Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0/ITimeSource)
|
||||
Launcher ─┘
|
||||
|
||||
Apps/GameA ─┐
|
||||
Apps/GameB ─┼──────────> Gfx
|
||||
Launcher ─┘
|
||||
```
|
||||
|
||||
禁止反向依赖:
|
||||
|
||||
```text
|
||||
Gfx -> Apps 禁止
|
||||
Gfx -> Shared 禁止
|
||||
GameA -> GameB 禁止
|
||||
GameB -> GameA 禁止
|
||||
Platform -> Game 禁止
|
||||
```
|
||||
|
||||
## 4. 应用统一接口
|
||||
|
||||
推荐为 Launcher、GameA、GameB 提供统一应用接口,例如:
|
||||
|
||||
```cpp
|
||||
class IApp
|
||||
{
|
||||
public:
|
||||
virtual ~IApp() {}
|
||||
virtual void on_enter() = 0;
|
||||
virtual void on_exit() = 0;
|
||||
virtual void update(uint32_t fixed_delta_ms) = 0;
|
||||
virtual void render(Gfx::DrawContext& ctx) = 0;
|
||||
virtual AppId next_app() const = 0;
|
||||
};
|
||||
```
|
||||
|
||||
主循环只认识 `IApp`:
|
||||
|
||||
```text
|
||||
poll input -> update current app -> render current app -> present framebuffer
|
||||
```
|
||||
|
||||
这样三个应用层共用同一个主循环、同一套 SDL2 初始化、framebuffer 提交流程和独立时间源。
|
||||
|
||||
注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。
|
||||
|
||||
## 5. 底层图形库优先提供的 API
|
||||
|
||||
Gfx 初期建议先做 2D 基础能力,不要一开始就做复杂引擎:
|
||||
|
||||
```cpp
|
||||
namespace Gfx
|
||||
{
|
||||
struct RectI { int32_t x, y, w, h; };
|
||||
struct PointI { int32_t x, y; };
|
||||
struct Color32 { uint32_t rgba; };
|
||||
|
||||
class DrawContext
|
||||
{
|
||||
public:
|
||||
void clear(Color32 color);
|
||||
void draw_pixel(int32_t x, int32_t y, Color32 color);
|
||||
void draw_line(PointI a, PointI b, Color32 color);
|
||||
void draw_rect(RectI rect, Color32 color);
|
||||
void fill_rect(RectI rect, Color32 color);
|
||||
void draw_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
|
||||
void fill_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
|
||||
void draw_sprite(int32_t x, int32_t y, const RenderData::Image& image);
|
||||
void draw_sprite_region(int32_t x, int32_t y,
|
||||
const RenderData::SpriteRegion& region);
|
||||
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||
RenderData::Color color, const char* text);
|
||||
void draw_tilemap(const RenderData::Tilemap& tilemap,
|
||||
int32_t screen_x, int32_t screen_y,
|
||||
int32_t viewport_w, int32_t viewport_h,
|
||||
int32_t camera_x, int32_t camera_y);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
后续再扩展:
|
||||
|
||||
- `set_clip_rect`
|
||||
- `set_camera_2d`
|
||||
- `batch sprite/tile`
|
||||
|
||||
## 6. 命名建议
|
||||
|
||||
为了避免“项目名、库名、游戏名”混乱,建议:
|
||||
|
||||
- 项目仓库仍叫 `IMX6U-Game`。
|
||||
- 底层图形库命名为 `Gfx` 或 `MiniGfx`。
|
||||
- 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。
|
||||
- CMake target 可以是:
|
||||
- `imx6u_gfx` 静态库
|
||||
- `imx6u_launcher`
|
||||
- `imx6u_game_a`
|
||||
- `imx6u_game_b`
|
||||
- 或初期单可执行文件 `imx6u_suite`
|
||||
|
||||
## 7. 推荐演进顺序
|
||||
|
||||
1. ~~先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。~~
|
||||
2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~
|
||||
3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**(`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、present)
|
||||
4. ~~底层代码迁移到 `src/Gfx/`,Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成**
|
||||
5. 新增 Launcher app,只做最小菜单和应用切换。
|
||||
6. 新增 GameA/GameB 空壳,验证三应用切换。
|
||||
7. 再逐步把现有 3D demo 能力恢复为独立验证入口,或把 2D 游戏逻辑迁入对应 Game 目录。
|
||||
8. 最后重构 CMake,按 `imx6u_gfx` + 应用 target 拆分。
|
||||
|
||||
## 8. 性能注意事项
|
||||
|
||||
- 应用切换不应重复销毁/创建 SDL window、renderer、texture。
|
||||
- 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源;Display 不承担计时职责。
|
||||
- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
|
||||
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
|
||||
- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。
|
||||
- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
|
||||
- 无 3D 内容的 2D 应用应使用只清颜色缓冲的路径,避免每帧清理 depth buffer。
|
||||
- `/dev/fb0` 后端提交可能是主要瓶颈;板端性能分析应拆分 `Frame` 和 `Present` 耗时,并确认 ARM 构建为 Release。
|
||||
- 直接写 framebuffer 不是原子换屏,LCD 扫描会让局部动画看起来比完整帧率更连续;判断性能应以计时数据为准。
|
||||
|
||||
## 9. 资源转换约定
|
||||
|
||||
- 运行时资源优先使用离线转换后的简单数组,不在 IMX6U 运行时解码 PNG/TTF。
|
||||
- `tools/png_to_header.py` 将 PNG 转为 `uint32_t` RGBA 数组,适用于 sprite、小图标、测试纹理等。
|
||||
- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas,并输出同名 PNG 预览和 C++ 头文件。
|
||||
- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。
|
||||
- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。
|
||||
|
||||
## 10. SpriteRegion 与 Tilemap 约定
|
||||
|
||||
- `RenderData::SpriteRegion` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。
|
||||
- `DrawContext::draw_sprite_ex` 是底层 sprite 绘制入口,负责源区域检查、目标屏幕裁剪、scale 和 flip;`draw_sprite_region` 系列只是对 atlas 子区域的语义包装。
|
||||
- `RenderData::Tilemap` 使用 `uint16_t` tile id 保存地图网格,`Tilemap::EmptyTile` (`0xFFFF`) 表示空 tile。
|
||||
- `Tilemap` 当前只支持一个 atlas、固定 tile 宽高和固定 `atlas_columns`;tile id 通过 `tile_id % atlas_columns` / `tile_id / atlas_columns` 映射到 atlas 中的源区域。
|
||||
- `DrawContext::draw_tilemap` 的裁剪分两层:
|
||||
- tilemap 层按 tile 计算需要尝试绘制的可见范围;
|
||||
- sprite 层按像素裁剪每个 tile,支持 camera 像素级滚动时显示半个 tile。
|
||||
- 带 `viewport_w` / `viewport_h` 的 `draw_tilemap` 重载用于子视口绘制,`screen_x` / `screen_y` 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。
|
||||
- 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。
|
||||
|
|
@ -87,11 +87,31 @@
|
|||
## 5. 渲染管线性能规范
|
||||
|
||||
- FrameBuffer / DepthBuffer 清理必须优先考虑批量填充(如 `memset`、`std::fill`、平台优化路径),不要逐像素走复杂逻辑。
|
||||
- 2D-only 或不使用深度测试的场景应只清颜色缓冲,例如使用 `DrawContext::clear_color()`;不要每帧无意义清理 depth buffer。
|
||||
- 像素写入路径应尽量减少分支和函数调用层级。
|
||||
- 像素级写入的 Release 快路径不要使用带额外边界检查的容器访问(例如 `std::vector::at()`);边界检查应在外层完成。
|
||||
- 裁剪、剔除、包围盒收缩要尽早执行,避免把不可见数据送入像素级循环。
|
||||
- 三角形属性插值、深度测试、纹理采样等未来功能必须先定义定点/整数方案,再接入热路径。
|
||||
- PC 调试版可以保留更易读的检查与可视化代码,但 ARM release 路径必须能关闭这些额外成本。
|
||||
|
||||
### 5.1 `/dev/fb0` 提交路径
|
||||
|
||||
Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::present()` 需要把 CPU 侧 framebuffer 写入 `/dev/fb0`,如果逐像素走通用格式转换,整屏 1024x600 提交会成为主瓶颈。
|
||||
|
||||
规范:
|
||||
|
||||
- ARM / framebuffer 性能测试必须使用 Release 构建;单配置生成器应确认 `CMAKE_BUILD_TYPE=Release`。
|
||||
- 性能结论必须拆分 `Frame` 和 `Present` 耗时;如果 `Present` 接近 `Frame`,优先优化显示提交,而不是游戏逻辑。
|
||||
- fb0 像素格式应在初始化时打印并据此走专用路径,常见格式包括 RGB565、ARGB8888/XRGB8888、RGBA8888。
|
||||
- 避免每帧重复做不必要的通用 RGBA 转换;可考虑目标格式 backbuffer、预转换资源、行拷贝、dirty rect 和局部提交。
|
||||
- 直接写 `/dev/fb0` 不是原子换屏;LCD 控制器可能边扫描边显示正在写入的内存,因此肉眼流畅度不等同于完整帧率。
|
||||
|
||||
已观察到的板端测试结论:
|
||||
|
||||
- 未优化 ARM 构建下,轻量 2D demo 曾出现约 `Frame:81ms / Present:69ms`。
|
||||
- 改为 Release 构建后,同一类 framebuffer 测试最高约 76 FPS。
|
||||
- 因此板端性能测试首先检查构建类型,再判断算法或硬件瓶颈。
|
||||
|
||||
## 6. STL 与标准库使用边界
|
||||
|
||||
允许:
|
||||
|
|
@ -129,6 +149,7 @@
|
|||
|
||||
新增或修改核心代码前,至少检查:
|
||||
|
||||
- [ ] ARM / framebuffer 性能测试是否确认使用 Release 构建?
|
||||
- [ ] 是否在热路径新增了 `float` / `double`?如果是,是否能改成整数/定点?
|
||||
- [ ] 是否在每帧或内层循环创建了 `std::vector` / `std::string` / 其他堆分配对象?
|
||||
- [ ] 容器是否提前 `reserve()`,或由上层复用?
|
||||
|
|
@ -214,3 +235,11 @@
|
|||
- 总帧时间、最低 FPS、峰值帧时间
|
||||
|
||||
性能日志默认低频输出,例如每 60 帧汇总一次;ARM release 中不得逐帧大量打印。
|
||||
|
||||
Framebuffer 对照后端同样需要至少显示或记录:
|
||||
|
||||
- `Frame`:从帧开始到提交完成的总耗时;
|
||||
- `Present`:`IDisplay::present()` 的耗时;
|
||||
- 当前 FPS。
|
||||
|
||||
当前 Demo 的板端测试 UI 已包含这些信息,后续正式 profiler 可替代该临时显示。
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "BitmapFont.h"
|
||||
#include <chrono>
|
||||
#include "Color.h"
|
||||
#include "Display.h"
|
||||
#include <cstdlib>
|
||||
#include "Timer.h"
|
||||
#include "DrawContext.h"
|
||||
#include "Image.h"
|
||||
#include "TimeSource.h"
|
||||
|
|
@ -23,10 +23,15 @@
|
|||
#include "SDLDisplay.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
const int32_t width = 1024;
|
||||
const int32_t height = 600;
|
||||
|
||||
static void PrintUsage(const char *program_name)
|
||||
{
|
||||
const int32_t DemoWidth = 800;
|
||||
const int32_t DemoHeight = 600;
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
struct ProgramOptions
|
||||
{
|
||||
|
|
@ -141,39 +146,46 @@ int main(int argc, char* argv[])
|
|||
font.columns = font_columns;
|
||||
font.first_char = font_first_char;
|
||||
|
||||
RenderData::Image sprite(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000u);
|
||||
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
|
||||
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
|
||||
|
||||
const std::array<uint16_t, 8 * 4> tileIds = {
|
||||
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
|
||||
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
|
||||
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
|
||||
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
|
||||
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
|
||||
|
||||
const RenderData::Color clearColor(18, 18, 24, 255);
|
||||
const RenderData::Color fpsColor(0, 255, 80, 255);
|
||||
const RenderData::Color fpsBg(0, 0, 0, 200);
|
||||
|
||||
int32_t fps = 0;
|
||||
int32_t frame_count = 0;
|
||||
uint32_t last_fps_time = 0;
|
||||
uint32_t animation_time_ms = 0;
|
||||
char fps_text[32];
|
||||
char perf_text[64];
|
||||
uint32_t last_frame_ms = 0;
|
||||
uint32_t last_present_ms = 0;
|
||||
|
||||
std::cout << "Demo started. Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
bool is_running = true;
|
||||
while (is_running)
|
||||
bool isRunning = true;
|
||||
while (isRunning)
|
||||
{
|
||||
timer.begin_frame(time_source.get_time_ms());
|
||||
animation_time_ms += timer.fixed_delta_ms();
|
||||
const uint32_t frame_start_ms = time_source.get_time_ms();
|
||||
timer.begin_frame(frame_start_ms);
|
||||
|
||||
bool should_quit = false;
|
||||
display->poll_events(should_quit);
|
||||
if (should_quit)
|
||||
{
|
||||
is_running = false;
|
||||
}
|
||||
display->poll_events(isRunning);
|
||||
|
||||
ctx.clear(RenderData::Color(18, 18, 24, 255));
|
||||
ctx.fill_rect(48, 72, 704, 360, RenderData::Color(42, 48, 60, 255));
|
||||
ctx.fill_rect(64, 88, 672, 328, RenderData::Color(24, 28, 36, 255));
|
||||
ctx.clear_color(clearColor);
|
||||
|
||||
const int32_t travel = std::max(1, DemoWidth - sprite.width - 128);
|
||||
const int32_t sprite_x = 64 + static_cast<int32_t>((animation_time_ms / 8u) % static_cast<uint32_t>(travel));
|
||||
const int32_t sprite_y = 220;
|
||||
ctx.draw_sprite(sprite_x, sprite_y, sprite);
|
||||
ctx.draw_text(font, 64, 96, RenderData::Color(240, 240, 240, 255), "GFX DEMO");
|
||||
// sprite 测试
|
||||
ctx.draw_sprite(10, 10, sprite_img);
|
||||
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
|
||||
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
|
||||
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(frame_start_ms / 20u) % 32, 0);
|
||||
|
||||
// FPS 计数
|
||||
++frame_count;
|
||||
const uint32_t now = time_source.get_time_ms();
|
||||
if (now - last_fps_time >= 1000u)
|
||||
|
|
@ -184,15 +196,14 @@ int main(int argc, char* argv[])
|
|||
}
|
||||
|
||||
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
|
||||
ctx.draw_text(
|
||||
font,
|
||||
4,
|
||||
4,
|
||||
RenderData::Color(0, 255, 80, 255),
|
||||
RenderData::Color(0, 0, 0, 200),
|
||||
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);
|
||||
ctx.draw_text(font, 4, 4 + font.char_h, fpsColor, fpsBg, perf_text);
|
||||
|
||||
const uint32_t present_start_ms = time_source.get_time_ms();
|
||||
ctx.present(display);
|
||||
last_present_ms = time_source.get_time_ms() - present_start_ms;
|
||||
last_frame_ms = time_source.get_time_ms() - frame_start_ms;
|
||||
SleepRemainingFrameTime(timer, time_source);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <vector>
|
||||
#include "Vector2.h"
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace Core
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ namespace Core
|
|||
}
|
||||
// Row-major layout with y = 0 on the first row, matching a top-left screen origin.
|
||||
size_t index = static_cast<size_t>(y) * width + x;
|
||||
buffer.at(index) = color;
|
||||
buffer[index] = color;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "Color.h"
|
||||
#include "Vector2.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -43,4 +44,4 @@ namespace Core
|
|||
|
||||
void set_pixel(const int32_t x, const int32_t y, const uint32_t color);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ namespace Core
|
|||
depthBuffer->clear();
|
||||
}
|
||||
|
||||
void DrawContext::clear_color(const RenderData::Color& color)
|
||||
{
|
||||
frameBuffer->clear(color);
|
||||
}
|
||||
|
||||
void DrawContext::clear_depth()
|
||||
{
|
||||
depthBuffer->clear();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ namespace Core
|
|||
int32_t get_height() const;
|
||||
|
||||
void clear(const RenderData::Color& color);
|
||||
void clear_color(const RenderData::Color& color);
|
||||
void clear_depth();
|
||||
|
||||
void draw_line(const Math::Vector2Int& from, const Math::Vector2Int& to, const RenderData::Color& color);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include "Vector3.h"
|
||||
#include "Vector4.h"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,22 @@
|
|||
|
||||
namespace Platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
inline uint16_t rgba_to_rgb565(uint32_t rgba)
|
||||
{
|
||||
const uint32_t r = (rgba >> 24) & 0xFFu;
|
||||
const uint32_t g = (rgba >> 16) & 0xFFu;
|
||||
const uint32_t b = (rgba >> 8) & 0xFFu;
|
||||
return static_cast<uint16_t>(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3));
|
||||
}
|
||||
|
||||
inline uint32_t rgba_to_argb8888(uint32_t rgba)
|
||||
{
|
||||
return ((rgba >> 8) & 0x00FFFFFFu) | ((rgba & 0xFFu) << 24);
|
||||
}
|
||||
}
|
||||
|
||||
bool FBDisplay::init(int w, int h)
|
||||
{
|
||||
width = w;
|
||||
|
|
@ -81,15 +97,65 @@ namespace Platform
|
|||
return;
|
||||
|
||||
const uint32_t* src = static_cast<const uint32_t*>(framebuffer->get_buffer());
|
||||
int dst_width = std::min(width, static_cast<int>(vinfo.xres));
|
||||
int dst_height = std::min(height, static_cast<int>(vinfo.yres));
|
||||
int bytes_per_pixel = vinfo.bits_per_pixel / 8;
|
||||
const int src_width = framebuffer->get_width();
|
||||
const int dst_width = std::min(src_width, static_cast<int>(vinfo.xres));
|
||||
const int dst_height = std::min(framebuffer->get_height(), static_cast<int>(vinfo.yres));
|
||||
const int bytes_per_pixel = vinfo.bits_per_pixel / 8;
|
||||
const bool is_rgb565 =
|
||||
vinfo.bits_per_pixel == 16 &&
|
||||
vinfo.red.offset == 11 && vinfo.red.length == 5 &&
|
||||
vinfo.green.offset == 5 && vinfo.green.length == 6 &&
|
||||
vinfo.blue.offset == 0 && vinfo.blue.length == 5;
|
||||
const bool is_argb8888 =
|
||||
vinfo.bits_per_pixel == 32 &&
|
||||
vinfo.red.offset == 16 && vinfo.red.length == 8 &&
|
||||
vinfo.green.offset == 8 && vinfo.green.length == 8 &&
|
||||
vinfo.blue.offset == 0 && vinfo.blue.length == 8;
|
||||
const bool is_rgba8888 =
|
||||
vinfo.bits_per_pixel == 32 &&
|
||||
vinfo.red.offset == 24 && vinfo.red.length == 8 &&
|
||||
vinfo.green.offset == 16 && vinfo.green.length == 8 &&
|
||||
vinfo.blue.offset == 8 && vinfo.blue.length == 8;
|
||||
|
||||
if (is_rgb565)
|
||||
{
|
||||
for (int y = 0; y < dst_height; ++y)
|
||||
{
|
||||
const uint32_t* src_row = src + y * src_width;
|
||||
uint16_t* dst_row = reinterpret_cast<uint16_t*>(fb_mem + y * finfo.line_length);
|
||||
for (int x = 0; x < dst_width; ++x)
|
||||
{
|
||||
dst_row[x] = rgba_to_rgb565(src_row[x]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_argb8888)
|
||||
{
|
||||
for (int y = 0; y < dst_height; ++y)
|
||||
{
|
||||
const uint32_t* src_row = src + y * src_width;
|
||||
uint32_t* dst_row = reinterpret_cast<uint32_t*>(fb_mem + y * finfo.line_length);
|
||||
for (int x = 0; x < dst_width; ++x)
|
||||
{
|
||||
dst_row[x] = rgba_to_argb8888(src_row[x]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_rgba8888 && dst_width == src_width && static_cast<int>(finfo.line_length) == dst_width * 4)
|
||||
{
|
||||
std::memcpy(fb_mem, src, static_cast<size_t>(dst_height) * static_cast<size_t>(dst_width) * sizeof(uint32_t));
|
||||
return;
|
||||
}
|
||||
|
||||
for (int y = 0; y < dst_height; ++y)
|
||||
{
|
||||
for (int x = 0; x < dst_width; ++x)
|
||||
{
|
||||
uint32_t pixel = convert_pixel(src[y * width + x]);
|
||||
uint32_t pixel = convert_pixel(src[y * src_width + x]);
|
||||
uint8_t* dst = fb_mem + y * finfo.line_length + x * bytes_per_pixel;
|
||||
if (vinfo.bits_per_pixel == 32)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue