feat(d3d12): 完善描述符堆延迟释放机制与FreeList栈式索引管理
- 添加完整的中文Doxygen注释文档 - 实现process_deferred_release()延迟释放处理 - 添加deferred_release模板函数和current_frame_index() - 实现延迟释放队列和帧索引管理 - 详细说明FreeList栈式索引分配/释放算法 - 更新D3D12学习Wiki,添加延迟释放机制章节
This commit is contained in:
250
docs/changelogs/2026-03/20260330-d3d12-deferred-release.md
Normal file
250
docs/changelogs/2026-03/20260330-d3d12-deferred-release.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 变更记录:延迟释放机制与描述符堆完善
|
||||
|
||||
**提交日期**: 2026-03-30
|
||||
**提交哈希**: `6e0df60`
|
||||
**变更类型**: 功能完善
|
||||
|
||||
---
|
||||
|
||||
## 变更概述
|
||||
|
||||
本次提交完善了描述符堆的延迟释放机制,实现了完整的 FreeList 栈式索引管理,并添加了详细的中文注释文档。
|
||||
|
||||
## 修改文件
|
||||
|
||||
### Engine/Graphics/Direct3D12/
|
||||
|
||||
| 文件 | 变更说明 |
|
||||
|------|----------|
|
||||
| `D3D12Resources.h` | 添加完整的中文 Doxygen 注释 |
|
||||
| `D3D12Resource.cpp` | 实现 `process_deferred_release()` 延迟释放处理 |
|
||||
| `D3D12Core.h` | 添加 `deferred_release` 模板函数和 `current_frame_index()` |
|
||||
| `D3D12Core.cpp` | 实现延迟释放队列和帧索引管理 |
|
||||
|
||||
---
|
||||
|
||||
## 技术要点
|
||||
|
||||
### 1. FreeList 栈式索引管理
|
||||
|
||||
#### 数据结构
|
||||
|
||||
```cpp
|
||||
// 空闲索引栈
|
||||
std::unique_ptr<u32[]> _free_handles{};
|
||||
|
||||
// 栈顶指针(同时也是已分配数量)
|
||||
u32 _size{0};
|
||||
|
||||
// 总容量
|
||||
u32 _capacity{0};
|
||||
```
|
||||
|
||||
#### 核心原理
|
||||
|
||||
`_free_handles` 是一个**预分配的数组**,同时充当**栈**的角色:
|
||||
- `_size` 既是已分配数量,也是栈顶指针
|
||||
- 分配时:从栈顶弹出索引
|
||||
- 释放时:将索引压入栈顶
|
||||
|
||||
```
|
||||
初始化状态(capacity = 5):
|
||||
┌─────────────────────────────────────┐
|
||||
│ _free_handles = [0, 1, 2, 3, 4] │
|
||||
│ _size = 0 (栈顶指向位置 0) │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
分配 2 个描述符后:
|
||||
┌─────────────────────────────────────┐
|
||||
│ _free_handles = [0, 1, 2, 3, 4] │
|
||||
│ _size = 2 (栈顶指向位置 2) │
|
||||
│ 已分配索引: 0, 1 │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
释放索引 0 后(延迟处理完成):
|
||||
┌─────────────────────────────────────┐
|
||||
│ _free_handles = [0, 0, 2, 3, 4] │
|
||||
│ _size = 1 (栈顶指向位置 1) │
|
||||
│ 已分配索引: 1 │
|
||||
│ 索引 0 已回收到空闲栈 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 分配算法
|
||||
|
||||
```cpp
|
||||
descriptor_handle allocate()
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
// 从栈顶取出索引
|
||||
const u32 index = _free_handles[_size];
|
||||
++_size; // 栈顶指针上移
|
||||
|
||||
// 计算句柄地址
|
||||
const u32 offset = index * _descriptor_size;
|
||||
handle.cpu.ptr = _cpu_start.ptr + offset;
|
||||
|
||||
return handle;
|
||||
}
|
||||
```
|
||||
|
||||
**时间复杂度**: O(1)
|
||||
|
||||
#### 释放算法
|
||||
|
||||
```cpp
|
||||
void free(descriptor_handle handle)
|
||||
{
|
||||
// 不立即释放,放入延迟队列
|
||||
const u32 frame_index = current_frame_index();
|
||||
_deferred_free_indices[frame_index].push_back(handle.index);
|
||||
}
|
||||
|
||||
void process_deferred_release(u32 frame_index)
|
||||
{
|
||||
for(auto index : _deferred_free_indices[frame_index])
|
||||
{
|
||||
--_size; // 栈顶指针下移
|
||||
_free_handles[_size] = index; // 索引压入栈顶
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**时间复杂度**: O(n),n 为该帧释放的描述符数量
|
||||
|
||||
---
|
||||
|
||||
### 2. 延迟释放机制
|
||||
|
||||
#### 为什么需要延迟释放?
|
||||
|
||||
```
|
||||
问题场景:
|
||||
帧 0: CPU 分配描述符索引 0 → 绑定纹理 A
|
||||
帧 1: CPU 释放描述符索引 0 → 立即重用 → 绑定纹理 B
|
||||
帧 0: GPU 还在执行,访问描述符索引 0 → 读到纹理 B 的数据!
|
||||
↑
|
||||
GPU 危险!数据竞争!
|
||||
```
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```
|
||||
帧 0: CPU 分配描述符索引 0 → 绑定纹理 A
|
||||
帧 1: CPU 释放描述符索引 0 → 放入延迟队列(帧 1)
|
||||
帧 2: CPU 继续工作...
|
||||
帧 0: GPU 完成帧 0 的执行
|
||||
帧 3: CPU 处理帧 0 的延迟释放 → 索引 0 回到空闲池
|
||||
↑
|
||||
安全!GPU 已完成使用
|
||||
```
|
||||
|
||||
#### 数据结构
|
||||
|
||||
```cpp
|
||||
// 每帧一个延迟释放队列
|
||||
utl::vector<u32> _deferred_free_indices[frame_buffer_count]{};
|
||||
|
||||
// 全局延迟释放资源队列
|
||||
utl::vector<IUnknown*> deferred_releases[frame_buffer_count]{};
|
||||
|
||||
// 延迟释放标志
|
||||
u32 deferred_release_flag[frame_buffer_count]{};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 完整流程图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 初始化 │
|
||||
│ _free_handles = [0, 1, 2, 3, 4] │
|
||||
│ _size = 0 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ allocate() │
|
||||
│ index = _free_handles[_size] // 取索引 0 │
|
||||
│ _size++ // _size = 1 │
|
||||
│ 返回描述符句柄(索引 0) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ allocate() │
|
||||
│ index = _free_handles[_size] // 取索引 1 │
|
||||
│ _size++ // _size = 2 │
|
||||
│ 返回描述符句柄(索引 1) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ free(索引 0) │
|
||||
│ 放入延迟队列: _deferred_free_indices[当前帧].push_back(0) │
|
||||
│ _size 不变,_free_handles 不变 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ GPU 完成帧(Fence 同步) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ process_deferred_release(frame_index) │
|
||||
│ --_size // _size = 1 │
|
||||
│ _free_handles[_size] = 0 // 索引 0 压入栈顶 │
|
||||
│ _free_handles = [0, 0, 2, 3, 4] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ allocate() │
|
||||
│ index = _free_handles[_size] // 取索引 0(重用) │
|
||||
│ _size++ // _size = 2 │
|
||||
│ 返回描述符句柄(索引 0) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 线程安全
|
||||
|
||||
```cpp
|
||||
// 所有操作都使用互斥锁保护
|
||||
std::mutex _mutex{};
|
||||
|
||||
void allocate() {
|
||||
std::lock_guard lock(_mutex); // 自动加锁
|
||||
// ... 操作
|
||||
} // 自动解锁
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计优势
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| **O(1) 分配** | 栈顶弹出,无需遍历查找 |
|
||||
| **O(1) 释放** | 栈顶压入,无需查找位置 |
|
||||
| **内存高效** | 预分配数组,无动态分配开销 |
|
||||
| **线程安全** | 互斥锁保护所有操作 |
|
||||
| **GPU 安全** | 延迟释放防止数据竞争 |
|
||||
|
||||
---
|
||||
|
||||
## 后续工作
|
||||
|
||||
- [ ] 实现交换链
|
||||
- [ ] 实现渲染目标视图
|
||||
- [ ] 渲染第一个三角形
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [D3D12学习Wiki](../wiki/D3D12学习Wiki.md)
|
||||
@@ -297,12 +297,196 @@ _cpu_start / _gpu_start
|
||||
_free_handles[] = [0, 1, 2, 3, ...] // 空闲索引池
|
||||
```
|
||||
|
||||
### 7.6 线程安全
|
||||
### 7.6 FreeList 栈式索引管理
|
||||
|
||||
#### 核心数据结构
|
||||
|
||||
```cpp
|
||||
// 空闲索引栈(预分配数组)
|
||||
std::unique_ptr<u32[]> _free_handles{};
|
||||
|
||||
// 栈顶指针(同时也是已分配数量)
|
||||
u32 _size{0};
|
||||
|
||||
// 总容量
|
||||
u32 _capacity{0};
|
||||
```
|
||||
|
||||
#### 工作原理
|
||||
|
||||
`_free_handles` 是一个**预分配数组**,同时充当**栈**的角色:
|
||||
|
||||
| 概念 | 说明 |
|
||||
|------|------|
|
||||
| `_free_handles` | 存储所有可用索引的数组 |
|
||||
| `_size` | 已分配数量 + 栈顶指针 |
|
||||
| 分配 | `_free_handles[_size++]`,从栈顶弹出 |
|
||||
| 释放 | `_free_handles[--_size] = index`,压入栈顶 |
|
||||
|
||||
#### 状态演变示例
|
||||
|
||||
```
|
||||
初始化状态(capacity = 5):
|
||||
┌─────────────────────────────────────┐
|
||||
│ _free_handles = [0, 1, 2, 3, 4] │
|
||||
│ _size = 0 (栈顶指向位置 0) │
|
||||
│ 可用索引: 0, 1, 2, 3, 4 │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
分配 2 个描述符后:
|
||||
┌─────────────────────────────────────┐
|
||||
│ _free_handles = [0, 1, 2, 3, 4] │
|
||||
│ _size = 2 (栈顶指向位置 2) │
|
||||
│ 已分配索引: 0, 1 │
|
||||
│ 可用索引: 2, 3, 4 │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
释放索引 0 后(延迟处理完成):
|
||||
┌─────────────────────────────────────┐
|
||||
│ _free_handles = [0, 0, 2, 3, 4] │
|
||||
│ _size = 1 (栈顶指向位置 1) │
|
||||
│ 已分配索引: 1 │
|
||||
│ 可用索引: 0, 2, 3, 4 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 分配算法
|
||||
|
||||
```cpp
|
||||
descriptor_handle allocate()
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
// 从栈顶取出索引
|
||||
const u32 index = _free_handles[_size];
|
||||
++_size; // 栈顶指针上移
|
||||
|
||||
// 计算句柄地址
|
||||
const u32 offset = index * _descriptor_size;
|
||||
handle.cpu.ptr = _cpu_start.ptr + offset;
|
||||
|
||||
return handle;
|
||||
}
|
||||
```
|
||||
|
||||
**时间复杂度**: O(1)
|
||||
|
||||
#### 释放算法(延迟释放)
|
||||
|
||||
```cpp
|
||||
// 释放时不立即回收,放入延迟队列
|
||||
void free(descriptor_handle handle)
|
||||
{
|
||||
const u32 frame_index = current_frame_index();
|
||||
_deferred_free_indices[frame_index].push_back(handle.index);
|
||||
}
|
||||
|
||||
// GPU 完成帧后处理延迟释放
|
||||
void process_deferred_release(u32 frame_index)
|
||||
{
|
||||
for(auto index : _deferred_free_indices[frame_index])
|
||||
{
|
||||
--_size; // 栈顶指针下移
|
||||
_free_handles[_size] = index; // 索引压入栈顶
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 设计优势
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| **O(1) 分配** | 栈顶弹出,无需遍历查找 |
|
||||
| **O(1) 释放** | 栈顶压入,无需查找位置 |
|
||||
| **内存高效** | 预分配数组,无动态分配开销 |
|
||||
| **简单可靠** | 栈结构天然保证索引不重复 |
|
||||
|
||||
### 7.7 延迟释放机制
|
||||
|
||||
#### 为什么需要延迟释放?
|
||||
|
||||
```
|
||||
问题场景(无延迟释放):
|
||||
帧 0: CPU 分配描述符索引 0 → 绑定纹理 A
|
||||
帧 1: CPU 释放描述符索引 0 → 立即重用 → 绑定纹理 B
|
||||
帧 0: GPU 还在执行,访问描述符索引 0 → 读到纹理 B 的数据!
|
||||
↑
|
||||
GPU 危险!数据竞争!
|
||||
```
|
||||
|
||||
#### 解决方案
|
||||
|
||||
```
|
||||
帧 0: CPU 分配描述符索引 0 → 绑定纹理 A
|
||||
帧 1: CPU 释放描述符索引 0 → 放入延迟队列(帧 1)
|
||||
帧 2: CPU 继续工作...
|
||||
帧 0: GPU 完成帧 0 的执行(Fence 同步确认)
|
||||
帧 3: CPU 处理帧 0 的延迟释放 → 索引 0 回到空闲池
|
||||
↑
|
||||
安全!GPU 已完成使用
|
||||
```
|
||||
|
||||
#### 数据结构
|
||||
|
||||
```cpp
|
||||
// 每帧一个延迟释放队列
|
||||
utl::vector<u32> _deferred_free_indices[frame_buffer_count]{};
|
||||
|
||||
// 全局延迟释放资源队列(用于 COM 对象)
|
||||
utl::vector<IUnknown*> deferred_releases[frame_buffer_count]{};
|
||||
|
||||
// 延迟释放标志
|
||||
u32 deferred_release_flag[frame_buffer_count]{};
|
||||
```
|
||||
|
||||
#### 完整流程图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 初始化 │
|
||||
│ _free_handles = [0, 1, 2, 3, 4], _size = 0 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ allocate() │
|
||||
│ index = _free_handles[_size] // 取索引 0 │
|
||||
│ _size++ // _size = 1 │
|
||||
│ 返回描述符句柄(索引 0) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ free(索引 0) │
|
||||
│ 放入延迟队列: _deferred_free_indices[当前帧].push_back(0) │
|
||||
│ _size 不变,_free_handles 不变 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ GPU 完成帧(Fence 同步) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ process_deferred_release(frame_index) │
|
||||
│ --_size // _size = 0 │
|
||||
│ _free_handles[_size] = 0 // 索引 0 压入栈顶 │
|
||||
│ _free_handles = [0, 1, 2, 3, 4] // 索引 0 可重用 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.8 线程安全
|
||||
|
||||
描述符堆可能被多线程并发访问,使用互斥锁保护:
|
||||
|
||||
```cpp
|
||||
std::lock_guard lock(_mutex);
|
||||
std::mutex _mutex{};
|
||||
|
||||
void allocate() {
|
||||
std::lock_guard lock(_mutex); // 自动加锁
|
||||
// ... 操作
|
||||
} // 自动解锁
|
||||
```
|
||||
|
||||
## 8. 渲染表面与窗口
|
||||
|
||||
Reference in New Issue
Block a user