Files
XCEngine/docs/D3D12后端测试设计.md

16 KiB
Raw Blame History

D3D12 后端测试设计

1. 概述

本文档描述 XCEngine D3D12 渲染后端的测试框架设计,旨在为 D3D12 各组件提供全面、规范的自动化测试覆盖。

1.1 测试目标

  • 验证 D3D12 各组件 API 的正确性
  • 确保组件间的协作正常工作
  • 捕获资源泄漏和内存错误
  • 支持持续集成CI自动化测试

1.2 现有问题

当前 tests/D3D12/main.cpp 存在以下问题:

问题 说明
非自动化测试 截图和对比需手动触发
缺乏单元测试 只有一个 Win32 图形程序,无法使用 Google Test
维护困难 GT.ppm 是纯黑色图像,对比无实际意义
容差问题 1% 阈值对硬件差异过于敏感

2. 测试目录结构

tests/D3D12/
├── CMakeLists.txt              # 构建配置(需改造为 Google Test
├── fixtures/
│   └── D3D12TestFixture.h     # 基础测试夹具
├── test_device.cpp             # D3D12Device 测试
├── test_command_queue.cpp      # D3D12CommandQueue 测试
├── test_command_allocator.cpp  # D3D12CommandAllocator 测试
├── test_command_list.cpp       # D3D12CommandList 测试
├── test_buffer.cpp             # D3D12Buffer 测试
├── test_texture.cpp            # D3D12Texture 测试
├── test_descriptor_heap.cpp    # D3D12DescriptorHeap 测试
├── test_pipeline_state.cpp     # D3D12PipelineState 测试
├── test_root_signature.cpp     # D3D12RootSignature 测试
├── test_fence.cpp              # D3D12Fence 测试
├── test_swap_chain.cpp         # D3D12SwapChain 测试(需窗口)
├── test_shader.cpp             # D3D12Shader 测试
├── test_views.cpp              # RTV/DSV/SRV/UAV 测试
└── test_screenshot.cpp         # 渲染结果测试

3. 测试夹具设计

3.1 基础夹具

// fixtures/D3D12TestFixture.h
#pragma once

#include <gtest/gtest.h>
#include <d3d12.h>
#include <wrl/client.h>

using namespace Microsoft::WRL;

class D3D12TestFixture : public ::testing::Test {
protected:
    static void SetUpTestSuite() {
        // 创建全局 D3D12 设备(所有测试共享)
        HRESULT hr = D3D12CreateDevice(
            nullptr,                    // 默认适配器
            D3D_FEATURE_LEVEL_12_0,     // 最低支持特性等级
            IID_PPV_ARGS(&mDevice)
        );
        ASSERT_TRUE(SUCCEEDED(hr)) << "Failed to create D3D12 device";
    }

    static void TearDownTestSuite() {
        mDevice.Reset();
    }

    void SetUp() override {
        // 创建命令队列
        D3D12_COMMAND_QUEUE_DESC queueDesc = {};
        queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
        
        HRESULT hr = mDevice->CreateCommandQueue(
            &queueDesc, 
            IID_PPV_ARGS(&mCommandQueue)
        );
        ASSERT_TRUE(SUCCEEDED(hr));

        // 创建命令分配器
        hr = mDevice->CreateCommandAllocator(
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            IID_PPV_ARGS(&mCommandAllocator)
        );
        ASSERT_TRUE(SUCCEEDED(hr));

        // 创建命令列表
        hr = mDevice->CreateCommandList(
            0,
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            mCommandAllocator.Get(),
            nullptr,
            IID_PPV_ARGS(&mCommandList)
        );
        ASSERT_TRUE(SUCCEEDED(hr));
    }

    void TearDown() override {
        // 等待所有命令执行完成
        WaitForGPU();

        mCommandList.Reset();
        mCommandAllocator.Reset();
        mCommandQueue.Reset();
    }

    // 常用辅助方法
    ID3D12Device* GetDevice() { return mDevice.Get(); }
    ID3D12CommandQueue* GetCommandQueue() { return mCommandQueue.Get(); }
    ID3D12CommandList* GetCommandList() { return mCommandList.Get(); }

    void WaitForGPU() {
        ComPtr<ID3D12Fence> fence;
        UINT64 fenceValue = 1;

        HRESULT hr = mDevice->CreateFence(
            0, 
            D3D12_FENCE_FLAG_NONE,
            IID_PPV_ARGS(&fence)
        );
        if (SUCCEEDED(hr)) {
            mCommandQueue->Signal(fence.Get(), fenceValue);
            HANDLE eventHandle = CreateEvent(nullptr, FALSE, FALSE, nullptr);
            fence->SetEventOnCompletion(fenceValue, eventHandle);
            WaitForSingleObject(eventHandle, INFINITE);
            CloseHandle(eventHandle);
        }
    }

private:
    static ComPtr<ID3D12Device> mDevice;
    ComPtr<ID3D12CommandQueue> mCommandQueue;
    ComPtr<ID3D12CommandAllocator> mCommandAllocator;
    ComPtr<ID3D12CommandList> mCommandList;
};

// 静态成员定义
ComPtr<ID3D12Device> D3D12TestFixture::mDevice;

3.2 资源测试夹具

// fixtures/D3D12ResourceFixture.h
#pragma once

#include "D3D12TestFixture.h"

class D3D12ResourceFixture : public D3D12TestFixture {
protected:
    void CreateUploadBuffer(size_t size, ID3D12Resource** outResource) {
        CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_UPLOAD);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(size);
        
        HRESULT hr = GetDevice()->CreateCommittedResource(
            &heapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(outResource)
        );
        ASSERT_TRUE(SUCCEEDED(hr));
    }

    void CreateDefaultBuffer(size_t size, ID3D12Resource** outResource) {
        CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_DEFAULT);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(size);
        
        HRESULT hr = GetDevice()->CreateCommittedResource(
            &heapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_COPY_DEST,
            nullptr,
            IID_PPV_ARGS(outResource)
        );
        ASSERT_TRUE(SUCCEEDED(hr));
    }
};

4. 测试分类

4.1 单元测试

测试单一 API 的基本功能,不依赖图形硬件渲染结果。

TEST(D3D12_Buffer, Create_ValidSize_ReturnsSuccess) {
    const size_t bufferSize = 1024;
    
    D3D12Buffer buffer;
    bool result = buffer.Initialize(GetDevice(), bufferSize, 
        D3D12_RESOURCE_STATE_GENERIC_READ, 
        D3D12_HEAP_TYPE_UPLOAD);
    
    ASSERT_TRUE(result);
    ASSERT_NE(buffer.GetResource(), nullptr);
    ASSERT_EQ(buffer.GetResource()->GetDesc().Width, bufferSize);
}

TEST(D3D12_Buffer, Map_ValidRange_ReturnsValidPointer) {
    D3D12Buffer buffer;
    buffer.Initialize(GetDevice(), 256, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_HEAP_TYPE_UPLOAD);
    
    void* mappedData = buffer.Map(0, nullptr);
    ASSERT_NE(mappedData, nullptr);
    
    // 写入测试数据
    memset(mappedData, 0xAB, 256);
    buffer.Unmap(0, nullptr);
}

TEST(D3D12_DescriptorHeap, Create_RTVHeap_ReturnsValidHeap) {
    D3D12DescriptorHeap heap;
    bool result = heap.Initialize(GetDevice(), D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 4);
    
    ASSERT_TRUE(result);
    ASSERT_NE(heap.GetDescriptorHeap(), nullptr);
}

4.2 集成测试

测试多个组件的协作,例如 Buffer 数据上传到 GPU。

TEST(D3D12_Buffer, UploadToGPU_DataIntegrity) {
    const size_t dataSize = sizeof(float) * 4;
    float testData[] = { 1.0f, 2.0f, 3.0f, 4.0f };
    
    // 创建默认缓冲GPU 只读)
    D3D12Buffer gpuBuffer;
    gpuBuffer.Initialize(GetDevice(), dataSize, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_HEAP_TYPE_DEFAULT);
    
    // 创建上传缓冲CPU 可写)
    D3D12Buffer uploadBuffer;
    uploadBuffer.Initialize(GetDevice(), dataSize, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_HEAP_TYPE_UPLOAD);
    
    // 复制数据到上传缓冲
    void* mappedData = uploadBuffer.Map(0, nullptr);
    memcpy(mappedData, testData, dataSize);
    uploadBuffer.Unmap(0, nullptr);
    
    // 通过命令列表复制
    GetCommandList()->CopyBufferRegion(
        gpuBuffer.GetResource(), 0,
        uploadBuffer.GetResource(), 0,
        dataSize
    );
    
    // 执行并等待
    GetCommandList()->Close();
    ID3D12CommandList* cmdLists[] = { GetCommandList() };
    GetCommandQueue()->ExecuteCommandLists(1, cmdLists);
    WaitForGPU();
    
    // 验证:使用映射读取返回的数据
    // (需要转换状态到 GENERIC_READ
}

4.3 渲染结果测试

渲染到纹理并验证像素数据,而非渲染到屏幕。

4.3.1 测试策略

方案 优点 缺点
渲染到 RTT 无窗口依赖,可 CI 需要 Mipmap 比较算法
Shader 输出测试值 精确验证 需要特殊 Shader
Golden Image 对比 直观 维护成本高

4.3.2 推荐方案:渲染特定图案

生成一个已知的测试图案(如渐变、棋盘格),渲染后读取像素验证:

TEST(D3D12_RenderTarget, ClearColor_VerifyPixelValue) {
    // 创建渲染目标纹理
    const uint32_t width = 64;
    const uint32_t height = 64;
    
    D3D12Texture renderTarget;
    renderTarget.InitializeAsRenderTarget(
        GetDevice(), width, height, 
        DXGI_FORMAT_R8G8B8A8_UNORM
    );
    
    // 创建 RTV
    D3D12DescriptorHeap rtvHeap;
    rtvHeap.Initialize(GetDevice(), D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1);
    
    D3D12RenderTargetView rtv;
    rtv.InitializeAt(
        GetDevice(), 
        renderTarget.GetResource(),
        rtvHeap.GetCPUDescriptorHandleForHeapStart(),
        nullptr
    );
    
    // 清空为特定颜色
    float clearColor[] = { 0.25f, 0.5f, 0.75f, 1.0f }; // R=64, G=128, B=192
    GetCommandList()->ClearRenderTargetView(
        rtvHeap.GetCPUDescriptorHandleForHeapStart(),
        clearColor, 0, nullptr
    );
    
    // 转换状态用于读取
    GetCommandList()->TransitionBarrier(
        renderTarget.GetResource(),
        D3D12_RESOURCE_STATE_RENDER_TARGET,
        D3D12_RESOURCE_STATE_COPY_SOURCE
    );
    
    GetCommandList()->Close();
    ID3D12CommandList* cmdLists[] = { GetCommandList() };
    GetCommandQueue()->ExecuteCommandLists(1, cmdLists);
    WaitForGPU();
    
    // 读取像素数据
    std::vector<uint8_t> pixels(width * height * 4);
    ReadBackTexture(GetDevice(), GetCommandQueue(), 
        renderTarget.GetResource(), pixels.data(), pixels.size());
    
    // 验证中心像素
    uint32_t centerIndex = (height / 2 * width + width / 2) * 4;
    EXPECT_NEAR(pixels[centerIndex + 0], 64, 2);  // R
    EXPECT_NEAR(pixels[centerIndex + 1], 128, 2); // G
    EXPECT_NEAR(pixels[centerIndex + 2], 192, 2); // B
}

4.3.3 图案化渲染测试

使用纯色 Shader 渲染特定图案:

// TestPattern.hlsl - 输出棋盘格图案
float4 PSMain(PSInput input) : SV_Target {
    uint2 pixelPos = uint2(input.Position.xy);
    bool isWhite = ((pixelPos.x / 8) % 2) == ((pixelPos.y / 8) % 2);
    return isWhite ? float4(1,1,1,1) : float4(0,0,0,1);
}

4.4 性能测试

使用 Google Benchmark 或简单计时:

TEST(D3D12_Performance, BufferCreation_1000Times) {
    const int iterations = 1000;
    
    auto start = std::chrono::high_resolution_clock::now();
    
    for (int i = 0; i < iterations; ++i) {
        D3D12Buffer buffer;
        buffer.Initialize(GetDevice(), 1024, 
            D3D12_RESOURCE_STATE_GENERIC_READ, 
            D3D12_HEAP_TYPE_UPLOAD);
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    printf("Created %d buffers in %ld ms\n", iterations, duration.count());
}

5. 组件测试详情

5.1 D3D12Device

TEST(D3D12_Device, CheckFeatureSupport_D3D12OK) {
    D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevels = {};
    featureLevels.NumFeatureLevels = 1;
    featureLevels.pFeatureLevelsRequested = &mFeatureLevel;
    
    HRESULT hr = GetDevice()->CheckFeatureSupport(
        D3D12_FEATURE_FEATURE_LEVELS,
        &featureLevels,
        sizeof(featureLevels)
    );
    
    ASSERT_TRUE(SUCCEEDED(hr));
    ASSERT_EQ(featureLevels.MaxSupportedFeatureLevel, D3D_FEATURE_LEVEL_12_0);
}

5.2 D3D12CommandList

TEST(D3D12_CommandList, Reset_AfterClose_Succeeds) {
    // 第一次使用
    GetCommandList()->Close();
    
    // 重置并再次使用
    HRESULT hr = GetCommandList()->Reset(mCommandAllocator.Get(), nullptr);
    ASSERT_TRUE(SUCCEEDED(hr));
}

5.3 D3D12Fence

TEST(D3D12_Fence, SignalAndWait) {
    ComPtr<ID3D12Fence> fence;
    UINT64 fenceValue = 1;
    
    HRESULT hr = GetDevice()->CreateFence(
        0, D3D12_FENCE_FLAG_NONE,
        IID_PPV_ARGS(&fence)
    );
    ASSERT_TRUE(SUCCEEDED(hr));
    
    // Signal
    GetCommandQueue()->Signal(fence.Get(), fenceValue);
    
    // 等待
    ASSERT_EQ(fence->GetCompletedValue(), fenceValue);
}

6. 资源泄漏检测

6.1 使用 D3D12 Debug Layer

#ifdef _DEBUG
class D3D12LeakDetector {
public:
    static void BeginFrame() {
        if (sDebugDevice) {
            sDebugDevice->SetName(L"Leak Detection Frame Start");
        }
    }
    
    static void EndFrame() {
        if (sDebugDevice) {
            sDebugDevice->SetName(L"Leak Detection Frame End");
            sDebugDevice->ReportLiveDeviceObjects(
                D3D12_RDO_FLAGS::D3D12_RDO_FLAG_NONE
            );
        }
    }
    
    static void SetDebugDevice(ID3D12DebugDevice* device) {
        sDebugDevice = device;
    }
    
private:
    static ID3D12DebugDevice* sDebugDevice;
};
#endif

6.2 在测试夹具中使用

class D3D12LeakCheckFixture : public D3D12TestFixture {
protected:
    void TearDown() override {
        D3D12TestFixture::TearDown();
        
#ifdef _DEBUG
        D3D12LeakDetector::EndFrame();
#endif
    }
};

7. 构建配置

7.1 CMakeLists.txt 修改

cmake_minimum_required(VERSION 3.15)

project(D3D12Tests)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Google Test
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://gitee.com/mirrors/googletest.git
    GIT_TAG v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

# 测试源文件
set(TEST_SOURCES
    test_device.cpp
    test_command_queue.cpp
    test_command_allocator.cpp
    test_command_list.cpp
    test_buffer.cpp
    test_texture.cpp
    test_descriptor_heap.cpp
    test_pipeline_state.cpp
    test_root_signature.cpp
    test_fence.cpp
    test_shader.cpp
    test_views.cpp
)

add_executable(d3d12_tests ${TEST_SOURCES})

target_link_libraries(d3d12_tests PRIVATE
    d3d12
    dxgi
    d3dcompiler
    XCEngine
    GTest::gtest
    GTest::gtest_main
)

target_include_directories(d3d12_tests PRIVATE
    ${CMAKE_SOURCE_DIR}/engine/include
    ${CMAKE_CURRENT_SOURCE_DIR}/fixtures
)

add_test(NAME D3D12Tests COMMAND d3d12_tests)

8. CI 集成

8.1 Windows CI 配置示例

# .github/workflows/d3d12-tests.yml
name: D3D12 Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: windows-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Configure
        run: cmake -B build -S . -G "Visual Studio 17 2022"
      
      - name: Build
        run: cmake --build build --config Debug
      
      - name: Run Tests
        run: ctest --test-dir build -C Debug --output-on-failure

9. 测试覆盖矩阵

组件 单元测试 集成测试 渲染测试 性能测试
D3D12Device - -
D3D12CommandQueue -
D3D12CommandAllocator - - -
D3D12CommandList - -
D3D12Buffer -
D3D12Texture
D3D12DescriptorHeap - - -
D3D12PipelineState - - -
D3D12RootSignature - - -
D3D12Fence - -
D3D12SwapChain - - -
D3D12Shader - - -
RTV/DSV/SRV/UAV -

10. 后续改进

  • 实现所有组件的基础单元测试
  • 添加资源泄漏检测工具
  • 完善渲染结果测试图案
  • 添加性能基准测试
  • 配置 CI 自动测试
  • 支持 Vulkan 后端测试(复用测试夹具抽象)