统一 2D 渲染链路以消除格式分叉和热路径中转

将图片主路径收口为 RGBA5551 输入、RGB565 backbuffer 输出,并删除 USE_RGB565_BACKBUFFER 双路径。sprite/tilemap 主绘制改为直接 RGBA5551 -> RGB565,透明语义固定为 1-bit alpha test。同步收口 SDL/FB present 路径、sprite 资源加载/导出规范,以及相关测试与文档。

Constraint: 运行时图片主路径需统一到 RGBA5551,不能继续保留主要 RGBA8888 分支
Constraint: 透明仅支持 1-bit 语义,不引入 alpha blending
Rejected: 保留 USE_RGB565_BACKBUFFER 宏双路径 | 持续增加维护成本并掩盖真实主链路
Rejected: 主绘制继续走 RGBA5551 -> RGBA8888 -> RGB565 | 热路径存在不必要中转
Confidence: high
Scope-risk: moderate
Directive: 后续图片资源默认按 RGBA5551 接入;不要重新引入通用 RGBA8888 主运行时路径
Directive: 字体 atlas 目前仍属兼容输入;若继续收口,优先改成 1-bit mask 而不是新的图片分支
Tested: render_pipeline_tests;SDL Demo Release 构建;搜索确认无 USE_RGB565_BACKBUFFER 残留
Not-tested: Linux framebuffer 目标实机构建与显示验证
This commit is contained in:
SepComet 2026-06-08 18:32:04 +08:00
parent 8785368913
commit 5ac61dca0e
19 changed files with 167075 additions and 166993 deletions

6
.gitignore vendored
View File

@ -55,11 +55,7 @@ AGENTS.md
.omc .omc
.omx .omx
build-win build-*
build-linux
build-arm-fb
build-arm-sdl
build-check
.idea .idea

View File

@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(IMX6U-Game) project(IMX6U-Game)
include(CTest)
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -9,7 +10,6 @@ if(NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE)
endif() endif()
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF) option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
option(USE_RGB565_BACKBUFFER "Use RGB565 internal backbuffer to eliminate present conversion on RGB565 fb0" OFF)
set(CORE_SOURCES set(CORE_SOURCES
src/Core/Asset/ObjLoader.cpp src/Core/Asset/ObjLoader.cpp
@ -63,10 +63,6 @@ if(USE_FRAMEBUFFER)
target_compile_definitions(imx6u_core PUBLIC USE_FRAMEBUFFER) target_compile_definitions(imx6u_core PUBLIC USE_FRAMEBUFFER)
endif() endif()
if(USE_RGB565_BACKBUFFER)
target_compile_definitions(imx6u_core PUBLIC USE_RGB565_BACKBUFFER)
endif()
if(USE_FRAMEBUFFER) if(USE_FRAMEBUFFER)
else() else()
if(WIN32) if(WIN32)
@ -139,3 +135,38 @@ endfunction()
add_subdirectory(src/Apps/Game) add_subdirectory(src/Apps/Game)
add_subdirectory(src/Apps/Demo) add_subdirectory(src/Apps/Demo)
if(BUILD_TESTING AND NOT USE_FRAMEBUFFER)
add_executable(render_pipeline_tests
tests/render_pipeline_tests.cpp
)
target_link_libraries(render_pipeline_tests PRIVATE imx6u_core)
target_include_directories(render_pipeline_tests PRIVATE ${CORE_INCLUDE_DIRS})
if(CMAKE_CONFIGURATION_TYPES)
foreach(config ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER "${config}" config_upper)
set_target_properties(render_pipeline_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${config_upper} "${CMAKE_BINARY_DIR}/${config}"
)
endforeach()
else()
set_target_properties(render_pipeline_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
endif()
if(WIN32)
add_custom_command(TARGET render_pipeline_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL}"
"$<TARGET_FILE_DIR:render_pipeline_tests>"
)
endif()
if(MSVC)
target_compile_options(render_pipeline_tests PRIVATE /utf-8 /W3)
endif()
add_test(NAME render_pipeline_tests COMMAND render_pipeline_tests)
endif()

View File

@ -321,15 +321,15 @@ IMX6U-Game/
### Framebuffer 性能说明 ### Framebuffer 性能说明
`FBDisplay` 是直接写 `/dev/fb0` 的对照后端。当前实现会从 CPU 侧 `FrameBuffer` 提交到系统 framebuffer并针对常见像素格式提供快速路径 `FBDisplay` 是直接写 `/dev/fb0` 的对照后端。当前实现会从 CPU 侧 `RGB565 FrameBuffer` 提交到系统 framebuffer并针对常见像素格式提供快速路径
- RGB565使用专用 RGBA -> RGB565 转换 - RGB565直接行拷贝
- ARGB8888 / XRGB8888 类 32bpp使用专用通道重排 - ARGB8888 / XRGB8888 类 32bpp使用专用 `RGB565 -> 32-bit` 转换
- RGBA8888 且行宽连续时:整块 `memcpy` - 其他格式:走统一 display conversion
板端性能测试必须使用 Release 构建。一次测试中,未优化 ARM 构建曾导致 `Frame:81ms / Present:69ms`,开启 Release 后同一轻量 2D demo 可达到约 76 FPS。该结果说明 `/dev/fb0` 整屏提交仍是关键热点,但构建类型会极大影响结论。后续优化优先级: 板端性能测试必须使用 Release 构建。一次测试中,未优化 ARM 构建曾导致 `Frame:81ms / Present:69ms`,开启 Release 后同一轻量 2D demo 可达到约 76 FPS。该结果说明 `/dev/fb0` 整屏提交仍是关键热点,但构建类型会极大影响结论。后续优化优先级:
1. 直接使用与目标 fb0 一致的 backbuffer 像素格式,例如 RGB565减少提交时转换; 1. 对 32-bit fb0 增加更快的 `RGB565 -> 目标格式` 批量转换;
2. 2D 场景使用 dirty rect / 局部提交,避免每帧整屏写入; 2. 2D 场景使用 dirty rect / 局部提交,避免每帧整屏写入;
3. 避免无 3D 内容时清理 depth buffer 3. 避免无 3D 内容时清理 depth buffer
4. 对 tile/sprite 增加不透明行拷贝、预转换资源或专用批处理路径。 4. 对 tile/sprite 增加不透明行拷贝、预转换资源或专用批处理路径。
@ -339,7 +339,7 @@ IMX6U-Game/
**已完成:** **已完成:**
- 可旋转立方体的 3D 渲染MVP 变换、背面剔除、扫描线填充、深度测试) - 可旋转立方体的 3D 渲染MVP 变换、背面剔除、扫描线填充、深度测试)
- 双平台显示后端SDL2 / Framebuffer - 双平台显示后端SDL2 / Framebuffer
- 离线资源转换工具PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header - 离线资源转换工具PNG sprite -> RGBA5551 C++ 头文件,像素字体 -> bitmap atlas/header
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap - 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
- Core 目录规范化,代码收敛到 `src/Core/` - Core 目录规范化,代码收敛到 `src/Core/`
- `Core::DrawContext` 统一绘制入口,封装现有绘制能力 - `Core::DrawContext` 统一绘制入口,封装现有绘制能力

View File

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

View File

@ -105,7 +105,8 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
- ARM / framebuffer 性能测试必须使用 Release 构建;单配置生成器应确认 `CMAKE_BUILD_TYPE=Release` - ARM / framebuffer 性能测试必须使用 Release 构建;单配置生成器应确认 `CMAKE_BUILD_TYPE=Release`
- 性能结论必须拆分 `Frame``Present` 耗时;如果 `Present` 接近 `Frame`,优先优化显示提交,而不是游戏逻辑。 - 性能结论必须拆分 `Frame``Present` 耗时;如果 `Present` 接近 `Frame`,优先优化显示提交,而不是游戏逻辑。
- fb0 像素格式应在初始化时打印并据此走专用路径,常见格式包括 RGB565、ARGB8888/XRGB8888、RGBA8888。 - fb0 像素格式应在初始化时打印并据此走专用路径,常见格式包括 RGB565、ARGB8888/XRGB8888、RGBA8888。
- 避免每帧重复做不必要的通用 RGBA 转换;可考虑目标格式 backbuffer、预转换资源、行拷贝、dirty rect 和局部提交。 - 当前规范固定 `FrameBuffer``RGB565`sprite/tilemap 主路径直接从 `RGBA5551` 写入 `RGB565`,透明仅支持 1-bit alpha test`A=0` 跳过,`A=1` 覆写)。
- 避免每帧重复做不必要的通用 RGBA 转换;优先使用 `RGB565` 行拷贝、预转换资源、dirty rect 和局部提交。
- 直接写 `/dev/fb0` 不是原子换屏LCD 控制器可能边扫描边显示正在写入的内存,因此肉眼流畅度不等同于完整帧率。 - 直接写 `/dev/fb0` 不是原子换屏LCD 控制器可能边扫描边显示正在写入的内存,因此肉眼流畅度不等同于完整帧率。
已观察到的板端测试结论: 已观察到的板端测试结论:
@ -139,9 +140,9 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
- 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。 - 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。
- 当前工具约定: - 当前工具约定:
- `tools/png_to_header.py`PNG -> `uint32_t` RGBA 头文件 - `tools/png_to_header.py`PNG -> `uint16_t` `RGBA5551` 头文件sprite 主路径)
- `tools/gen_font_atlas.py`:像素字体 TTF -> ASCII bitmap font atlas/header。 - `tools/gen_font_atlas.py`:像素字体 TTF -> ASCII bitmap font atlas/header。
- 生成像素格式统一为 `(R << 24) | (G << 16) | (B << 8) | A`,与 `RenderData::Color::to_rgba()``Core::FrameBuffer` 当前格式保持一致 - sprite 运行时输入规范统一为 `RGBA5551`,内部 backbuffer 统一为 `RGB565`;字体 atlas 目前仍允许局部兼容 `RGBA8888` 输入,但只使用 1-bit alpha 语义,不支持 alpha blending
- 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。 - 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。
- 运行时绘制 sprite/font/tilemap 时只做裁剪、透明判断、颜色替换、tile id 查表或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。 - 运行时绘制 sprite/font/tilemap 时只做裁剪、透明判断、颜色替换、tile id 查表或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。
- Tilemap 绘制应按视口可见范围遍历 tile不能每帧无条件扫描整张地图视口边缘允许通过 sprite 像素裁剪显示半个 tile。 - Tilemap 绘制应按视口可见范围遍历 tile不能每帧无条件扫描整张地图视口边缘允许通过 sprite 像素裁剪显示半个 tile。

View File

@ -133,13 +133,21 @@ int main(int argc, char* argv[])
Platform::SteadyTimeSource time_source; 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,
RenderData::PixelFormat::RGBA8888);
font.char_w = font_char_w; font.char_w = font_char_w;
font.char_h = font_char_h; font.char_h = font_char_h;
font.columns = font_columns; font.columns = font_columns;
font.first_char = font_first_char; font.first_char = font_first_char;
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000); RenderData::Image sprite_img(
test_sprite_pixels,
test_sprite_width,
test_sprite_height,
RenderData::PixelFormat::RGBA5551);
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height); RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
const std::array<uint16_t, 8 * 4> tileIds = { const std::array<uint16_t, 8 * 4> tileIds = {

File diff suppressed because it is too large Load Diff

View File

@ -271,12 +271,25 @@ namespace
return atlas_pixels; return atlas_pixels;
} }
static void WritePixel(std::ofstream& file, uint32_t pixel) static uint16_t rgba8888_to_rgba5551(uint32_t c)
{
uint32_t r = (c >> 24) & 0xFFu;
uint32_t g = (c >> 16) & 0xFFu;
uint32_t b = (c >> 8) & 0xFFu;
uint32_t a = c & 0xFFu;
return static_cast<uint16_t>(
((r >> 3) << 11) |
((g >> 3) << 6) |
((b >> 3) << 1) |
(a ? 1u : 0u));
}
static void WritePixel5551(std::ofstream& file, uint16_t pixel)
{ {
file << "0x" file << "0x"
<< std::uppercase << std::uppercase
<< std::hex << std::hex
<< std::setw(8) << std::setw(4)
<< std::setfill('0') << std::setfill('0')
<< pixel << pixel
<< std::dec << std::dec
@ -305,7 +318,7 @@ namespace
file << "{\n"; file << "{\n";
file << "\tstatic const int32_t tom_atlas_width = " << AtlasWidth << ";\n"; file << "\tstatic const int32_t tom_atlas_width = " << AtlasWidth << ";\n";
file << "\tstatic const int32_t tom_atlas_height = " << atlas_height << ";\n\n"; file << "\tstatic const int32_t tom_atlas_height = " << atlas_height << ";\n\n";
file << "\tstatic const uint32_t tom_atlas_pixels[] = {\n"; file << "\tstatic const uint16_t tom_atlas_pixels[] = {\n";
for (size_t i = 0; i < atlas_pixels.size(); i += 12) for (size_t i = 0; i < atlas_pixels.size(); i += 12)
{ {
@ -313,7 +326,7 @@ namespace
const size_t end = std::min(i + 12, atlas_pixels.size()); const size_t end = std::min(i + 12, atlas_pixels.size());
for (size_t j = i; j < end; ++j) for (size_t j = i; j < end; ++j)
{ {
WritePixel(file, atlas_pixels[j]); WritePixel5551(file, rgba8888_to_rgba5551(atlas_pixels[j]));
file << ", "; file << ", ";
} }
file << "\n"; file << "\n";
@ -321,7 +334,8 @@ namespace
file << "\t};\n\n"; file << "\t};\n\n";
file << "\tstatic const RenderData::Image image(" file << "\tstatic const RenderData::Image image("
<< "tom_atlas_pixels, tom_atlas_width, tom_atlas_height, 0x00000000u);\n\n"; << "tom_atlas_pixels, tom_atlas_width, tom_atlas_height, 0x0000, "
<< "RenderData::PixelFormat::RGBA5551);\n\n";
for (size_t i = 0; i < regions.size(); ++i) for (size_t i = 0; i < regions.size(); ++i)
{ {

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

@ -8,7 +8,7 @@ namespace
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' }; static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
static const size_t SpriteHeaderSize = 20; static const size_t SpriteHeaderSize = 20;
static const uint32_t SpriteVersion = 1; static const uint32_t SpriteVersion = 1;
static const uint32_t SpriteFormatRgba8888 = 1; static const uint32_t SpriteFormatRgba5551 = 2;
static const uint64_t MaxSpritePixels = 8192ull * 8192ull; static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
static bool ReadExact(std::ifstream& file, char* data, size_t size) static bool ReadExact(std::ifstream& file, char* data, size_t size)
@ -52,7 +52,7 @@ namespace
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height); const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
if (total > MaxSpritePixels || if (total > MaxSpritePixels ||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t))) total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint16_t)))
{ {
return false; return false;
} }
@ -61,9 +61,9 @@ namespace
return true; return true;
} }
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels) static bool ReadPixels5551(std::ifstream& file, std::vector<uint16_t>& pixels)
{ {
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0); std::vector<uint8_t> bytes(pixels.size() * sizeof(uint16_t), 0);
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size())) if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
{ {
return false; return false;
@ -71,23 +71,28 @@ namespace
for (size_t i = 0; i < pixels.size(); ++i) for (size_t i = 0; i < pixels.size(); ++i)
{ {
uint32_t value = 0; pixels[i] = static_cast<uint16_t>(bytes[i * 2]) |
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value)) (static_cast<uint16_t>(bytes[i * 2 + 1]) << 8);
{
return false;
}
pixels[i] = value;
} }
return true; return true;
} }
static void WritePixel5551(std::ofstream& file, uint16_t pixel)
{
const char bytes[2] = {
static_cast<char>(pixel & 0xFF),
static_cast<char>((pixel >> 8) & 0xFF)
};
file.write(bytes, sizeof(bytes));
}
} }
namespace Asset namespace Asset
{ {
bool SpriteAssetLoader::Load( bool SpriteAssetLoader::Load(
const std::string& path, const std::string& path,
std::vector<uint32_t>& pixels, std::vector<uint16_t>& pixels,
RenderData::Image& image) RenderData::Image& image)
{ {
std::ifstream file(path.c_str(), std::ios::binary); std::ifstream file(path.c_str(), std::ios::binary);
@ -124,14 +129,14 @@ namespace Asset
size_t pixelCount = 0; size_t pixelCount = 0;
if (version != SpriteVersion || if (version != SpriteVersion ||
format != SpriteFormatRgba8888 || format != SpriteFormatRgba5551 ||
!CheckPixelCount(width, height, pixelCount)) !CheckPixelCount(width, height, pixelCount))
{ {
return false; return false;
} }
std::vector<uint32_t> loadedPixels(pixelCount, 0); std::vector<uint16_t> loadedPixels(pixelCount, 0);
if (!ReadPixels(file, loadedPixels)) if (!ReadPixels5551(file, loadedPixels))
{ {
return false; return false;
} }
@ -144,7 +149,11 @@ namespace Asset
} }
pixels.swap(loadedPixels); pixels.swap(loadedPixels);
image = RenderData::Image(pixels.data(), static_cast<int32_t>(width), static_cast<int32_t>(height)); image = RenderData::Image(
pixels.data(),
static_cast<int32_t>(width),
static_cast<int32_t>(height),
RenderData::PixelFormat::RGBA5551);
return image.pixels != nullptr && image.width > 0 && image.height > 0; return image.pixels != nullptr && image.width > 0 && image.height > 0;
} }
@ -171,10 +180,23 @@ namespace Asset
WriteU32LE(file, SpriteVersion); WriteU32LE(file, SpriteVersion);
WriteU32LE(file, static_cast<uint32_t>(image.width)); WriteU32LE(file, static_cast<uint32_t>(image.width));
WriteU32LE(file, static_cast<uint32_t>(image.height)); WriteU32LE(file, static_cast<uint32_t>(image.height));
WriteU32LE(file, SpriteFormatRgba8888); WriteU32LE(file, SpriteFormatRgba5551);
for (size_t i = 0; i < pixelCount; ++i)
if (image.format == RenderData::PixelFormat::RGBA5551)
{ {
WriteU32LE(file, image.pixels[i]); const uint16_t* pixels16 = static_cast<const uint16_t*>(image.pixels);
for (size_t i = 0; i < pixelCount; ++i)
{
WritePixel5551(file, pixels16[i]);
}
}
else
{
const uint32_t* pixels32 = static_cast<const uint32_t*>(image.pixels);
for (size_t i = 0; i < pixelCount; ++i)
{
WritePixel5551(file, RenderData::rgba8888_to_rgba5551(pixels32[i]));
}
} }
return file.good(); return file.good();

View File

@ -11,7 +11,7 @@ namespace Asset
public: public:
static const char* GetFileExtension() { return ".sprite"; } static const char* GetFileExtension() { return ".sprite"; }
static bool Load(const std::string& path, std::vector<uint32_t>& pixels, RenderData::Image& image); static bool Load(const std::string& path, std::vector<uint16_t>& pixels, RenderData::Image& image);
static bool Save(const std::string& path, const RenderData::Image& image); static bool Save(const std::string& path, const RenderData::Image& image);
}; };
} }

View File

@ -7,7 +7,6 @@
namespace Core namespace Core
{ {
#ifdef USE_RGB565_BACKBUFFER
typedef uint16_t FramePixel; typedef uint16_t FramePixel;
inline uint16_t rgba_to_frame_pixel(uint32_t rgba) inline uint16_t rgba_to_frame_pixel(uint32_t rgba)
{ {
@ -16,10 +15,6 @@ namespace Core
const uint32_t b = (rgba >> 8) & 0xFFu; const uint32_t b = (rgba >> 8) & 0xFFu;
return static_cast<uint16_t>(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3)); return static_cast<uint16_t>(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3));
} }
#else
typedef uint32_t FramePixel;
inline uint32_t rgba_to_frame_pixel(uint32_t rgba) { return rgba; }
#endif
class FrameBuffer class FrameBuffer
{ {

View File

@ -124,25 +124,67 @@ namespace Core
if (dst_x + end_dx > screen_w) end_dx = screen_w - dst_x; if (dst_x + end_dx > screen_w) end_dx = screen_w - dst_x;
if (dst_y + end_dy > screen_h) end_dy = screen_h - dst_y; if (dst_y + end_dy > screen_h) end_dy = screen_h - dst_y;
const uint32_t* src = img.pixels; if (img.format == RenderData::PixelFormat::RGBA5551)
{
Core::FramePixel* dst = static_cast<Core::FramePixel*>(frameBuffer->get_buffer());
const int32_t fb_w = frameBuffer->get_width();
const uint16_t* src = static_cast<const uint16_t*>(img.pixels);
if (scale == 1)
{
for (int32_t sy = start_dy; sy < end_dy; ++sy)
{
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const int32_t dst_y_abs = dst_y + sy;
Core::FramePixel* dst_row = dst + dst_y_abs * fb_w;
for (int32_t sx = start_dx; sx < end_dx; ++sx)
{
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint16_t pixel = src[read_y * img_w + read_x];
if (!RenderData::rgba5551_is_opaque(pixel)) continue;
dst_row[dst_x + sx] = RenderData::rgba5551_to_rgb565(pixel);
}
}
return;
}
for (int32_t dy_abs = start_dy; dy_abs < end_dy; ++dy_abs)
{
const int32_t sy = dy_abs / scale;
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const int32_t dst_y_abs = dst_y + dy_abs;
Core::FramePixel* dst_row = dst + dst_y_abs * fb_w;
for (int32_t dx_abs = start_dx; dx_abs < end_dx; ++dx_abs)
{
const int32_t sx = dx_abs / scale;
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint16_t pixel = src[read_y * img_w + read_x];
if (!RenderData::rgba5551_is_opaque(pixel)) continue;
dst_row[dst_x + dx_abs] = RenderData::rgba5551_to_rgb565(pixel);
}
}
return;
}
const bool has_key = img.has_color_key; const bool has_key = img.has_color_key;
const uint32_t key = img.color_key; const uint32_t key = RenderData::rgba5551_to_rgba8888(img.color_key);
const uint32_t* src32 = static_cast<const uint32_t*>(img.pixels);
if (scale == 1) if (scale == 1)
{ {
for (int32_t sy = start_dy; sy < end_dy; ++sy) for (int32_t sy = start_dy; sy < end_dy; ++sy)
{ {
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy); const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + sy; const int32_t dst_y_abs = dst_y + sy;
for (int32_t sx = start_dx; sx < end_dx; ++sx) for (int32_t sx = start_dx; sx < end_dx; ++sx)
{ {
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx); const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x]; const uint32_t pixel = src32[read_y * img_w + read_x];
if ((pixel & 0xFFu) == 0u) continue;
if (has_key && pixel == key) continue; if (has_key && pixel == key) continue;
frameBuffer->set_pixel_unsafe(dst_x + sx, dst_y_abs, pixel); frameBuffer->set_pixel_unsafe(dst_x + sx, dst_y_abs, pixel);
} }
} }
@ -153,17 +195,15 @@ namespace Core
{ {
const int32_t sy = dy_abs / scale; const int32_t sy = dy_abs / scale;
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy); const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
const uint32_t* row = src + read_y * img_w;
const int32_t dst_y_abs = dst_y + dy_abs; const int32_t dst_y_abs = dst_y + dy_abs;
for (int32_t dx_abs = start_dx; dx_abs < end_dx; ++dx_abs) for (int32_t dx_abs = start_dx; dx_abs < end_dx; ++dx_abs)
{ {
const int32_t sx = dx_abs / scale; const int32_t sx = dx_abs / scale;
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx); const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
const uint32_t pixel = row[read_x]; const uint32_t pixel = src32[read_y * img_w + read_x];
if ((pixel & 0xFFu) == 0u) continue;
if (has_key && pixel == key) continue; if (has_key && pixel == key) continue;
frameBuffer->set_pixel_unsafe(dst_x + dx_abs, dst_y_abs, pixel); frameBuffer->set_pixel_unsafe(dst_x + dx_abs, dst_y_abs, pixel);
} }
} }
@ -273,13 +313,14 @@ namespace Core
if (x0 >= x1 || y0 >= y1) return; if (x0 >= x1 || y0 >= y1) return;
uint32_t* buf = static_cast<uint32_t*>(frameBuffer->get_buffer()); const Core::FramePixel pixel = Core::rgba_to_frame_pixel(rgba);
Core::FramePixel* buf = static_cast<Core::FramePixel*>(frameBuffer->get_buffer());
for (int32_t row = y0; row < y1; ++row) for (int32_t row = y0; row < y1; ++row)
{ {
uint32_t* dst = buf + row * fb_w + x0; Core::FramePixel* dst = buf + row * fb_w + x0;
for (int32_t i = 0; i < x1 - x0; ++i) for (int32_t i = 0; i < x1 - x0; ++i)
{ {
dst[i] = rgba; dst[i] = pixel;
} }
} }
} }
@ -293,7 +334,7 @@ namespace Core
const int32_t cw = font.char_w; const int32_t cw = font.char_w;
const int32_t ch = font.char_h; const int32_t ch = font.char_h;
const int32_t atlas_w = font.atlas.width; const int32_t atlas_w = font.atlas.width;
const uint32_t* src = font.atlas.pixels; const uint32_t* src = static_cast<const uint32_t*>(font.atlas.pixels);
const int32_t screen_w = frameBuffer->get_width(); const int32_t screen_w = frameBuffer->get_width();
const int32_t screen_h = frameBuffer->get_height(); const int32_t screen_h = frameBuffer->get_height();

View File

@ -15,18 +15,18 @@ namespace Platform
{ {
namespace 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) inline uint32_t rgba_to_argb8888(uint32_t rgba)
{ {
return ((rgba >> 8) & 0x00FFFFFFu) | ((rgba & 0xFFu) << 24); return ((rgba >> 8) & 0x00FFFFFFu) | ((rgba & 0xFFu) << 24);
} }
inline uint32_t rgb565_to_rgba8888(uint16_t c)
{
uint32_t r = (c >> 11) & 0x1Fu;
uint32_t g = (c >> 5) & 0x3Fu;
uint32_t b = c & 0x1Fu;
return (r << 27) | (r << 24) | (g << 18) | (g << 16) | (b << 11) | (b << 8) | 0xFFu;
}
} }
bool FBDisplay::init(int w, int h) bool FBDisplay::init(int w, int h)
@ -96,7 +96,7 @@ namespace Platform
if (!fb_mem || !framebuffer) if (!fb_mem || !framebuffer)
return; return;
const uint32_t* src = static_cast<const uint32_t*>(framebuffer->get_buffer()); const uint16_t* src = static_cast<const uint16_t*>(framebuffer->get_buffer());
const int src_width = framebuffer->get_width(); const int src_width = framebuffer->get_width();
const int dst_width = std::min(src_width, static_cast<int>(vinfo.xres)); 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 dst_height = std::min(framebuffer->get_height(), static_cast<int>(vinfo.yres));
@ -119,8 +119,6 @@ namespace Platform
if (is_rgb565) if (is_rgb565)
{ {
#ifdef USE_RGB565_BACKBUFFER
// backbuffer 内部已是 RGB565直接行拷贝提交
const uint8_t* src_bytes = static_cast<const uint8_t*>(framebuffer->get_buffer()); const uint8_t* src_bytes = static_cast<const uint8_t*>(framebuffer->get_buffer());
if (static_cast<int>(finfo.line_length) == dst_width * 2) if (static_cast<int>(finfo.line_length) == dst_width * 2)
{ {
@ -135,17 +133,6 @@ namespace Platform
dst_width * 2); dst_width * 2);
} }
} }
#else
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]);
}
}
#endif
return; return;
} }
@ -153,19 +140,27 @@ namespace Platform
{ {
for (int y = 0; y < dst_height; ++y) for (int y = 0; y < dst_height; ++y)
{ {
const uint32_t* src_row = src + y * src_width; const uint16_t* src_row = src + y * src_width;
uint32_t* dst_row = reinterpret_cast<uint32_t*>(fb_mem + y * finfo.line_length); uint32_t* dst_row = reinterpret_cast<uint32_t*>(fb_mem + y * finfo.line_length);
for (int x = 0; x < dst_width; ++x) for (int x = 0; x < dst_width; ++x)
{ {
dst_row[x] = rgba_to_argb8888(src_row[x]); dst_row[x] = rgba_to_argb8888(rgb565_to_rgba8888(src_row[x]));
} }
} }
return; return;
} }
if (is_rgba8888 && dst_width == src_width && static_cast<int>(finfo.line_length) == dst_width * 4) if (is_rgba8888)
{ {
std::memcpy(fb_mem, src, static_cast<size_t>(dst_height) * static_cast<size_t>(dst_width) * sizeof(uint32_t)); for (int y = 0; y < dst_height; ++y)
{
const uint16_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] = rgb565_to_rgba8888(src_row[x]);
}
}
return; return;
} }
@ -173,7 +168,7 @@ namespace Platform
{ {
for (int x = 0; x < dst_width; ++x) for (int x = 0; x < dst_width; ++x)
{ {
uint32_t pixel = convert_pixel(src[y * src_width + x]); uint32_t pixel = convert_pixel(rgb565_to_rgba8888(src[y * src_width + x]));
uint8_t* dst = fb_mem + y * finfo.line_length + x * bytes_per_pixel; uint8_t* dst = fb_mem + y * finfo.line_length + x * bytes_per_pixel;
if (vinfo.bits_per_pixel == 32) if (vinfo.bits_per_pixel == 32)
{ {

View File

@ -42,13 +42,8 @@ namespace Platform
return false; return false;
} }
#ifdef USE_RGB565_BACKBUFFER
const Uint32 sdl_pixel_format = SDL_PIXELFORMAT_RGB565; const Uint32 sdl_pixel_format = SDL_PIXELFORMAT_RGB565;
const int pixel_bytes = 2; const int pixel_bytes = 2;
#else
const Uint32 sdl_pixel_format = SDL_PIXELFORMAT_RGBA8888;
const int pixel_bytes = 4;
#endif
texture = SDL_CreateTexture( texture = SDL_CreateTexture(
renderer, renderer,
sdl_pixel_format, sdl_pixel_format,
@ -67,11 +62,7 @@ namespace Platform
void SDLDisplay::present(const Core::FrameBuffer* framebuffer) void SDLDisplay::present(const Core::FrameBuffer* framebuffer)
{ {
#ifdef USE_RGB565_BACKBUFFER
const int pixel_bytes = 2; const int pixel_bytes = 2;
#else
const int pixel_bytes = 4;
#endif
SDL_UpdateTexture(texture, nullptr, framebuffer->get_buffer(), width * pixel_bytes); SDL_UpdateTexture(texture, nullptr, framebuffer->get_buffer(), width * pixel_bytes);
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderCopy(renderer, texture, nullptr, nullptr);

View File

@ -4,25 +4,86 @@
namespace RenderData namespace RenderData
{ {
enum class PixelFormat
{
RGBA8888,
RGBA5551
};
inline uint16_t rgba5551_to_rgb565(uint16_t c)
{
const uint16_t r = static_cast<uint16_t>((c >> 11) & 0x1Fu);
const uint16_t g = static_cast<uint16_t>((c >> 6) & 0x1Fu);
const uint16_t b = static_cast<uint16_t>((c >> 1) & 0x1Fu);
const uint16_t g6 = static_cast<uint16_t>((g << 1) | (g >> 4));
return static_cast<uint16_t>((r << 11) | (g6 << 5) | b);
}
inline bool rgba5551_is_opaque(uint16_t c)
{
return (c & 0x1u) != 0;
}
inline uint32_t rgba5551_to_rgba8888(uint16_t c)
{
uint32_t r = (c >> 11) & 0x1Fu;
uint32_t g = (c >> 6) & 0x1Fu;
uint32_t b = (c >> 1) & 0x1Fu;
uint32_t a = c & 0x1u;
return (r << 27) | (r << 24) |
(g << 19) | (g << 16) |
(b << 11) | (b << 8) |
(a ? 0xFFu : 0x00u);
}
inline uint16_t rgba8888_to_rgba5551(uint32_t c)
{
uint32_t r = (c >> 24) & 0xFFu;
uint32_t g = (c >> 16) & 0xFFu;
uint32_t b = (c >> 8) & 0xFFu;
uint32_t a = c & 0xFFu;
return static_cast<uint16_t>(
((r >> 3) << 11) |
((g >> 3) << 6) |
((b >> 3) << 1) |
(a ? 1u : 0u));
}
struct Image struct Image
{ {
const uint32_t* pixels; const void* pixels;
int32_t width; int32_t width;
int32_t height; int32_t height;
uint32_t color_key; uint16_t color_key;
bool has_color_key; bool has_color_key;
PixelFormat format;
Image() : pixels(nullptr), width(0), height(0), color_key(0), has_color_key(false) {} Image()
: pixels(nullptr), width(0), height(0),
color_key(0), has_color_key(false),
format(PixelFormat::RGBA8888) {}
Image(const uint32_t* pixels, int32_t width, int32_t height) Image(const void* pixels, int32_t width, int32_t height,
: pixels(pixels), width(width), height(height), color_key(0), has_color_key(false) {} PixelFormat fmt = PixelFormat::RGBA8888)
: pixels(pixels), width(width), height(height),
color_key(0), has_color_key(false),
format(fmt) {}
Image(const uint32_t* pixels, int32_t width, int32_t height, uint32_t color_key) Image(const void* pixels, int32_t width, int32_t height,
: pixels(pixels), width(width), height(height), color_key(color_key), has_color_key(true) {} uint16_t color_key,
PixelFormat fmt = PixelFormat::RGBA8888)
: pixels(pixels), width(width), height(height),
color_key(color_key), has_color_key(true),
format(fmt) {}
uint32_t get_pixel(int32_t x, int32_t y) const uint32_t get_pixel(int32_t x, int32_t y) const
{ {
return pixels[y * width + x]; if (format == PixelFormat::RGBA5551)
{
return rgba5551_to_rgba8888(
static_cast<const uint16_t*>(pixels)[y * width + x]);
}
return static_cast<const uint32_t*>(pixels)[y * width + x];
} }
}; };
} }

View File

@ -0,0 +1,118 @@
#include "Display.h"
#include "DrawContext.h"
#include "FrameBuffer.h"
#include "Image.h"
#include "SpriteAssetLoader.h"
#include <cassert>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>
namespace
{
class CaptureDisplay : public Platform::IDisplay
{
public:
const Core::FrameBuffer* framebuffer;
CaptureDisplay()
: framebuffer(nullptr)
{
}
bool init(int, int) override { return true; }
void present(const Core::FrameBuffer* fb) override { framebuffer = fb; }
void poll_events(bool&) override {}
void shutdown() override {}
};
void TestFramePixelIsRgb565()
{
static_assert(sizeof(Core::FramePixel) == sizeof(uint16_t), "FramePixel must stay RGB565");
assert(RenderData::rgba5551_to_rgb565(0xF801u) == 0xF800u);
assert(RenderData::rgba5551_to_rgb565(0x07C1u) == 0x07E0u);
assert(RenderData::rgba5551_to_rgb565(0x003Fu) == 0x001Fu);
}
void TestFrameBufferStoresRgb565()
{
Core::FrameBuffer framebuffer(2, 1);
framebuffer.clear(RenderData::Color(255, 0, 0, 255));
const Core::FramePixel* pixels = static_cast<const Core::FramePixel*>(framebuffer.get_buffer());
assert(pixels[0] == 0xF800u);
assert(pixels[1] == 0xF800u);
}
void TestDrawSpriteUsesOneBitAlpha()
{
const uint16_t sprite_pixels[] = {
0xF800u,
0x07C1u
};
Core::DrawContext ctx(2, 1);
ctx.clear_color(RenderData::Color(0, 0, 255, 255));
RenderData::Image sprite(
sprite_pixels,
2,
1,
RenderData::PixelFormat::RGBA5551);
ctx.draw_sprite(0, 0, sprite);
CaptureDisplay display;
ctx.present(&display);
assert(display.framebuffer != nullptr);
const Core::FramePixel* pixels =
static_cast<const Core::FramePixel*>(display.framebuffer->get_buffer());
assert(pixels[0] == 0x001Fu);
assert(pixels[1] == 0x07E0u);
}
void TestSpriteAssetLoaderRoundTripsRgba5551()
{
const std::string path = "render_pipeline_test.sprite";
const uint16_t original_pixels[] = {
0x0000u,
0xF801u,
0x07C1u,
0x003Fu
};
RenderData::Image original(
original_pixels,
2,
2,
RenderData::PixelFormat::RGBA5551);
assert(Asset::SpriteAssetLoader::Save(path, original));
std::vector<uint16_t> loaded_pixels;
RenderData::Image loaded;
const bool loaded_ok = Asset::SpriteAssetLoader::Load(path, loaded_pixels, loaded);
std::remove(path.c_str());
assert(loaded_ok);
assert(loaded.format == RenderData::PixelFormat::RGBA5551);
assert(loaded.width == 2);
assert(loaded.height == 2);
assert(loaded_pixels.size() == 4u);
for (size_t i = 0; i < loaded_pixels.size(); ++i)
{
assert(loaded_pixels[i] == original_pixels[i]);
}
}
}
int main()
{
TestFramePixelIsRgb565();
TestFrameBufferStoresRgb565();
TestDrawSpriteUsesOneBitAlpha();
TestSpriteAssetLoaderRoundTripsRgba5551();
std::cout << "render_pipeline_tests: PASS\n";
return 0;
}

View File

@ -1,13 +1,11 @@
""" """
PNG 图片转换为 C 头文件uint32_t 像素数组 PNG 图片转换为 C 头文件RGBA5551 / uint16_t 像素数组
用法: 用法:
python tools/png_to_header.py input.png output.h [var_name] python tools/png_to_header.py input.png output.h [var_name]
像素格式: (R << 24) | (G << 16) | (B << 8) | A 像素格式: RGBA55515-bit R, 5-bit G, 5-bit B, 1-bit A
FrameBuffer uint32_t 格式一致 运行时 sprite 主路径只保留 1-bit 透明A=0 跳过A=1 覆写
如果图片没有 alpha 通道A 默认为 0xFF
""" """
import sys import sys
@ -23,18 +21,23 @@ def convert(input_path: str, output_path: str, var_name: str):
packed = [] packed = []
for r, g, b, a in pixels: for r, g, b, a in pixels:
packed.append((r << 24) | (g << 16) | (b << 8) | a) packed.append(
((r >> 3) << 11)
| ((g >> 3) << 6)
| ((b >> 3) << 1)
| (1 if a else 0)
)
with open(output_path, "w", encoding="utf-8") as f: with open(output_path, "w", encoding="utf-8") as f:
f.write("// Auto-generated by tools/png_to_header.py\n") f.write("// Auto-generated by tools/png_to_header.py\n")
f.write(f"#pragma once\n#include <cstdint>\n\n") f.write(f"#pragma once\n#include <cstdint>\n\n")
f.write(f"static const int32_t {var_name}_width = {w};\n") f.write(f"static const int32_t {var_name}_width = {w};\n")
f.write(f"static const int32_t {var_name}_height = {h};\n\n") f.write(f"static const int32_t {var_name}_height = {h};\n\n")
f.write(f"static const uint32_t {var_name}_pixels[] = {{\n") f.write(f"static const uint16_t {var_name}_pixels[] = {{\n")
for i in range(0, len(packed), 16): for i in range(0, len(packed), 16):
chunk = packed[i : i + 16] chunk = packed[i : i + 16]
line = ", ".join(f"0x{p:08X}" for p in chunk) line = ", ".join(f"0x{p:04X}" for p in chunk)
f.write(f" {line},\n") f.write(f" {line},\n")
f.write("};\n") f.write("};\n")