- 添加完整的中文Doxygen注释文档 - 实现process_deferred_release()延迟释放处理 - 添加deferred_release模板函数和current_frame_index() - 实现延迟释放队列和帧索引管理 - 详细说明FreeList栈式索引分配/释放算法 - 更新D3D12学习Wiki,添加延迟释放机制章节
9.8 KiB
9.8 KiB
变更记录:延迟释放机制与描述符堆完善
提交日期: 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 栈式索引管理
数据结构
// 空闲索引栈
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 已回收到空闲栈 │
└─────────────────────────────────────┘
分配算法
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)
释放算法
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 已完成使用
数据结构
// 每帧一个延迟释放队列
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. 线程安全
// 所有操作都使用互斥锁保护
std::mutex _mutex{};
void allocate() {
std::lock_guard lock(_mutex); // 自动加锁
// ... 操作
} // 自动解锁
设计优势
| 特性 | 说明 |
|---|---|
| O(1) 分配 | 栈顶弹出,无需遍历查找 |
| O(1) 释放 | 栈顶压入,无需查找位置 |
| 内存高效 | 预分配数组,无动态分配开销 |
| 线程安全 | 互斥锁保护所有操作 |
| GPU 安全 | 延迟释放防止数据竞争 |
后续工作
- 实现交换链
- 实现渲染目标视图
- 渲染第一个三角形