Files
DX12/docs/changelogs/2026-03/20260330-d3d12-deferred-release.md
SpecialX b6c0211d6a feat(d3d12): 完善描述符堆延迟释放机制与FreeList栈式索引管理
- 添加完整的中文Doxygen注释文档
- 实现process_deferred_release()延迟释放处理
- 添加deferred_release模板函数和current_frame_index()
- 实现延迟释放队列和帧索引管理
- 详细说明FreeList栈式索引分配/释放算法
- 更新D3D12学习Wiki,添加延迟释放机制章节
2026-03-30 16:59:27 +08:00

9.8 KiB
Raw Blame History

变更记录:延迟释放机制与描述符堆完善

提交日期: 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 安全 延迟释放防止数据竞争

后续工作

  • 实现交换链
  • 实现渲染目标视图
  • 渲染第一个三角形

相关文档