跳转至

大型引擎的资源 Handle

提示

本文由 AI 生成,使用模型 gpt-5.5。

在项目早期,资源系统通常直接使用指针:

Texture* texture;
Mesh* mesh;
Material* material;

这种“资源即对象”的设计简单、直观,访问成本也很低,不需要额外的资源表、资源管理器或间接层。但是随着项目规模扩大,资源不再只是一个固定地址上的普通对象,直接使用指针会逐渐遇到问题。

指针面临的问题

热重载

当纹理等资源需要热更新时,直接修改原资源会产生并发访问冲突。

使用读写锁在理论上可行,但资源解码和上传可能非常耗时。如果热路径需要等待这些操作完成,系统效率可能接近甚至低于串行执行。

因此,可以采用类似 RCU(Read-Copy-Update)的思想:

  1. 创建一份新资源。
  2. 让之后的访问切换到新资源。
  3. 暂时保留旧资源。
  4. 确认旧资源不再被引用后安全销毁。

这要求外部引用不能永久绑定某个具体资源对象。

Streaming

现代引擎不会让所有资源永久驻留内存。例如,玩家离开城市进入森林后,城市资源可以卸载,森林资源则开始加载。

在这个过程中,资源的内存地址可能发生变化。指针天然绑定具体地址,因此资源被卸载、重新加载或迁移后,原有指针就会失效。

GPU 资源重建

在 Vulkan、D3D12 等图形后端中,以下操作都可能导致底层资源被重新创建:

  • Swapchain 重建
  • Device Lost
  • 显存迁移
  • Streaming Upload

从系统角度看,它仍然是同一个纹理;从内存角度看,底层对象已经被替换。指针只能表示具体对象,无法表达“资源身份不变,但底层对象已经变化”。

生命周期不属于调用者

普通对象通常由创建者负责销毁,生命周期比较明确。资源则可能同时被多个系统引用,例如:

  • Renderer
  • Material
  • Scene
  • Streaming System

因此,资源的生命周期通常不能由某个调用者单独决定,而需要由统一的资源表进行管理。

当真实资源已经集中存放在表中时,外部只需要持有表项的标识,不再需要直接持有资源指针。此时也就自然形成了 Handle。

Handle 的作用

Handle 不直接指向资源,而是用于标识资源:

struct TextureHandle {
    uint32_t index;
};

真实资源存放在资源表中:

Handle → Resource Table → Texture

Handle 的核心作用是引入一层间接性,使系统能够修改底层资源而不影响外部引用。

例如,Texture A 被重新加载时,只需要更新 ResourceTable[index]。所有持有对应 Handle 的调用方都会访问新资源,不需要逐一更新引用。

Windows 和 Vulkan 的 Handle 可能使用指针实现,但 Handle 的意义并不取决于具体表示方式。它代表的是资源身份,而不是普通的 C++ 对象指针。

Generation Counter

只使用资源表索引仍然存在槽位复用问题。

假设索引 5 原本对应 Texture A。Texture A 被销毁后,索引 5 被 Texture B 复用。此时旧 Handle 会错误地访问 Texture B,这就是 ABA 问题。

为了解决这个问题,可以在 Handle 中增加 generation:

struct TextureHandle {
    uint32_t index;
    uint32_t generation;
};

访问资源时验证:

handle.generation == table[handle.index].generation

如果 generation 不一致,就说明该 Handle 已经过期,应当判定为无效。

大型引擎中的结构

经过长期演化,大型引擎通常会形成类似的资源访问结构:

TextureID
    ↓
Resource Manager
    ↓
Resource Table
    ↓
Texture
    ↓
Backend API

调用者只接触稳定的 TextureID 或 Handle,不直接接触底层资源。在这层间接访问的基础上,资源系统可以支持:

  • 热重载
  • Streaming
  • 延迟销毁
  • 异步创建
  • GPU 资源重建

资源的地址和底层实现可以发生变化,而 Handle 始终保持稳定。

资源不是对象

对象通常属于某个作用域,并强调所有权;资源属于整个系统,更强调身份。

Handle 不是简单的指针替代品,而是一种身份标识。当资源开始拥有独立于普通对象的生命周期时,Handle 几乎会自然出现。

这也是游戏引擎、数据库、浏览器和操作系统内核都会使用 Handle 体系的原因:对于大型系统而言,资源最重要的属性不是它在哪里,而是它是谁。

参考