Texture资源泄漏不被Valgrind或ASan检测,因其绕过malloc/new,需hook图形API(如glGenTextures/glDeleteTextures)并比对纹理ID生命周期。

Texture 资源泄漏 ≠ 普通堆内存泄漏
游戏里纹理没释放,Valgrind 或 AddressSanitizer 很可能完全不报——因为它们只监控 malloc/new,而纹理通常由 GPU 驱动管理:分配走的是 glTexImage2D、vkCreateImage 或 DirectX 的 ID3D11Device::CreateTexture2D,释放靠 glDeleteTextures、vkDestroyImage 等 API。这些调用不经过 C 运行时堆,CRT 调试堆也看不见。
必须挂钩图形 API 分配/销毁点
检测驻留纹理泄漏,核心是记录每次纹理创建和销毁的上下文,并在程序退出或快照时刻比对未配对的 ID。实操建议如下:
- 对 OpenGL:用
glDebugMessageCallback+ 自定义 wrapper 替换所有glGenTextures/glDeleteTextures,把GLuint和调用栈(__FILE__+__LINE__)存入全局std::unordered_map - 对 Vulkan:在
vkCreateImage/vkDestroyImage前后插入 hook(通过vkGetInstanceProcAddr获取函数指针并重定向),记录VkImagehandle 和VkAllocationCallbacks中的pUserData用于打标记 - 对 DirectX 11/12:使用
ID3D11Debug::ReportLiveDeviceObjects(仅 Debug Layer 启用时有效),或在封装的Texture2D::Create构造/析构中手动计数 - 避免在多线程渲染线程中直接记录——加锁或用
thread_local缓存再批量合并到主线程 registry
纹理泄漏常被误判为“显存不足”,但真正线索在 CPU 端
GPU 显存本身难直接采样(尤其跨厂商),但泄漏一定伴随 CPU 端可观察现象:
-
glGetError()频繁返回GL_OUT_OF_MEMORY—— 实际未必是显存满,更可能是驱动内部纹理句柄表溢出 - 纹理加载函数(如
stbi_load)返回非空指针,但后续glTexImage2D失败且没检查错误码 - 资源池类(如
TextureCache)未实现 LRU 驱逐逻辑,导致std::map<:string textureref></:string>不断增长却无上限 - Shader 中
sampler2D绑定后,对应Texture对象被提前delete,但 GPU command buffer 仍在引用 —— 这属于 use-after-free,AddressSanitizer也抓不到,需启用 OpenGL Debug Context 或 Vulkan Validation Layers
上线前必须关掉的“假阳性”干扰项
游戏引擎常自带资源延迟卸载机制(比如帧后延迟 2 帧再删纹理),这会导致工具在主循环退出瞬间误报“泄漏”。关键处理点:
立即学习“C++免费学习笔记(深入)”;
- 在关机流程中显式调用
TextureManager::FlushPendingDeletes(),等所有 pending release 完成后再触发泄漏检查 - 禁用所有异步加载线程(
AssetLoader::StopAllWorkers()),防止后台线程悄悄 new 出新纹理 - 若用 Unity 或 Unreal 封装层,确认是否启用了
Texture Streaming—— 它会动态加载/卸载 Mipmap 级别,此时“驻留”是预期行为,不应按传统泄漏对待 - Windows 上
ID3D11Debug::ReportLiveDeviceObjects(D3D11_RLDO_DETAIL)输出的 “Live Objects” 数量,要和你自己的 registry 计数交叉验证;两者不一致,说明有裸指针绕过 RAII 管理
最易被忽略的是:纹理对象析构函数里没调用 glDeleteTextures(比如忘了写,或条件编译漏掉了 Release 模式分支),而日志只打印“Texture destroyed”,实际 GPU 句柄还活着。这种 bug 必须靠 API hook + ID 生命周期比对才能揪出来。



















