feat(d3d12): 完善描述符堆延迟释放机制与FreeList栈式索引管理

- 添加完整的中文Doxygen注释文档
- 实现process_deferred_release()延迟释放处理
- 添加deferred_release模板函数和current_frame_index()
- 实现延迟释放队列和帧索引管理
- 详细说明FreeList栈式索引分配/释放算法
- 更新D3D12学习Wiki,添加延迟释放机制章节
This commit is contained in:
SpecialX
2026-03-30 16:58:35 +08:00
parent 54916b0ac6
commit b6c0211d6a
6 changed files with 1055 additions and 51 deletions

View File

@@ -1,67 +1,420 @@
#pragma once
#include "D3D12CommonHeaders.h"
#include "D3D12CommonHeader.h"
namespace XEngine::graphics::d3d12{
namespace XEngine::graphics::d3d12::core {
/**
* @brief 前向声明,用于友元类关系
*/
class descriptor_heap;
/**
* @struct descriptor_handle
* @brief 封装 CPU 和 GPU 描述符句柄的包装结构体
*
* @details
* 该结构体为 D3D12 中的描述符句柄管理提供统一接口。
* 描述符句柄表示描述符堆中特定描述符的指针。
*
* - **CPU 句柄**CPU 用于创建和修改描述符视图SRV、UAV、CBV、RTV、DSV
* - **GPU 句柄**GPU 在着色器执行期间访问描述符(仅着色器可见)
*
* @note
* - CPU 句柄在分配后始终有效
* - GPU 句柄仅在从着色器可见的描述符堆分配时有效
* - DEBUG 模式下存储额外的跟踪信息用于验证和调试
*
* @see descriptor_heap
* @see D3D12_CPU_DESCRIPTOR_HANDLE
* @see D3D12_GPU_DESCRIPTOR_HANDLE
*
* @example
* @code
* descriptor_handle handle = heap.allocate();
* if (handle.is_valid()) {
* device->CreateShaderResourceView(texture, &srvDesc, handle.cpu);
* if (handle.is_shader_visible()) {
* cmdList->SetGraphicsRootDescriptorTable(0, handle.gpu);
* }
* }
* @endcode
*/
struct descriptor_handle
{
/**
* @brief CPU 端描述符句柄
*
* @details
* 用于 CPU 端操作如创建视图CreateShaderResourceView、
* CreateUnorderedAccessView、CreateRenderTargetView 等)。
* 从任何类型的描述符堆成功分配后,此句柄始终有效。
*/
D3D12_CPU_DESCRIPTOR_HANDLE cpu{};
/**
* @brief GPU 端描述符句柄
*
* @details
* 用于通过根描述符表将描述符绑定到图形管线。
* 仅当从着色器可见的描述符堆分配时有效
* CBV_SRV_UAV 或 SAMPLER 类型,带有 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
*
* @note 值为 0 表示此句柄不是着色器可见的
*/
D3D12_GPU_DESCRIPTOR_HANDLE gpu{};
/**
* @brief 检查此句柄是否指向有效的描述符
*
* @return 如果 CPU 句柄非空则返回 true否则返回 false
*
* @note 有效句柄不保证底层描述符已正确初始化
*/
constexpr bool is_valid() const { return cpu.ptr != 0; }
/**
* @brief 检查此句柄是否可从 GPU 着色器访问
*
* @return 如果 GPU 句柄非空(着色器可见)则返回 true否则返回 false
*
* @details
* 着色器可见句柄可通过 SetGraphicsRootDescriptorTable 或
* SetComputeRootDescriptorTable 绑定到管线。非着色器可见句柄RTV、DSV
* 只能通过 OMSetRenderTargets 或类似方法绑定。
*/
constexpr bool is_shader_visible() const { return gpu.ptr != 0; }
#ifdef _DEBUG
private:
/**
* @brief 指向所属描述符堆的指针(仅 DEBUG 模式)
*
* @details
* 用于验证以确保句柄被释放到正确的堆。
* 此指针在分配时设置,在释放操作时验证。
*/
friend class descriptor_heap;
descriptor_heap* container{ nullptr };
u32 index{ u32_invalid_id };
descriptor_heap* container{nullptr};
/**
* @brief 描述符堆内的索引(仅 DEBUG 模式)
*
* @details
* 存储此描述符在其所属堆中的逻辑索引。
* 用于调试和验证目的,跟踪描述符分配情况。
*/
u32 index{u32_invalid_id};
#endif
}; // descriptor_handle
};
/**
* @class descriptor_heap
* @brief 管理 D3D12 描述符堆的分配、释放和生命周期
*
* @details
* 该类为 D3D12 描述符堆提供高级抽象,实现:
*
* - **池化分配**:基于空闲列表管理,实现 O(1) 分配/释放
* - **线程安全**:互斥锁保护操作,支持多线程资源创建
* - **延迟释放**:帧感知的资源生命周期管理,防止 GPU 危险
* - **类型安全**:通过构造函数参数实现编译时类型绑定
*
* ## 描述符堆类型
*
* | 类型 | 着色器可见 | 典型用途 |
* |------|------------|----------|
* | CBV_SRV_UAV | 可选 | 常量缓冲区、纹理、UAV |
* | SAMPLER | 可选 | 纹理采样器 |
* | RTV | 否 | 渲染目标视图 |
* | DSV | 否 | 深度模板视图 |
*
* ## 内存模型
*
* - **着色器可见堆**:分配在 GPU 可见内存(显存或映射到 GPU 的系统内存)
* - **非着色器可见堆**仅分配在系统内存CPU 可访问)
*
* ## 线程安全
*
* 所有公有方法都是线程安全的。多个线程可以同时从同一个堆分配和释放描述符。
* 内部互斥锁确保操作的原子性。
*
* ## 延迟释放机制
*
* 当描述符被释放时,不会立即返回到空闲池。而是放入帧索引的延迟释放队列。
* 当对应帧在 GPU 上完成后,调用 process_deferred_release() 时,描述符才会
* 返回到空闲池,确保不会发生 GPU-GPU 或 CPU-GPU 危险。
*
* @see descriptor_handle
* @see D3D12_DESCRIPTOR_HEAP_TYPE
* @see ID3D12DescriptorHeap
*
* @example
* @code
* // 创建一个包含 1024 个描述符的着色器可见 CBV/SRV/UAV 堆
* descriptor_heap cbv_srv_heap{D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV};
* cbv_srv_heap.initialize(1024, true);
*
* // 分配描述符
* descriptor_handle tex_handle = cbv_srv_heap.allocate();
* device->CreateShaderResourceView(texture, &srvDesc, tex_handle.cpu);
*
* // 绑定到管线
* ID3D12DescriptorHeap* heaps[] = {cbv_srv_heap.heap()};
* cmdList->SetDescriptorHeaps(1, heaps);
* cmdList->SetGraphicsRootDescriptorTable(0, tex_handle.gpu);
*
* // 不再需要时释放(延迟到帧完成)
* cbv_srv_heap.free(tex_handle);
* @endcode
*/
class descriptor_heap
{
public:
explicit descriptor_heap(const D3D12_DESCRIPTOR_HEAP_TYPE type): _type(type){}
DISABLE_COPY_AND_MOVE(descriptor_heap);
~descriptor_heap(){assert(!_heap);}
/**
* @brief 构造指定类型的描述符堆
*
* @param type D3D12 描述符堆类型CBV_SRV_UAV、SAMPLER、RTV 或 DSV
*
* @note 此构造函数不分配任何资源。调用 initialize() 创建堆
*/
explicit descriptor_heap(const D3D12_DESCRIPTOR_HEAP_TYPE type) : _type(type) {}
/**
* @brief 禁用拷贝构造以防止资源重复
*/
DISABLE_COPY_AND_MOVE(descriptor_heap);
/**
* @brief 析构函数,断言所有资源已释放
*
* @note 堆必须在析构前通过 release() 显式释放
*/
~descriptor_heap() { assert(!_heap); }
/**
* @brief 初始化具有指定容量的描述符堆
*
* @param capacity 此堆可容纳的最大描述符数量
* @param is_shader_visible 堆是否应着色器可见
*
* @return 初始化成功返回 true否则返回 false
*
* @details
* - 对于 RTV/DSV 堆is_shader_visible 强制为 false设计上 GPU 不可访问)
* - 容量不得超过 D3D12_MAX_SHADER_VISIBLE_DESCRIPTOR_HEAP_SIZE_TIER_2
* - 对于采样器堆,容量不得超过 D3D12_MAX_SHADER_VISIBLE_SAMPLER_HEAP_SIZE
*
* @pre 堆必须未初始化(如需重新初始化,先调用 release()
* @post 堆已准备好进行 allocate() 调用
*/
bool initialize(u32 capacity, bool is_shader_visible);
/**
* @brief 处理特定帧的延迟释放
*
* @param frame_index 应处理其延迟释放的帧索引
*
* @details
* 此方法应在新帧开始时调用,确认 GPU 已完成对应帧的处理。
* 它将该帧期间释放的所有描述符返回到空闲池。
*
* @pre GPU 必须已完成指定帧(通过 Fence 同步确认)
*/
void process_deferred_release(u32 frame_index);
/**
* @brief 释放此堆持有的所有资源
*
* @details
* - 通过 flush() 等待所有帧完成
* - 释放底层 ID3D12DescriptorHeap
* - 清除所有内部跟踪结构
*
* @post 堆处于未初始化状态,可重新初始化
*/
void release();
/**
* @brief 从堆中分配描述符
*
* @return 指向已分配描述符的 descriptor_handle
*
* @throws 如果堆已满或未初始化则触发断言失败
*
* @details
* - 线程安全:使用互斥锁保护内部状态
* - O(1) 复杂度:从空闲列表弹出
* - 返回的句柄包含 CPU 和 GPU如果着色器可见地址
*
* @pre 堆必须已初始化且有可用容量
* @note 标记 [[nodiscard]] 以防止意外丢弃句柄
*/
[[nodiscard]] descriptor_handle allocate();
/**
* @brief 释放描述符,将其放入延迟释放队列
*
* @param handle 要释放的描述符句柄
*
* @details
* - 线程安全:使用互斥锁保护内部状态
* - 描述符不会立即可重用
* - 在 GPU 完成当前帧后调用 process_deferred_release() 时,
* 描述符才会返回到空闲池
* - 无效句柄会被安全忽略
*
* @note DEBUG 模式下验证句柄属于此堆
*/
void free(descriptor_handle handle);
/**
* @brief 返回 D3D12 描述符堆类型
* @return 此堆的类型CBV_SRV_UAV、SAMPLER、RTV 或 DSV
*/
constexpr D3D12_DESCRIPTOR_HEAP_TYPE type() const { return _type; }
/**
* @brief 返回堆中第一个描述符的 CPU 句柄
* @return 起始 CPU 描述符句柄
*/
constexpr D3D12_CPU_DESCRIPTOR_HANDLE cpu_start() const { return _cpu_start; }
/**
* @brief 返回堆中第一个描述符的 GPU 句柄
* @return 起始 GPU 描述符句柄,如果非着色器可见则为 {0}
*/
constexpr D3D12_GPU_DESCRIPTOR_HANDLE gpu_start() const { return _gpu_start; }
constexpr ID3D12DescriptorHeap *const heap() const { return _heap; }
/**
* @brief 返回底层 D3D12 描述符堆接口
* @return 指向 ID3D12DescriptorHeap 的指针,如果未初始化则为 nullptr
*/
constexpr ID3D12DescriptorHeap* const heap() const { return _heap; }
/**
* @brief 返回此堆可容纳的最大描述符数量
* @return 堆的总容量
*/
constexpr u32 capacity() const { return _capacity; }
/**
* @brief 返回当前已分配的描述符数量
* @return 已分配描述符计数(不包括延迟释放的)
*/
constexpr u32 size() const { return _size; }
/**
* @brief 返回单个描述符的字节大小
* @return 描述符大小,因堆类型和硬件而异
*/
constexpr u32 descriptor_size() const { return _descriptor_size; }
/**
* @brief 检查此堆是否着色器可见
* @return 如果 GPU 句柄有效则返回 true否则返回 false
*/
constexpr bool is_shader_visible() const { return _gpu_start.ptr != 0; }
private:
// 一个描述符堆是一个内存块,基于他是否着色器可见,分配在系统内存还是显存
ID3D12DescriptorHeap* _heap;
/**
* @brief 底层 D3D12 描述符堆接口
*
* @details
* 内存位置取决于着色器可见性:
* - 着色器可见:分配在 GPU 可访问内存(显存或映射到 GPU 的系统内存)
* - 非着色器可见:仅分配在系统内存
*/
ID3D12DescriptorHeap* _heap{nullptr};
// CPU起始句柄用于CPU端描述符操作
D3D12_CPU_DESCRIPTOR_HANDLE _cpu_start{};
// GPU起始句柄仅当堆为着色器可见时有效
D3D12_GPU_DESCRIPTOR_HANDLE _gpu_start{};
/**
* @brief 堆中第一个描述符的 CPU 句柄
*
* @details
* 用作计算单个描述符 CPU 句柄的基地址。
* initialize() 成功调用后立即有效。
*/
D3D12_CPU_DESCRIPTOR_HANDLE _cpu_start{};
// 空闲的描述符句柄
std::unique_ptr<u32[]> _free_handles{};
/**
* @brief 堆中第一个描述符的 GPU 句柄
*
* @details
* 仅对着色器可见堆有效。用作计算可通过根描述符表绑定到管线的
* GPU 句柄的基地址。值为 {0} 表示非着色器可见堆。
*/
D3D12_GPU_DESCRIPTOR_HANDLE _gpu_start{};
// 用于保护资源初始化的互斥锁
std::mutex _mutex;
/**
* @brief 存储可用描述符索引的空闲列表
*
* @details
* 实现为预分配数组,实现 O(1) 分配/释放。
* 初始填充所有索引 [0, capacity-1]。
* 分配描述符时,从前面消耗索引。
* 释放描述符后(延迟处理完成),索引返回。
*/
std::unique_ptr<u32[]> _free_handles{};
// 描述符堆的容量
u32 _capacity{0};
// 已经分配的描述符数量
u32 _size{0};
// 不同的描述符堆类型在不同的硬件上有不同的大小,所以需要记录下描述符的大小
u32 _descriptor_size{0};
const D3D12_DESCRIPTOR_HEAP_TYPE _type;
/**
* @brief 每帧的延迟释放队列
*
* @details
* 当调用 free() 时,描述符索引放入当前帧的队列。
* 只有在该帧 GPU 完成后调用 process_deferred_release() 时,
* 索引才会返回到 _free_handles。
*
* 这防止了 GPU 危险:当描述符可能仍被 GPU 使用时被新资源重用。
*
* 数组大小为 frame_buffer_count通常为 3三重缓冲
*/
utl::vector<u32> _deferred_free_indices[frame_buffer_count]{};
/**
* @brief 用于线程安全访问内部状态的互斥锁
*
* @details
* 保护所有可变状态_size、_free_handles 和 _deferred_free_indices。
* 必需,因为资源创建/销毁可能发生在多个线程上。
*/
std::mutex _mutex{};
/**
* @brief 此堆可容纳的最大描述符数量
*
* @details
* 在 initialize() 时设置,此后不可变。
* 用于边界检查和空闲列表初始化。
*/
u32 _capacity{0};
/**
* @brief 当前已分配的描述符数量
*
* @details
* 表示从 _free_handles 消耗的索引数量。
* 不包括延迟释放队列中的描述符。
*/
u32 _size{0};
/**
* @brief 单个描述符的字节大小
*
* @details
* 从 ID3D12Device::GetDescriptorHandleIncrementSize() 获取。
* 因堆类型和硬件而异。用于句柄偏移计算。
*/
u32 _descriptor_size{0};
/**
* @brief D3D12 描述符堆类型
*
* @details
* 构造时设置,不可变。决定:
* - 可创建的视图类型SRV、UAV、RTV 等)
* - 是否支持着色器可见性
* - 允许的最大容量
*/
const D3D12_DESCRIPTOR_HEAP_TYPE _type;
};
}
} // namespace XEngine::graphics::d3d12::core