大型引擎的资源 Handle¶
提示
本文由 AI 生成,使用模型 gpt-5.5。
在项目早期,资源系统通常直接使用指针:
Texture* texture;
Mesh* mesh;
Material* material;
这种“资源即对象”的设计简单、直观,访问成本也很低,不需要额外的资源表、资源管理器或间接层。但是随着项目规模扩大,资源不再只是一个固定地址上的普通对象,直接使用指针会逐渐遇到问题。
指针面临的问题¶
热重载¶
当纹理等资源需要热更新时,直接修改原资源会产生并发访问冲突。
使用读写锁在理论上可行,但资源解码和上传可能非常耗时。如果热路径需要等待这些操作完成,系统效率可能接近甚至低于串行执行。
因此,可以采用类似 RCU(Read-Copy-Update)的思想:
- 创建一份新资源。
- 让之后的访问切换到新资源。
- 暂时保留旧资源。
- 确认旧资源不再被引用后安全销毁。
这要求外部引用不能永久绑定某个具体资源对象。
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 体系的原因:对于大型系统而言,资源最重要的属性不是它在哪里,而是它是谁。
参考¶
- 资源不是对象:为什么大型引擎最终都会走向 Handle,正在重构的某人