Files
XCEngine/docs/used/D3D12_Texture_Architecture_Fix_Plan.md
ssdfasd 16e2065c6c 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.
2026-03-25 16:13:02 +08:00

14 KiB
Raw Blame History

D3D12 Texture 封装架构修复方案

1. 问题分析

1.1 当前问题

问题 A双重 Texture 包装导致悬垂指针

// 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 对象
  • 但它们内部都持有指向同一个 ID3D12ResourceComPtr
  • 当其中一个调用 Shutdown()m_resource.Reset(),另一个就变成悬垂指针!

问题 BInitializeFromExisting 不区分所有权语义

void D3D12Texture::Shutdown() {
    m_resource.Reset();  // 无条件释放资源
}

如果通过 InitializeFromExisting 包装了 SwapChain 的 back bufferShutdown 会错误地释放它!

问题 CD3D12SwapChain::GetBackBuffer() 返回内部引用,用户可能误用

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

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

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()。如果我们要实现"不拥有但不释放",需要用原始指针存储。

修正方案

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

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 返回值

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

// 修改前(有问题):
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]
  • 方案 BgColorRTs[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. 修改 D3D12SwapChainGetBackBuffer 返回引用
  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 行为澄清

// ComPtr::Reset() 调用 Release()
// 如果多个 ComPtr 指向同一资源Reset 只会减少引用计数
// 只有最后一个 Reset 才会真正释放

ComPtr<ID3D12Resource> a = resource;  // AddRef
ComPtr<ID3D12Resource> b = resource;  // AddRef

a.Reset();  // Release资源仍未释放b 还持有)
b.Reset();  // 最后一个 Release资源被释放

关键点:如果 D3D12Texturem_backBuffers 都持有同一个 ID3D12ResourceComPtr,那么:

  • 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

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 被传给了 InitializeFromExistingComPtr 构造时会 AddRef。但 resource 是局部变量,函数结束后 resource 局部变量销毁但不影响 COM 对象的引用计数。

等等,这里有个问题:

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_resourcem_swapChain 内部都持有同一个 COM 对象的引用。

然后 minimal/main.cpp 又做了一次:

ID3D12Resource* buffer = nullptr;
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer));  // buffer 引用计数 = 3
gColorRTs[i].InitializeFromExisting(buffer);                      // gColorRTs[i].m_resource 引用计数 = 4
// buffer 局部变量销毁

现在有三个 ComPtrm_backBuffers[i].m_resource, gColorRTs[i].m_resource, m_swapChain 内部)指向同一个对象。引用计数 = 4。

这不是问题!因为 ComPtr 的拷贝构造会 AddRef。最终:

  • m_backBuffers[i] 销毁 → 引用计数--
  • gColorRTs[i] 销毁 → 引用计数--
  • m_swapChain 销毁 → 引用计数--

引用计数最终归零,资源被正确释放。

那问题是什么?

回到 Shutdown

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 中:

std::vector<D3D12Texture> m_backBuffers;

如果 minimal/main.cppgSwapChain.Shutdown() 被调用:

  • D3D12SwapChain::~D3D12SwapChain() 调用 Shutdown()
  • Shutdown() 调用 m_swapChain.Reset()(仅重置 swapchain 指针)
  • 然后 vector m_backBuffers 销毁,每个 D3D12Texture 析构调用 Shutdown()
  • 每个 Shutdown() 调用 m_resource.Reset() → 释放 back buffer

但是gColorRTs[i] 是在全局变量中独立创建的:

D3D12Texture gColorRTs[2];  // 独立于 SwapChain

如果 gSwapChain.Shutdown() 先执行,m_backBuffersm_resource 被释放,那么 gColorRTs[i].m_resource 就变成悬垂的 ComPtr

// 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 节的修改清单执行。