From 600892bbe2c040f3ac48484ff688543d9fd12631 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 25 Mar 2026 01:31:09 +0800 Subject: [PATCH] Refactor RHI documentation and remove unused files --- RHI模块测试重构.md | 778 ++++++++++++++++++ .../plan/used/RHI_Design_Issues.md | 0 minimal.ppm | Bin 2764816 -> 0 bytes 3 files changed, 778 insertions(+) create mode 100644 RHI模块测试重构.md rename RHI_Design_Issues.md => docs/plan/used/RHI_Design_Issues.md (100%) delete mode 100644 minimal.ppm diff --git a/RHI模块测试重构.md b/RHI模块测试重构.md new file mode 100644 index 00000000..6f7607da --- /dev/null +++ b/RHI模块测试重构.md @@ -0,0 +1,778 @@ +# RHI 模块单元测试重构计划 + +## 1. 项目背景 + +本文档分析 XCEngine RHI(渲染硬件抽象层)模块的三个单元测试层次的覆盖度和质量问题,并提出重构建议。 + +### 1.1 测试层次结构 + +| 层次 | 位置 | 测试数量 | 特点 | +|------|------|----------|------| +| **RHI 抽象层** | `tests/RHI/unit/` | 75 tests × 2 backends = 150 | 参数化测试,跨 D3D12/OpenGL | +| **D3D12 后端** | `tests/RHI/D3D12/unit/` | ~58 tests | 非参数化,直接测试 D3D12 API | +| **OpenGL 后端** | `tests/RHI/OpenGL/unit/` | ~58 tests | 非参数化,直接测试 OpenGL API | + +### 1.2 当前测试文件分布 + +#### RHI 抽象层 (`tests/RHI/unit/`) + +| 文件 | 测试数 | 测试类 | +|------|--------|--------| +| `test_device.cpp` | 8 | RHIDevice | +| `test_buffer.cpp` | 8 | RHIBuffer | +| `test_texture.cpp` | 5 | RHITexture | +| `test_swap_chain.cpp` | 4 | RHISwapChain | +| `test_command_list.cpp` | 14 | RHICommandList | +| `test_command_queue.cpp` | 6 | RHICommandQueue | +| `test_shader.cpp` | 9 | RHIShader | +| `test_fence.cpp` | 10 | RHIFence | +| `test_sampler.cpp` | 4 | RHISampler | +| `test_factory.cpp` | 5 | RHIFactory | + +#### D3D12 后端 (`tests/RHI/D3D12/unit/`) + +| 文件 | 测试数 | 测试类/功能 | +|------|--------|-------------| +| `test_device.cpp` | 6 | D3D12Device | +| `test_fence.cpp` | 9 | D3D12Fence | +| `test_command_queue.cpp` | 2 | D3D12CommandQueue | +| `test_command_allocator.cpp` | 3 | D3D12CommandAllocator | +| `test_command_list.cpp` | 2 | D3D12CommandList | +| `test_buffer.cpp` | 6 | D3D12Buffer | +| `test_texture.cpp` | 4 | D3D12Texture | +| `test_descriptor_heap.cpp` | 9 | D3D12DescriptorHeap | +| `test_shader.cpp` | 3 | D3D12Shader (trivial) | +| `test_root_signature.cpp` | 2 | D3D12RootSignature | +| `test_pipeline_state.cpp` | 2 | D3D12 PSO (trivial) | +| `test_views.cpp` | 4 | RTV/DSV/CBV | +| `test_swap_chain.cpp` | 5 | D3D12SwapChain | + +#### OpenGL 后端 (`tests/RHI/OpenGL/unit/`) + +| 文件 | 测试数 | 测试类/功能 | +|------|--------|-------------| +| `test_device.cpp` | 2 | OpenGLDevice | +| `test_buffer.cpp` | 6 | OpenGLBuffer | +| `test_fence.cpp` | 8 | OpenGLFence | +| `test_texture.cpp` | 5 | OpenGLTexture | +| `test_shader.cpp` | 4 | OpenGLShader | +| `test_pipeline_state.cpp` | 3 | OpenGLPipelineState | +| `test_vertex_array.cpp` | 5 | OpenGLVertexArray | +| `test_command_list.cpp` | 9 | OpenGLCommandList | +| `test_render_target_view.cpp` | 4 | OpenGLRenderTargetView | +| `test_depth_stencil_view.cpp` | 4 | OpenGLDepthStencilView | +| `test_swap_chain.cpp` | 3 | OpenGLSwapChain | +| `test_sampler.cpp` | 4 | OpenGLSampler | + +--- + +## 2. 严重问题(P0 - 必须修复) + +### 2.1 `tests/RHI/unit/test_shader.cpp` - 9个测试完全相同 + +**严重程度**: 🔴 致命 + +**问题描述**: + +所有 9 个 Shader 测试逻辑完全相同,都只测试空描述符返回 nullptr: + +```cpp +// 文件: tests/RHI/unit/test_shader.cpp + +// 9个测试全部是这种结构 +TEST_P(RHITestFixture, Shader_Compile_EmptyDesc_ReturnsNullptr) { + RHIShader* shader = GetDevice()->CompileShader({}); // 空描述符 + EXPECT_EQ(shader, nullptr); // 期望返回nullptr +} + +TEST_P(RHITestFixture, Shader_GetType_WithNullShader) { + RHIShader* shader = GetDevice()->CompileShader({}); // 空描述符 + EXPECT_EQ(shader, nullptr); // 期望返回nullptr - 与上面完全相同! +} + +TEST_P(RHITestFixture, Shader_IsValid_WithNullShader) { + RHIShader* shader = GetDevice()->CompileShader({}); + EXPECT_EQ(shader, nullptr); // 与上面完全相同! +} +// ... 剩下的6个测试也是同样的模式 +``` + +**影响**: + +- Shader 模块完全没有真实功能测试 +- Shader 编译、绑定、uniform 设置等功能完全未覆盖 +- 这 9 个测试等效于只有 1 个测试 + +**修复建议**: + +```cpp +// 1. 保留错误处理测试 (1-2个) +TEST_P(RHITestFixture, Shader_Compile_EmptyDesc_ReturnsNullptr) { + RHIShader* shader = GetDevice()->CompileShader({}); + EXPECT_EQ(shader, nullptr); +} + +// 2. 添加真实 shader 编译测试 (新增) +// 注意: 需要 shader 文件或内嵌 GLSL/HLSL 源码 +TEST_P(RHITestFixture, Shader_Compile_ValidShader) { + ShaderCompileDesc desc = {}; + desc.entryPoint = "main"; + desc.shaderType = ShaderType::Vertex; + // 需要实际的 shader 源码或文件路径 + RHIShader* shader = GetDevice()->CompileShader(desc); + ASSERT_NE(shader, nullptr); + shader->Shutdown(); + delete shader; +} + +// 3. 添加 shader 绑定测试 (新增) +TEST_P(RHITestFixture, Shader_Bind_ValidShader) { + // 编译 shader 后绑定 + RHIShader* shader = GetDevice()->CompileShader(validDesc); + ASSERT_NE(shader, nullptr); + + RHICommandList* cmdList = GetDevice()->CreateCommandList({}); + cmdList->Reset(); + cmdList->SetShader(shader); + cmdList->Close(); + + cmdList->Shutdown(); + delete cmdList; + shader->Shutdown(); + delete shader; +} + +// 4. 添加 uniform 设置测试 (新增) +TEST_P(RHITestFixture, Shader_SetUniform_Int) { + RHIShader* shader = GetDevice()->CompileShader(validDesc); + ASSERT_NE(shader, nullptr); + + shader->SetInt("uniformName", 42); + shader->Shutdown(); + delete shader; +} + +TEST_P(RHITestFixture, Shader_SetUniform_Float) { + // ... +} + +// 5. 添加 GetNativeHandle 测试 (保留一个) +TEST_P(RHITestFixture, Shader_GetNativeHandle_ValidShader) { + RHIShader* shader = GetDevice()->CompileShader(validDesc); + ASSERT_NE(shader, nullptr); + EXPECT_NE(shader->GetNativeHandle(), nullptr); + shader->Shutdown(); + delete shader; +} +``` + +--- + +### 2.2 关键 RHI 抽象类完全没有测试 + +**严重程度**: 🔴 致命 + +**问题描述**: + +以下 RHI 抽象类在 `tests/RHI/unit/` 中完全没有测试: + +| 类 | 说明 | 影响 | +|----|------|------| +| `RHIPipelineState` | 管线状态对象 | 无法验证 PSO 创建、配置、绑定 | +| `RHIPipelineLayout` | 管线布局 | 无法验证 descriptor layout | +| `RHIDescriptorPool` | 描述符池 | Descriptor 管理完全未覆盖 | +| `RHIDescriptorSet` | 描述符集 | 无法验证 descriptor 更新 | +| `RHIRenderPass` | 渲染通道 | RenderPass API 完全未覆盖 | +| `RHIFramebuffer` | 帧缓冲 | Framebuffer API 完全未覆盖 | + +**修复建议 - 新增 `test_pipeline_state.cpp`**: + +```cpp +// 文件: tests/RHI/unit/test_pipeline_state.cpp +#include "fixtures/RHITestFixture.h" +#include "XCEngine/RHI/RHIPipelineState.h" +#include "XCEngine/RHI/RHIDevice.h" + +using namespace XCEngine::RHI; + +TEST_P(RHITestFixture, PipelineState_Create) { + GraphicsPipelineDesc desc = {}; + // 配置基本描述符 + RHIPipelineState* pso = GetDevice()->CreatePipelineState(desc); + ASSERT_NE(pso, nullptr); + pso->Shutdown(); + delete pso; +} + +TEST_P(RHITestFixture, PipelineState_SetGet_RasterizerState) { + GraphicsPipelineDesc desc = {}; + RHIPipelineState* pso = GetDevice()->CreatePipelineState(desc); + ASSERT_NE(pso, nullptr); + + RasterizerDesc rasterizer = {}; + rasterizer.cullMode = CullMode::Back; + pso->SetRasterizerState(rasterizer); + + EXPECT_EQ(pso->GetRasterizerState().cullMode, CullMode::Back); + pso->Shutdown(); + delete pso; +} + +TEST_P(RHITestFixture, PipelineState_SetGet_BlendState) { + // ... +} + +TEST_P(RHITestFixture, PipelineState_SetGet_DepthStencilState) { + // ... +} + +TEST_P(RHITestFixture, PipelineState_Finalize) { + GraphicsPipelineDesc desc = {}; + RHIPipelineState* pso = GetDevice()->CreatePipelineState(desc); + ASSERT_NE(pso, nullptr); + + bool finalized = pso->Finalize(); + EXPECT_TRUE(finalized); + EXPECT_TRUE(pso->IsFinalized()); + + pso->Shutdown(); + delete pso; +} + +TEST_P(RHITestFixture, PipelineState_Bind_Unbind) { + // ... +} +``` + +**修复建议 - 新增 `test_render_pass.cpp`**: + +```cpp +// 文件: tests/RHI/unit/test_render_pass.cpp +#include "fixtures/RHITestFixture.h" +#include "XCEngine/RHI/RHIRenderPass.h" + +using namespace XCEngine::RHI; + +TEST_P(RHITestFixture, RenderPass_Create) { + AttachmentDesc colorAttachment = {}; + colorAttachment.format = Format::R8G8B8A8_UNorm; + colorAttachment.loadOp = LoadAction::Clear; + colorAttachment.storeOp = StoreAction::Store; + + RHIRenderPass* renderPass = GetDevice()->CreateRenderPass(1, &colorAttachment, nullptr); + ASSERT_NE(renderPass, nullptr); + + EXPECT_EQ(renderPass->GetColorAttachmentCount(), 1); + renderPass->Shutdown(); + delete renderPass; +} + +TEST_P(RHITestFixture, RenderPass_BeginEnd) { + // 需要先创建 framebuffer + // cmdList->BeginRenderPass(renderPass, framebuffer, ...); + // cmdList->EndRenderPass(); +} +``` + +**修复建议 - 新增 `test_framebuffer.cpp`**: + +```cpp +// 文件: tests/RHI/unit/test_framebuffer.cpp +#include "fixtures/RHITestFixture.h" +#include "XCEngine/RHI/RHIFramebuffer.h" + +using namespace XCEngine::RHI; + +TEST_P(RHITestFixture, Framebuffer_Create) { + // 先创建 renderPass 和 texture + RHIRenderPass* renderPass = ...; + RHITexture* texture = ...; + RHIResourceView* rtv = GetDevice()->CreateRenderTargetView(texture, {}); + + RHIFramebuffer* fb = GetDevice()->CreateFramebuffer(renderPass, 1, &rtv, nullptr, 256, 256); + ASSERT_NE(fb, nullptr); + + EXPECT_EQ(fb->GetWidth(), 256); + EXPECT_EQ(fb->GetHeight(), 256); + EXPECT_TRUE(fb->IsValid()); + + fb->Shutdown(); + delete fb; +} +``` + +**修复建议 - 新增 `test_descriptor.cpp`**: + +```cpp +// 文件: tests/RHI/unit/test_descriptor.cpp +#include "fixtures/RHITestFixture.h" +#include "XCEngine/RHI/RHIDescriptorPool.h" +#include "XCEngine/RHI/RHIDescriptorSet.h" + +using namespace XCEngine::RHI; + +TEST_P(RHITestFixture, DescriptorPool_Create) { + DescriptorPoolDesc desc = {}; + desc.maxSets = 10; + desc.poolSize = 100; + + RHIDescriptorPool* pool = GetDevice()->CreateDescriptorPool(desc); + ASSERT_NE(pool, nullptr); + pool->Shutdown(); +} + +TEST_P(RHITestFixture, DescriptorSet_Allocate_Update) { + // 创建 pool + RHIDescriptorPool* pool = GetDevice()->CreateDescriptorPool(desc); + + // 创建 layout + DescriptorSetLayoutDesc layoutDesc = {}; + DescriptorSetLayoutBinding binding = {}; + binding.binding = 0; + binding.type = DescriptorType::CBV; + binding.count = 1; + layoutDesc.bindings = &binding; + layoutDesc.bindingCount = 1; + + RHIDescriptorSet* set = GetDevice()->CreateDescriptorSet(pool, layoutDesc); + ASSERT_NE(set, nullptr); + + EXPECT_EQ(set->GetBindingCount(), 1); + + set->Shutdown(); + pool->Shutdown(); +} +``` + +--- + +## 3. 中等问题(P1 - 应该修复) + +### 3.1 `tests/RHI/unit/test_command_list.cpp` - 大量测试传递 nullptr + +**问题描述**: + +很多 CommandList 测试只验证调用不崩溃,不验证实际功能: + +```cpp +// 这些测试传递 nullptr,无法验证真实功能 +TEST_P(RHITestFixture, CommandList_ClearRenderTarget) { + // ... 创建了 texture 但实际传递 nullptr + cmdList->ClearRenderTarget(static_cast(nullptr), color); + // 没有验证任何实际行为 +} + +TEST_P(RHITestFixture, CommandList_TransitionBarrier) { + // 创建了 texture 但传递 nullptr + cmdList->TransitionBarrier(static_cast(nullptr), ...); +} + +TEST_P(RHITestFixture, CommandList_SetVertexBuffer_WithResourceView) { + // 直接传递 nullptr + RHIResourceView* buffer = nullptr; + cmdList->SetVertexBuffers(0, 1, &buffer, nullptr, nullptr); +} +``` + +**缺少的真实测试**: + +| 测试内容 | 状态 | 建议 | +|----------|------|------| +| 有资源的 SetVertexBuffer | ❌ | 添加 | +| 有资源的 SetIndexBuffer | ❌ | 添加 | +| 有资源的 ClearRenderTarget | ❌ | 添加 | +| BeginRenderPass/EndRenderPass | ❌ | 添加 | +| Dispatch (计算着色器) | ❌ | 添加 | +| SetGraphicsDescriptorSets | ❌ | 添加 | +| SetComputeDescriptorSets | ❌ | 添加 | + +**修复建议**: + +```cpp +// 新增真实资源测试 +TEST_P(RHITestFixture, CommandList_SetVertexBuffer_WithRealBuffer) { + // 1. 创建 buffer + BufferDesc bufDesc = {}; + bufDesc.size = 1024; + RHIBuffer* buffer = GetDevice()->CreateBuffer(bufDesc); + ASSERT_NE(buffer, nullptr); + + // 2. 创建 cmdList 并设置 + RHICommandList* cmdList = GetDevice()->CreateCommandList({}); + cmdList->Reset(); + + RHIResourceView* bufferView = buffer->GetView(); + cmdList->SetVertexBuffers(0, 1, &bufferView, nullptr, nullptr); + + cmdList->Close(); + + // 3. 清理 + cmdList->Shutdown(); + buffer->Shutdown(); + delete buffer; + delete cmdList; +} + +TEST_P(RHITestFixture, CommandList_BeginEndRenderPass) { + // 1. 创建 renderPass + AttachmentDesc colorDesc = {}; + colorDesc.format = Format::R8G8B8A8_UNorm; + RHIRenderPass* renderPass = GetDevice()->CreateRenderPass(1, &colorDesc, nullptr); + ASSERT_NE(renderPass, nullptr); + + // 2. 创建 framebuffer + TextureDesc texDesc = {}; + texDesc.width = 256; + texDesc.height = 256; + RHITexture* texture = GetDevice()->CreateTexture(texDesc); + RHIResourceView* rtv = GetDevice()->CreateRenderTargetView(texture, {}); + + RHIFramebuffer* fb = GetDevice()->CreateFramebuffer(renderPass, 1, &rtv, nullptr, 256, 256); + + // 3. 测试 Begin/End + RHICommandList* cmdList = GetDevice()->CreateCommandList({}); + cmdList->Reset(); + + Rect renderArea = {0, 0, 256, 256}; + ClearValue clearValue = {}; + cmdList->BeginRenderPass(renderPass, fb, renderArea, 1, &clearValue); + cmdList->EndRenderPass(); + + cmdList->Close(); + + // 清理 + cmdList->Shutdown(); + fb->Shutdown(); + delete fb; + delete rtv; + texture->Shutdown(); + delete texture; + renderPass->Shutdown(); + delete renderPass; +} + +TEST_P(RHITestFixture, CommandList_Dispatch) { + // 1. 创建 compute shader + ShaderCompileDesc csDesc = {}; + RHIShader* computeShader = GetDevice()->CompileShader(csDesc); + // ... + + // 2. 创建并设置 compute pipeline + RHIPipelineState* computePSO = GetDevice()->CreatePipelineState(computeDesc); + computePSO->SetComputeShader(computeShader); + computePSO->Finalize(); + + // 3. 测试 Dispatch + RHICommandList* cmdList = GetDevice()->CreateCommandList({}); + cmdList->Reset(); + cmdList->SetPipelineState(computePSO); + cmdList->Dispatch(8, 8, 1); + cmdList->Close(); + + // 清理 + cmdList->Shutdown(); + computePSO->Shutdown(); + delete computePSO; + computeShader->Shutdown(); + delete computeShader; +} +``` + +--- + +### 3.2 D3D12 后端测试问题 + +| 文件 | 问题 | 建议 | +|------|------|------| +| `test_pipeline_state.cpp` | 只测试 struct 默认值 | 添加真实 PSO 创建测试 | +| `test_shader.cpp` | 只做字符串比较 | 添加 shader 编译测试 | +| `test_root_signature.cpp` | 只测试空签名和 CBV | 添加复杂参数测试 | +| `test_command_list.cpp` | 只有 2 个测试 | 补充 Reset/ClearRenderTargetView 等 | + +**修复建议 - `test_pipeline_state.cpp`**: + +```cpp +// 当前只有 trivial 测试 +TEST_F(D3D12TestFixture, PipelineState_Get_GraphicsPipelineDescDefaults) { + D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; + // 只验证 struct 默认值 + EXPECT_EQ(psoDesc.PrimitiveTopologyType, ...); +} + +// 应该添加 +TEST_F(D3D12TestFixture, PipelineState_Create_GraphicsPipeline) { + // 1. 创建 root signature + D3D12_ROOT_SIGNATURE_DESC rootSigDesc = {}; + D3D12RootSignature rootSig; + rootSig.Initialize(GetDevice()->GetDevice(), rootSigDesc); + + // 2. 创建 shader (需要 HLSL 源码或编译好的 bytecode) + D3D12Shader* vs = new D3D12Shader(); + vs->CompileFromFile(L"shaders/vertex.cso"); + + // 3. 创建 PSO + D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; + psoDesc.pRootSignature = rootSig.GetRootSignature(); + psoDesc.VS = vs->GetBytecode(); + // ... 设置其他字段 + + ID3D12PipelineState* d3d12PSO = nullptr; + HRESULT hr = GetDevice()->GetDevice()->CreateGraphicsPipelineState( + &psoDesc, IID_PPV_ARGS(&d3d12PSO)); + ASSERT_EQ(hr, S_OK); + + d3d12PSO->Release(); + rootSig.Shutdown(); +} +``` + +--- + +### 3.3 OpenGL 后端测试问题 + +| 文件 | 问题 | 建议 | +|------|------|------| +| `test_command_list.cpp` | 缺少 SetShader/SetUniform 测试 | 添加 | +| `test_shader.cpp` | 无 compute shader 测试 | 添加 GL_COMPUTE_SHADER | +| 无 compute 相关测试 | 缺少 Dispatch 测试 | 添加 | + +**修复建议**: + +```cpp +// 新增 test_compute.cpp +TEST_F(OpenGLTestFixture, ComputeShader_Compile) { + const char* computeSrc = R"( + #version 460 + layout(local_size_x = 1, local_size_y = 1) in; + void main() { + // compute shader + } + )"; + + OpenGLShader computeShader; + bool compiled = computeShader.CompileCompute(computeSrc); + EXPECT_TRUE(compiled); +} + +TEST_F(OpenGLTestFixture, CommandList_Dispatch) { + // 编译 compute shader + OpenGLShader computeShader; + computeShader.CompileCompute(computeSrc); + + // 创建 pipeline state + OpenGLPipelineState pipeline; + pipeline.SetComputeShader(&computeShader); + pipeline.Apply(); + + // 测试 Dispatch + OpenGLCommandList cmdList; + cmdList.Reset(); + cmdList.SetPipelineState(&pipeline); + cmdList.Dispatch(8, 8, 1); + cmdList.Close(); +} +``` + +--- + +## 4. 测试覆盖度矩阵 + +### 4.1 RHI 抽象层覆盖度 + +| RHI 抽象接口 | 覆盖状态 | 备注 | +|--------------|----------|------| +| Device 创建/初始化 | ✅ 已覆盖 | | +| Buffer 创建/映射 | ✅ 已覆盖 | | +| Texture 创建 | ✅ 已覆盖 | | +| SwapChain | ✅ 已覆盖 | | +| CommandList 基础 | ⚠️ 太弱 | 大部分传递 nullptr | +| CommandQueue | ✅ 已覆盖 | | +| Fence | ✅ 已覆盖 | | +| Sampler | ✅ 已覆盖 | | +| Shader 编译 | ❌ 无效 | 9个测试完全相同 | +| **PipelineState** | ❌ 无测试 | **必须添加** | +| **PipelineLayout** | ❌ 无测试 | **必须添加** | +| **DescriptorPool** | ❌ 无测试 | **必须添加** | +| **DescriptorSet** | ❌ 无测试 | **必须添加** | +| **RenderPass** | ❌ 无测试 | **必须添加** | +| **Framebuffer** | ❌ 无测试 | **必须添加** | +| **Compute/Dispatch** | ❌ 无测试 | **必须添加** | +| Resource Barriers | ⚠️ 太弱 | 传递 nullptr | + +### 4.2 D3D12 后端覆盖度 + +| D3D12 特定功能 | 覆盖状态 | 备注 | +|----------------|----------|------| +| 设备特性检测 | ✅ 已覆盖 | | +| Descriptor Heap | ✅ 已覆盖 | | +| Command Allocator | ✅ 已覆盖 | | +| Fence Timeline | ✅ 已覆盖 | | +| RootSignature | ⚠️ 弱 | 只测试空签名和 CBV | +| PipelineState | ⚠️ 弱 | 只测试 struct 默认值 | +| Shader | ⚠️ 弱 | 只做字符串比较 | +| SwapChain | ✅ 已覆盖 | | +| Views (RTV/DSV) | ✅ 已覆盖 | | + +### 4.3 OpenGL 后端覆盖度 + +| OpenGL 特定功能 | 覆盖状态 | 备注 | +|----------------|----------|------| +| Buffer (VBO/IBO/UBO) | ✅ 已覆盖 | | +| Texture (2D/Cube) | ✅ 已覆盖 | | +| VertexArray | ✅ 已覆盖 | | +| Shader 编译 | ✅ 已覆盖 | | +| PipelineState | ✅ 已覆盖 | | +| Sampler | ✅ 已覆盖 | | +| Fence | ✅ 已覆盖 | | +| SwapChain | ✅ 已覆盖 | | +| **Compute Shader** | ❌ 无测试 | **必须添加** | +| **Dispatch** | ❌ 无测试 | **必须添加** | +| Memory Barrier | ❌ 无测试 | 可选 | + +--- + +## 5. 重构优先级总结 + +### P0 - 必须修复(影响功能验证) + +| 优先级 | 问题 | 工作量 | 影响 | +|--------|------|--------|------| +| 1 | Shader 测试重构 | 中 | 9个测试无效 | +| 2 | 添加 PipelineState 测试 | 大 | RHI 核心组件无测试 | +| 3 | 添加 RenderPass 测试 | 中 | 重要 API 未覆盖 | +| 4 | 添加 Framebuffer 测试 | 中 | 重要 API 未覆盖 | + +### P1 - 应该修复(提高覆盖率) + +| 优先级 | 问题 | 工作量 | 影响 | +|--------|------|--------|------| +| 5 | 添加 DescriptorPool/Set 测试 | 中 | 已实现未测试 | +| 6 | CommandList 测试增强 | 大 | 大部分传递 nullptr | +| 7 | 添加 Compute/Dispatch 测试 | 中 | 重要功能缺失 | +| 8 | D3D12 PSO/shader 测试增强 | 小 | 当前测试 trivial | + +### P2 - 可以修复(完善细节) + +| 优先级 | 问题 | 工作量 | 影响 | +|--------|------|--------|------| +| 9 | OpenGL CommandList SetShader 测试 | 小 | 缺失测试 | +| 10 | 复杂 RootSignature 参数测试 | 小 | 当前只测试 CBV | +| 11 | OpenGL Compute Shader 测试 | 小 | 缺失测试 | + +--- + +## 6. 新增测试文件清单 + +| 文件路径 | 测试内容 | 优先级 | +|----------|----------|--------| +| `tests/RHI/unit/test_pipeline_state.cpp` | PipelineState 创建/配置/绑定 | P0 | +| `tests/RHI/unit/test_render_pass.cpp` | RenderPass 创建/Begin/End | P0 | +| `tests/RHI/unit/test_framebuffer.cpp` | Framebuffer 创建/绑定 | P0 | +| `tests/RHI/unit/test_descriptor.cpp` | DescriptorPool/Set 创建/更新 | P1 | +| `tests/RHI/unit/test_compute.cpp` | Compute shader/Dispatch | P1 | +| `tests/RHI/unit/test_pipeline_layout.cpp` | PipelineLayout 创建 | P2 | + +--- + +## 7. 测试质量改进建议 + +### 7.1 测试命名规范 + +遵循 `Component_Category_SubBehavior` 格式: + +``` +Good: +- Buffer_Create_DefaultHeap +- CommandList_SetViewport_ValidRect +- Shader_Compile_ValidSource + +Bad: +- Test1 +- BufferTest +- test_buffer +``` + +### 7.2 测试结构规范 + +每个测试应包含: + +```cpp +TEST_P(RHITestFixture, CommandList_SetVertexBuffer_WithRealBuffer) { + // 1. Arrange - 准备测试数据 + BufferDesc bufDesc = {}; + bufDesc.size = 1024; + RHIBuffer* buffer = GetDevice()->CreateBuffer(bufDesc); + ASSERT_NE(buffer, nullptr); // 使用 ASSERT 防止空指针解引用 + + // 2. Act - 执行被测操作 + RHICommandList* cmdList = GetDevice()->CreateCommandList({}); + cmdList->Reset(); + RHIResourceView* view = buffer->GetView(); + cmdList->SetVertexBuffers(0, 1, &view, nullptr, nullptr); + cmdList->Close(); + + // 3. Assert - 验证结果 + // 注意:有些测试难以直接验证结果,可以通过不崩溃来间接验证 + + // 4. Cleanup - 清理资源 + cmdList->Shutdown(); + delete cmdList; + buffer->Shutdown(); + delete buffer; +} +``` + +### 7.3 避免的问题 + +```cpp +// 问题1: 测试逻辑完全相同 +TEST_P(RHITestFixture, Shader_Test1) { shader = nullptr; EXPECT_EQ(shader, nullptr); } +TEST_P(RHITestFixture, Shader_Test2) { shader = nullptr; EXPECT_EQ(shader, nullptr); } // 相同! + +// 问题2: 传递 nullptr 不验证功能 +TEST_P(RHITestFixture, CommandList_Test) { + cmdList->ClearRenderTarget(nullptr, color); // 无意义 +} + +// 问题3: 不清理资源 +TEST_P(RHITestFixture, Buffer_Test) { + RHIBuffer* buffer = device->CreateBuffer(desc); + // 忘记 buffer->Shutdown() 和 delete buffer +} + +// 问题4: 缺少 ASSERT +TEST_P(RHITestFixture, Buffer_Test) { + RHIBuffer* buffer = device->CreateBuffer(desc); + buffer->SetData(...); // 如果 buffer 是 nullptr 则崩溃 + // 应该先 ASSERT_NE(buffer, nullptr); +} +``` + +--- + +## 8. 附录:测试执行命令 + +```bash +# 增量构建 RHI 测试 +cmake --build . --target rhi_unit_tests --config Debug +cmake --build . --target rhi_d3d12_tests --config Debug +cmake --build . --target rhi_opengl_tests --config Debug + +# 运行 RHI 抽象层测试 (同时验证 D3D12 和 OpenGL) +ctest -R "^D3D12/|^OpenGL/" -C Debug --output-on-failure + +# 运行 D3D12 后端专用测试 +ctest -R "D3D12TestFixture|SwapChainTestFixture" -C Debug --output-on-failure + +# 运行 OpenGL 后端专用测试 +ctest -R "OpenGLTestFixture" -C Debug --output-on-failure + +# 运行所有 RHI 测试 +ctest -R "D3D12|OpenGL|RHITestFixture" -C Debug --output-on-failure +``` + +--- + +**文档版本**: 1.0 +**最后更新**: 2026-03-25 +**作者**: XCEngine Team diff --git a/RHI_Design_Issues.md b/docs/plan/used/RHI_Design_Issues.md similarity index 100% rename from RHI_Design_Issues.md rename to docs/plan/used/RHI_Design_Issues.md diff --git a/minimal.ppm b/minimal.ppm deleted file mode 100644 index 2217d8f07a547a8e2b5ef4b64f14bbab3d65cb0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2764816 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 z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* i1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r#(@C*8F^0t