[显存/性能] SpriteRenderer 缺少显存资源释放:WebGL2 纹理、缓冲与程序持续泄漏 #9

Closed
opened 2026-09-14 07:01:31 +00:00 by troytt · 1 comment
Owner

Parent Issue: #7 ([Code Review] 整体代码质量、架构缺陷与技术债务全景审查报告)

模块位置

src/render/renderer.ts, src/scene/act-scene.ts

问题背景与现象

  • SpriteRenderer 在初始化时分配了 Program、VertexShader、FragmentShader、VAO、VBO 以及白底/默认图集纹理。
  • addAtlas 每次加载关卡切片和角色动画时均调用 gl.createTexture() 创建 2048×2048 的 GPU 纹理。
  • 全工程中没有任何一处调用 gl.deleteTexture、gl.deleteProgram、gl.deleteBuffer 或 gl.deleteVertexArray。
  • SpriteRenderer 甚至未对外暴露 destroy() 或 dispose() 方法。

造成的影响

  1. 每次用户切换关卡、切场景或重新加载时,前序纹理占用完全不被 GPU 回收。
  2. 62 个预置地图块各自包含多个 2048² 页面,在低显存设备、集成显卡或移动端浏览器中长时间漫游极易触发 WebGL: CONTEXT_LOST_WEBGL 崩溃。

解决建议

  1. 在 SpriteRenderer 中增加 dispose() 方法,显式清理 Program, Shaders, VAO, VBO 及所有托管纹理;
  2. 增加 deleteAtlas(handle: AtlasHandle) 方法,支持按需释放指定 GPU 纹理对象;
  3. 在关卡卸载、场景重载与 SPA 页面销毁钩子中显式调用释放;
  4. 增加 WebGL Context Lost 事件监听(webglcontextlost / webglcontextrestored)以提供友好的降级提示与恢复逻辑。
> **Parent Issue**: #7 ([Code Review] 整体代码质量、架构缺陷与技术债务全景审查报告) ## 模块位置 `src/render/renderer.ts`, `src/scene/act-scene.ts` ## 问题背景与现象 - `SpriteRenderer` 在初始化时分配了 Program、VertexShader、FragmentShader、VAO、VBO 以及白底/默认图集纹理。 - `addAtlas` 每次加载关卡切片和角色动画时均调用 `gl.createTexture()` 创建 2048×2048 的 GPU 纹理。 - 全工程中**没有任何一处**调用 `gl.deleteTexture`、`gl.deleteProgram`、`gl.deleteBuffer` 或 `gl.deleteVertexArray`。 - `SpriteRenderer` 甚至未对外暴露 `destroy()` 或 `dispose()` 方法。 ## 造成的影响 1. 每次用户切换关卡、切场景或重新加载时,前序纹理占用完全不被 GPU 回收。 2. 62 个预置地图块各自包含多个 2048² 页面,在低显存设备、集成显卡或移动端浏览器中长时间漫游极易触发 `WebGL: CONTEXT_LOST_WEBGL` 崩溃。 ## 解决建议 1. 在 `SpriteRenderer` 中增加 `dispose()` 方法,显式清理 Program, Shaders, VAO, VBO 及所有托管纹理; 2. 增加 `deleteAtlas(handle: AtlasHandle)` 方法,支持按需释放指定 GPU 纹理对象; 3. 在关卡卸载、场景重载与 SPA 页面销毁钩子中显式调用释放; 4. 增加 WebGL Context Lost 事件监听(`webglcontextlost` / `webglcontextrestored`)以提供友好的降级提示与恢复逻辑。
Author
Owner

修复方案与验证报告

该问题已在 Commit a80ee2c 中彻底修复并推送至 main 分支。


一、问题根因与修复实施

  1. 着色器驱动层泄漏消除(src/render/renderer.ts):

    • 在 createProgram 内部,Shader 链接完成后立即执行 gl.detachShader 和 gl.deleteShader;
    • 链接失败时捕获日志并显式 gl.deleteProgram,彻底杜绝驱动层底层 Shader 对象的残留堆积。
  2. 全量显存纹理追踪与按需释放(src/render/renderer.ts):

    • SpriteRenderer 新增 allocatedTextures = new Set<WebGLTexture>(),对构造函数初始分配以及 addAtlas 动态上传的所有 GPU 纹理进行统一追踪;
    • 新增 deleteAtlas(handle: AtlasHandle): boolean 方法:
      • 按需释放指定切片纹理;
      • 若待删除纹理恰好为当前批次活跃采样纹理(batchAtlas),先自动调用 flush() 提交已有顶点,并安全回退到默认纹理,避免悬挂采样或渲染崩溃。
  3. 显式销毁生命周期(src/render/renderer.ts):

    • 新增 dispose() 方法与 isDisposed 只读状态标志;
    • 彻底销毁所有托管纹理、VBO 缓冲(gl.deleteBuffer)、VAO(gl.deleteVertexArray)以及 Shader Program(gl.deleteProgram);
    • 移除 Canvas 事件监听;
    • 支持幂等操作(重复调用安全退出);
    • 内部集成 gl.isContextLost() 状态防护,在 WebGL 上下文已丢失时不调用非法的 GL 删除指令;
    • 已销毁实例调用 begin / setAtlas / addAtlas 会立即拦截并抛出 RendererError。
  4. WebGL Context Lost 保护与自动恢复(src/render/renderer.ts):

    • SpriteRenderer 构造函数接收 RendererOptions(onContextLost / onContextRestored);
    • 监听 Canvas webglcontextlost 事件并强制触发 event.preventDefault()(遵循 WebGL 规范,只有 preventDefault 浏览器才会允许后续触发 context restored 恢复);
    • 场景层接入状态提示并在恢复后自动重新拉起场景。
  5. 业务场景生命周期与关卡切换集成:

    • src/scene/map-scene.ts / src/scene/net-scene.ts / src/walk.ts:模块级维护 activeRenderer,在切换关卡、重新选择地图或拖入新 MPQ 时,先调用 activeRenderer?.dispose();并在 beforeunload 时清理运行循环与显存;
    • src/scene/act-scene.ts:在卸载前对 runtime.pages 中的所有 Atlas 以及角色 Handle 执行 deleteAtlas,随后彻底 dispose()。

二、工程验证与自动化测试

  1. 生命周期自动化测试套件(scripts/verify-renderer-lifecycle.ts):

    • 注册了 npm run verify:renderer 命令;
    • 基于 WebGL2 模拟环境对 26 项断言进行覆盖测试(包括着色器编译后解绑删除、动态纹理增删、活跃批次解绑安全 flush、全量 dispose 幂等性、上下文丢失防崩溃与回调等):
    == Running SpriteRenderer Lifecycle Verification ==
      ✓ Two shaders created (VS & FS)
      ✓ Both shaders detached and deleted right after program link
      ✓ Program created
      ✓ VAO created
      ✓ VBO buffer created
      ✓ Initial textures created (atlas + white)
      ✓ Renderer is active and not disposed
      ✓ addAtlas creates a new GPU texture (3 total)
      ✓ deleteAtlas returned true for managed handle
      ✓ GPU texture was deleted via gl.deleteTexture
      ✓ deleteAtlas returned false on already deleted handle
      ✓ 4 total textures created before dispose
      ✓ isDisposed is true after dispose()
      ✓ All 4 textures were deleted on dispose()
      ✓ VBO was deleted on dispose()
      ✓ VAO was deleted on dispose()
      ✓ Program was deleted on dispose()
      ✓ Second dispose() is safe and idempotent
      ✓ begin() throws RendererError when called on disposed renderer
      ✓ addAtlas() throws RendererError when called on disposed renderer
      ✓ webglcontextlost calls event.preventDefault() to allow recovery
      ✓ onContextLost callback fired
      ✓ onContextRestored callback fired
      ✓ Context lost listener removed on dispose
      ✓ Context restored listener removed on dispose
      ✓ dispose() safely completes when context is already lost
    
    == Summary: 26 passed, 0 failed ==
    
  2. 生产构建与类型检查:

    npm run build # (tsc --noEmit && vite build)
    ✓ 56 modules transformed.
    ✓ built in 1.11s
    
## 修复方案与验证报告 该问题已在 Commit [`a80ee2c`](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/a80ee2cafcfb1ebef947caaf65c50d9874aec6ed) 中彻底修复并推送至 `main` 分支。 --- ### 一、问题根因与修复实施 1. **着色器驱动层泄漏消除**(`src/render/renderer.ts`): - 在 `createProgram` 内部,Shader 链接完成后立即执行 `gl.detachShader` 和 `gl.deleteShader`; - 链接失败时捕获日志并显式 `gl.deleteProgram`,彻底杜绝驱动层底层 Shader 对象的残留堆积。 2. **全量显存纹理追踪与按需释放**(`src/render/renderer.ts`): - `SpriteRenderer` 新增 `allocatedTextures = new Set<WebGLTexture>()`,对构造函数初始分配以及 `addAtlas` 动态上传的所有 GPU 纹理进行统一追踪; - 新增 `deleteAtlas(handle: AtlasHandle): boolean` 方法: - 按需释放指定切片纹理; - 若待删除纹理恰好为当前批次活跃采样纹理(`batchAtlas`),先自动调用 `flush()` 提交已有顶点,并安全回退到默认纹理,避免悬挂采样或渲染崩溃。 3. **显式销毁生命周期**(`src/render/renderer.ts`): - 新增 `dispose()` 方法与 `isDisposed` 只读状态标志; - 彻底销毁所有托管纹理、VBO 缓冲(`gl.deleteBuffer`)、VAO(`gl.deleteVertexArray`)以及 Shader Program(`gl.deleteProgram`); - 移除 Canvas 事件监听; - 支持**幂等操作**(重复调用安全退出); - 内部集成 `gl.isContextLost()` 状态防护,在 WebGL 上下文已丢失时不调用非法的 GL 删除指令; - 已销毁实例调用 `begin` / `setAtlas` / `addAtlas` 会立即拦截并抛出 `RendererError`。 4. **WebGL Context Lost 保护与自动恢复**(`src/render/renderer.ts`): - `SpriteRenderer` 构造函数接收 `RendererOptions`(`onContextLost` / `onContextRestored`); - 监听 Canvas `webglcontextlost` 事件并强制触发 `event.preventDefault()`(**遵循 WebGL 规范,只有 preventDefault 浏览器才会允许后续触发 context restored 恢复**); - 场景层接入状态提示并在恢复后自动重新拉起场景。 5. **业务场景生命周期与关卡切换集成**: - `src/scene/map-scene.ts` / `src/scene/net-scene.ts` / `src/walk.ts`:模块级维护 `activeRenderer`,在切换关卡、重新选择地图或拖入新 MPQ 时,先调用 `activeRenderer?.dispose()`;并在 `beforeunload` 时清理运行循环与显存; - `src/scene/act-scene.ts`:在卸载前对 `runtime.pages` 中的所有 Atlas 以及角色 Handle 执行 `deleteAtlas`,随后彻底 `dispose()`。 --- ### 二、工程验证与自动化测试 1. **生命周期自动化测试套件**(`scripts/verify-renderer-lifecycle.ts`): - 注册了 `npm run verify:renderer` 命令; - 基于 WebGL2 模拟环境对 26 项断言进行覆盖测试(包括着色器编译后解绑删除、动态纹理增删、活跃批次解绑安全 flush、全量 dispose 幂等性、上下文丢失防崩溃与回调等): ```text == Running SpriteRenderer Lifecycle Verification == ✓ Two shaders created (VS & FS) ✓ Both shaders detached and deleted right after program link ✓ Program created ✓ VAO created ✓ VBO buffer created ✓ Initial textures created (atlas + white) ✓ Renderer is active and not disposed ✓ addAtlas creates a new GPU texture (3 total) ✓ deleteAtlas returned true for managed handle ✓ GPU texture was deleted via gl.deleteTexture ✓ deleteAtlas returned false on already deleted handle ✓ 4 total textures created before dispose ✓ isDisposed is true after dispose() ✓ All 4 textures were deleted on dispose() ✓ VBO was deleted on dispose() ✓ VAO was deleted on dispose() ✓ Program was deleted on dispose() ✓ Second dispose() is safe and idempotent ✓ begin() throws RendererError when called on disposed renderer ✓ addAtlas() throws RendererError when called on disposed renderer ✓ webglcontextlost calls event.preventDefault() to allow recovery ✓ onContextLost callback fired ✓ onContextRestored callback fired ✓ Context lost listener removed on dispose ✓ Context restored listener removed on dispose ✓ dispose() safely completes when context is already lost == Summary: 26 passed, 0 failed == ``` 2. **生产构建与类型检查**: ```bash npm run build # (tsc --noEmit && vite build) ✓ 56 modules transformed. ✓ built in 1.11s ```
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Reference: troytt/diablo2-web#9
No description provided.