Files
DX12/docs/changelogs/2026-03/20260331-surface-freelist-refactor.md
SpecialX 4d13d8df89 feat(d3d12): 新增纹理资源类,修复 Surface 重复释放问题
核心变更:
- 新增 d3d12_texture 和 d3d12_render_texture 类
- 新增 d3d12_texture_init_info 结构,支持三种资源创建方式
- 新增 D3D12Helpers.h,提供堆属性辅助结构
- 改用 utl::free_list 管理 surface,解决重复释放问题
- 为 d3d12_surface 添加移动语义,支持撕裂检测

文档完善:
- 为 FreeList.h 和 Vector.h 添加完整 Doxygen 中文注释
- 更新 D3D12 学习 Wiki,添加 SRV、资源创建方式、纹理资源类章节
- 新增变更记录文档
2026-04-01 16:17:42 +08:00

10 KiB
Raw Blame History

变更记录Surface 管理重构、纹理资源与文档完善

提交日期: 2026-04-01
提交哈希: b72fcf4
变更类型: Bug修复 + 功能新增 + 文档完善


变更概述

本次提交修复了 surface 管理中的资源重复释放问题,新增纹理资源类,并为容器和 D3D12 资源添加了完整的文档。

修改文件

核心修复

文件 变更说明
D3D12Core.cpp 改用 utl::free_list<d3d12_surface> 管理 surface
D3D12Surface.h 添加移动语义,禁用拷贝,支持撕裂检测
D3D12Surface.cpp 完善交换链创建和资源释放逻辑

新增功能

文件 变更说明
D3D12Helpers.h 新增堆属性辅助结构 d3dx::heap_properties
D3D12Resources.h 新增 d3d12_texture_init_infod3d12_textured3d12_render_texture
D3D12Resource.cpp 实现纹理资源创建和 SRV 绑定

文档完善

文件 变更说明
FreeList.h 添加完整 Doxygen 中文注释
Vector.h 添加完整 Doxygen 中文注释

Bug 修复详情

问题Surface 重复释放

现象:关闭多个窗口时,release() 被调用超过预期次数,导致空指针崩溃。

根本原因

  1. 手动调用析构函数

    // 错误做法
    surfaces[id].~d3d12_surface();  // 对象仍在 vector 中
    // vector 析构时会再次调用析构函数
    
  2. Vector 扩容浅拷贝

    vector 扩容时:
    1. 分配新内存
    2. 移动元素(默认移动是浅拷贝)
    3. 析构旧元素 → 释放资源
    4. 新元素持有悬空指针 → 崩溃
    
  3. 描述符句柄未重置

    core::rtv_heap().free(data.rtv);
    // free() 只修改局部变量data.rtv 仍指向已释放的描述符
    

解决方案

1. 使用 utl::free_list 替代 utl::vector

// D3D12Core.cpp
using surface_collection = utl::free_list<d3d12_surface>;
surface_collection surfaces;

// 创建 surface
surface create_surface(platform::window window)
{
    surfaces.emplace_back(window);
    surface_id id{ (u32)surfaces.size() - 1 };
    surfaces[id].create_swap_chain(...);
    return surface{id};
}

// 删除 surface
void remove_surface(surface_id id)
{
    gfx_command.flush();
    surfaces.remove(id);  // free_list 的 remove 正确处理
}

2. 实现移动语义

class d3d12_surface
{
public:
    // 移动构造函数
    d3d12_surface(d3d12_surface&& other) noexcept
        : _swap_chain{other._swap_chain}
        , _window{other._window}
        // ... 转移所有资源
    {
        other._swap_chain = nullptr;  // 源对象置空
    }

    // 禁用拷贝
    d3d12_surface(const d3d12_surface&) = delete;
    d3d12_surface& operator=(const d3d12_surface&) = delete;
};

3. 重置描述符句柄

void d3d12_surface::release()
{
    for(u32 i = 0; i < frame_buffer_count; ++i)
    {
        render_target_data& data{ _render_target_data[i] };
        core::release(data.resource);
        core::rtv_heap().free(data.rtv);
        data.rtv = {};  // 重置句柄,防止重复释放
    }
    core::release(_swap_chain);
}

FreeList 容器详解

数据结构原理

utl::free_list 使用空闲链表管理已删除的槽位:

初始状态:
_array: [ 空 | 空 | 空 | 空 ]
_next_free_index = invalid_id
_size = 0

添加元素 A、B、C 后:
_array: [ A | B | C | 空 ]
_next_free_index = invalid_id
_size = 3

删除元素 B 后(槽位 1 被标记为空闲):
_array: [ A | ->2 | C | 空 ]   // 槽位 1 存储下一个空闲索引
_next_free_index = 1
_size = 2

添加新元素 D 时(复用槽位 0:
_array: [ D | ->2 | C | 空 ]
_next_free_index = 1
_size = 2

核心优势

特性 说明
O(1) 增删 新增和删除操作近似常数时间
槽位复用 删除后的槽位被回收,避免频繁内存分配
稳定索引 使用 u32 索引作为外部句柄,与 ID 系统配合
内存紧凑 底层使用连续内存,缓存友好

与 Vector 的区别

特性 free_list vector
删除操作 O(1),槽位复用 O(n) 需要移动元素
索引稳定性 删除后索引可能被复用 删除后索引失效
内存碎片 无碎片,槽位复用 删除后内存不释放
适用场景 游戏对象、资源句柄 顺序容器、临时数据

Vector 容器详解

析构策略

模板参数 destruct 控制元素析构行为:

// 默认:删除时调用析构函数
utl::vector<std::string> strings;  // destruct = true

// POD 类型优化:跳过析构
utl::vector<float, false> vertices;  // destruct = false

扩容策略

扩容公式new_capacity = (old_capacity + 1) * 3 / 2

初始容量 0 → 1
容量 4 → 6
容量 10 → 15

无序删除优化

// 普通删除O(n),保持顺序
vec.erase(index);

// 无序删除O(1),用末尾元素替换
vec.erase_unordered(index);  // 适用于顺序不重要的场景

交换链详解

核心职责

交换链连接窗口与渲染管线,管理一组后台缓冲区,并通过 Present() 实现缓冲区翻转,将绘制内容显示到窗口。

交换链只负责

  • 缓冲区的分配与翻转

开发者需显式完成

  • 绑定渲染目标(OMSetRenderTargets
  • GPU 同步Fence
  • 状态转换(资源屏障)

标准使用流程

1. 获取当前后台缓冲区GetBuffer
       ↓
2. 将其绑定为渲染目标OMSetRenderTargets
       ↓
3. 执行绘制命令(写入该缓冲区)
       ↓
4. 提交命令并同步Fence
       ↓
5. 调用 Present翻转缓冲区显示图像

职责边界

操作 负责方 说明
缓冲区分配 交换链 创建指定数量的后台缓冲区
缓冲区翻转 交换链 Present() 切换前后缓冲区
渲染目标绑定 开发者 OMSetRenderTargets() 绑定 RTV
GPU 同步 开发者 使用 Fence 确保 GPU 完成渲染
状态转换 开发者 资源屏障管理缓冲区状态
窗口大小调整 开发者 调用 ResizeBuffers() 重新分配

SRV 创建与资源创建方式

CreateShaderResourceView 参数说明

void CreateShaderResourceView(
    ID3D12Resource *pResource,
    const D3D12_SHADER_RESOURCE_VIEW_DESC *pDesc,
    D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor
);
参数 说明
pResource 要创建 SRV 的 GPU 资源指针(必须是有效资源,不能为 nullptr
pDesc 视图描述符指定格式、维度、Mip 级别范围等。为 nullptr 时使用资源默认属性
DestDescriptor SRV 描述符堆中 CPU 描述符句柄的位置

关键点

  • pDescnullptr 时,视图描述符默认使用资源本身的格式和全部子资源(空描述符初始化)
  • 同一个资源可以创建多个不同的 SRV不同格式、不同 Mip 切片)

D3D12 资源创建函数对比

函数 堆类型 说明
CreateCommittedResource 隐式堆 D3D12 自动分配堆,资源直接映射。适用于大多数常规资源
CreatePlacedResource 显式堆 资源放置在用户创建的堆的特定偏移位置。用于精确控制内存布局
CreateReservedResource 预留资源 仅预留虚拟地址,不提交物理内存。用于稀疏资源,支持流式加载

选择建议

  • Committed:最常用,堆由系统隐式管理
  • Placed:需要显式堆,资源放置于堆的指定偏移
  • Reserved:仅预留虚拟地址,用于稀疏资源,实现内存的按需提交

纹理资源类

d3d12_texture_init_info 结构

纹理初始化信息结构,支持三种资源创建方式:

struct d3d12_texture_init_info
{
    ID3D12Heap1*                        heap{nullptr};           // 显式堆Placed Resource
    ID3D12Resource*                     resource{nullptr};       // 已有资源(直接使用)
    D3D12_SHADER_RESOURCE_VIEW_DESC*    srv_desc{nullptr};       // SRV 描述nullptr 使用默认)
    D3D12_RESOURCE_DESC*                desc{nullptr};           // 资源描述
    D3D12_RESOURCE_ALLOCATION_INFO1     allocation_info{};       // 分配信息(偏移量)
    D3D12_RESOURCE_STATES               initial_state{};         // 初始状态
    D3D12_CLEAR_VALUE                   clear_value{};           // 清除值RTV/DSV
};

d3d12_texture 类

基础纹理类,封装资源创建和 SRV 绑定:

class d3d12_texture
{
public:
    constexpr static u32 max_mips{ 14 };
    
    explicit d3d12_texture(d3d12_texture_init_info info);
    
    // 移动语义
    d3d12_texture(d3d12_texture&& o);
    d3d12_texture& operator=(d3d12_texture&& o);
    
    // 禁用拷贝
    DISABLE_COPY(d3d12_texture);
    
    void release();
    ID3D12Resource* resource() const;
    descriptor_handle srv() const;
    
private:
    ID3D12Resource* _resource{nullptr};
    descriptor_handle _srv;
};

资源创建逻辑

d3d12_texture::d3d12_texture(d3d12_texture_init_info info)
{
    // 优先级 1使用已有资源
    if (info.resource) {
        _resource = info.resource;
    }
    // 优先级 2Placed Resource显式堆
    else if (info.heap && info.desc) {
        device->CreatePlacedResource(
            info.heap,
            info.allocation_info.Offset,
            info.desc, ...);
    }
    // 优先级 3Committed Resource隐式堆
    else if (info.desc) {
        device->CreateCommittedResource(
            &d3dx::heap_properties.default_heap,
            D3D12_HEAP_FLAG_NONE,
            info.desc, ...);
    }
    
    // 创建 SRV
    _srv = core::srv_heap().allocate();
    device->CreateShaderResourceView(_resource, info.srv_desc, _srv.cpu);
}

d3dx::heap_properties 辅助结构

namespace XEngine::graphics::d3d12::d3dx
{
constexpr struct {
    D3D12_HEAP_PROPERTIES default_heap{
        D3D12_HEAP_TYPE_DEFAULT,         // GPU 可读写CPU 不可访问
        D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
        D3D12_MEMORY_POOL_UNKNOWN,
        0,                               // 单 GPU 系统
        0
    };
} heap_properties;
}

后续工作

  • 实现深度模板视图
  • 渲染第一个三角形
  • 实现根签名和管线状态对象

相关文档