fix(studio): the repair keeps to a worker wherever it falls back, so a device that cannot run the graph cannot freeze the page

This commit is contained in:
2026-10-14 00:52:58 +07:00
parent c192aa1ee2
commit 836858c36c
4 changed files with 28 additions and 28 deletions
+1 -1
View File
@@ -1050,7 +1050,7 @@ export function Workspace() {
if (!engine || !previewBytes || shooting) return;
const token = ++renderToken.current;
const timer = setTimeout(async () => {
setBusy(t('act.exporting'));
setBusy(t('act.rendering'));
try {
// The geometry is the frame's own; only the recipe differs between the
// render and the compare copy, so the two can never drift apart.
+23 -27
View File
@@ -71,23 +71,6 @@ async function warmsUp(ort: typeof Ort, session: Ort.InferenceSession): Promise<
}
}
// Whether the page has a device of its own to run the graph on. Asked of the
// browser rather than of the runtime: creating a session of any kind — the
// WebGPU one included — builds the wasm backend underneath it, and
// `env.wasm.proxy` is read at that moment. A proxy set after it is a proxy that
// never took, and every run of the fallback then ends the way it did the first
// time this was tried: `no available backend found. ERR: [wasm] Error: worker
// not ready`. So the question is put before anything is created.
async function hasDevice(): Promise<boolean> {
const gpu = (navigator as Navigator & { gpu?: { requestAdapter(): Promise<unknown> } }).gpu;
if (!gpu) return false;
try {
return !!(await gpu.requestAdapter());
} catch {
return false;
}
}
// WebGPU first, the processor as the answer to every way that can fail: no
// adapter, no `shader-f16` needed here but an op the provider never
// implemented, a driver that opens a session and dies on the first dispatch.
@@ -116,16 +99,29 @@ function load(): Promise<Loaded> {
ort.env.wasm.numThreads = self.crossOriginIsolated
? Math.min(4, navigator.hardwareConcurrency || 1)
: 1;
// The fallback's turn is taken in a worker of the page's own, wherever
// there is no device to take it on: a wasm run is seconds of arithmetic
// that never hands the thread back in the middle of it, and a thread that
// cannot answer a finger for three seconds is a tab a phone takes away
// (measured: 3.5s of frozen page per repair, on a desktop). Proxied, the
// page keeps its frames and its touches while the repair is worked out,
// and the repair lands under the spinner the studio already shows. It is
// left off where a device is there, because the WebGPU path is a device of
// its own and must be built on the page's own thread.
ort.env.wasm.proxy = !(await hasDevice());
// The fallback's turn is taken in a worker of the page's own, always: a
// wasm run is seconds of arithmetic that never hands the thread back in
// the middle of it, and a thread that cannot answer a finger for three
// seconds is a tab a phone takes away (measured: 3.5s of frozen page per
// repair, on a desktop). Proxied, the page keeps its frames and its
// touches while the repair is worked out, and the repair lands under the
// spinner the studio already shows.
//
// Always, and not "wherever there is no device": `requestAdapter` answers
// for the browser, not for this graph, and a page holding an adapter the
// model cannot run on (measured: chromium's JSEP provider, which fails
// MI-GAN's own GatherND on the first run) took the fallback on its own
// thread — 2.1s frozen per repair against the 0.3s this costs — which is
// the freeze the worker is here to prevent. The WebGPU session is
// unaffected either way: the proxy is the wasm backend's, and the device
// session above is built and run on the page's thread as before.
//
// Set before anything is created: `env.wasm.proxy` is read at the moment
// the wasm backend is built, and a proxy set after one was — the WebGPU
// session builds one underneath it — is a proxy that never took (its runs
// then end in `no available backend found. ERR: [wasm] Error: worker not
// ready`).
ort.env.wasm.proxy = true;
ort.env.webgpu.powerPreference = 'high-performance';
const session = await openSession(ort);
return { ort, session };
+3
View File
@@ -28,6 +28,9 @@ export const en: Dict = {
'act.reset': 'RESET',
'act.export': 'EXPORT',
'act.exporting': 'Exporting…',
// The stage's own re-render behind a brush stroke or a knob: no file is being
// written anywhere, and "Exporting…" over a HEAL was read as exactly that.
'act.rendering': 'Rendering…',
'act.upscaling': 'Upscaling the photo {done}/{total}…',
'act.cancel': 'Cancel',
+1
View File
@@ -39,6 +39,7 @@ export const vi = {
'act.reset': 'ĐẶT LẠI',
'act.export': 'XUẤT ẢNH',
'act.exporting': 'Đang xuất…',
'act.rendering': 'Đang dựng ảnh…',
'act.upscaling': 'Đang phóng to ảnh {done}/{total}…',
'act.cancel': 'Huỷ',