WebGPU 设备丢失后的恢复:保留用户任务与页面状态
设计有界的 WebGPU 恢复流程,在保留页面状态的同时阻止过期异步任务覆盖新设备。
BotBrowser Team
WebGPU 设备丢失后,图形功能可能停止,但页面其他部分仍然正常。W3C 规范通过 GPUDevice.lost 表示这一生命周期事件;MDN 说明,该 Promise 在设备生命周期内保持 pending,设备丢失时会以丢失信息完成。应用应把用户任务保存在普通应用状态中,及时切换到可用的非 GPU 界面,并把重新初始化视为一个新代次。代次令牌可以阻止旧设备留下的迟到异步结果影响新设备。
将应用状态与 GPU 资源分开
图形设备只是用于呈现或计算结果的资源,不应成为文档、当前选择、表单值或其他用户工作唯一的存放位置。把这些数据保存在常规页面或应用状态中。这样设备丢失时可以释放渲染资源,而不必清除用户任务。
GPUDevice.lost 是属于单个设备的 Promise。设备丢失时它会完成,但这不代表一定能创建替代设备。WebGPU 规范定义的是设备可用性发生变化,并不是对物理硬件的诊断。界面应准确描述受影响的功能、保留仍可访问的数据,也不要猜测某台设备丢失的原因。
这与 WebGPU 指纹识别概览所讨论的隐私问题不同;那篇文章关注暴露信号如何用于跟踪。浏览器版本发布验证则帮助运维人员评估浏览器与配置组合的发布及回滚,不负责处理会话内的 GPU 设备丢失。本文回答的是运行时问题:正在使用的设备不可用后,应用应该如何处理当前功能?
使用代次令牌保护恢复状态机
每当初始化被取代或停止时,递增一个单调增长的代次编号。每项异步工作都记录启动时的编号;在改变可见状态之前,必须确认它仍属于当前代次。设备的 lost 处理器也需要相同检查,避免旧设备的迟到事件覆盖新设备状态。
下面的控制器展示了可复制的基本做法。showFallback、showGraphics、announce、stopRenderLoop、detachGraphics 和 disposeDeviceResources 由应用实现:先停止旧渲染循环并解除旧视图,再释放应用拥有的 buffer、texture 等资源,最后销毁设备。应用数据留在控制器之外。只在用户打开功能或主动重试时调用 initialize。此样例不会自动重试,也不实现重试计数;功能所有者需在控制器外执行产品规定的重试上限。
let generation = 0;
let activeDevice = null;
function isCurrent(token) {
return token === generation;
}
function stopGraphics() {
generation += 1;
const oldDevice = activeDevice;
activeDevice = null;
try {
retireDevice(oldDevice);
} finally {
showFallback('Graphics stopped. Your page state is still available.');
}
}
function retireDevice(device) {
if (!device) return;
try {
stopRenderLoop(device);
} finally {
try {
detachGraphics(device);
} finally {
try {
disposeDeviceResources(device);
} finally {
device.destroy();
}
}
}
}
async function initialize(gpu = navigator.gpu) {
const token = ++generation;
const oldDevice = activeDevice;
activeDevice = null;
showFallback('Starting the graphics feature…');
try {
retireDevice(oldDevice);
if (!gpu) throw new Error('WebGPU is unavailable in this context.');
const adapter = await gpu.requestAdapter();
if (!isCurrent(token)) return;
if (!adapter) throw new Error('No adapter was returned.');
const device = await adapter.requestDevice();
if (!isCurrent(token)) {
retireDevice(device);
return;
}
activeDevice = device;
showGraphics(device);
announce('Graphics are ready.');
void device.lost.then(info => {
if (!isCurrent(token)) return;
generation += 1;
activeDevice = null;
try {
retireDevice(device);
} finally {
showFallback(`Graphics stopped (${info.reason || 'device lost'}). Your page state is still available.`);
}
});
} catch {
if (isCurrent(token)) {
const failedDevice = activeDevice;
activeDevice = null;
try {
retireDevice(failedDevice);
} finally {
showFallback('Graphics could not start. Your page state is still available.');
}
}
}
}
实际产品应按 API 约定处理丢失信息,只记录支持工作所需的内容,不要据此分类硬件。若 requestDevice() 在 stopGraphics() 或下一次 initialize() 后才完成,令牌检查会销毁过期设备,而不是把它显示出来。旧设备的 lost Promise 如果在新代次开始后完成,也不能覆盖新视图。
选择面向用户的结果
状态转换应说明功能还剩下什么,而不是猜测设备丢失原因。
WebGPU 不存在、adapter 为 null 或初始化被拒绝。 保留普通 HTML 视图,并说明增强图形无法启动。不要自动重试;让该功能保持可选。
就绪设备的 lost Promise 完成。 只将该代次标记为丢失,同时保留页面数据和控件。只有在用户有理由重试时才提供重试操作。
活动设备被新初始化取代,而新初始化失败或成功。 请求替代设备前,停止并解除旧 renderer,释放其所属资源并销毁设备。如果请求失败,保留降级视图,不要恢复旧设备;如果成功,只连接新设备。
旧请求仍在等待时,新代次已经开始。 将旧请求的完成视为过期,并释放它所创建的设备。不要让它更改新代次的状态或视图。
用户主动重试成功。 连接新设备,并根据当前应用状态进行渲染。只监听新设备的丢失 Promise。
用户主动重试失败或应用重试预算已用尽。 保留可用的降级界面,并说明增强图形仍不可用。停止重试,直到用户再次操作或应用定义的重置发生;重试预算由功能所有者负责执行,本示例不负责。
如果产品允许重试,应使用具有明确加载和禁用状态的按钮。紧密循环重试可能反复申请资源,却不改善体验。此样例不会自动重试或执行计数上限;功能所有者应在控制器外设置较小的重试预算,或在失败后要求用户再次操作。规范和 GPUDevice.lost 都不承诺重试一定能恢复同一工作负载。
让恢复行为可观察、可测试
由拥有用户任务的组件决定图形视图是否可选、哪些数据必须保留,以及设备不可用时哪种视图仍能继续。渲染器可以报告设备不可用,但不应自行丢弃文档或启动重试。页面层应统一协调状态、控件和用户意图。
把每次初始化当作短事务:先标记旧代次失效、分离旧渲染路径并保留应用数据,再为新代次申请资源。只有最后一次代次检查通过后才发布新设备;发生错误时,也只能由当前令牌更新界面。
设备丢失和应用停止可能与清理操作并发。销毁或分离活动设备前先递增令牌,避免清理排队的 Promise 回调写入旧代次。清理应可重复调用而结果一致,也不能误销毁替代设备。
不要把每种丢失原因都当作恢复指令。丢失信息可以用于 API 定义的大类状态或支持记录,但不能识别特定 GPU,也不能证明运行环境变化的原因。原因缺失、陌生或对用户无用时,界面仍应给出安全、简洁的说明。
检查功能能否从普通应用状态重建。例如,图表可用保存的行数据和筛选项重绘;只存在 GPU 内存中的中间结果则可能无法恢复。若任务要求保留这类工作,应在应用拥有的存储中保存检查点,并明确说明哪些结果保留、哪些操作需要重做。
用户操作进行中也可能发生设备丢失。只禁用依赖图形设备的控件;在仍可用时保留导航、取消、导出和辅助视图。如果待处理操作尚未写入应用状态,就应改由非 GPU 路径完成,或说明需要重试。状态提示可以通过辅助技术播报,而不移动当前焦点。
不要默认通过重载页面恢复。重载可能清除未提交表单、重置导航、重复网络请求或迫使用户重建操作上下文。设备丢失是资源生命周期事件,不自动意味着整个应用需要重启;只有存在独立、明确的应用原因且用户数据已受保护时,才考虑整页重载。
显式调用 GPUDevice.destroy() 也会结束设备的可用生命周期。因此应在清理前使其代次失效,否则随后的 lost 完成可能与停止流程竞态,看起来像意外故障。当前设备的丢失可以切换到降级视图;旧代次被停止后的丢失事件则不应再修改界面。
竞态测试应使用可延迟的 Promise,而不是任意计时器。先启动代次 A 并挂起 adapter 请求,再启动 B 并让 B 显示,最后完成 A;最终视图必须仍属于 B。再让 A 的 requestDevice() 延迟到 B 就绪之后,确认 A 返回的设备会被释放、不会绑定到渲染器。
增加一组针对活动设备的测试:先让 A 初始化成功并绑定渲染器,再调用 initialize() 启动 B。应在 B 的 adapter 请求前停止 A 的渲染循环、解除 A 的视图、释放其资源并销毁 A。若 B 的 adapter/device 请求失败,预期保持普通视图且 A 不会复活;若 B 成功,只有 B 可以绑定渲染器。两条分支都要检查迟到的 A lost 事件不改变当前状态。
测试替身应暴露和 API 相同的异步边界:可控的 adapter 请求、device 请求,以及每台设备独立的 lost Promise。记录 destroy() 和设备绑定次数。旧代次结束后,它不能再次调用状态回调;当前设备应保持绑定。该夹具不依赖真实 adapter,因而可以稳定地在自动化测试中运行。
停止和重试是不同操作。停止应递增代次、移除旧设备并回到稳定降级视图,且不发起新请求。用户主动重试才开始新代次并显示加载状态;失败后回到降级视图,并按有限策略决定是否还能尝试。重试次数表示应用发起的尝试,不是单台设备报告的丢失事件数。
设备被替代时也要检查资源归属。迟到 Promise 创建的设备属于已失效代次,应按 API 生命周期释放。旧代次的 catch 或丢失回调不能清除当前设备。尽量将设备引用保存在各自代次内,并在写共享状态前检查令牌。
重建不等于重新连接旧设备。Buffer、texture、pipeline、bind group 和 command encoder 都属于创建它们的设备。新设备就绪后,只重建当前模式需要的资源,并从应用数据重建绑定。若 canvas 上下文使用设备,应先配置替代设备的渲染路径,再提交工作。
保留 CPU 侧模型或其他权威输入,让新资源能够呈现同一用户任务。不要把旧设备资源放进全局缓存并假设可以交给新设备使用。只有从应用状态重建的资源才能跨越设备生命周期边界。
恢复协调和渲染实现应有清晰边界。渲染器可以提供接收设备并准备资源的接口,页面则负责判断该准备结果是否属于当前代次。若资源准备本身是异步的,也要传递同一令牌,并在发布前再次验证。
第二次丢失或用户取消可能发生在 shader、纹理或其他资源仍在加载时。可取消时应取消过期请求;无法取消时,必须忽略迟到结果,并释放它们拥有的陈旧资源。这样旧渲染器不会继续提交到已替换的设备。
按状态转换记录预期,而不是按设备型号记录。初始化成功表示当前任务所需功能已就绪;设备丢失表示该图形工作不可继续;恢复成功则表示新设备已从当前应用数据重建必要资源。这些结果可跨环境验证,无需创造硬件类别或保证普遍恢复。
测试还应确认渲染器只有在当前代次完成所有准备后才可见。若资源加载期间启动了更新代次,旧准备结果不得闪现,即使之后很快又会被替换。对用户而言,唯一可见的状态应是当前选择对应的结果。
使用受控 Promise 测试生命周期,不要依赖某一种图形环境。可以先让第一次 adapter 或 device 请求保持未完成,再启动第二次初始化。预期结果是第一代次永远不能发布结果;若它已创建设备,就应释放该设备。另一个测试是在替代设备就绪后才完成旧设备的 lost Promise;此时替代视图和状态必须保持不变。
还要覆盖常规分支:API 不存在、adapter 请求返回 null、adapter 或 device 请求被拒绝、初始化成功后设备丢失、用户取消、重试成功和重试次数耗尽。每种情况下都检查页面控件仍可操作、所选值未丢失、状态可被辅助技术读取,而且渲染视图与应用状态一致。如果恢复过程使用动画,还应单独验证减少动态效果的行为;不能依靠动画传达状态变化。
只收集理解应用状态转换所需的运维信息,例如粗略的失败阶段和是否已显示降级界面。不要采集 adapter 详情、使用硬件阈值、探测 GPU,或将丢失事件当成指纹信号。本文所述 API 行为以公开的 WebGPU 规范和 MDN GPUDevice.lost 参考为依据;两者都不保证不同浏览器或主机上的恢复结果。
让降级界面真正可用
降级界面是功能的一部分,而不是错误页。将重要信息与操作保留在普通 HTML 中,保留当前选择,并明确说明增强视图已停止、页面其他部分仍可使用。如果某些结果只存在于 GPU 内存而无法重建,应如实说明,而不是暗示数据已保存。
BotBrowser 的公开 WebGPU 文档介绍了可选择的 WebGPU 模式,这些模式可以属于应用支持的测试配置。但这并不保证设备丢失后可以恢复、控制主机资源回收,或让所有运行环境都支持 WebGPU。应在目标环境中验证用户可见的恢复行为,并将应用级恢复契约与 adapter 身份识别或指纹工作分开。