设备丢失与恢复
驱动重置、资源压力或系统图形配置变化,都可能导致浏览器失去 GPU 访问能力。应将设备丢失视为应用正常使用中可能遇到的情况。
在 WebGL2 中,渲染上下文丢失后,浏览器可能将其恢复。在 WebGPU 中,丢失的原生设备无法复用:PlayCanvas 会请求替代设备并重建 GPU 资源。应用仍持有同一个 PlayCanvas 图形设备对象。
引擎会恢复什么
设备丢失期间,PlayCanvas 暂停应用更新和渲染循环,恢复成功后继续执行。JavaScript 对象、实体和应用状态仍保留在内存中。在暂停期间,浏览器定时器、网络请求和异步回调仍可能运行。
需要区分重建资源与恢复资源的内容:
| 资源或数据 | 恢复行为 |
|---|---|
| 引擎管理的着色器、绑定和管线 | 为恢复的上下文或替代设备重新创建。 |
| 保留了 CPU 数据源的纹理和顶点/索引缓冲区 | 从这些数据源重新上传。 |
| 渲染目标 | 重新创建,但之前渲染的像素会丢失,需要重新渲染内容。 |
| 存储缓冲区 | 按原大小重新创建,需要重新写入输入数据或通过计算重新生成。 |
| 运行时光照贴图、生成的环境和一次性纹理复制 | 重新执行烘焙、生成或复制操作。 |
| 仅保存在 GPU 上的粒子、模拟或绘画历史 | 历史状态丢失,需要重启或从应用管理的备份中恢复。 |
应在 GPU 内存之外保留权威数据或重新生成它的方法。加载的纹理可以从源图像恢复,但之后在纹理上累积的绘画内容无法如此恢复。常规逐帧渲染会自动重新生成许多中间目标。
恢复应用数据
监听 app.graphicsDevice 上的 devicelost 和 devicerestored。丢失时停止应用自行发起的 GPU 工作,恢复后重新填充生成的资源。两个后端都应使用这些引擎事件,不要替换引擎的原生丢失处理逻辑。
下面的 WebGPU 示例为存储缓冲区保留 CPU 输入,同时跟踪中断的读取,供下一节使用:
import {
BUFFERUSAGE_COPY_DST,
BUFFERUSAGE_COPY_SRC,
StorageBuffer
} from 'playcanvas';
const device = app.graphicsDevice;
const values = new Float32Array([1, 2, 3, 4]);
const buffer = new StorageBuffer(
device,
values.byteLength,
BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST
);
let readGeneration = 0;
const restoreData = () => {
buffer.write(0, values);
app.renderNextFrame = true;
};
const lost = device.on('devicelost', () => {
readGeneration++;
});
const restored = device.on('devicerestored', restoreData);
restoreData();
app.on('destroy', () => {
readGeneration++;
lost.off();
restored.off();
buffer.destroy();
});
应用输入变化时,同步更新保留的 values。恢复过程应可重复执行,因为之后可能再次丢失设备。复用引擎资源对象,避免每次恢复都重复创建监听器或资源。
对于运行时光照贴图,恢复后应安排再次烘焙,做法参见烘焙光照示例。应用创建的环境贴图也应重新生成。如果 app.autoRender 为 false,应在重新生成的内容准备就绪之后,通过 app.renderNextFrame = true 请求一帧。
处理异步读取
正在执行的读取可能在 devicelost 到达前拒绝,也可能在恢复后才完成。对于 WebGPU 存储缓冲区读取,应处理 AbortError 并丢弃旧代次的结果:
async function readValues() {
const generation = readGeneration;
try {
const result = await buffer.read(
0,
values.byteLength,
new Float32Array(values.length),
true
);
return generation === readGeneration ? result : null;
} catch (error) {
if (
error.name === 'AbortError' ||
generation !== readGeneration
) {
return null;
}
throw error;
}
}
最后一个 true 会立即提交这次一次性读取,即使在渲染循环外也是如此。立即提交会增加开销,应避免用于常规逐帧读取。
这里的 null 表示调用者应等待新的结果,其他错误仍向上传播。只有设备可用时才发起新的 GPU 工作,包括定时器和网络回调发起的工作。
所属对象销毁时移除恢复监听器。显式销毁不再需要的资源,包括由设备保留以便恢复的 Compute 实例。不要跨设备替换缓存原生 WebGPU 缓冲区、纹理或管线。
测试恢复
在本地构建、使用调试版引擎的示例中,将以下代码粘贴到浏览器控制台:
pc.AppBase.getApplication().graphicsDevice.debugLoseContext(1000);
这个内部测试辅助方法并非公共 API。它会触发真正的 WebGL 上下文丢失或 WebGPU 设备销毁,并在延迟 1,000 毫秒后尝试恢复。该延迟不保证恢复完成的时间。非调试构建会移除其实现,不要将其用于生产应用逻辑。
应多次测试,包括读取或计算工作正在执行时。确认渲染和交互恢复、生成的光照重新出现,以及按需渲染的场景无需移动相机就能重绘。检查两个后端,并查看控制台有无验证错误。
恢复失败时
恢复并无保证:浏览器可能无法恢复上下文或取得另一个 GPU 设备。例如 GPU 进程连续崩溃后,Chrome 可能暂时阻止该站点访问 GPU 数分钟。不要假定每次 devicelost 后都会出现 devicerestored。
无法取得替代 WebGPU 设备时,设备触发 devicerestorefailed。处理函数接收错误,设备保持丢失状态。WebGL2 没有对应信号,浏览器也可能永不恢复上下文,因此还应在应用自行设定的超时后停止等待:
const message = document.getElementById('gpu-lost-message');
let recoveryTimeout = null;
device.on('devicelost', () => {
// WebGL2 does not report a failed restore, so stop waiting after a while
recoveryTimeout = setTimeout(() => {
message.hidden = false;
}, 10000);
});
device.on('devicerestored', () => {
clearTimeout(recoveryTimeout);
message.hidden = true;
});
device.on('devicerestorefailed', (error) => {
clearTimeout(recoveryTimeout);
console.error(error);
message.hidden = false;
});
由于渲染已暂停,应在 HTML 中而非画布内显示消息,并提供重新加载页面的选项。将重要用户工作保存在 GPU 内存之外。
更多 WebGPU 生命周期背景,参见 Brandon Jones 的 WebGPU 设备丢失。