Unified logging: Replace LogSystem with EditorConsoleSink

- Created EditorConsoleSink (implements ILogSink interface)
- EditorConsoleSink stores logs in memory buffer (max 1000 entries)
- Added to Debug::Logger in Application::Initialize()
- ConsolePanel now reads from EditorConsoleSink via static GetInstance()
- Removed separate LogSystem singleton
- Removed editor/src/Core/LogEntry.h (no longer needed)

Now Editor and Engine share the same Debug::Logger, with ConsolePanel
displaying logs via EditorConsoleSink.
This commit is contained in:
2026-03-25 16:13:02 +08:00
parent b08f682e5c
commit 16e2065c6c
28 changed files with 355 additions and 121 deletions

View File

@@ -1,403 +0,0 @@
# D3D12 Texture 封装架构修复方案
## 1. 问题分析
### 1.1 当前问题
**问题 A双重 Texture 包装导致悬垂指针**
```cpp
// D3D12SwapChain 内部:
std::vector<D3D12Texture> m_backBuffers; // SwapChain 内部的包装
// minimal/main.cpp
D3D12Texture gColorRTs[2]; // 用户代码又创建了一套包装!
for (int i = 0; i < 2; i++) {
ID3D12Resource* buffer = nullptr;
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer)); // 获取原生指针
gColorRTs[i].InitializeFromExisting(buffer); // 再包装一次
}
```
**问题**
- `m_backBuffers[i]``gColorRTs[i]` 是两个不同的 `D3D12Texture` 对象
- 但它们内部都持有指向**同一个** `ID3D12Resource``ComPtr`
- 当其中一个调用 `Shutdown()``m_resource.Reset()`,另一个就变成悬垂指针!
**问题 B`InitializeFromExisting` 不区分所有权语义**
```cpp
void D3D12Texture::Shutdown() {
m_resource.Reset(); // 无条件释放资源
}
```
如果通过 `InitializeFromExisting` 包装了 SwapChain 的 back bufferShutdown 会错误地释放它!
**问题 C`D3D12SwapChain::GetBackBuffer()` 返回内部引用,用户可能误用**
```cpp
D3D12Texture* D3D12SwapChain::GetBackBuffer(uint32_t index) const {
return const_cast<D3DTexture*>(&m_backBuffers[index]); // 返回内部成员引用
}
// 用户可能写出:
D3D12Texture* rt = gSwapChain.GetBackBuffer(0);
gSwapChain.Shutdown(); // rt 变成悬垂指针!
```
### 1.2 问题根因
| 问题 | 根因 |
|------|------|
| 双重包装 | 没有阻止用户创建额外的包装 |
| 所有权模糊 | `InitializeFromExisting` 不区分"接管"和"借用" |
| 悬垂引用 | `GetBackBuffer` 返回内部指针,生命周期不安全 |
---
## 2. 修复方案
### 2.1 方案概述
引入**所有权语义标记**和**生命周期安全保证**
1. **`InitializeFromExisting` 增加 `ownsResource` 参数**
2. **`D3D12Texture` 内部根据标记决定是否释放资源**
3. **`D3D12SwapChain::GetBackBuffer` 返回安全引用(弱引用或克隆)**
4. **提供统一的 Back Buffer 访问接口,避免用户直接调用 `GetSwapChain()->GetBuffer()`**
### 2.2 详细设计
#### 2.2.1 修改 `D3D12Texture`
**文件**: `engine/include/XCEngine/RHI/D3D12/D3D12Texture.h`
```cpp
class D3D12Texture : public RHITexture {
public:
D3D12Texture();
~D3D12Texture() override;
// 创建并拥有资源
bool Initialize(ID3D12Device* device, const D3D12_RESOURCE_DESC& desc,
D3D12_RESOURCE_STATES initialState = D3D12_RESOURCE_STATE_COMMON);
// 包装已有资源,明确所有权语义
// ownsResource = true : 获取所有权Shutdown 时释放
// ownsResource = false : 不获取所有权Shutdown 时不释放
bool InitializeFromExisting(ID3D12Resource* resource, bool ownsResource = false);
bool InitializeFromData(ID3D12Device* device, ID3D12GraphicsCommandList* commandList,
const void* pixelData, uint32_t width, uint32_t height, DXGI_FORMAT format);
bool InitializeDepthStencil(ID3D12Device* device, uint32_t width, uint32_t height,
DXGI_FORMAT format = DXGI_FORMAT_D24_UNORM_S8_UINT);
void Shutdown() override;
ID3D12Resource* GetResource() const { return m_resource.Get(); }
D3D12_RESOURCE_DESC GetDesc() const { return m_resource->GetDesc(); }
// 检查是否拥有资源所有权
bool OwnsResource() const { return m_ownsResource; }
// ... 其他现有方法 ...
private:
ComPtr<ID3D12Resource> m_resource;
ResourceStates m_state = ResourceStates::Common;
std::string m_name;
bool m_ownsResource = false; // 新增:所有权标记
};
```
**文件**: `engine/src/RHI/D3D12/D3D12Texture.cpp`
```cpp
bool D3D12Texture::InitializeFromExisting(ID3D12Resource* resource, bool ownsResource) {
m_resource = resource;
m_ownsResource = ownsResource; // 明确设置所有权
return true;
}
void D3D12Texture::Shutdown() {
if (m_ownsResource) {
// 仅当拥有所有权时才释放
m_resource.Reset();
}
// 如果不拥有资源,只是清除引用,不释放底层的 COM 对象
m_resource.Reset(); // ComPtr::Reset() 只是减少引用计数,不是释放
// 但要注意:如果不拥有所有权,我们需要保留原始指针以防止意外释放
// 实际上 ComPtr::Reset() 总是会调用 Release()
// 所以我们需要用不同的策略
}
```
**等等,`ComPtr::Reset()` 总是会调用 Release()。如果我们要实现"不拥有但不释放",需要用原始指针存储。**
**修正方案**
```cpp
class D3D12Texture : public RHITexture {
private:
ComPtr<ID3D12Resource> m_resource; // 始终持有
ID3D12Resource* m_externalResource = nullptr; // 外部资源指针(不拥有)
bool m_ownsResource = false;
public:
void Shutdown() override {
if (m_ownsResource) {
m_resource.Reset(); // 释放拥有的资源
}
// 如果是外部资源(不拥有),只是清除引用
m_externalResource = nullptr;
m_resource.Reset(); // 总是 Reset因为 m_resource 可能持有原始指针的副本
}
};
```
**更简洁的方案**:让用户自己决定是否通过 `Initialize` 创建 texture。`InitializeFromExisting` 包装但不拥有,使用者负责保证生命周期。
---
#### 2.2.2 修改 `D3D12SwapChain`
**目标**:提供安全的 BackBuffer 访问接口,阻止用户创建重复包装。
**文件**: `engine/include/XCEngine/RHI/D3D12/D3D12SwapChain.h`
```cpp
class D3D12SwapChain : public RHISwapChain {
public:
// ... 现有接口 ...
// 获取 BackBuffer - 返回引用而非指针,防止悬垂
// 返回的引用在 SwapChain 存活期间有效
D3D12Texture& GetBackBuffer(uint32_t index);
const D3D12Texture& GetBackBuffer(uint32_t index) const;
// 获取当前 BackBuffer
D3D12Texture& GetCurrentBackBuffer();
const D3D12Texture& GetCurrentBackBuffer() const;
// 删除不安全的 GetSwapChain() 暴露方法!
// 旧接口IDXGISwapChain3* GetSwapChain() const { return m_swapChain.Get(); }
// 新策略:如果确实需要原生指针,提供 GetNativeHandle() 但不返回具体类型
private:
// 确保 BackBuffer 不能被外部直接修改
void SetBackBuffer(uint32_t index, D3D12Texture& texture) = delete;
};
```
**修正 `GetBackBuffer` 返回值**
```cpp
D3D12Texture& D3D12SwapChain::GetBackBuffer(uint32_t index) {
assert(index < m_backBuffers.size() && "BackBuffer index out of range");
return m_backBuffers[index];
}
const D3D12Texture& D3D12SwapChain::GetBackBuffer(uint32_t index) const {
assert(index < m_backBuffers.size() && "BackBuffer index out of range");
return m_backBuffers[index];
}
```
---
#### 2.2.3 更新 `minimal/main.cpp`
```cpp
// 修改前(有问题):
for (int i = 0; i < 2; i++) {
ID3D12Resource* buffer = nullptr;
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer)); // ❌ 原生 API
gColorRTs[i].InitializeFromExisting(buffer);
// ...
}
// 修改后(使用封装):
for (int i = 0; i < 2; i++) {
// 直接使用封装好的 BackBuffer不再重复包装
D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i);
// RTV 创建
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(i);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
gRTVs[i].InitializeAt(device, backBuffer.GetResource(), rtvHandle, nullptr);
}
```
**但是**`gColorRTs[i]` 仍然存在且被用于后续渲染。我们需要决定是:
- 方案 A直接使用 `gSwapChain.GetBackBuffer(i)` 替代 `gColorRTs[i]`
- 方案 B`gColorRTs[i]` 变成对 `gSwapChain.GetBackBuffer(i)` 的引用
**推荐方案 A**:移除 `gColorRTs` 数组,直接使用 `gSwapChain.GetBackBuffer()`。这样避免重复引用。
---
### 2.3 完整修改清单
| 文件 | 修改内容 |
|------|----------|
| `D3D12Texture.h` | 添加 `m_ownsResource` 成员,修改 `InitializeFromExisting` 签名 |
| `D3D12Texture.cpp` | 实现所有权语义,`Shutdown` 根据所有权决定是否释放 |
| `D3D12SwapChain.h` | 修改 `GetBackBuffer` 返回引用,删除 `GetSwapChain()` 暴露 |
| `D3D12SwapChain.cpp` | 实现返回引用的 `GetBackBuffer`,添加断言检查 |
| `minimal/main.cpp` | 使用封装后的 `GetBackBuffer()`,移除原生 API 调用 |
| `TEST_SPEC.md` | 更新架构说明,移除已知限制 7.2 |
---
## 3. 实施步骤
### Phase 1: 基础修改
1. 修改 `D3D12Texture` 添加所有权语义
2. 修改 `D3D12SwapChain``GetBackBuffer` 返回引用
3. 运行单元测试确保基础功能正常
### Phase 2: 更新集成测试
1. 修改 `minimal/main.cpp` 使用新的 `GetBackBuffer()` API
2. 移除 `gColorRTs` 数组(如果可能)
3. 验证截图功能正常
### Phase 3: 文档和清理
1. 更新 `TEST_SPEC.md`
2. 删除 `OpenGL_Test_Restructuring_Plan.md` 中对 D3D12 的过时引用
3. 提交所有更改
---
## 4. 风险和注意事项
### 4.1 兼容性风险
- `InitializeFromExisting` 签名变更会影响所有调用方
- 需要检查所有使用此方法的代码
### 4.2 生命周期风险
- `GetBackBuffer()` 返回的引用在 `Shutdown()` 后无效
- 用户必须确保在 SwapChain 存活期间使用
### 4.3 ComPtr 行为澄清
```cpp
// ComPtr::Reset() 调用 Release()
// 如果多个 ComPtr 指向同一资源Reset 只会减少引用计数
// 只有最后一个 Reset 才会真正释放
ComPtr<ID3D12Resource> a = resource; // AddRef
ComPtr<ID3D12Resource> b = resource; // AddRef
a.Reset(); // Release资源仍未释放b 还持有)
b.Reset(); // 最后一个 Release资源被释放
```
**关键点**:如果 `D3D12Texture``m_backBuffers` 都持有同一个 `ID3D12Resource``ComPtr`,那么:
- `gColorRTs[i].InitializeFromExisting(buffer)` 会让 `gColorRTs[i].m_resource` 指向 `buffer`
- `m_backBuffers[i].InitializeFromExisting(buffer)` 已经让 `m_backBuffers[i].m_resource` 指向 `buffer`
- 现在有两个 `ComPtr` 指向同一个资源
**问题**:这两个 `ComPtr` 是在不同对象中的,它们各自会增加引用计数。但原始的 `GetBuffer()` 返回的 `buffer` 指针被两个 `ComPtr` 接管了。
让我重新审视 `D3D12SwapChain::Initialize`
```cpp
m_backBuffers.resize(m_bufferCount);
for (uint32_t i = 0; i < m_bufferCount; ++i) {
ID3D12Resource* resource = nullptr;
m_swapChain->GetBuffer(i, IID_PPV_ARGS(&resource)); // resource 的引用计数 = 1
m_backBuffers[i].InitializeFromExisting(resource); // ComPtr 接管,引用计数 = 2
}
```
**问题**`resource` 被传给了 `InitializeFromExisting`ComPtr 构造时会 `AddRef`。但 `resource` 是局部变量,函数结束后 `resource` 局部变量销毁但不影响 COM 对象的引用计数。
等等,这里有个问题:
```cpp
ID3D12Resource* resource = nullptr;
m_swapChain->GetBuffer(i, IID_PPV_ARGS(&resource)); // GetBuffer 返回的指针赋给 resource引用计数 = 1
m_backBuffers[i].InitializeFromExisting(resource); // ComPtr 拷贝构造,引用计数 = 2
// 函数结束resource 局部变量销毁(不影响引用计数,因为是指针变量)
```
所以 `m_backBuffers[i].m_resource``m_swapChain` 内部都持有同一个 COM 对象的引用。
然后 `minimal/main.cpp` 又做了一次:
```cpp
ID3D12Resource* buffer = nullptr;
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer)); // buffer 引用计数 = 3
gColorRTs[i].InitializeFromExisting(buffer); // gColorRTs[i].m_resource 引用计数 = 4
// buffer 局部变量销毁
```
现在有三个 `ComPtr``m_backBuffers[i].m_resource`, `gColorRTs[i].m_resource`, `m_swapChain` 内部)指向同一个对象。引用计数 = 4。
**这不是问题**!因为 `ComPtr` 的拷贝构造会 `AddRef`。最终:
- `m_backBuffers[i]` 销毁 → 引用计数--
- `gColorRTs[i]` 销毁 → 引用计数--
- `m_swapChain` 销毁 → 引用计数--
引用计数最终归零,资源被正确释放。
**那问题是什么?**
回到 `Shutdown`
```cpp
void D3D12Texture::Shutdown() {
m_resource.Reset(); // ComPtr::Reset() 调用 Release()
}
```
如果 `gColorRTs[i]` 先于 `m_swapChain` 销毁:
- `gColorRTs[i].Shutdown()``m_resource.Reset()` → Release() → 引用计数从 4 变成 3
- `m_swapChain` 销毁时 → 内部资源 Release() → 引用计数从 3 变成 2
**这也应该是正常的...**
但等等!`m_backBuffers[i]` 是在 `D3D12SwapChain` 内部的 vector 中:
```cpp
std::vector<D3D12Texture> m_backBuffers;
```
如果 `minimal/main.cpp``gSwapChain.Shutdown()` 被调用:
- `D3D12SwapChain::~D3D12SwapChain()` 调用 `Shutdown()`
- `Shutdown()` 调用 `m_swapChain.Reset()`(仅重置 swapchain 指针)
- 然后 vector `m_backBuffers` 销毁,每个 `D3D12Texture` 析构调用 `Shutdown()`
- 每个 `Shutdown()` 调用 `m_resource.Reset()` → 释放 back buffer
**但是**`gColorRTs[i]` 是在全局变量中独立创建的:
```cpp
D3D12Texture gColorRTs[2]; // 独立于 SwapChain
```
如果 `gSwapChain.Shutdown()` 先执行,`m_backBuffers``m_resource` 被释放,那么 `gColorRTs[i].m_resource` 就变成悬垂的 `ComPtr`
```cpp
// gSwapChain.Shutdown() 执行后:
m_backBuffers[0].m_resource.Reset(); // 资源被释放!
// 但 gColorRTs[0].m_resource 仍然持有同一个(已释放的)指针!
// 调用 gColorRTs[0].GetResource() 会返回已释放的 COM 对象!
```
**这就是 bug** 当 SwapChain 先 shutdown用户代码中的 `gColorRTs` 就变成悬垂指针。
---
## 5. 最终结论
### 根因
`minimal/main.cpp` 创建了与 `D3D12SwapChain` 内部 `m_backBuffers` **重复包装**的 `gColorRTs` 数组。当任何一个先销毁,另一个就变成悬垂指针。
### 修复方案
1. **最小改动**:直接使用 `gSwapChain.GetBackBuffer(i)` 而非创建新的 `gColorRTs`
2. **长期方案**:增强 `D3D12Texture` 的所有权语义,区分拥有和非拥有资源
### 实施
按照第 2.3 节的修改清单执行。

View File

@@ -1,731 +0,0 @@
# OpenGL 测试架构重构方案
本文档是 XCEngine 测试规范的 OpenGL 专项补充,旨在将 OpenGL 后端测试体系重构为与 D3D12 同样规范的标准架构。
**前置阅读**:
- [tests/TEST_SPEC.md](../tests/TEST_SPEC.md) - 通用测试规范
- [tests/RHI/D3D12/TEST_SPEC.md](./D3D12/TEST_SPEC.md) - D3D12 专项规范(参考模板)
**规范版本**: 1.0
**最后更新**: 2026-03-20
---
## 1. 现状分析
### 1.1 当前目录结构
```
tests/RHI/OpenGL/
├── CMakeLists.txt # 混乱的一级配置
├── fixtures/
│ ├── OpenGLTestFixture.h
│ └── OpenGLTestFixture.cpp
├── test_device.cpp
├── test_buffer.cpp # 被注释,未启用
├── test_fence.cpp # 被注释,未启用
├── test_texture.cpp
├── test_shader.cpp # 被注释,未启用
├── test_pipeline_state.cpp
├── test_vertex_array.cpp
├── test_command_list.cpp
├── test_render_target_view.cpp
├── test_depth_stencil_view.cpp
├── test_swap_chain.cpp
├── test_sampler.cpp
└── Res/ # 空文件夹,无实际资源
├── Data/
├── Shader/
└── Texture/
```
### 1.2 当前测试统计
| 组件 | 文件 | 测试数 | 状态 |
|------|------|--------|------|
| Device | `test_device.cpp` | 6 | ✅ 启用 |
| Buffer | `test_buffer.cpp` | 6 | ❌ 被注释 |
| Fence | `test_fence.cpp` | 5 | ❌ 被注释 |
| Texture | `test_texture.cpp` | 4 | ✅ 启用 |
| Shader | `test_shader.cpp` | 4 | ❌ 被注释 |
| PipelineState | `test_pipeline_state.cpp` | 3 | ✅ 启用 |
| VertexArray | `test_vertex_array.cpp` | 1 | ✅ 启用 |
| CommandList | `test_command_list.cpp` | 14 | ✅ 启用 |
| Sampler | `test_sampler.cpp` | 2 | ✅ 启用 |
| SwapChain | `test_swap_chain.cpp` | 3 | ✅ 启用 |
| RTV | `test_render_target_view.cpp` | 2 | ✅ 启用 |
| DSV | `test_depth_stencil_view.cpp` | 2 | ✅ 启用 |
| **总计** | | **52** | **37 启用, 15 被注释** |
### 1.3 与 D3D12 对比
| 方面 | D3D12 (规范) | OpenGL (现状) |
|------|-------------|--------------|
| **目录分层** | `unit/` + `integration/` 分离 | 全部扁平 |
| **CMake 结构** | 顶层 + unit + integration 三级 | 仅一级 |
| **Fixture 设计** | 每测试独立设备 | 静态共享上下文 |
| **被注释测试** | 无 | 3 个文件被注释 |
| **集成测试** | 有 (Python + Golden Image) | **缺失** |
| **测试规范文档** | `TEST_SPEC.md` | **缺失** |
| **Res 资源** | 按测试隔离 | 空文件夹 |
| **资源复制** | POST_BUILD 自动复制 | 未配置 |
---
## 2. 问题详解
### 2.1 Fixture 设计缺陷
**当前问题**:
```cpp
// OpenGL - 静态成员所有测试共享,存在状态污染
static GLFWwindow* m_window; // 共享窗口
static bool m_contextInitialized; // 共享状态
static OpenGLDevice* m_device; // 共享设备
```
**D3D12 规范做法**:
```cpp
// 每个测试独立创建设备
void D3D12TestFixture::SetUp() {
D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_12_0, ...);
}
```
**OpenGL 重构方向**:
```cpp
// 方案A: 每测试创建独立上下文 (推荐)
class OpenGLTestFixture : public ::testing::Test {
protected:
void SetUp() override {
// 创建独立 OpenGL 上下文
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
m_window = glfwCreateWindow(640, 480, "Test", nullptr, nullptr);
glfwMakeContextCurrent(m_window);
gladLoadGLLoader(...);
}
};
```
### 2.2 CMake 配置混乱
**当前问题**:
```cmake
# 硬编码绝对路径
include_directories(${CMAKE_SOURCE_DIR}/tests/OpenPackage/include/)
include_directories(${CMAKE_SOURCE_DIR}/tests/OpenPackage/lib/)
link_directories(${CMAKE_SOURCE_DIR}/tests/OpenPackage/lib/)
# 被注释的测试源文件
# test_buffer.cpp
# test_fence.cpp
# test_shader.cpp
```
**D3D12 规范做法**:
```cmake
# 顶层 CMakeLists.txt 仅做 add_subdirectory
add_subdirectory(unit)
add_subdirectory(integration)
# unit/CMakeLists.txt 独立配置
# integration/CMakeLists.txt 独立配置
```
### 2.3 资源目录为空
当前 `Res/` 下的 `Data/``Shader/``Texture/` 均为空目录,没有实际测试资源。
---
## 3. 重构目标
### 3.1 目标目录结构
```
tests/RHI/OpenGL/
├── CMakeLists.txt # 顶层配置
├── TEST_SPEC.md # 本文档 (OpenGL 专项)
├── TEST_IMPROVEMENT_PLAN.md # 改进计划
├── unit/
│ ├── CMakeLists.txt # 单元测试构建
│ ├── fixtures/
│ │ ├── OpenGLTestFixture.h
│ │ └── OpenGLTestFixture.cpp
│ ├── test_device.cpp
│ ├── test_buffer.cpp
│ ├── test_fence.cpp
│ ├── test_texture.cpp
│ ├── test_shader.cpp
│ ├── test_pipeline_state.cpp
│ ├── test_vertex_array.cpp
│ ├── test_command_list.cpp
│ ├── test_render_target_view.cpp
│ ├── test_depth_stencil_view.cpp
│ ├── test_swap_chain.cpp
│ └── test_sampler.cpp
└── integration/
├── CMakeLists.txt
├── run_integration_test.py # 公共脚本
├── compare_ppm.py # PPM 图像比对
├── run.bat # Windows 启动脚本
├── minimal/ # 最小化测试
│ ├── main.cpp
│ ├── GT_minimal.ppm
│ └── Res/
│ └── Shader/
│ ├── simple.vert
│ └── simple.frag
└── render_model/ # 模型渲染测试
├── main.cpp
├── GT.ppm
└── Res/
├── Image/
├── Model/
└── Shader/
```
### 3.2 测试数量目标
| 组件 | 当前 | 重构后 | 变化 |
|------|------|--------|------|
| Device | 6 | 6 | - |
| Buffer | 0 (被注释) | 6 | +6 |
| Fence | 0 (被注释) | 5 | +5 |
| Texture | 4 | 4 | - |
| Shader | 0 (被注释) | 4 | +4 |
| PipelineState | 3 | 3 | - |
| VertexArray | 1 | 2 | +1 |
| CommandList | 14 | 14 | - |
| Sampler | 2 | 2 | - |
| SwapChain | 3 | 3 | - |
| RTV | 2 | 2 | - |
| DSV | 2 | 2 | - |
| **单元测试总计** | **37** | **53** | **+16** |
| **集成测试** | **0** | **2** | **+2** |
---
## 4. 分阶段实施计划
### Phase 1: 目录结构重构
**目标**: 建立与 D3D12 一致的目录分层
**步骤**:
1. 创建 `unit/``integration/` 子目录
2. 移动现有测试文件到 `unit/`
3. 创建顶层 `CMakeLists.txt``add_subdirectory`
4. 重构 `unit/CMakeLists.txt` 独立配置
**目录变更**:
```
# 重构前
tests/RHI/OpenGL/CMakeLists.txt
tests/RHI/OpenGL/test_*.cpp
tests/RHI/OpenGL/fixtures/
# 重构后
tests/RHI/OpenGL/CMakeLists.txt # 顶层,仅 add_subdirectory
tests/RHI/OpenGL/unit/CMakeLists.txt # 单元测试构建
tests/RHI/OpenGL/unit/test_*.cpp # 测试文件
tests/RHI/OpenGL/unit/fixtures/ # Fixture
tests/RHI/OpenGL/integration/ # 新增
tests/RHI/OpenGL/integration/...
```
### Phase 2: Fixture 重构
**目标**: 消除静态共享状态,避免测试间污染
**重构内容**:
```cpp
// OpenGLTestFixture.h 重构
class OpenGLTestFixture : public ::testing::Test {
protected:
void SetUp() override;
void TearDown() override;
GLFWwindow* GetWindow() { return m_window; }
void MakeContextCurrent();
void DoneContextCurrent();
void ClearGLErrors();
bool CheckGLError(const char* file, int line);
void ResetGLState();
private:
GLFWwindow* m_window = nullptr;
OpenGLDevice* m_device = nullptr;
};
// OpenGLTestFixture.cpp 重构
void OpenGLTestFixture::SetUp() {
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
m_window = glfwCreateWindow(640, 480, "OpenGL Tests", nullptr, nullptr);
if (!m_window) {
GTEST_SKIP() << "Failed to create GLFW window";
return;
}
glfwMakeContextCurrent(m_window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
GTEST_SKIP() << "Failed to initialize GLAD";
glfwDestroyWindow(m_window);
m_window = nullptr;
return;
}
m_device = new OpenGLDevice();
m_device->CreateRenderWindow(640, 480, "Test Window", false);
ClearGLErrors();
}
void OpenGLTestFixture::TearDown() {
ResetGLState();
if (m_device) {
delete m_device;
m_device = nullptr;
}
if (m_window) {
glfwDestroyWindow(m_window);
m_window = nullptr;
}
}
```
### Phase 3: 启用被注释的测试
**目标**: 激活 `test_buffer.cpp``test_fence.cpp``test_shader.cpp`
**步骤**:
1. 取消 CMakeLists.txt 中的注释
2. 检查并修复可能的编译错误
3. 运行测试验证
**预期新增测试**:
- Buffer: 6 tests
- Fence: 5 tests
- Shader: 4 tests
### Phase 4: 完善单元测试
**目标**: 补充缺失的测试用例
**新增测试计划**:
| 组件 | 新增测试 | 说明 |
|------|---------|------|
| VertexArray | 1 | `VertexArray_Bind_MultipleAttributes` |
### Phase 5: 建立集成测试体系
**目标**: 建立与 D3D12 一致的集成测试框架
**步骤**:
1. 创建 `integration/` 目录结构
2. 复制 `run_integration_test.py``compare_ppm.py`
3. 创建 `minimal/` 集成测试
4. 创建 `render_model/` 集成测试
5. 配置 CTest 注册
**minimal/ 集成测试**:
```cpp
// integration/minimal/main.cpp
// 渲染一个简单三角形,输出 PPM 截图
int main() {
// 1. 初始化 GLFW + OpenGL
// 2. 创建窗口
// 3. 渲染简单场景
// 4. 截图保存为 minimal.ppm
// 5. 退出
}
```
**Golden Image 生成流程**:
1. 在干净硬件环境运行集成测试
2. 截图保存为 `GT_<name>.ppm`
3. 人工验证截图正确性
4. 提交到版本控制
### Phase 6: 编写测试规范文档
**目标**: 创建 `TEST_SPEC.md``TEST_IMPROVEMENT_PLAN.md`
---
## 5. CMake 重构详细方案
### 5.1 顶层 CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.15)
project(OpenGLEngineTests)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(unit)
add_subdirectory(integration)
```
### 5.2 unit/CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.15)
get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
find_package(OpenGL REQUIRED)
include_directories(${PROJECT_ROOT_DIR}/engine/include)
include_directories(${PROJECT_ROOT_DIR}/engine/src)
link_directories(${CMAKE_SOURCE_DIR}/tests/OpenGL/package/lib/)
find_package(GTest REQUIRED)
set(TEST_SOURCES
${CMAKE_SOURCE_DIR}/tests/OpenGL/package/src/glad.c
fixtures/OpenGLTestFixture.cpp
test_device.cpp
test_buffer.cpp
test_fence.cpp
test_texture.cpp
test_shader.cpp
test_pipeline_state.cpp
test_vertex_array.cpp
test_command_list.cpp
test_render_target_view.cpp
test_depth_stencil_view.cpp
test_swap_chain.cpp
test_sampler.cpp
)
add_executable(opengl_engine_tests ${TEST_SOURCES})
target_link_libraries(opengl_engine_tests PRIVATE
opengl32
glfw3
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(opengl_engine_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/fixtures
${PROJECT_ROOT_DIR}/engine/include
${PROJECT_ROOT_DIR}/engine/src
)
target_compile_definitions(opengl_engine_tests PRIVATE
TEST_RESOURCES_DIR="${PROJECT_ROOT_DIR}/tests/RHI/OpenGL/integration/minimal/Res"
)
add_custom_command(TARGET opengl_engine_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${PROJECT_ROOT_DIR}/tests/RHI/OpenGL/integration/minimal/Res
$<TARGET_FILE_DIR:opengl_engine_tests>/Res
)
enable_testing()
add_test(NAME OpenGLEngineTests COMMAND opengl_engine_tests)
```
### 5.3 integration/CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.15)
project(OpenGL_Integration)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
find_package(Python3 REQUIRED)
enable_testing()
# Minimal test
add_executable(OpenGL_Minimal
WIN32
minimal/main.cpp
)
target_include_directories(OpenGL_Minimal PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/minimal
${ENGINE_ROOT_DIR}/include
)
target_link_libraries(OpenGL_Minimal PRIVATE
opengl32
glfw3
d3d12
dxgi
d3dcompiler
XCEngine
)
# Copy Res folder
add_custom_command(TARGET OpenGL_Minimal POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/minimal/Res
$<TARGET_FILE_DIR:OpenGL_Minimal>/Res
)
# Copy test scripts
add_custom_command(TARGET OpenGL_Minimal POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/run.bat
$<TARGET_FILE_DIR:OpenGL_Minimal>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/compare_ppm.py
$<TARGET_FILE_DIR:OpenGL_Minimal>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/run_integration_test.py
$<TARGET_FILE_DIR:OpenGL_Minimal>/
)
# Register integration test with CTest
add_test(NAME OpenGL_Minimal_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:OpenGL_Minimal>/run_integration_test.py
$<TARGET_FILE:OpenGL_Minimal>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/minimal/GT_minimal.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:OpenGL_Minimal>
)
```
---
## 6. 测试前缀对应表
| 类名 | 测试前缀 |
|------|---------|
| OpenGLDevice | Device |
| OpenGLBuffer | Buffer |
| OpenGLFence | Fence |
| OpenGLTexture | Texture |
| OpenGLShader | Shader |
| OpenGLPipelineState | PipelineState |
| OpenGLVertexArray | VertexArray |
| OpenGLCommandList | CommandList |
| OpenGLSampler | Sampler |
| OpenGLSwapChain | SwapChain |
| OpenGLRenderTargetView | RTV |
| OpenGLDepthStencilView | DSV |
---
## 7. 测试执行
### 7.1 单元测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --unit-only
# 方式 2: 直接使用 CTest
cd build/tests/RHI/OpenGL/unit
ctest -C Debug --output-on-failure
```
### 7.2 集成测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --integration
# 方式 2: 直接使用 CTest
cd build/tests/RHI/OpenGL/integration
ctest -C Debug --output-on-failure
```
### 7.3 构建和测试
```bash
# 构建
cmake --build . --target OpenGL_Minimal --config Debug
# 运行测试
python scripts/run_tests.py --build
```
---
## 8. CI 集成
### 8.1 GitHub Actions 配置
```yaml
name: OpenGL Tests
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build OpenGL Tests
run: cmake --build build --target opengl_engine_tests OpenGL_Minimal --config Debug
- name: Run Unit Tests
run: cd build/tests/RHI/OpenGL/unit && ctest -C Debug --output-on-failure
- name: Run Integration Tests
run: python scripts/run_tests.py --integration
```
### 8.2 CI 模式
`--ci` 模式会跳过需要 GUI 的集成测试:
```bash
python scripts/run_tests.py --ci # 仅运行单元测试
```
---
## 9. 文件清单
### 9.1 需创建的文件
| 文件路径 | 说明 |
|---------|------|
| `tests/RHI/OpenGL/CMakeLists.txt` | 顶层 CMake 配置 |
| `tests/RHI/OpenGL/TEST_SPEC.md` | OpenGL 专项规范 |
| `tests/RHI/OpenGL/TEST_IMPROVEMENT_PLAN.md` | 改进计划 |
| `tests/RHI/OpenGL/unit/CMakeLists.txt` | 单元测试构建配置 |
| `tests/RHI/OpenGL/integration/CMakeLists.txt` | 集成测试构建配置 |
| `tests/RHI/OpenGL/integration/run_integration_test.py` | 集成测试运行脚本 |
| `tests/RHI/OpenGL/integration/compare_ppm.py` | PPM 图像比对脚本 |
| `tests/RHI/OpenGL/integration/run.bat` | Windows 启动脚本 |
| `tests/RHI/OpenGL/integration/minimal/main.cpp` | 最小化集成测试 |
| `tests/RHI/OpenGL/integration/minimal/GT_minimal.ppm` | Golden Image |
| `tests/RHI/OpenGL/integration/minimal/Res/Shader/*.vert` | Vertex Shader |
| `tests/RHI/OpenGL/integration/minimal/Res/Shader/*.frag` | Fragment Shader |
### 9.2 需修改的文件
| 文件路径 | 修改内容 |
|---------|---------|
| `tests/RHI/OpenGL/fixtures/OpenGLTestFixture.h` | 重构 Fixture 接口 |
| `tests/RHI/OpenGL/fixtures/OpenGLTestFixture.cpp` | 重构 Fixture 实现 |
| `tests/RHI/OpenGL/CMakeLists.txt` | 简化为 add_subdirectory |
### 9.3 需移动的文件
| 原路径 | 新路径 |
|-------|-------|
| `tests/RHI/OpenGL/test_*.cpp` | `tests/RHI/OpenGL/unit/test_*.cpp` |
| `tests/RHI/OpenGL/fixtures/*` | `tests/RHI/OpenGL/unit/fixtures/*` |
---
## 10. OpenGL 与 D3D12 测试差异说明
### 10.1 平台特性差异
| 方面 | D3D12 | OpenGL |
|------|-------|--------|
| **设备创建** | `D3D12CreateDevice()` 每测试独立 | 共享 GLFWcontext + Glad |
| **上下文管理** | 无 | GLFWwindow 生命周期 |
| **错误检查** | `HRESULT` 返回值 | `glGetError()` 状态码 |
| **渲染目标** | RTV/DSV descriptor | OpenGL Framebuffer Object |
| **同步原语** | `ID3D12Fence` | `glFenceSync` + `glClientWaitSync` |
| **管线状态** | PSO 对象 | OpenGL State Machine |
| **资源绑定** | CommandList + DescriptorHeap | `glBindBuffer`, `glBindTexture` |
### 10.2 Fixture 设计差异
```cpp
// D3D12 - 每测试独立 COM 对象
class D3D12TestFixture : public ::testing::Test {
ComPtr<ID3D12Device> mDevice;
ComPtr<ID3D12CommandQueue> mCommandQueue;
};
// OpenGL - 需要共享 context但每测试独立 window
class OpenGLTestFixture : public ::testing::Test {
GLFWwindow* m_window; // 每测试独立
OpenGLDevice* m_device; // 每测试独立
};
```
### 10.3 测试资源差异
| 方面 | D3D12 | OpenGL |
|------|-------|--------|
| **Shader 格式** | HLSL (.hlsl) | GLSL (.vert, .frag, .geom) |
| **纹理格式** | DDS | PNG/BMP/TGA |
| **模型格式** | 自定义 .lhsm | 自定义 .lhsm |
---
## 11. 已知问题与待办
### 11.1 Phase 1 待办
- [ ] 创建 `unit/``integration/` 目录
- [ ] 移动测试文件到 `unit/`
- [ ] 创建顶层 `CMakeLists.txt`
- [ ] 重构 `unit/CMakeLists.txt`
### 11.2 Phase 2 待办
- [ ] 重构 `OpenGLTestFixture` 消除静态成员
- [ ] 验证测试隔离效果
### 11.3 Phase 3 待办
- [ ] 启用 `test_buffer.cpp`
- [ ] 启用 `test_fence.cpp`
- [ ] 启用 `test_shader.cpp`
- [ ] 修复编译错误
### 11.4 Phase 4 待办
- [ ] 补充 `VertexArray_Bind_MultipleAttributes` 测试
### 11.5 Phase 5 待办
- [ ] 创建 `integration/` 目录结构
- [ ] 复制并适配 `run_integration_test.py`
- [ ] 复制并适配 `compare_ppm.py`
- [ ] 创建 `minimal/` 集成测试
- [ ] 创建 `render_model/` 集成测试
- [ ] 生成 Golden Image
### 11.6 Phase 6 待办
- [ ] 编写 `TEST_SPEC.md`
- [ ] 编写 `TEST_IMPROVEMENT_PLAN.md`
---
## 12. 规范更新记录
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0 | 2026-03-20 | 初始版本,参考 D3D12 TEST_SPEC.md 制定重构方案 |
---
**规范版本**: 1.0
**最后更新**: 2026-03-20
**前置文档**:
- [tests/TEST_SPEC.md](../tests/TEST_SPEC.md)
- [tests/RHI/D3D12/TEST_SPEC.md](./D3D12/TEST_SPEC.md)

View File

@@ -1,609 +0,0 @@
# RHI 模块设计问题分析报告(第二版)
## 1. 项目背景
本项目 RHI 模块参考 Unity 渲染架构设计,面向 **Direct3D 12****Vulkan** 等现代图形 API目标是为引擎上层SRP/RenderGraph提供统一的渲染硬件抽象层屏蔽 API 差异,实现跨后端移植。
当前已实现两个后端D3D12 和 OpenGL。基础框架已有雏形Reset/Close、TransitionBarrier、PipelineState但存在**被 OpenGL 风格带偏、不符合现代 D3D12/Vulkan 显式设计**的关键问题。
---
## 2. 做得好的地方
1. **有显式生命周期管理**`Reset()` / `Close()` 是 D3D12/Vulkan 风格,理解"录制-提交"的分离
2. **考虑了资源状态转换**`TransitionBarrier()` 是现代 API 必须的,预留了接口
3. **覆盖了图形+计算**`Draw` / `DrawIndexed` / `Dispatch` 都有,功能完整
4. **预留了原生句柄**`GetNativeHandle()` 方便调试和特殊场景扩展
---
## 3. 核心问题(按严重程度排序)
### 3.1 最大的坑:字符串查找 SetUniform 是 OpenGL 风格
**问题描述**:当前接口使用字符串名称设置 Uniform/Texture
```cpp
virtual void SetUniformInt(const char* name, int value) = 0;
virtual void SetGlobalTexture(const char* name, RHIResourceView* texture) = 0;
```
这是**典型的 OpenGL 立即模式风格**,问题极大:
| 问题 | 说明 |
|------|------|
| **性能差** | 运行时通过字符串查找 uniform 位置,驱动开销大 |
| **无法多线程** | 字符串查找不是线程安全的,不利于多线程录制 CommandList |
| **接 Vulkan 不可能** | D3D12/Vulkan 用 **DescriptorSet + PipelineLayout**,根本没有"按名字设 uniform"的概念 |
**根本原因**:这是 OpenGL 风格的设计,没有对齐 D3D12/Vulkan 的显式 Descriptor 绑定模型。
**D3D12/Vulkan 正确做法**
```cpp
// D3D12: 通过 Root Signature + Descriptor Table 绑定
commandList->SetGraphicsRootDescriptorTable(rootIndex, gpuDescriptorHandle);
// Vulkan: 通过 DescriptorSet 绑定
vkCmdBindDescriptorSets(commandBuffer, ..., descriptorSet, ...);
```
**修改建议**
```cpp
// 新增DescriptorSet 绑定接口(替代 SetUniform/SetGlobal
virtual void SetGraphicsDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
virtual void SetComputeDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
```
---
### 3.1.1 ✅ 已完成DescriptorSet 抽象
**实现状态**2026-03-25 已完成
**新增抽象**
| 类名 | 说明 |
|------|------|
| `RHIDescriptorSet` | DescriptorSet 基类,含 Update/UpdateSampler/GetNativeHandle/GetBindingCount/GetBindings |
| `RHIDescriptorPool` | DescriptorPool 基类,含 AllocateSet/FreeSet |
| `D3D12DescriptorSet` | D3D12 实现,使用 DescriptorHeap 分配 |
| `D3D12DescriptorHeap` | 扩展支持 AllocateSet/FreeSet |
| `OpenGLDescriptorSet` | OpenGL 实现,使用 TextureUnitAllocator |
| `OpenGLDescriptorPool` | OpenGL 实现 |
**RHICommandList 新增接口**
```cpp
virtual void SetGraphicsDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
virtual void SetComputeDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
```
**RHIDevice 新增工厂方法**
```cpp
virtual RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc& desc) = 0;
virtual RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool* pool, const DescriptorSetDesc& desc) = 0;
```
**实现说明**
- D3D12使用 `D3D12DescriptorHeap` 的 GPU/CPU descriptor 分配
- OpenGL使用 `TextureUnitAllocator` 分配 texture unit
- 旧的 `SetUniform*/SetGlobal*` 仍保留,向后兼容
---
### 3.2 缺少显式 RenderPass
**问题描述**:当前只有 `SetRenderTargets()`,没有 `BeginRenderPass()` / `EndRenderPass()`
```cpp
// 当前设计
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
virtual void ClearRenderTarget(RHIResourceView* renderTarget, const float color[4]) = 0;
```
这是**不符合现代 API 设计**的:
| 问题 | 说明 |
|------|------|
| **不符合显式设计** | D3D12/Vulkan 要求显式定义渲染通道的开始和结束 |
| **清屏逻辑不规范** | 现代 API 推荐用 RenderPass 的 `LoadOp`Clear/Load/DontCare清屏 |
| **Tile GPU 性能差** | 对移动端 Tile GPU 至关重要,隐式行为无法优化 |
**根本原因**OpenGL 的 `glClear()` 是立即模式,没有 RenderPass 概念。
**D3D12/Vulkan 正确做法**
```cpp
// D3D12: Begin/End Render Pass
commandList->BeginRenderPass(...);
commandList->EndRenderPass();
// Vulkan: vkCmdBeginRenderPass / vkCmdEndRenderPass
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, ...);
vkCmdEndRenderPass(commandBuffer);
```
**修改建议**
```cpp
// 新增:显式渲染通道
virtual void BeginRenderPass(
RHIRenderPass* renderPass,
RHIFramebuffer* framebuffer,
const Rect& renderArea,
uint32_t clearValueCount,
const ClearValue* clearValues) = 0;
virtual void EndRenderPass() = 0;
// 移除 SetRenderTargets改由 Framebuffer 定义
// 移除或废弃 ClearRenderTarget改为 BeginRenderPass 的 LoadOp
```
### 3.2.1 ✅ 已完成:显式 RenderPass 实现
**实现状态**2026-03-24 已完成
**新增抽象**
| 类名 | 说明 |
|------|------|
| `RHIFramebuffer` | Framebuffer 基类,含 Initialize/Bind/GetHandle |
| `RHIRenderPass` | RenderPass 基类,含 AttachmentDesc |
| `D3D12Framebuffer` | D3D12 Framebuffer 实现 |
| `D3D12RenderPass` | D3D12 RenderPass 实现(使用 OMSetRenderTargets |
| `OpenGLFramebuffer` | OpenGL 实现(继承自 RHIFramebuffer |
| `OpenGLRenderPass` | OpenGL RenderPass 实现 |
**RHICommandList 新增接口**
```cpp
virtual void BeginRenderPass(RHIRenderPass* renderPass, RHIFramebuffer* framebuffer,
const Rect& renderArea, uint32_t clearValueCount,
const ClearValue* clearValues) = 0;
virtual void EndRenderPass() = 0;
```
**AttachmentDesc 结构**(定义在 RHIRenderPass.h
```cpp
struct AttachmentDesc {
Format format = Format::Unknown;
LoadAction loadOp = LoadAction::Undefined; // Undefined/Load/Clear
StoreAction storeOp = StoreAction::Store; // Store/Resolve/Discard
LoadAction stencilLoadOp = LoadAction::Undefined;
StoreAction stencilStoreOp = StoreAction::Undefined;
ClearValue clearValue;
};
```
**实现说明**
- D3D12使用 `OMSetRenderTargets` + `ClearRenderTargetView`/`ClearDepthStencilView`
- OpenGL使用 `glBindFramebuffer` + `glClearBufferfv` 处理 LoadOp
- 旧的 `SetRenderTargets`/`ClearRenderTarget` 仍保留,向后兼容
---
### 3.3 动态状态太多,应该收敛到 PipelineState
**问题描述**:当前把 DepthStencilState、BlendState、PrimitiveTopology 都放在 CommandList 里动态设置:
```cpp
virtual void SetDepthStencilState(const DepthStencilState& state) = 0;
virtual void SetBlendState(const BlendState& state) = 0;
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0;
```
这是**OpenGL 风格的残留**
| 问题 | 说明 |
|------|------|
| **驱动开销大** | 现代 API 中这些状态大部分是 PipelineState 的一部分,是不可变的 |
| **不符合显式设计** | 动态状态应该只保留极少数Viewport/Scissor/StencilRef/BlendFactor |
| **状态不一致风险** | CommandList 动态设置的状态可能与 PSO 中定义的状态冲突 |
**根本原因**OpenGL 是状态机可以在任何时候设置任何状态。D3D12/Vulkan 把大部分状态打包到 PSO 中。
**D3D12/Vulkan 正确做法**
- `DepthStencilState``BlendState``PrimitiveTopology`**PSO 创建时就确定**
- CommandList 只设置**真正的动态状态**Viewport、Scissor、StencilRef、BlendFactor
**修改建议**
```cpp
// PipelineState 应该包含:
// - Shader
// - VertexLayout
// - RenderPass 兼容
// - DepthStencilState不可变
// - BlendState不可变
// - PrimitiveTopology不可变
// - 动态状态掩码
// CommandList 只保留必要的动态状态:
virtual void SetViewports(uint32_t count, const Viewport* viewports) = 0;
virtual void SetScissorRects(uint32_t count, const Rect* rects) = 0;
virtual void SetStencilRef(uint8_t ref) = 0;
virtual void SetBlendFactor(const float factor[4]) = 0;
// 移除:
virtual void SetDepthStencilState(const DepthStencilState& state) = 0; // 移到 PSO
virtual void SetBlendState(const BlendState& state) = 0; // 移到 PSO
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0; // 移到 PSO
```
### 3.3.1 ✅ 已完成:动态状态移至 PSO
**实现状态**2026-03-25 已完成
**移除的无效方法**
| 方法 | 问题 |
|------|------|
| `SetDepthStencilState` | D3D12 中是空实现TODO调用无效OpenGL 中直接调用 GL 函数但状态应通过 PSO 配置 |
| `SetBlendState` | D3D12 中只设置 BlendFactor其他参数被忽略OpenGL 中直接调用 GL 函数但状态应通过 PSO 配置 |
**保留的真正动态状态**
| 方法 | 说明 |
|------|------|
| `SetViewport/SetViewports` | 真正的动态状态 |
| `SetScissorRect/SetScissorRects` | 真正的动态状态 |
| `SetStencilRef` | D3D12 动态状态 |
| `SetBlendFactor` | D3D12 动态状态 |
**修改的文件**
- `RHICommandList.h` - 移除 SetDepthStencilState/SetBlendState 纯虚方法
- `D3D12CommandList.h/cpp` - 移除 SetDepthStencilState/SetBlendState 实现
- `OpenGLCommandList.h/cpp` - 移除 SetDepthStencilState/SetBlendState 实现
- `tests/RHI/unit/test_command_list.cpp` - 移除相关测试
- `tests/RHI/OpenGL/unit/test_command_list.cpp` - 移除相关测试
**说明**
- D3D12 中 PSO 是不可变的SetDepthStencilState/SetBlendState 调用原本就是无效代码
- OpenGL 中状态通过 OpenGLPipelineState::Apply() 在绑定 PSO 时应用
- PrimitiveTopology 保留在 CommandList因为 D3D12 允许动态改变 topology type
---
### 3.4 ResourceView 类型不明确 ✅ 基本完成
**问题描述**:当前用 `RHIResourceView*` 代表所有视图:
```cpp
virtual void SetVertexBuffer(uint32_t slot, RHIResourceView* buffer, ...) = 0;
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
```
这会导致:
| 问题 | 说明 |
|------|------|
| **类型不安全** | 容易把 SRV 当成 RTV 传,运行时才发现错误 |
| **后端处理麻烦** | D3D12/Vulkan 对不同视图有严格区分RTV/DSV/SRV/UAV/VBV/IBV |
| **语义模糊** | `RHIResourceView` 到底是哪种视图?调用者必须查文档 |
**根本原因**:没有在类型层面区分不同视图的语义。
**修改建议**:定义明确的视图类型层次结构
```cpp
// 基类
class RHIResourceView {};
// 具体视图类型
class RHIRenderTargetView : public RHIResourceView {};
class RHIDepthStencilView : public RHIResourceView {};
class RHIShaderResourceView : public RHIResourceView {};
class RHIUnorderedAccessView : public RHIResourceView {};
class RHIVertexBufferView : public RHIResourceView {};
class RHIIndexBufferView : public RHIResourceView {};
class RHIConstantBufferView : public RHIResourceView {};
```
**实现状态**:✅ 基本完成 - `RHIResourceView.h` 中定义了具体视图类型,但 `RHICommandList` 仍使用 `RHIResourceView*` 以保持兼容性
---
### 3.5 TransitionBarrier 针对 View 而非 Resource ✅ 已完成
**问题描述**:当前接口:
```cpp
virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;
```
资源状态转换是针对 **ResourceBuffer/Texture 本身)** 的,不是针对 View 的。一张 Texture 可能创建多个 ViewSRV、RTV、DSV但状态转换是对整个 Texture 做的。
**根本原因**:没有区分 Resource 和 ResourceView 的概念。
**修改建议**
```cpp
// 新增 Resource 基类
class RHIResource {};
class RHIBuffer : public RHIResource {};
class RHITexture : public RHIResource {};
// 修改 TransitionBarrier
struct ResourceBarrier {
RHIResource* resource;
ResourceStates stateBefore;
ResourceStates stateAfter;
uint32_t subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
};
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
```
**实现状态**:✅ 已完成 - `RHIResource.h` 已创建,`RHIBuffer``RHITexture` 已继承自 `RHIResource`
**问题描述**:当前接口:
```cpp
virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;
```
资源状态转换是针对 **ResourceBuffer/Texture 本身)** 的,不是针对 View 的。一张 Texture 可能创建多个 ViewSRV、RTV、DSV但状态转换是对整个 Texture 做的。
**根本原因**:没有区分 Resource 和 ResourceView 的概念。
**修改建议**
```cpp
// 新增 Resource 基类
class RHIResource {};
class RHIBuffer : public RHIResource {};
class RHITexture : public RHIResource {};
// 修改 TransitionBarrier
struct ResourceBarrier {
RHIResource* resource;
ResourceStates stateBefore;
ResourceStates stateAfter;
uint32_t subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
};
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
```
---
## 4. 其他需要修改的问题
### 4.1 SetGlobal* 是空操作 ✅ 已完成
**问题**SetGlobal* 方法SetGlobalInt/SetGlobalFloat/SetGlobalVec3/SetGlobalVec4/SetGlobalMat4/SetGlobalTexture在 D3D12 和 OpenGL 中只是缓存值,从不提交到 GPU。
**实现状态**2026-03-25 已完成 - 移除所有 SetGlobal* 方法
**移除的方法**共6个
| 方法 | 问题 |
|------|------|
| `SetGlobalInt` | 只缓存到 unordered_map从未提交到 GPU |
| `SetGlobalFloat` | 只缓存到 unordered_map从未提交到 GPU |
| `SetGlobalVec3` | 只缓存到 unordered_map从未提交到 GPU |
| `SetGlobalVec4` | 只缓存到 unordered_map从未提交到 GPU |
| `SetGlobalMat4` | 只缓存到 unordered_map从未提交到 GPU |
| `SetGlobalTexture` | 只缓存到 unordered_map从未提交到 GPU |
**移除的缓存成员变量**
- D3D12CommandList: `m_globalIntCache`, `m_globalFloatCache`, `m_globalVec3Cache`, `m_globalVec4Cache`, `m_globalMat4Cache`, `m_globalTextureCache`
- OpenGLCommandList: 同上
**说明**
- `SetGlobal*` 从未被代码库中任何地方调用(死代码)
- `SetUniform*` 方法已正常工作,使用 shader reflection + 实际 GPU 绑定
- 移除后无功能损失
### 4.2 PrimitiveType 和 PrimitiveTopology 冲突
详见第一版文档,此处不再赘述。
### 4.3 OpenGL 特有方法暴露 ✅ 已完成
**实现状态**2026-03-25 已完成
**问题**OpenGLDevice 和 OpenGLCommandList 暴露了 OpenGL 特有方法,违反"上层只调用抽象接口"原则。
**修复内容**
1. **OpenGLDevice 内部化方法**
- 移除 `GetTextureUnitAllocator()``GetUniformBufferManager()` 公开访问(移到 private
- 移除 `SwapBuffers()` 公开方法OpenGLSwapChain 作为 friend 仍可访问)
- `GetPresentationDC()``GetGLContext()` 保留在 private通过 friend 访问
2. **新增抽象接口**
- `MakeContextCurrent()` - 封装 `wglMakeCurrent` 操作
- `GetNativeContext()` - 返回 GL Context 供 RenderDoc 使用
3. **保留的逃生舱方法**
- OpenGLCommandList 中的 `PrimitiveType` 相关方法是显式的 OpenGL 逃生舱
- 这些方法使用 OpenGL 特有类型,文档化为"backend-specific escape hatch"
- 抽象层正确使用 `PrimitiveTopology` 枚举
**修改的文件**
- `engine/include/XCEngine/RHI/OpenGL/OpenGLDevice.h` - 内部化特有方法,新增 MakeContextCurrent/GetNativeContext
- `engine/src/RHI/OpenGL/OpenGLDevice.cpp` - 实现 MakeContextCurrent
- `tests/RHI/OpenGL/unit/test_device.cpp` - 移除 SwapBuffers 测试
- `tests/RHI/OpenGL/integration/minimal/main.cpp` - 使用 MakeContextCurrent
- `tests/RHI/OpenGL/integration/triangle/main.cpp` - 使用 MakeContextCurrent
- `tests/RHI/OpenGL/integration/quad/main.cpp` - 使用 MakeContextCurrent
- `tests/RHI/OpenGL/integration/sphere/main.cpp` - 使用 MakeContextCurrent
**说明**
- `PrimitiveType` 枚举在 OpenGL 专用逃生舱方法中使用是可接受的,因为这些是显式的后端特定接口
- 抽象层正确使用 `PrimitiveTopology`,不存在类型泄漏问题
### 4.4 缺少 Compute Pipeline 抽象
详见第一版文档,此处不再赘述。
### 4.5 RHIPipelineLayout 空壳
详见第一版文档,此处不再赘述。
---
## 5. 修改后的 CommandList 核心接口
```cpp
class RHICommandList {
public:
virtual ~RHICommandList() = default;
// ========== 生命周期 ==========
virtual void Reset(RHICommandPool* pool = nullptr) = 0;
virtual void Close() = 0;
// ========== 资源状态转换 ==========
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
// ========== 渲染通道(新增)==========
virtual void BeginRenderPass(
RHIRenderPass* renderPass,
RHIFramebuffer* framebuffer,
const Rect& renderArea,
uint32_t clearValueCount,
const ClearValue* clearValues) = 0;
virtual void EndRenderPass() = 0;
// ========== 管线与描述符 ==========
virtual void SetPipelineState(RHIPipelineState* pso) = 0;
virtual void SetGraphicsDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
// ========== 动态状态(精简后)==========
virtual void SetViewports(uint32_t count, const Viewport* viewports) = 0;
virtual void SetScissorRects(uint32_t count, const Rect* rects) = 0;
virtual void SetStencilRef(uint8_t ref) = 0;
virtual void SetBlendFactor(const float factor[4]) = 0;
// ========== 顶点/索引 ==========
virtual void SetVertexBuffers(
uint32_t startSlot,
uint32_t count,
RHIVertexBufferView** buffers,
const uint64_t* offsets,
const uint32_t* strides) = 0;
virtual void SetIndexBuffer(
RHIIndexBufferView* buffer,
uint64_t offset,
IndexFormat format) = 0;
// ========== 绘制 ==========
virtual void Draw(uint32_t vertexCount, uint32_t instanceCount = 1,
uint32_t startVertex = 0, uint32_t startInstance = 0) = 0;
virtual void DrawIndexed(uint32_t indexCount, uint32_t instanceCount = 1,
uint32_t startIndex = 0, int32_t baseVertex = 0,
uint32_t startInstance = 0) = 0;
// ========== 计算 ==========
virtual void SetComputePipelineState(RHIPipelineState* pso) = 0;
virtual void SetComputeDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
virtual void Dispatch(uint32_t x, uint32_t y, uint32_t z) = 0;
// ========== 复制 ==========
virtual void CopyResource(RHIResource* dst, RHIResource* src) = 0;
// ========== 原生句柄 ==========
virtual void* GetNativeHandle() = 0;
};
```
---
## 6. 需要同时新增的抽象
### 6.1 RHIDescriptorSet
```cpp
class RHIDescriptorSet {
public:
virtual ~RHIDescriptorSet() = default;
virtual void Update(uint32_t offset, RHIResourceView* view) = 0;
virtual void UpdateSampler(uint32_t offset, RHISampler* sampler) = 0;
virtual void* GetNativeHandle() = 0;
};
```
### 6.2 RHIRenderPass
```cpp
struct AttachmentDesc {
Format format;
LoadAction loadOp = LoadAction::DontCare;
StoreAction storeOp = StoreAction::Store;
LoadAction stencilLoadOp = LoadAction::DontCare;
StoreAction stencilStoreOp = StoreAction::DontCare;
};
class RHIRenderPass {
public:
virtual ~RHIRenderPass() = default;
virtual bool Initialize(uint32_t colorAttachmentCount, const AttachmentDesc* colorAttachments,
const AttachmentDesc* depthStencilAttachment) = 0;
virtual void Shutdown() = 0;
virtual void* GetNativeHandle() = 0;
};
```
### 6.3 RHIFramebuffer
```cpp
class RHIFramebuffer {
public:
virtual ~RHIFramebuffer() = default;
virtual bool Initialize(RHIRenderPass* renderPass, uint32_t attachmentCount,
RHIResourceView** attachments, uint32_t width, uint32_t height) = 0;
virtual void Shutdown() = 0;
virtual void* GetNativeHandle() = 0;
};
```
### 6.4 RHIResource 基类
```cpp
class RHIResource {
public:
virtual ~RHIResource() = default;
virtual void* GetNativeHandle() = 0;
virtual ResourceStates GetState() const = 0;
virtual void SetState(ResourceStates state) = 0;
};
class RHIBuffer : public RHIResource { ... };
class RHITexture : public RHIResource { ... };
```
---
## 7. 问题优先级总结
| 优先级 | 问题 | 严重性 | 修复难度 | 状态 |
|--------|------|--------|----------|------|
| 1 | 字符串查找 SetUniform 不符合 D3D12/Vulkan | 🔴 致命 | 高 | ✅ 已完成 |
| 2 | 缺少显式 RenderPass | 🔴 致命 | 高 | ✅ 已完成 |
| 3 | 动态状态太多 | 🔴 高 | 高 | ✅ 已完成 |
| 4 | ResourceView 类型不明确 | 🟡 中 | 中 | ✅ 基本完成 |
| 5 | TransitionBarrier 针对 View 而非 Resource | 🟡 中 | 中 | ✅ 已完成 |
| 6 | SetGlobal* 空操作 | 🟡 中 | 低 | ✅ 已完成 |
| 7 | OpenGL 特有方法暴露 | 🟡 中 | 高 | ✅ 已完成 |
| 8 | 缺少 Compute Pipeline 抽象 | 🟡 中 | 中 | ✅ 已完成 |
---
## 8. 总结
当前 RHI 模块的基础框架有 D3D12/Vulkan 的影子,但**被 OpenGL 风格带偏**了。最核心的问题是:
1. **用字符串查找设 uniform** —— 这是 OpenGL 风格,必须改成 DescriptorSet 绑定
2. **缺少显式 RenderPass** —— 现代 API 的基础,必须添加
3. **动态状态太多** —— 应该收敛到 PSO只保留必要的动态状态
修复这些问题后RHI 抽象层将完全对齐 D3D12/Vulkan 的显式模型,未来接入 Vulkan 将会非常顺利。

View File

@@ -1,285 +0,0 @@
# XCEngine 测试体系文档
> **版本**: 1.0
> **日期**: 2026-03-13
---
## 1. 测试架构
```
tests/
├── CMakeLists.txt # 测试构建配置
├── run_tests.cmake # 测试运行脚本
├── fixtures/ # 测试夹具
│ └── MathFixtures.h
├── math/ # Math 单元测试
│ ├── CMakeLists.txt
│ ├── test_vector.cpp
│ ├── test_matrix.cpp
│ ├── test_quaternion.cpp
│ └── test_geometry.cpp
├── core/ # Core 测试
├── threading/ # 线程测试
├── memory/ # 内存测试
├── containers/ # 容器测试
└── rendering/ # 渲染测试
├── unit/ # 单元测试
├── integration/ # 集成测试
└── screenshots/ # 参考图
```
---
## 2. 测试分类
| 类型 | 目录 | 目的 | 运行频率 |
|------|------|------|---------|
| **Unit Test** | `tests/*/` | 验证单个函数/类 | 每次提交 |
| **Integration Test** | `tests/rendering/integration/` | 验证多模块协作 | 每次提交 |
| **Benchmark** | `tests/benchmark/` | 性能回归检测 | 每日/每周 |
| **Screenshot Test** | `tests/rendering/screenshots/` | 渲染正确性 | 每次提交 |
---
## 3. 测试命名规范
```cpp
// 格式: test_<模块>_<功能>_<场景>
TEST(Math_Vector3, Dot_TwoVectors_ReturnsCorrectValue) { }
TEST(Math_Vector3, Normalize_ZeroVector_ReturnsZeroVector) { }
TEST(Math_Matrix4, Inverse_Identity_ReturnsIdentity) { }
TEST(Math_Matrix4, TRS_Decompose_RecoversOriginalValues) { }
TEST(Math_Quaternion, Slerp_ShortestPath_InterpolatesCorrectly) { }
// 边界情况
TEST(Math_Vector3, Normalize_ZeroVector_DoesNotCrash) { }
TEST(Math_Matrix4, Inverse_SingularMatrix_ReturnsIdentity) { }
```
---
## 4. 断言规范
### 4.1 浮点数比较
```cpp
// 必须使用容差
EXPECT_NEAR(actual, expected, 1e-5f);
ASSERT_FLOAT_EQ(actual, expected); // gtest 内部有容差
// 数组比较
for (int i = 0; i < 4; i++) {
EXPECT_NEAR(actual.m[i], expected.m[i], 1e-5f);
}
```
### 4.2 常用断言
```cpp
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);
EXPECT_EQ(actual, expected);
EXPECT_NE(actual, expected);
EXPECT_STREQ(actual, expected);
EXPECT_THROW(expression, exception_type);
```
---
## 5. 测试夹具 (Fixture)
```cpp
class MathFixture : public ::testing::Test {
protected:
void SetUp() override {
v1 = Vector3(1, 0, 0);
v2 = Vector3(0, 1, 0);
v3 = Vector3(1, 1, 1);
m1 = Matrix4x4::Identity();
m2 = Matrix4x4::Translation(Vector3(1, 2, 3));
}
Vector3 v1, v2, v3;
Matrix4x4 m1, m2;
const float epsilon = 1e-5f;
};
TEST_F(MathFixture, Dot_OrthogonalVectors_ReturnsZero) {
EXPECT_FLOAT_EQ(Vector3::Dot(v1, v2), 0.0f);
}
```
---
## 6. 参数化测试
```cpp
class MatrixInverseTest : public ::testing::TestWithParam<Matrix4x4> {};
INSTANTIATE_TEST_SUITE_P(
InverseCases,
MatrixInverseTest,
testing::Values(
Matrix4x4::Identity(),
Matrix4x4::Translation(Vector3(1,2,3)),
Matrix4x4::Scale(Vector3(2,2,2))
)
);
TEST_P(MatrixInverseTest, InverseOfInverse_EqualsOriginal) {
Matrix4x4 original = GetParam();
Matrix4x4 inverted = original.Inverse();
Matrix4x4 recovered = inverted.Inverse();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
EXPECT_NEAR(original.m[i][j], recovered.m[i][j], 1e-4f);
}
}
}
```
---
## 7. Math 测试用例
### 7.1 Vector2/3/4 测试
| 测试类别 | 测试用例 |
|---------|---------|
| **构造** | 默认构造、参数构造、从 Vector3 构造 |
| **运算** | 加、减、乘、除、点积、叉积 |
| **归一化** | Normalize、Normalized、Magnitude、SqrMagnitude |
| **插值** | Lerp、MoveTowards |
| **投影** | Project、ProjectOnPlane |
| **角度** | Angle、Reflect |
### 7.2 Matrix 测试
| 测试类别 | 测试用例 |
|---------|---------|
| **构造** | Identity、Zero |
| **变换** | Translation、Rotation、Scale、TRS |
| **相机** | LookAt、Perspective、Orthographic |
| **运算** | 乘法、点乘、叉乘 |
| **分解** | Inverse、Transpose、Determinant、Decompose |
### 7.3 Quaternion 测试
| 测试类别 | 测试用例 |
|---------|---------|
| **构造** | Identity、FromAxisAngle、FromEulerAngles |
| **转换** | ToEulerAngles、ToMatrix4x4、FromRotationMatrix |
| **插值** | Slerp |
| **运算** | 乘法和逆 |
### 7.4 几何测试
| 测试类型 | 测试用例 |
|---------|---------|
| **Ray** | GetPoint、Intersects(Sphere/Box/Plane) |
| **Sphere** | Contains、Intersects |
| **Box** | Contains、Intersects |
| **Plane** | FromPoints、GetDistanceToPoint、Intersects |
| **Frustum** | Contains(Point/Sphere/Bounds)、Intersects |
| **Bounds** | GetMinMax、Intersects、Contains、Encapsulate |
---
## 8. 构建与运行
### 8.1 构建测试
```bash
# 创建构建目录
mkdir build && cd build
# 配置 CMake
cmake .. -G "Visual Studio 17 2022" -A x64
# 构建测试
cmake --build . --config Debug --target xcengine_math_tests
```
### 8.2 运行测试
```bash
# 运行所有测试
ctest --output-on-failure
# 运行 Math 测试
./tests/xcengine_math_tests.exe
# 运行特定测试
./tests/xcengine_math_tests.exe --gtest_filter=Math_Vector3.*
# 运行测试并显示详细信息
./tests/xcengine_math_tests.exe --gtest_also_run_disabled_tests --gtest_print_time=1
```
### 8.3 测试过滤器
```bash
# 运行所有 Vector3 测试
--gtest_filter=Math_Vector3.*
# 运行除某测试外的所有测试
--gtest_filter=-Math_Matrix4.SingularMatrix*
# 运行多个测试
--gtest_filter=Math_Vector3.*:Math_Matrix4.*
```
---
## 9. 覆盖率要求
| 模块 | 最低覆盖率 | 关键测试 |
|------|-----------|---------|
| Math | 90% | 所有公开 API |
| Core | 80% | 智能指针、Event |
| Containers | 85% | 边界、迭代器 |
| Memory | 90% | 分配/泄漏 |
| Threading | 70% | 基本功能 |
---
## 10. 持续集成
```yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Configure
run: cmake -B build -DENABLE_COVERAGE=ON
- name: Build
run: cmake --build build --config Debug
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Coverage
run: cmake --build build --target coverage
```
---
## 11. 注意事项
1. **浮点数比较** - 必须使用容差 (通常 1e-5 或 1e-6)
2. **边界条件** - 必须测试零向量、奇异矩阵等
3. **随机性** - 如需固定 seed 保证确定性
4. **线程安全** - 线程测试需设置超时
5. **内存泄漏** - 使用 Valgrind 或 CRT 检测

View File

@@ -1,550 +0,0 @@
# UI-Editor GameObject 缺口分析
> **基于**XCEngine渲染引擎架构设计.md
> **目标**:识别 UI 编辑器 GameObject.h 相对于 Engine 架构设计文档的缺失功能
> **日期**2026-03-20
---
## 架构参考
XCEngine 组件系统参考 **Unity 传统架构 (GameObject-Component Pattern)**
- **GameObject**场景中的实体UI Editor 中称 `Entity`
- **Component**:挂载到 GameObject 的功能模块(变换、渲染器等)
- **TransformComponent**:每个 GameObject 都有的变换组件,管理位置/旋转/缩放
- **Scene/SceneManager**:场景管理和场景切换
---
## Engine 模块现状
| 模块 | 状态 | UI Editor 对齐情况 |
|-----|------|------------------|
| Core (Event, Types) | ✅ 已实现 | UI 编辑器已复用 `XCEngine::Core::Event<T>` |
| Debug (Logger, LogLevel, LogEntry) | ✅ 已实现 | UI 编辑器已复用 `XCEngine::Debug::LogLevel` |
| **Math (Vector3, Quaternion, Matrix4x4, Transform, Color)** | ✅ **已实现** | **可直接复用 Engine Math 类型** |
| Memory | ✅ 已实现 | |
| Threading | ✅ 已实现 | |
| Containers | ✅ 已实现 | |
| RHI / D3D12 | ✅ 已实现 | UI 编辑器自行实现(与 Engine 技术栈一致) |
| Resources | ✅ 已实现 | |
| **Components (GameObject, Component, System)** | ❌ **缺失** | UI 编辑器自行实现,待 Engine 实现后迁移 |
| **Scene (Scene, SceneManager, SceneLoader, SceneSerializer)** | ❌ **缺失** | UI 编辑器自行实现,待 Engine 实现后迁移 |
---
## 一、Component 基类缺口
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `transform()` | `TransformComponent& transform() const` | ❌ 缺失 | 返回所在实体的 TransformComponent 引用 |
| `GetScene()` | `Scene* GetScene() const` | ❌ 缺失 | 返回所属 Scene 指针 |
| `FixedUpdate()` | `virtual void FixedUpdate()` | ❌ 缺失 | 物理更新(每固定帧) |
| `LateUpdate()` | `virtual void LateUpdate(float deltaTime)` | ❌ 缺失 | 晚于 Update 执行 |
| `OnEnable()` | `virtual void OnEnable()` | ❌ 缺失 | 组件启用时调用 |
| `OnDisable()` | `virtual void OnDisable()` | ❌ 缺失 | 组件禁用时调用 |
| `friend class` | `friend class GameObject` | ⚠️ 差异 | UI Editor 是 `friend class Entity` |
**补充说明**
- UI Editor 的 `Component` 需要增加 `TransformComponent*` 获取方法以支持 Inspector 面板的变换编辑
- Engine 的 `m_gameObject` 在 UI Editor 中对应 `m_entity`
---
## 二、TransformComponent 缺口
### 2.1 World 空间方法
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetPosition()` | `XCEngine::Math::Vector3 GetPosition() const` | ❌ 缺失 | 获取世界坐标 |
| `SetPosition()` | `void SetPosition(const XCEngine::Math::Vector3& position)` | ❌ 缺失 | 设置世界坐标 |
| `GetRotation()` | `XCEngine::Math::Quaternion GetRotation() const` | ❌ 缺失 | 获取世界旋转 |
| `SetRotation()` | `void SetRotation(const XCEngine::Math::Quaternion& rotation)` | ❌ 缺失 | 设置世界旋转 |
| `GetScale()` | `XCEngine::Math::Vector3 GetScale() const` | ❌ 缺失 | 获取世界缩放 |
| `SetScale()` | `void SetScale(const XCEngine::Math::Vector3& scale)` | ❌ 缺失 | 设置世界缩放 |
### 2.2 方向向量
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetForward()` | `XCEngine::Math::Vector3 GetForward() const` | ❌ 缺失 | 获取前向向量 |
| `GetRight()` | `XCEngine::Math::Vector3 GetRight() const` | ❌ 缺失 | 获取右向量 |
| `GetUp()` | `XCEngine::Math::Vector3 GetUp() const` | ❌ 缺失 | 获取上向量 |
### 2.3 矩阵变换
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetLocalToWorldMatrix()` | `const XCEngine::Math::Matrix4x4& GetLocalToWorldMatrix() const` | ❌ 缺失 | 本地到世界矩阵(含缓存) |
| `GetWorldToLocalMatrix()` | `XCEngine::Math::Matrix4x4 GetWorldToLocalMatrix() const` | ❌ 缺失 | 世界到本地矩阵 |
### 2.4 父子层级
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetParent()` | `TransformComponent* GetParent() const` | ❌ 缺失 | 获取父变换 |
| `SetParent()` | `void SetParent(TransformComponent* parent, bool worldPositionStays = true)` | ❌ 缺失 | 设置父变换 |
| `GetChild()` | `TransformComponent* GetChild(int index) const` | ❌ 缺失 | 按索引获取子变换 |
| `Find()` | `TransformComponent* Find(const String& name) const` | ❌ 缺失 | 按名称查找子变换 |
| `DetachChildren()` | `void DetachChildren()` | ❌ 缺失 | 断开所有子节点 |
| `SetAsFirstSibling()` | `void SetAsFirstSibling()` | ❌ 缺失 | 设为第一个同级 |
| `SetAsLastSibling()` | `void SetAsLastSibling()` | ❌ 缺失 | 设为最后一个同级 |
| `GetSiblingIndex()` | `int GetSiblingIndex() const` | ❌ 缺失 | 获取同级索引 |
| `SetSiblingIndex()` | `void SetSiblingIndex(int index)` | ❌ 缺失 | 设置同级索引 |
### 2.5 变换操作
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `LookAt(target)` | `void LookAt(const XCEngine::Math::Vector3& target)` | ❌ 缺失 | 朝向目标点 |
| `LookAt(target, up)` | `void LookAt(const XCEngine::Math::Vector3& target, const XCEngine::Math::Vector3& up)` | ❌ 缺失 | 朝向目标点(指定上向量) |
| `Rotate(eulers)` | `void Rotate(const XCEngine::Math::Vector3& eulers)` | ❌ 缺失 | 欧拉角旋转 |
| `Rotate(axis, angle)` | `void Rotate(const XCEngine::Math::Vector3& axis, float angle)` | ❌ 缺失 | 轴角旋转 |
| `Translate(translation)` | `void Translate(const XCEngine::Math::Vector3& translation)` | ❌ 缺失 | 平移(世界空间) |
| `Translate(translation, relativeTo)` | `void Translate(const XCEngine::Math::Vector3& translation, Space relativeTo)` | ❌ 缺失 | 平移(指定空间) |
### 2.6 点/方向变换
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `TransformPoint()` | `XCEngine::Math::Vector3 TransformPoint(const XCEngine::Math::Vector3& point) const` | ❌ 缺失 | 变换点(含平移旋转) |
| `InverseTransformPoint()` | `XCEngine::Math::Vector3 InverseTransformPoint(const XCEngine::Math::Vector3& point) const` | ❌ 缺失 | 逆变换点 |
| `TransformDirection()` | `XCEngine::Math::Vector3 TransformDirection(const XCEngine::Math::Vector3& direction) const` | ❌ 缺失 | 变换方向(仅旋转) |
| `InverseTransformDirection()` | `XCEngine::Math::Vector3 InverseTransformDirection(const XCEngine::Math::Vector3& direction) const` | ❌ 缺失 | 逆变换方向 |
### 2.7 脏标记与缓存
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `SetDirty()` | `void SetDirty()` | ⚠️ 局部 | UI Editor 只有简单 flag缺少完整缓存机制 |
| 缓存成员 | `mutable XCEngine::Math::Matrix4x4 m_localToWorldMatrix` 等 | ❌ 缺失 | 矩阵缓存 |
| 更新方法 | `void UpdateWorldTransform() const` | ❌ 缺失 | 缓存失效时重新计算 |
**当前 UI Editor 实现**
```cpp
class TransformComponent : public Component {
public:
float position[3] = {0.0f, 0.0f, 0.0f}; // 仅有本地坐标
float rotation[3] = {0.0f, 0.0f, 0.0f}; // 欧拉角
float scale[3] = {1.0f, 1.0f, 1.0f}; // 仅有本地缩放
// 缺少世界空间方法、矩阵缓存、父子指针等
};
```
**架构设计要求**(应使用 Engine Math 类型):
```cpp
class TransformComponent : public Component {
public:
// 使用 Engine Math 类型
XCEngine::Math::Vector3 GetLocalPosition() const { return m_localPosition; }
void SetLocalPosition(const XCEngine::Math::Vector3& pos) { m_localPosition = pos; SetDirty(); }
XCEngine::Math::Quaternion GetLocalRotation() const { return m_localRotation; }
void SetLocalRotation(const XCEngine::Math::Quaternion& rot) { m_localRotation = rot; SetDirty(); }
XCEngine::Math::Vector3 GetLocalScale() const { return m_localScale; }
void SetLocalScale(const XCEngine::Math::Vector3& sc) { m_localScale = sc; SetDirty(); }
private:
XCEngine::Math::Vector3 m_localPosition = XCEngine::Math::Vector3::Zero();
XCEngine::Math::Quaternion m_localRotation = XCEngine::Math::Quaternion::Identity();
XCEngine::Math::Vector3 m_localScale = XCEngine::Math::Vector3::One();
TransformComponent* m_parent = nullptr;
std::vector<TransformComponent*> m_children;
mutable XCEngine::Math::Matrix4x4 m_localToWorldMatrix;
mutable XCEngine::Math::Matrix4x4 m_worldToLocalMatrix;
mutable XCEngine::Math::Vector3 m_worldPosition;
mutable XCEngine::Math::Quaternion m_worldRotation;
mutable XCEngine::Math::Vector3 m_worldScale;
mutable bool m_dirty = true;
void UpdateWorldTransform() const;
};
```
---
## 三、GameObject缺口
### 3.1 基础方法
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetTransform()` | `TransformComponent& GetTransform()` | ❌ 缺失 | 获取变换组件引用 |
| `GetScene()` | `Scene* GetScene() const` | ❌ 缺失 | 获取所属场景 |
### 3.2 组件查找
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetComponentInChildren<T>()` | `T* GetComponentInChildren() const` | ❌ 缺失 | 在子实体中查找组件 |
| `GetComponentsInChildren<T>()` | `std::vector<T*> GetComponentsInChildren() const` | ❌ 缺失 | 获取所有子实体中的组件 |
| `GetComponentInParent<T>()` | `T* GetComponentInParent() const` | ❌ 缺失 | 在父实体中查找组件 |
### 3.3 父子层级
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `SetParent(parent)` | `void SetParent(Entity* parent)` | ❌ 缺失 | 2参数版本含 worldPositionStays |
| `GetChildren()` | `const std::vector<Entity*>& GetChildren() const` | ❌ 缺失 | 获取子实体列表 |
| `GetChild()` | `Entity* GetChild(int index) const` | ❌ 缺失 | 按索引获取子实体 |
### 3.4 激活状态
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `active` 成员 | `bool active = true` | ❌ 缺失 | 实体激活状态 |
| `SetActive()` | `void SetActive(bool active)` | ❌ 缺失 | 设置激活状态 |
| `IsActive()` | `bool IsActive() const` | ❌ 缺失 | 检查激活状态 |
| `IsActiveInHierarchy()` | `bool IsActiveInHierarchy() const` | ❌ 缺失 | 检查层级中的激活状态 |
### 3.5 静态查找
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `Find()` | `static Entity* Find(const String& name)` | ❌ 缺失 | 按名称查找实体 |
| `FindObjectsOfType()` | `static std::vector<Entity*> FindObjectsOfType()` | ❌ 缺失 | 查找所有实体 |
| `FindGameObjectsWithTag()` | `static std::vector<Entity*> FindGameObjectsWithTag(const String& tag)` | ❌ 缺失 | 按标签查找 |
### 3.6 销毁
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `Destroy()` | `void Destroy()` | ❌ 缺失 | 实例方法销毁(通过 Scene |
---
## 四、Scene / SceneManager 缺口
### 4.1 Scene 类
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `GetName()` | `const String& GetName() const` | ❌ 缺失 | 获取场景名称 |
| `SetName()` | `void SetName(const String& name)` | ❌ 缺失 | 设置场景名称 |
| `CreateGameObject(name, parent)` | `Entity* CreateGameObject(const String& name, Entity* parent)` | ❌ 缺失 | 带父实体创建 |
| `GetRootGameObjects()` | `std::vector<Entity*> GetRootGameObjects() const` | ❌ 缺失 | 获取根实体列表 |
| `Find()` | `Entity* Find(const String& name) const` | ❌ 缺失 | 查找实体 |
| `FindGameObjectWithTag()` | `Entity* FindGameObjectWithTag(const String& tag) const` | ❌ 缺失 | 按标签查找 |
| `FindObjectsOfType<T>()` | `std::vector<T*> FindObjectsOfType() const` | ❌ 缺失 | 模板查找 |
| `FindObjectOfType<T>()` | `T* FindObjectOfType() const` | ❌ 缺失 | 查找单个 |
| `IsActive()` | `bool IsActive() const` | ❌ 缺失 | 场景激活状态 |
| `SetActive()` | `void SetActive(bool active)` | ❌ 缺失 | 设置激活状态 |
| `Load()` | `void Load(const String& filePath)` | ❌ 缺失 | 加载场景 |
| `Save()` | `void Save(const String& filePath)` | ❌ 缺失 | 保存场景 |
| `Update()` | `void Update(float deltaTime)` | ❌ 缺失 | 场景更新 |
| `FixedUpdate()` | `void FixedUpdate(float fixedDeltaTime)` | ❌ 缺失 | 物理更新 |
| `LateUpdate()` | `void LateUpdate(float deltaTime)` | ❌ 缺失 | 晚更新 |
**当前 UI Editor 缺失 Scene 概念**,所有实体都在 SceneManager 全局管理。
### 4.2 SceneManager 类
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| `CreateScene()` | `Scene* CreateScene(const String& name)` | ❌ 缺失 | 创建场景 |
| `LoadScene()` | `void LoadScene(const String& filePath)` | ❌ 缺失 | 同步加载 |
| `LoadSceneAsync()` | `void LoadSceneAsync(const String& filePath, std::function<void(Scene*)> callback)` | ❌ 缺失 | 异步加载 |
| `UnloadScene(scene)` | `void UnloadScene(Scene* scene)` | ❌ 缺失 | 卸载场景 |
| `UnloadScene(name)` | `void UnloadScene(const String& sceneName)` | ❌ 缺失 | 按名称卸载 |
| `SetActiveScene(scene)` | `void SetActiveScene(Scene* scene)` | ❌ 缺失 | 设置激活场景 |
| `SetActiveScene(name)` | `void SetActiveScene(const String& sceneName)` | ❌ 缺失 | 按名称设置 |
| `GetActiveScene()` | `Scene* GetActiveScene() const` | ❌ 缺失 | 获取激活场景 |
| `GetScene(name)` | `Scene* GetScene(const String& name) const` | ❌ 缺失 | 按名称获取场景 |
| `GetAllScenes()` | `std::vector<Scene*> GetAllScenes() const` | ❌ 缺失 | 获取所有场景 |
| `OnSceneLoaded` | `Event<Scene*>` | ⚠️ 差异 | UI Editor 是 `OnSceneChanged` |
| `OnSceneUnloaded` | `Event<Scene*>` | ❌ 缺失 | 场景卸载事件 |
| `OnActiveSceneChanged` | `Event<Scene*>` | ❌ 缺失 | 激活场景变更事件 |
### 4.3 GameObjectBuilder 类
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|------------|---------|------|------|
| 完整类 | `class GameObjectBuilder` | ❌ 缺失 | 实体创建辅助类 |
---
## 五、实现优先级
### P0 - 核心缺失(必须实现)
| 模块 | 原因 | 涉及文件 |
|-----|------|---------|
| TransformComponent 世界空间方法 | Editor 变换编辑基础 | GameObject.h |
| Entity `GetTransform()` | Inspector 面板需要 | GameObject.h |
| Component `transform()` | 组件访问变换 | GameObject.h |
**依赖**Engine Math 模块 ✅ 已实现,可直接使用 `XCEngine::Math::Vector3` / `Quaternion` / `Matrix4x4`
### P1 - 功能完整(应该实现)
| 模块 | 原因 | 涉及文件 |
|-----|------|---------|
| TransformComponent 矩阵缓存 | 性能优化 | GameObject.h |
| TransformComponent 父子指针 | 层级操作 | GameObject.h |
| Entity 激活状态 | 运行时状态 | GameObject.h |
| Entity 父子层级操作 | 层级编辑 | GameObject.h |
| Scene 概念 | 多场景支持 | SceneManager.h |
| SceneManager 多场景管理 | 场景隔离 | SceneManager.h |
### P2 - 高级功能(可选实现)
| 模块 | 原因 | 涉及文件 |
|-----|------|---------|
| Scene 序列化/反序列化 | 持久化 | SceneManager.h |
| TransformComponent 点/方向变换 | Gizmos 操作 | GameObject.h |
| SceneManager 异步加载 | 体验优化 | SceneManager.h |
| Entity 静态查找 | Editor 操作 | GameObject.h |
---
## 六、数据结构对齐表
### 6.1 当前 UI Editor vs 架构设计
| 项目 | UI Editor 当前 | 架构设计 | 对齐建议 |
|-----|--------------|---------|---------|
| 实体内组件存储 | `vector<unique_ptr<Component>>` | `vector<unique_ptr<Component>>` | ✅ 已对齐 |
| 变换类型 | `float[3]` 数组 | `XCEngine::Math::Vector3` / `Quaternion` | ⚠️ **应改用 Engine Math** |
| 父子关系 | `EntityID` (uint64) | `Entity*` 指针 | ⚠️ UI 用 IDEngine 用指针 |
| 场景管理 | 全局单例 SceneManager | 多 Scene + SceneManager | ❌ 需要重构 |
### 6.2 命名差异
| UI Editor | Engine 架构设计 | 说明 |
|-----------|---------------|------|
| `Entity` | `GameObject` | UI Editor 使用 Entity 避免与 std 冲突 |
| `EntityID` | `uint64_t` | UI Editor 自定义类型别名 |
| `INVALID_ENTITY_ID` | N/A | UI Editor 自定义常量 |
| `ComponentRegistry` | `ComponentTypeRegistry` | 功能相似,命名不同 |
### 6.3 类型复用情况
| Engine 类型 | UI Editor 现状 | 建议 |
|-----------|--------------|------|
| `XCEngine::Math::Vector3` | UI Editor 用 `float[3]` | **直接复用** |
| `XCEngine::Math::Quaternion` | UI Editor 用 `float[3]` (欧拉角) | **直接复用**,但欧拉角存储可保留 |
| `XCEngine::Math::Matrix4x4` | 无 | **直接复用** |
| `XCEngine::Core::Event<T>` | ✅ 已复用 | 保持 |
| `XCEngine::Debug::LogLevel` | ✅ 已复用 | 保持 |
---
## 七、依赖关系分析
```
实现优先级 - 依赖链:
P0: TransformComponent 世界空间
├── 需要: XCEngine::Math::Vector3 ✅ 已实现
├── 需要: XCEngine::Math::Quaternion ✅ 已实现
├── 需要: XCEngine::Math::Matrix4x4 ✅ 已实现
└── 影响: Entity::GetTransform()
P0: Component::transform()
└── 依赖: Entity::GetTransform()
P1: TransformComponent 父子指针
├── 需要: TransformComponent* parent
├── 需要: vector<TransformComponent*> children
└── 影响: Entity 父子操作
P1: Entity 激活状态
├── 需要: active 成员
└── 影响: IsActiveInHierarchy()
P1: Scene 多场景
├── 需要: Scene 类
├── 需要: Scene::CreateGameObject()
└── 影响: SceneManager 重构
```
---
## 八、与 Unity 架构的对照
### Unity 传统架构 (GameObject-Component)
```
Scene
└── GameObject ("MyEntity")
├── Transform
├── MeshRenderer
└── (其他组件...)
GameObject.GetComponent<T>() → Entity.GetComponent<T>()
GameObject.transform → Entity.GetTransform()
GameObject.SetActive(bool) → 缺失
GameObject.Find("name") → 缺失
Scene.GetRootGameObjects() → 缺失
SceneManager.GetActiveScene() → 缺失
```
### UI Editor 当前实现
```
SceneManager (全局单例)
└── m_entities (unordered_map<EntityID, Entity>)
└── Entity ("MyEntity")
├── id, name, parent, children
├── components (vector<unique_ptr<Component>>)
└── selected
Entity.AddComponent<T>() → ✅ 已实现
Entity.GetComponent<T>() → ✅ 已实现
Entity.GetTransform() → ❌ 缺失
TransformComponent 世界空间方法 → ❌ 缺失
```
---
## 九、附录:当前 GameObject.h 完整内容
```cpp
// ui_editor/src/Core/GameObject.h 当前实现
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>
#include <functional>
#include <cstdint>
#include <XCEngine/Core/Event.h>
namespace UI {
using EntityID = uint64_t;
constexpr EntityID INVALID_ENTITY_ID = 0;
class Component {
public:
virtual ~Component() = default;
virtual std::string GetName() const = 0;
virtual void Awake() {}
virtual void Start() {}
virtual void Update(float deltaTime) {}
virtual void OnDestroy() {}
class Entity* GetEntity() const { return m_entity; }
bool IsEnabled() const { return m_enabled; }
void SetEnabled(bool enabled) { m_enabled = enabled; }
protected:
class Entity* m_entity = nullptr;
bool m_enabled = true;
friend class Entity;
};
class TransformComponent : public Component {
public:
float position[3] = {0.0f, 0.0f, 0.0f};
float rotation[3] = {0.0f, 0.0f, 0.0f};
float scale[3] = {1.0f, 1.0f, 1.0f};
std::string GetName() const override { return "Transform"; }
};
class MeshRendererComponent : public Component {
public:
std::string materialName = "Default-Material";
std::string meshName = "";
std::string GetName() const override { return "Mesh Renderer"; }
};
class Entity {
public:
EntityID id = INVALID_ENTITY_ID;
std::string name;
EntityID parent = INVALID_ENTITY_ID;
std::vector<EntityID> children;
std::vector<std::unique_ptr<Component>> components;
bool selected = false;
template<typename T, typename... Args>
T* AddComponent(Args&&... args) {
auto comp = std::make_unique<T>(std::forward<Args>(args)...);
comp->m_entity = this;
T* ptr = comp.get();
components.push_back(std::move(comp));
return ptr;
}
template<typename T>
T* GetComponent() {
for (auto& comp : components) {
if (auto casted = dynamic_cast<T*>(comp.get())) {
return casted;
}
}
return nullptr;
}
template<typename T>
std::vector<T*> GetComponents() {
std::vector<T*> result;
for (auto& comp : components) {
if (auto casted = dynamic_cast<T*>(comp.get())) {
result.push_back(casted);
}
}
return result;
}
};
using ComponentInspectorFn = std::function<void(Component*)>;
struct ComponentInspectorInfo {
std::string name;
ComponentInspectorFn renderFn;
};
class ComponentRegistry {
public:
static ComponentRegistry& Get() {
static ComponentRegistry instance;
return instance;
}
template<typename T>
void RegisterComponent(const std::string& name, ComponentInspectorFn inspectorFn) {
m_inspectors[name] = {name, inspectorFn};
m_factories[name] = []() -> std::unique_ptr<Component> {
return std::make_unique<T>();
};
}
ComponentInspectorInfo* GetInspector(const std::string& name) {
auto it = m_inspectors.find(name);
if (it != m_inspectors.end()) {
return &it->second;
}
return nullptr;
}
private:
ComponentRegistry() = default;
std::unordered_map<std::string, ComponentInspectorInfo> m_inspectors;
std::unordered_map<std::string, std::function<std::unique_ptr<Component>()>> m_factories;
};
} // namespace UI
```
---
**文档结束**

File diff suppressed because it is too large Load Diff

View File

@@ -1,546 +0,0 @@
# XCEngine 输入系统设计与实现
> **文档信息**
> - **版本**: 1.0
> - **日期**: 2026-03-22
> - **状态**: 设计文档
> - **目标**: 设计引擎级输入系统
---
## 1. 概述
### 1.1 设计目标
XCEngine 输入系统需要提供:
1. **统一的跨平台输入抽象** - 支持键盘、鼠标、手柄、触摸
2. **与引擎架构无缝集成** - 使用现有的 `Core::Event` 系统
3. **轮询 + 事件混合模式** - 既支持 `IsKeyDown()` 轮询,也支持事件回调
4. **UI 系统支持** - 为 UI 组件 (Button, Slider 等) 提供指针事件
### 1.2 当前状态分析
| 模块 | 状态 | 说明 |
|-----|------|------|
| Core::Event | ✅ 完备 | 线程安全Subscribe/Unsubscribe 模式 |
| RHI::RHISwapChain | ⚠️ PollEvents 空实现 | 需要填充 Windows 消息泵 |
| 现有 Input (mvs) | ❌ 耦合 Windows | 直接处理 HWND 消息,不适合引擎架构 |
| Platform/Window | ❌ 不存在 | 需要新建 |
---
## 2. 架构设计
### 2.1 模块结构
```
engine/include/XCEngine/
├── Core/
│ └── Event.h # 已有,复用
├── Input/
│ ├── InputTypes.h # 枚举和结构体定义
│ ├── InputEvent.h # 输入事件结构体
│ ├── InputManager.h # 输入管理器(单例)
│ └── InputModule.h # 平台相关输入处理模块接口
└── Platform/
├── PlatformTypes.h # 平台类型抽象
├── Window.h # 窗口抽象接口
└── Windows/
├── WindowsPlatform.h # Windows 平台实现
└── WindowsInputModule.h # Windows 输入模块实现
```
### 2.2 核心设计原则
1. **事件驱动 + 状态轮询双模式**
- 事件:用于 UI 交互、一次性按键响应
- 轮询:用于连续输入检测(角色移动、视角控制)
2. **平台抽象层**
- `InputModule` 接口:抽象平台特定的输入处理
- `Window` 接口:抽象平台特定的窗口管理
3. **与现有引擎组件集成**
- 使用 `Core::Event` 作为事件系统
- 使用 `Math::Vector2` 作为 2D 坐标类型
---
## 3. 详细设计
### 3.1 输入类型定义 (`InputTypes.h`)
```cpp
#pragma once
#include "Core/Types.h"
#include "Math/Vector2.h"
namespace XCEngine {
namespace Input {
enum class KeyCode : uint8 {
None = 0,
A = 4, B = 5, C = 6, D = 7, E = 8, F = 9, G = 10,
H = 11, I = 12, J = 13, K = 14, L = 15, M = 16, N = 17,
O = 18, P = 19, Q = 20, R = 21, S = 22, T = 23, U = 24,
V = 25, W = 26, X = 27, Y = 28, Z = 29,
F1 = 58, F2 = 59, F3 = 60, F4 = 61, F5 = 62, F6 = 63,
F7 = 64, F8 = 65, F9 = 66, F10 = 67, F11 = 68, F12 = 69,
Space = 49, Tab = 48, Enter = 36, Escape = 53,
LeftShift = 56, RightShift = 60, LeftCtrl = 59, RightCtrl = 62,
LeftAlt = 58, RightAlt = 61,
Up = 126, Down = 125, Left = 123, Right = 124,
Home = 115, End = 119, PageUp = 116, PageDown = 121,
Delete = 51, Backspace = 51
};
enum class MouseButton : uint8 {
Left = 0,
Right = 1,
Middle = 2,
Button4 = 3,
Button5 = 4
};
enum class JoystickButton : uint8 {
Button0 = 0,
Button1 = 1,
Button2 = 2,
// ... Xbox/PlayStation 标准按钮
};
enum class JoystickAxis : uint8 {
LeftX = 0,
LeftY = 1,
RightX = 2,
RightY = 3,
LeftTrigger = 4,
RightTrigger = 5
};
} // namespace Input
} // namespace XCEngine
```
### 3.2 输入事件结构体 (`InputEvent.h`)
```cpp
#pragma once
#include "InputTypes.h"
#include "Math/Vector2.h"
#include "Containers/String.h"
namespace XCEngine {
namespace Input {
struct KeyEvent {
KeyCode keyCode;
bool alt;
bool ctrl;
bool shift;
bool meta;
enum Type { Down, Up, Repeat } type;
};
struct MouseButtonEvent {
MouseButton button;
Math::Vector2 position;
enum Type { Pressed, Released } type;
};
struct MouseMoveEvent {
Math::Vector2 position;
Math::Vector2 delta; // 相对上一帧的移动量
};
struct MouseWheelEvent {
Math::Vector2 position;
float delta; // 滚轮滚动量
};
struct TextInputEvent {
char character;
Containers::String text; // 完整输入文本
};
struct TouchState {
int touchId;
Math::Vector2 position;
Math::Vector2 deltaPosition;
float deltaTime;
int tapCount;
enum Phase { Began, Moved, Stationary, Ended, Canceled } phase;
};
} // namespace Input
} // namespace XCEngine
```
### 3.3 输入管理器 (`InputManager.h`)
```cpp
#pragma once
#include "Core/Event.h"
#include "InputTypes.h"
#include "InputEvent.h"
#include "Math/Vector2.h"
namespace XCEngine {
namespace Input {
class InputManager {
public:
static InputManager& Get();
void Initialize(void* platformWindowHandle);
void Shutdown();
void Update(); // 每帧调用,更新输入状态
// ============ 轮询接口 ============
// 键盘
bool IsKeyDown(KeyCode key) const;
bool IsKeyUp(KeyCode key) const;
bool IsKeyPressed(KeyCode key) const; // 当前帧按下
// 鼠标
Math::Vector2 GetMousePosition() const;
Math::Vector2 GetMouseDelta() const;
float GetMouseScrollDelta() const;
bool IsMouseButtonDown(MouseButton button) const;
bool IsMouseButtonUp(MouseButton button) const;
bool IsMouseButtonClicked(MouseButton button) const; // 当前帧点击
// 手柄
int GetJoystickCount() const;
bool IsJoystickConnected(int joystick) const;
Math::Vector2 GetJoystickAxis(int joystick, JoystickAxis axis) const;
bool IsJoystickButtonDown(int joystick, JoystickButton button) const;
// ============ 事件接口 ============
Core::Event<const KeyEvent&>& OnKeyEvent() { return m_onKeyEvent; }
Core::Event<const MouseButtonEvent&>& OnMouseButton() { return m_onMouseButton; }
Core::Event<const MouseMoveEvent&>& OnMouseMove() { return m_onMouseMove; }
Core::Event<const MouseWheelEvent&>& OnMouseWheel() { return m_onMouseWheel; }
Core::Event<const TextInputEvent&>& OnTextInput() { return m_onTextInput; }
// 内部方法(供 PlatformInputModule 调用)
void ProcessKeyDown(KeyCode key, bool repeat);
void ProcessKeyUp(KeyCode key);
void ProcessMouseMove(int x, int y, int deltaX, int deltaY);
void ProcessMouseButton(MouseButton button, bool pressed, int x, int y);
void ProcessMouseWheel(float delta, int x, int y);
void ProcessTextInput(char c);
private:
InputManager() = default;
~InputManager() = default;
void* m_platformWindowHandle = nullptr;
// 键盘状态
std::vector<bool> m_keyDownThisFrame;
std::vector<bool> m_keyDownLastFrame;
std::vector<bool> m_keyDown;
// 鼠标状态
Math::Vector2 m_mousePosition;
Math::Vector2 m_mouseDelta;
float m_mouseScrollDelta = 0.0f;
std::vector<bool> m_mouseButtonDownThisFrame;
std::vector<bool> m_mouseButtonDownLastFrame;
std::vector<bool> m_mouseButtonDown;
// 事件
Core::Event<const KeyEvent&> m_onKeyEvent;
Core::Event<const MouseButtonEvent&> m_onMouseButton;
Core::Event<const MouseMoveEvent&> m_onMouseMove;
Core::Event<const MouseWheelEvent&> m_onMouseWheel;
Core::Event<const TextInputEvent&> m_onTextInput;
};
} // namespace Input
} // namespace XCEngine
```
### 3.4 平台输入模块接口 (`InputModule.h`)
```cpp
#pragma once
namespace XCEngine {
namespace Input {
class InputModule {
public:
virtual ~InputModule() = default;
virtual void Initialize(void* windowHandle) = 0;
virtual void Shutdown() = 0;
virtual void PumpEvents() = 0; // 抽取并处理平台事件
protected:
InputModule() = default;
};
} // namespace Input
} // namespace XCEngine
```
### 3.5 Windows 输入模块实现 (`WindowsInputModule.h`)
```cpp
#pragma once
#include "InputModule.h"
#include <Windows.h>
namespace XCEngine {
namespace Input {
namespace Platform {
class WindowsInputModule : public InputModule {
public:
void Initialize(void* windowHandle) override;
void Shutdown() override;
void PumpEvents() override;
// 供 Window 调用的消息处理
void HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
private:
void ProcessKeyDown(WPARAM wParam, LPARAM lParam);
void ProcessKeyUp(WPARAM wParam, LPARAM lParam);
void ProcessMouseMove(WPARAM wParam, LPARAM lParam);
void ProcessMouseButton(WPARAM wParam, LPARAM lParam, bool pressed);
void ProcessMouseWheel(WPARAM wParam, LPARAM lParam);
void ProcessCharInput(WPARAM wParam, LPARAM lParam);
HWND m_hwnd = nullptr;
bool m_captureMouse = false;
};
} // namespace Platform
} // namespace Input
} // namespace XCEngine
```
---
## 4. 与引擎其他模块的集成
### 4.1 与 RHI/SwapChain 的集成
`RHISwapChain::PollEvents()` 需要调用 `InputManager::Update()` 和平台输入模块的 `PumpEvents()`:
```cpp
// D3D12SwapChain.cpp
void D3D12SwapChain::PollEvents() {
// 抽取 Windows 消息
if (m_inputModule) {
m_inputModule->PumpEvents();
}
// 更新输入管理器状态
Input::InputManager::Get().Update();
// 处理关闭请求
MSG msg;
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
m_shouldClose = true;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
```
### 4.2 与 UI 系统的集成
UI 组件 (Button, Slider 等) 通过订阅 `InputManager` 的事件来响应用户输入:
```cpp
// ButtonComponent.cpp
void ButtonComponent::Update(float deltaTime) {
if (!IsEnabled()) return;
auto& input = Input::InputManager::Get();
Vector2 mousePos = input.GetMousePosition();
// 射线检测是否悬停在按钮上
if (IsPointInRect(mousePos, m_rect)) {
if (input.IsMouseButtonClicked(Input::MouseButton::Left)) {
OnClick.Invoke();
}
}
}
```
### 4.3 与场景生命周期的集成
```cpp
// Scene.cpp
void Scene::Awake() {
// 初始化输入系统
Input::InputManager::Get().Initialize(m_windowHandle);
}
void Scene::OnDestroy() {
Input::InputManager::Get().Shutdown();
}
```
---
## 5. Windows 消息到引擎事件的映射
| Windows Message | Engine Event |
|----------------|---------------|
| WM_KEYDOWN | `InputManager::ProcessKeyDown` |
| WM_KEYUP | `InputManager::ProcessKeyUp` |
| WM_CHAR | `InputManager::ProcessTextInput` |
| WM_MOUSEMOVE | `InputManager::ProcessMouseMove` |
| WM_LBUTTONDOWN/RBUTTONDOWN/MBUTTONDOWN | `InputManager::ProcessMouseButton` |
| WM_LBUTTONUP/RBUTTONUP/MBUTTONUP | `InputManager::ProcessMouseButton` |
| WM_MOUSEWHEEL | `InputManager::ProcessMouseWheel` |
### 5.1 Windows VK 码到 KeyCode 的映射
```cpp
// WindowsVKToKeyCode 映射表
static const KeyCode VK_TO_KEYCODE[] = {
// VK 0-29 对应 KeyCode A-Z
KeyCode::A, KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E,
KeyCode::F, KeyCode::G, KeyCode::H, KeyCode::I, KeyCode::J,
KeyCode::K, KeyCode::L, KeyCode::M, KeyCode::N, KeyCode::O,
KeyCode::P, KeyCode::Q, KeyCode::R, KeyCode::S, KeyCode::T,
KeyCode::U, KeyCode::V, KeyCode::W, KeyCode::X, KeyCode::Y,
KeyCode::Z,
// ... 其他映射
};
```
---
## 6. 实现计划
### Phase 1: 核心输入系统
| 任务 | 文件 | 说明 |
|-----|------|------|
| 创建 Input 模块目录 | `engine/include/XCEngine/Input/` | |
| 实现 InputTypes.h | KeyCode, MouseButton 等枚举 | |
| 实现 InputEvent.h | 事件结构体 | |
| 实现 InputManager.h/cpp | 单例管理器 | |
| 实现 WindowsInputModule.h/cpp | Windows 平台实现 | |
### Phase 2: 与 RHI 集成
| 任务 | 文件 | 说明 |
|-----|------|------|
| 修改 RHISwapChain | 添加 InputModule 成员 | |
| 实现 D3D12SwapChain::PollEvents | 填充消息泵逻辑 | |
| 实现 OpenGLSwapChain::PollEvents | GL 消息处理 | |
### Phase 3: UI 输入支持
| 任务 | 说明 |
|-----|------|
| 实现 Canvas 组件的射线检测 | 将屏幕坐标转换为 UI 元素 |
| Button/Slider 事件集成 | 使用 InputManager 事件 |
---
## 7. 使用示例
### 7.1 轮询模式(角色移动)
```cpp
void PlayerController::Update(float deltaTime) {
auto& input = Input::InputManager::Get();
if (input.IsKeyDown(Input::KeyCode::W)) {
m_velocity.z = 1.0f;
} else if (input.IsKeyDown(Input::KeyCode::S)) {
m_velocity.z = -1.0f;
}
if (input.IsKeyDown(Input::KeyCode::A)) {
m_velocity.x = -1.0f;
} else if (input.IsKeyDown(Input::KeyCode::D)) {
m_velocity.x = 1.0f;
}
}
```
### 7.2 事件模式UI 交互)
```cpp
class MyUIButton : public Component {
uint64_t m_clickHandlerId = 0;
void Awake() override {
m_clickHandlerId = Input::InputManager::Get().OnMouseButton().Subscribe(
[this](const Input::MouseButtonEvent& event) {
if (event.type == Input::MouseButtonEvent::Pressed &&
event.button == Input::MouseButton::Left &&
IsPointInButton(event.position)) {
OnClicked();
}
}
);
}
void OnDestroy() override {
Input::InputManager::Get().OnMouseButton().Unsubscribe(m_clickHandlerId);
}
};
```
### 7.3 文本输入
```cpp
void InputFieldComponent::Update(float deltaTime) {
auto& input = Input::InputManager::Get();
uint64_t textHandlerId = input.OnTextInput().Subscribe(
[this](const Input::TextInputEvent& event) {
if (m_isFocused) {
m_text += event.character;
OnValueChanged.Invoke(m_text);
}
}
);
// 清理
input.OnTextInput().Unsubscribe(textHandlerId);
}
```
---
## 8. 待讨论问题
1. **多窗口支持**: 引擎是否需要支持多窗口?如果是,输入系统如何路由事件到正确的窗口?
2. **手柄/游戏手柄支持**: 是否需要实现 XInput 支持?这会增加复杂性。
3. **原始输入 vs 输入模式**: Unity 有 "Input.GetAxis" vs "Input.GetButtonDown"。是否需要实现类似的轴概念?
4. **IME/输入法支持**: 对于中文、日文输入,是否需要特殊处理?
5. **平台模块位置**: `InputModule` 放在 `Input/` 还是 `Platform/` 命名空间下?
---
## 9. 结论
XCEngine 输入系统设计遵循以下核心原则:
1. **复用现有 Core::Event** - 不重复造轮子
2. **平台抽象** - 通过 `InputModule` 接口隔离平台差异
3. **双模式支持** - 轮询 + 事件,兼顾性能和响应性
4. **与 UI 架构协同** - 为 Canvas/Button 提供必要的事件支持
该设计参考了 Unity 的 Input 系统架构,同时适配了 XCEngine 现有的组件模式和事件系统。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,151 +0,0 @@
# XCEngine 游戏引擎 - 第一阶段计划
> **目标**: 构建核心基础层,为上层渲染系统提供底层依赖
> **版本**: 1.0
> **日期**: 2026-03-13
---
## 阶段目标
第一阶段聚焦于引擎底层基础设施的建设,确保后续渲染系统开发有稳定的基础。
---
## 模块规划
### 1.1 数学库 (Math Library)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 5天 |
| **包含类型** | `Vector2`, `Vector3`, `Vector4`, `Matrix3x3`, `Matrix4x4`, `Quaternion`, `Transform`, `Color`, `Rect`, `RectInt`, `Viewport`, `Ray`, `Sphere`, `Box`, `Plane`, `Frustum`, `Bounds`, `AABB`, `OBB` |
| **功能要求** | 向量运算、矩阵变换、四元数、欧拉角转换、视锥体剔除基础 |
### 1.2 Core 基础类型
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 2天 |
| **包含类型** | 基础类型别名 (`int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64`, `byte`)、`RefCounted``Ref<T>``UniqueRef<T>``Event<T>` |
| **功能要求** | 基础类型别名、引用计数、智能指针、事件系统 |
| **依赖** | 无 |
### 1.3 线程系统 (Threading)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 4天 |
| **包含类型** | `ITask`, `LambdaTask`, `TaskGroup`, `TaskSystem`, `TaskSystemConfig`, `Mutex`, `SpinLock`, `ReadWriteLock`, `Thread` |
| **功能要求** | 任务调度、依赖管理、并行计算、同步原语 |
| **依赖** | Core基础类型 |
### 1.4 内存管理 (Memory Management)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 3天 |
| **包含类型** | `IAllocator`, `LinearAllocator`, `PoolAllocator`, `ProxyAllocator`, `MemoryManager` |
| **功能要求** | 内存分配、追踪、泄漏检测、线性/池化分配策略 |
| **依赖** | 线程系统ProxyAllocator需要Mutex |
### 1.5 容器库 (Containers)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 3天 |
| **包含类型** | `Array<T>`, `String`, `String::npos`, `HashMap<K,V>` |
| **功能要求** | 动态数组、字符串操作含npos常量、哈希映射 |
| **依赖** | Core基础类型, 内存管理 |
### 1.6 日志与调试系统
| 项目 | 内容 |
|------|------|
| **优先级** | P1 |
| **预计工作量** | 2天 |
| **包含类型** | `Logger`, `ConsoleLogSink`, `FileLogSink`, `FileWriter`, `Profiler`, `Assert` |
| **功能要求** | 分级日志、分类输出、文件写入、性能分析、断言 |
| **依赖** | Core基础类型, 容器库(String) |
---
## 时间安排
| 周次 | 内容 |
|------|------|
| 第1周 | 数学库 + Core基础类型 |
| 第2周 | 线程系统 + 内存管理 |
| 第3周 | 容器库 + 日志系统 |
> 内存管理依赖线程系统完成ProxyAllocator需要Mutex因此调整顺序
---
## 测试方案
### 测试框架
- **推荐**: Google Test (gtest) 或 Doctest
### 测试用例设计
| 模块 | 测试类别 | 测试用例示例 |
|------|---------|-------------|
| **Math** | 向量运算 | `Vector3::Dot`, `Cross`, `Normalize`, `Lerp` 精度测试 |
| **Math** | 矩阵运算 | `Matrix4x4::TRS`, `LookAt`, `Perspective` 结果正确性 |
| **Math** | 四元数 | `FromEulerAngles`, `Slerp`, `ToMatrix4x4` 精度验证 |
| **Core** | 引用计数 | `RefCounted` 多线程安全释放 |
| **Core** | 事件系统 | 订阅/取消订阅、线程安全调用 |
| **Threading** | 任务系统 | 依赖链、优先级、并行For、TaskSystemConfig |
| **Threading** | 同步原语 | 锁竞争、死锁检测 |
| **Memory** | 分配器 | 边界检查、碎片率、线性分配器回滚测试 |
| **Memory** | 泄漏检测 | 分配/释放计数、峰值追踪 |
| **Containers** | Array | 边界访问、迭代器、内存增长策略 |
| **Containers** | String | 子串、查找、大小写转换 |
| **Containers** | HashMap | 冲突处理、负载因子重分布 |
| **Logger** | 日志级别 | 过滤、分类、格式化 |
### 执行命令
```bash
# 编译并运行所有单元测试
cmake --build build --target xcengine_tests
./build/tests/xcengine_tests.exe
# 性能基准测试
./build/tests/xcengine_tests.exe --benchmark
```
---
## 验收标准
- [ ] 数学库通过全部运算正确性测试
- [ ] Core基础类型引用计数、智能指针工作正常
- [ ] 线程系统在高并发下稳定运行
- [ ] 内存分配器无内存泄漏
- [ ] 容器操作边界安全
- [ ] 日志系统输出格式正确
---
## 依赖关系
```
Math Library (无依赖)
├──▶ Core 基础类型 (无依赖)
│ │
│ ├──▶ Threading (依赖 Core)
│ │
│ ├──▶ Memory Management (依赖 Threading)
│ │ │
│ │ └──▶ Containers (依赖 Memory, Core)
│ │
│ └──▶ Logging & Debug (依赖 Core, Containers)
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,922 +0,0 @@
# XCEngine 输入系统设计与实现
> **文档信息**
> - **版本**: 1.1
> - **日期**: 2026-03-22
> - **状态**: 设计文档
> - **目标**: 设计引擎级输入系统
---
## 1. 概述
### 1.1 设计目标
XCEngine 输入系统需要提供:
1. **统一的跨平台输入抽象** - 支持键盘、鼠标、触摸(手柄暂不考虑)
2. **与引擎架构无缝集成** - 使用现有的 `Core::Event` 系统
3. **轮询 + 事件混合模式** - 既支持 `IsKeyDown()` 轮询,也支持事件回调
4. **UI 系统支持** - 为 UI 组件 (Button, Slider 等) 提供指针事件
5. **输入轴支持** - 参考 Unity 的 Input.GetAxis 设计,支持键盘/手柄统一映射
### 1.2 当前状态分析
| 模块 | 状态 | 说明 |
|-----|------|------|
| Core::Event | ✅ 完备 | 线程安全Subscribe/Unsubscribe 模式 |
| RHI::RHISwapChain | ⚠️ PollEvents 空实现 | 需要填充 Windows 消息泵 |
| 现有 Input (mvs) | ❌ 耦合 Windows | 直接处理 HWND 消息,不适合引擎架构 |
| Platform/Window | ❌ 不存在 | 需要新建 |
### 1.3 设计决策
| 问题 | 决策 |
|-----|------|
| 多窗口 | ❌ 单窗口,简化设计 |
| 手柄支持 | ❌ 暂不考虑,预留接口即可 |
| 输入轴 | ✅ 参考 Unity 实现 Input.GetAxis |
| IME/多语言 | ❌ 暂不考虑,只处理英文字符 |
| 模块位置 | ✅ InputModule 放在 Input/ 命名空间 |
---
## 2. 架构设计
### 2.1 模块结构
```
engine/include/XCEngine/
├── Core/
│ └── Event.h # 已有,复用
├── Input/
│ ├── InputTypes.h # 枚举和结构体定义
│ ├── InputEvent.h # 输入事件结构体
│ ├── InputAxis.h # 输入轴定义
│ ├── InputManager.h # 输入管理器(单例)
│ └── InputModule.h # 平台相关输入处理模块接口
└── Platform/
├── PlatformTypes.h # 平台类型抽象
├── Window.h # 窗口抽象接口
└── Windows/
├── WindowsPlatform.h # Windows 平台实现
└── WindowsInputModule.h # Windows 输入模块实现
```
### 2.2 核心设计原则
1. **事件驱动 + 状态轮询双模式**
- 事件:用于 UI 交互、一次性按键响应
- 轮询:用于连续输入检测(角色移动、视角控制)
2. **平台抽象层**
- `InputModule` 接口:抽象平台特定的输入处理
- `Window` 接口:抽象平台特定的窗口管理
3. **输入轴映射**
- 键盘和手柄可以映射到同一个轴,实现统一输入
- 支持在配置文件中定义轴映射关系
4. **与现有引擎组件集成**
- 使用 `Core::Event` 作为事件系统
- 使用 `Math::Vector2` 作为 2D 坐标类型
---
## 3. 详细设计
### 3.1 输入类型定义 (`InputTypes.h`)
```cpp
#pragma once
#include "Core/Types.h"
namespace XCEngine {
namespace Input {
enum class KeyCode : uint8 {
None = 0,
// 字母键 A-Z
A = 4, B = 5, C = 6, D = 7, E = 8, F = 9, G = 10,
H = 11, I = 12, J = 13, K = 14, L = 15, M = 16, N = 17,
O = 18, P = 19, Q = 20, R = 21, S = 22, T = 23, U = 24,
V = 25, W = 26, X = 27, Y = 28, Z = 29,
// 功能键 F1-F12
F1 = 58, F2 = 59, F3 = 60, F4 = 61, F5 = 62, F6 = 63,
F7 = 64, F8 = 65, F9 = 66, F10 = 67, F11 = 68, F12 = 69,
// 控制键
Space = 49, Tab = 48, Enter = 36, Escape = 53,
LeftShift = 56, RightShift = 60, LeftCtrl = 59, RightCtrl = 62,
LeftAlt = 58, RightAlt = 61,
// 方向键
Up = 126, Down = 125, Left = 123, Right = 124,
// 编辑键
Home = 115, End = 119, PageUp = 116, PageDown = 121,
Delete = 51, Backspace = 51,
// 数字键 0-9
Zero = 39, One = 30, Two = 31, Three = 32,
Four = 33, Five = 34, Six = 35, Seven = 37,
Eight = 38, Nine = 40,
// 符号键
Minus = 43, Equals = 46, BracketLeft = 47, BracketRight = 54,
Semicolon = 42, Quote = 40, Comma = 54, Period = 55,
Slash = 44, Backslash = 45, Backtick = 41
};
enum class MouseButton : uint8 {
Left = 0,
Right = 1,
Middle = 2,
Button4 = 3,
Button5 = 4
};
enum class JoystickAxis : uint8 {
LeftX = 0,
LeftY = 1,
RightX = 2,
RightY = 3,
LeftTrigger = 4,
RightTrigger = 5
};
} // namespace Input
} // namespace XCEngine
```
### 3.2 输入事件结构体 (`InputEvent.h`)
```cpp
#pragma once
#include "InputTypes.h"
#include "Math/Vector2.h"
#include "Containers/String.h"
namespace XCEngine {
namespace Input {
struct KeyEvent {
KeyCode keyCode;
bool alt;
bool ctrl;
bool shift;
bool meta;
enum Type { Down, Up, Repeat } type;
};
struct MouseButtonEvent {
MouseButton button;
Math::Vector2 position;
enum Type { Pressed, Released } type;
};
struct MouseMoveEvent {
Math::Vector2 position;
Math::Vector2 delta;
};
struct MouseWheelEvent {
Math::Vector2 position;
float delta;
};
struct TextInputEvent {
char character;
Containers::String text;
};
struct TouchState {
int touchId;
Math::Vector2 position;
Math::Vector2 deltaPosition;
float deltaTime;
int tapCount;
enum Phase { Began, Moved, Stationary, Ended, Canceled } phase;
};
} // namespace Input
} // namespace XCEngine
```
### 3.3 输入轴定义 (`InputAxis.h`)
```cpp
#pragma once
#include "InputTypes.h"
#include "Math/Vector2.h"
#include "Containers/String.h"
namespace XCEngine {
namespace Input {
class InputAxis {
public:
InputAxis() = default;
InputAxis(const Containers::String& name, KeyCode positive, KeyCode negative = KeyCode::None)
: m_name(name), m_positiveKey(positive), m_negativeKey(negative) {}
const Containers::String& GetName() const { return m_name; }
KeyCode GetPositiveKey() const { return m_positiveKey; }
KeyCode GetNegativeKey() const { return m_negativeKey; }
void SetKeys(KeyCode positive, KeyCode negative) {
m_positiveKey = positive;
m_negativeKey = negative;
}
float GetValue() const { return m_value; }
void SetValue(float value) { m_value = value; }
private:
Containers::String m_name;
KeyCode m_positiveKey = KeyCode::None;
KeyCode m_negativeKey = KeyCode::None;
float m_value = 0.0f;
};
} // namespace Input
} // namespace XCEngine
```
### 3.4 输入管理器 (`InputManager.h`)
```cpp
#pragma once
#include "Core/Event.h"
#include "InputTypes.h"
#include "InputEvent.h"
#include "InputAxis.h"
#include "Math/Vector2.h"
namespace XCEngine {
namespace Input {
class InputManager {
public:
static InputManager& Get();
void Initialize(void* platformWindowHandle);
void Shutdown();
void Update(float deltaTime);
// ============ 轮询接口 ============
// 键盘
bool IsKeyDown(KeyCode key) const;
bool IsKeyUp(KeyCode key) const;
bool IsKeyPressed(KeyCode key) const;
// 鼠标
Math::Vector2 GetMousePosition() const;
Math::Vector2 GetMouseDelta() const;
float GetMouseScrollDelta() const;
bool IsMouseButtonDown(MouseButton button) const;
bool IsMouseButtonUp(MouseButton button) const;
bool IsMouseButtonClicked(MouseButton button) const;
// 触摸
int GetTouchCount() const;
TouchState GetTouch(int index) const;
// ============ 轴接口 (参考 Unity) ============
float GetAxis(const Containers::String& axisName) const;
float GetAxisRaw(const Containers::String& axisName) const;
bool GetButton(const Containers::String& buttonName) const;
bool GetButtonDown(const Containers::String& buttonName) const;
bool GetButtonUp(const Containers::String& buttonName) const;
// 注册轴
void RegisterAxis(const InputAxis& axis);
void RegisterButton(const Containers::String& name, KeyCode key);
void ClearAxes();
// ============ 事件接口 ============
Core::Event<const KeyEvent&>& OnKeyEvent() { return m_onKeyEvent; }
Core::Event<const MouseButtonEvent&>& OnMouseButton() { return m_onMouseButton; }
Core::Event<const MouseMoveEvent&>& OnMouseMove() { return m_onMouseMove; }
Core::Event<const MouseWheelEvent&>& OnMouseWheel() { return m_onMouseWheel; }
Core::Event<const TextInputEvent&>& OnTextInput() { return m_onTextInput; }
// 内部方法(供 PlatformInputModule 调用)
void ProcessKeyDown(KeyCode key, bool repeat);
void ProcessKeyUp(KeyCode key);
void ProcessMouseMove(int x, int y, int deltaX, int deltaY);
void ProcessMouseButton(MouseButton button, bool pressed, int x, int y);
void ProcessMouseWheel(float delta, int x, int y);
void ProcessTextInput(char c);
private:
InputManager() = default;
~InputManager() = default;
void* m_platformWindowHandle = nullptr;
// 键盘状态
std::vector<bool> m_keyDownThisFrame;
std::vector<bool> m_keyDownLastFrame;
std::vector<bool> m_keyDown;
// 鼠标状态
Math::Vector2 m_mousePosition;
Math::Vector2 m_mouseDelta;
float m_mouseScrollDelta = 0.0f;
std::vector<bool> m_mouseButtonDownThisFrame;
std::vector<bool> m_mouseButtonDownLastFrame;
std::vector<bool> m_mouseButtonDown;
// 触摸状态
std::vector<TouchState> m_touches;
// 轴映射
std::unordered_map<Containers::String, InputAxis> m_axes;
std::unordered_map<Containers::String, KeyCode> m_buttons;
std::vector<bool> m_buttonDownThisFrame;
std::vector<bool> m_buttonDownLastFrame;
// 事件
Core::Event<const KeyEvent&> m_onKeyEvent;
Core::Event<const MouseButtonEvent&> m_onMouseButton;
Core::Event<const MouseMoveEvent&> m_onMouseMove;
Core::Event<const MouseWheelEvent&> m_onMouseWheel;
Core::Event<const TextInputEvent&> m_onTextInput;
};
} // namespace Input
} // namespace XCEngine
```
### 3.5 平台输入模块接口 (`InputModule.h`)
```cpp
#pragma once
namespace XCEngine {
namespace Input {
class InputModule {
public:
virtual ~InputModule() = default;
virtual void Initialize(void* windowHandle) = 0;
virtual void Shutdown() = 0;
virtual void PumpEvents() = 0;
protected:
InputModule() = default;
};
} // namespace Input
} // namespace XCEngine
```
### 3.6 Windows 输入模块实现 (`WindowsInputModule.h`)
```cpp
#pragma once
#include "InputModule.h"
#include <Windows.h>
namespace XCEngine {
namespace Input {
namespace Platform {
class WindowsInputModule : public InputModule {
public:
void Initialize(void* windowHandle) override;
void Shutdown() override;
void PumpEvents() override;
void HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
private:
void ProcessKeyDown(WPARAM wParam, LPARAM lParam);
void ProcessKeyUp(WPARAM wParam, LPARAM lParam);
void ProcessMouseMove(WPARAM wParam, LPARAM lParam);
void ProcessMouseButton(WPARAM wParam, LPARAM lParam, bool pressed);
void ProcessMouseWheel(WPARAM wParam, LPARAM lParam);
void ProcessCharInput(WPARAM wParam, LPARAM lParam);
KeyCode VKCodeToKeyCode(int vkCode);
HWND m_hwnd = nullptr;
Math::Vector2 m_lastMousePosition;
bool m_captureMouse = false;
bool m_isInitialized = false;
};
} // namespace Platform
} // namespace Input
} // namespace XCEngine
```
---
## 4. InputManager 实现细节
### 4.1 状态追踪机制
```
每帧 Update() 调用时:
1. m_keyDownLastFrame = m_keyDownThisFrame
2. 处理 m_keyDownThisFrame = m_keyDown (当前帧按下状态)
3. 清零 m_mouseDelta, m_mouseScrollDelta
IsKeyPressed(key) = m_keyDownThisFrame[key] && !m_keyDownLastFrame[key] // 当前帧按下
IsKeyDown(key) = m_keyDown[key] // 当前正按下
IsKeyUp(key) = !m_keyDown[key] // 当前已释放
```
### 4.2 轴值计算
```cpp
float InputManager::GetAxis(const Containers::String& axisName) const {
auto it = m_axes.find(axisName);
if (it == m_axes.end()) return 0.0f;
const auto& axis = it->second;
float value = 0.0f;
if (axis.GetPositiveKey() != KeyCode::None && IsKeyDown(axis.GetPositiveKey())) {
value += 1.0f;
}
if (axis.GetNegativeKey() != KeyCode::None && IsKeyDown(axis.GetNegativeKey())) {
value -= 1.0f;
}
return value;
}
float InputManager::GetAxisRaw(const Containers::String& axisName) const {
auto it = m_axes.find(axisName);
if (it == m_axes.end()) return 0.0f;
const auto& axis = it->second;
float value = 0.0f;
if (axis.GetPositiveKey() != KeyCode::None && IsKeyPressed(axis.GetPositiveKey())) {
value += 1.0f;
}
if (axis.GetNegativeKey() != KeyCode::None && IsKeyPressed(axis.GetNegativeKey())) {
value -= 1.0f;
}
return value;
}
```
### 4.3 默认轴配置
```cpp
void InputManager::Initialize(void* platformWindowHandle) {
// 注册默认轴 (类似 Unity)
RegisterAxis(InputAxis("Horizontal", KeyCode::D, KeyCode::A));
RegisterAxis(InputAxis("Vertical", KeyCode::W, KeyCode::S));
RegisterAxis(InputAxis("Mouse X", KeyCode::None, KeyCode::None)); // 鼠标驱动
RegisterAxis(InputAxis("Mouse Y", KeyCode::None, KeyCode::None)); // 鼠标驱动
// 注册默认按钮
RegisterButton("Jump", KeyCode::Space);
RegisterButton("Fire1", KeyCode::LeftCtrl);
RegisterButton("Fire2", KeyCode::LeftAlt);
RegisterButton("Fire3", KeyCode::LeftShift);
}
```
---
## 5. Windows 消息到引擎事件的映射
### 5.1 消息映射表
| Windows Message | Engine Method | 说明 |
|----------------|---------------|------|
| WM_KEYDOWN | `ProcessKeyDown` | 按键按下wParam=VK码 |
| WM_KEYUP | `ProcessKeyUp` | 按键释放wParam=VK码 |
| WM_CHAR | `ProcessTextInput` | 字符输入wParam=字符 |
| WM_MOUSEMOVE | `ProcessMouseMove` | 鼠标移动 |
| WM_LBUTTONDOWN | `ProcessMouseButton(Left, Pressed)` | 左键按下 |
| WM_LBUTTONUP | `ProcessMouseButton(Left, Released)` | 左键释放 |
| WM_RBUTTONDOWN | `ProcessMouseButton(Right, Pressed)` | 右键按下 |
| WM_RBUTTONUP | `ProcessMouseButton(Right, Released)` | 右键释放 |
| WM_MBUTTONDOWN | `ProcessMouseButton(Middle, Pressed)` | 中键按下 |
| WM_MBUTTONUP | `ProcessMouseButton(Middle, Released)` | 中键释放 |
| WM_MOUSEWHEEL | `ProcessMouseWheel` | 滚轮滚动 |
| WM_XBUTTONDOWN | `ProcessMouseButton(Button4/5, Pressed)` | 侧键按下 |
| WM_XBUTTONUP | `ProcessMouseButton(Button4/5, Released)` | 侧键释放 |
### 5.2 VK 码到 KeyCode 映射
```cpp
KeyCode WindowsInputModule::VKCodeToKeyCode(int vkCode) {
switch (vkCode) {
case 'A': return KeyCode::A;
case 'B': return KeyCode::B;
case 'C': return KeyCode::C;
// ... Z
case VK_SPACE: return KeyCode::Space;
case VK_TAB: return KeyCode::Tab;
case VK_RETURN: return KeyCode::Enter;
case VK_ESCAPE: return KeyCode::Escape;
case VK_SHIFT: return KeyCode::LeftShift;
case VK_CONTROL: return KeyCode::LeftCtrl;
case VK_MENU: return KeyCode::LeftAlt;
case VK_UP: return KeyCode::Up;
case VK_DOWN: return KeyCode::Down;
case VK_LEFT: return KeyCode::Left;
case VK_RIGHT: return KeyCode::Right;
case VK_HOME: return KeyCode::Home;
case VK_END: return KeyCode::End;
case VK_PRIOR: return KeyCode::PageUp;
case VK_NEXT: return KeyCode::PageDown;
case VK_DELETE: return KeyCode::Delete;
case VK_BACK: return KeyCode::Backspace;
case VK_F1: return KeyCode::F1;
case VK_F2: return KeyCode::F2;
// ... F12
case '0': return KeyCode::Zero;
case '1': return KeyCode::One;
// ... 9
case VK_OEM_MINUS: return KeyCode::Minus;
case VK_OEM_PLUS: return KeyCode::Equals;
// ... 其他 OEM 键
default: return KeyCode::None;
}
}
```
---
## 6. 与引擎其他模块的集成
### 6.1 与 RHI/SwapChain 的集成
`RHISwapChain::PollEvents()` 需要调用 `InputModule::PumpEvents()``InputManager::Update()`:
```cpp
// D3D12SwapChain.cpp
void D3D12SwapChain::PollEvents() {
// 抽取 Windows 消息
if (m_inputModule) {
m_inputModule->PumpEvents();
}
// 处理其他 Windows 消息(关闭请求等)
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
m_shouldClose = true;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 在 GameLoop 中每帧调用
void GameLoop::Update(float deltaTime) {
// 更新输入状态(计算 IsKeyPressed 等)
Input::InputManager::Get().Update(deltaTime);
// 更新场景和组件
m_currentScene->Update(deltaTime);
}
```
### 6.2 与 UI 系统的集成
UI 组件通过订阅 `InputManager` 的事件来响应用户输入:
```cpp
// ButtonComponent.cpp
void ButtonComponent::Update(float deltaTime) {
if (!IsEnabled()) return;
auto& input = Input::InputManager::Get();
Vector2 mousePos = input.GetMousePosition();
// 检测鼠标是否在按钮区域内
if (IsPointInRect(mousePos, m_rect)) {
// 检测悬停状态变化
if (!m_isHovered) {
m_isHovered = true;
OnPointerEnter.Invoke();
}
// 检测点击
if (input.IsMouseButtonClicked(Input::MouseButton::Left)) {
OnClick.Invoke();
}
// 检测按下/释放
if (input.IsMouseButtonDown(Input::MouseButton::Left)) {
OnPointerDown.Invoke();
} else if (m_wasMouseDown && !input.IsMouseButtonDown(Input::MouseButton::Left)) {
OnPointerUp.Invoke();
}
} else if (m_isHovered) {
m_isHovered = false;
OnPointerExit.Invoke();
}
m_wasMouseDown = input.IsMouseButtonDown(Input::MouseButton::Left);
}
```
### 6.3 与场景生命周期的集成
```cpp
// Scene.cpp
void Scene::Awake() {
// 获取窗口句柄并初始化输入系统
void* windowHandle = GetEngine()->GetWindowHandle();
Input::InputManager::Get().Initialize(windowHandle);
}
void Scene::OnDestroy() {
Input::InputManager::Get().Shutdown();
}
```
---
## 7. 使用示例
### 7.1 轮询模式(角色移动)
```cpp
void PlayerController::Update(float deltaTime) {
auto& input = Input::InputManager::Get();
// 使用轴 (推荐方式,兼容手柄)
float horizontal = input.GetAxis("Horizontal");
float vertical = input.GetAxis("Vertical");
m_velocity.x = horizontal * m_moveSpeed;
m_velocity.z = vertical * m_moveSpeed;
// 或者使用原始轴(无平滑)
float rawH = input.GetAxisRaw("Horizontal");
// 直接轮询也可以
if (input.IsKeyDown(Input::KeyCode::W)) {
m_velocity.z = 1.0f;
}
}
```
### 7.2 事件模式UI 交互)
```cpp
class MyUIButton : public Component {
uint64_t m_clickHandlerId = 0;
uint64_t m_hoverHandlerId = 0;
void Awake() override {
// 订阅鼠标按钮事件
m_clickHandlerId = Input::InputManager::Get().OnMouseButton().Subscribe(
[this](const Input::MouseButtonEvent& event) {
if (event.type == Input::MouseButtonEvent::Pressed &&
event.button == Input::MouseButton::Left &&
IsPointInButton(event.position)) {
OnClicked();
}
}
);
// 订阅鼠标移动事件用于悬停检测
m_hoverHandlerId = Input::InputManager::Get().OnMouseMove().Subscribe(
[this](const Input::MouseMoveEvent& event) {
bool isHovering = IsPointInButton(event.position);
if (isHovering && !m_isHovered) {
m_isHovered = true;
OnPointerEnter.Invoke();
} else if (!isHovering && m_isHovered) {
m_isHovered = false;
OnPointerExit.Invoke();
}
}
);
}
void OnDestroy() override {
Input::InputManager::Get().OnMouseButton().Unsubscribe(m_clickHandlerId);
Input::InputManager::Get().OnMouseMove().Unsubscribe(m_hoverHandlerId);
}
};
```
### 7.3 文本输入
```cpp
void InputFieldComponent::Update(float deltaTime) {
static uint64_t textHandlerId = 0;
if (m_isFocused) {
if (textHandlerId == 0) {
textHandlerId = Input::InputManager::Get().OnTextInput().Subscribe(
[this](const Input::TextInputEvent& event) {
if (event.character == '\b') { // Backspace
if (!m_text.empty()) {
m_text.pop_back();
}
} else if (event.character == '\r') { // Enter
OnSubmit.Invoke();
} else if (isprint(event.character)) {
if (m_characterLimit == 0 || m_text.length() < m_characterLimit) {
m_text += event.character;
OnValueChanged.Invoke(m_text);
}
}
}
);
}
} else if (textHandlerId != 0) {
Input::InputManager::Get().OnTextInput().Unsubscribe(textHandlerId);
textHandlerId = 0;
}
}
```
### 7.4 射击游戏开火
```cpp
void WeaponComponent::Update(float deltaTime) {
auto& input = Input::InputManager::Get();
// 方式1: 按钮按下检测
if (input.GetButtonDown("Fire1")) {
Fire();
}
// 方式2: 轮询检测(适合连发)
if (input.GetButton("Fire1") && m_fireRateTimer <= 0) {
Fire();
m_fireRateTimer = m_fireRate;
}
}
```
---
## 8. 实现计划
### Phase 1: 核心输入系统
| 任务 | 文件 | 优先级 |
|-----|------|-------|
| 创建目录 | `engine/include/XCEngine/Input/` | P0 |
| 实现 InputTypes.h | KeyCode, MouseButton 枚举 | P0 |
| 实现 InputEvent.h | 事件结构体 | P0 |
| 实现 InputAxis.h | 输入轴定义 | P1 |
| 实现 InputManager.h/cpp | 单例管理器 | P0 |
| 实现 InputModule.h | 平台输入模块接口 | P0 |
| 实现 WindowsInputModule.h/cpp | Windows 平台实现 | P0 |
### Phase 2: 与 RHI 集成
| 任务 | 文件 | 优先级 |
|-----|------|-------|
| 修改 RHISwapChain | 添加 InputModule 成员 | P0 |
| 实现 D3D12SwapChain::PollEvents | 填充消息泵逻辑 | P0 |
| 实现 OpenGLSwapChain::PollEvents | GL 消息处理 | P1 |
### Phase 3: 轴系统完善
| 任务 | 说明 | 优先级 |
|-----|------|-------|
| 默认轴配置 | 注册 Horizontal, Vertical 等默认轴 | P1 |
| 轴平滑处理 | GetAxis 的平滑插值 | P2 |
| 配置文件支持 | 从 JSON/配置加载轴映射 | P2 |
### Phase 4: UI 输入支持
| 任务 | 说明 | 优先级 |
|-----|------|-------|
| Canvas 射线检测 | 将屏幕坐标转换为 UI 元素 | P1 |
| Button 事件 | OnClick, OnPointerDown/Up/Enter/Exit | P1 |
| Slider 拖拽 | 使用鼠标事件实现滑块拖拽 | P1 |
| InputField 文本输入 | 接收 TextInputEvent | P1 |
---
## 9. 测试策略
### 9.1 单元测试
```cpp
// test_input_manager.cpp
TEST(InputManager, KeyDownDetection) {
InputManager::Get().Initialize(nullptr);
// 模拟按下 W 键
InputManager::Get().ProcessKeyDown(KeyCode::W, false);
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::W));
EXPECT_TRUE(InputManager::Get().IsKeyPressed(KeyCode::W)); // 第一帧按下
EXPECT_FALSE(InputManager::Get().IsKeyUp(KeyCode::W));
// 模拟释放
InputManager::Get().ProcessKeyUp(KeyCode::W);
EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::W));
EXPECT_TRUE(InputManager::Get().IsKeyUp(KeyCode::W));
InputManager::Get().Shutdown();
}
TEST(InputManager, AxisRegistration) {
InputManager::Get().Initialize(nullptr);
InputManager::Get().RegisterAxis(InputAxis("TestAxis", KeyCode::W, KeyCode::S));
EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), 0.0f);
InputManager::Get().ProcessKeyDown(KeyCode::W, false);
EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), 1.0f);
InputManager::Get().ProcessKeyUp(KeyCode::W);
InputManager::Get().ProcessKeyDown(KeyCode::S, false);
EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), -1.0f);
InputManager::Get().Shutdown();
}
TEST(InputManager, MousePositionTracking) {
InputManager::Get().Initialize(nullptr);
InputManager::Get().ProcessMouseMove(100, 200, 0, 0);
EXPECT_EQ(InputManager::Get().GetMousePosition(), Math::Vector2(100, 200));
InputManager::Get().ProcessMouseMove(105, 205, 5, 5);
EXPECT_EQ(InputManager::Get().GetMouseDelta(), Math::Vector2(5, 5));
InputManager::Get().Shutdown();
}
```
### 9.2 集成测试
- 测试输入系统与 SwapChain 的集成
- 测试输入系统与 Scene 的集成
- 测试 UI Button 点击响应
---
## 10. 文件清单
### 头文件
```
engine/include/XCEngine/Input/
├── InputTypes.h # 48 行
├── InputEvent.h # 78 行
├── InputAxis.h # 45 行
├── InputManager.h # 130 行
└── InputModule.h # 20 行
engine/include/XCEngine/Platform/
├── PlatformTypes.h # (新建)
├── Window.h # (新建)
└── Windows/
└── WindowsInputModule.h # 55 行
```
### 源文件
```
engine/src/Input/
├── InputManager.cpp # 180 行
└── Platform/
└── Windows/
└── WindowsInputModule.cpp # 200 行
```
### 测试文件
```
tests/Input/
├── test_input_types.cpp # KeyCode/MouseButton 枚举测试
├── test_input_event.cpp # 事件结构体测试
├── test_input_axis.cpp # 轴注册和值计算测试
└── test_input_manager.cpp # 核心功能测试
```
---
## 11. 设计原则总结
1. **复用 Core::Event** - 使用现有的线程安全事件系统
2. **平台抽象** - InputModule 接口隔离 Windows 消息处理
3. **双模式支持** - 轮询 + 事件,兼顾性能和响应性
4. **轴系统** - 参考 Unity支持键盘/手柄统一输入映射
5. **单窗口** - 简化设计,满足当前需求
6. **UI 协同** - 为 Canvas/Button 提供完整的事件支持