Files
XCEngine/D3D12_Texture_Architecture_Fix_Plan.md
ssdfasd 34c04af6cb D3D12: Fix texture ownership semantics and remove GetSwapChain() exposure
This commit fixes the D3D12 texture architecture issues:

1. D3D12Texture ownership semantics:
   - Add m_ownsResource member to track resource ownership
   - InitializeFromExisting() now takes ownsResource parameter (default false)
   - Shutdown() only releases resource if ownsResource is true
   - Initialize() sets m_ownsResource = true for created resources

2. D3D12SwapChain changes:
   - Remove GetSwapChain() method (was exposing native D3D12 API)
   - Change GetBackBuffer() to return reference instead of pointer
   - Back buffers initialized with ownsResource = false

3. minimal/main.cpp updates:
   - Remove gColorRTs[2] array (was duplicating back buffer wrapping)
   - Direct use of gSwapChain.GetBackBuffer(i) instead
   - All references updated to use encapsulated API

4. Documentation:
   - Update TEST_SPEC.md v1.3
   - Remove known limitation 7.2 (minimal GetBuffer issue fixed)
   - Add D3D12_Texture_Architecture_Fix_Plan.md design document
2026-03-20 17:58:27 +08:00

403 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 节的修改清单执行。