Add Music fluctuations project and Chinese plan docs

This commit is contained in:
2026-03-21 15:55:54 +08:00
parent 629455df07
commit a172d75e36
462 changed files with 382904 additions and 0 deletions

View File

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

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