Files
DX12/docs/changelogs/2026-03/20260331-surface-freelist-refactor.md
SpecialX 80cb696a3c fix(d3d12): 修复 Surface 重复释放问题,完善容器文档
- 改用 utl::free_list 管理 surface,避免 vector 扩容导致的资源重复释放
- 为 d3d12_surface 添加移动语义,禁用拷贝构造
- 添加撕裂检测支持(DXGI_PRESENT_ALLOW_TEARING)
- 为 FreeList.h 和 Vector.h 添加完整的 Doxygen 中文注释
- 更新 D3D12 学习 Wiki,添加 free_list 章节
2026-03-31 16:49:25 +08:00

5.0 KiB
Raw Blame History

变更记录Surface 管理重构与容器文档完善

提交日期: 2026-03-31
提交哈希: b284d81
变更类型: Bug修复 + 文档完善


变更概述

本次提交修复了 surface 管理中的资源重复释放问题,重构为使用 utl::free_list 容器,并为 FreeList.hVector.h 添加了完整的国际标准中文注释。

修改文件

核心修复

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

文档完善

文件 变更说明
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);  // 适用于顺序不重要的场景

后续工作

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

相关文档