fix(studio): a repair lands under the brush with no pill over it, and a browser with no device is not asked for one

This commit is contained in:
2026-10-14 08:59:13 +07:00
parent 24ce97edcc
commit 5aa6a5ba23
4 changed files with 28 additions and 11 deletions
+6 -1
View File
@@ -1050,7 +1050,12 @@ export function Workspace() {
if (!engine || !previewBytes || shooting) return;
const token = ++renderToken.current;
const timer = setTimeout(async () => {
setBusy(t('act.rendering'));
// No spinner for this one, deliberately. The render is the page keeping up
// with the reader — the brush's own spots are already drawn on the layer
// above the photo, so the repair arriving under them a moment later is the
// photo catching up and not a thing being waited for. A pill saying so was
// read as the studio doing something to the frame (an export, a second
// copy), and it stayed up through a HEAL long enough to be noticed.
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.
+22 -6
View File
@@ -71,16 +71,32 @@ async function warmsUp(ort: typeof Ort, session: Ort.InferenceSession): Promise<
}
}
// Whether the browser has a device at all. `requestAdapter` answers for the
// browser and not for this graph — an adapter is no promise the model runs on
// it, the trial below is what settles that — but a *null* answer is a promise
// it does not, and the attempt it saves is not nothing: a `create` that cannot
// have a device still opens a session and tears it down (measured: 0.4s off the
// first repair, and one backend fewer built on the way to the one that lasts).
async function hasDevice(): Promise<boolean> {
try {
return Boolean(await navigator.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.
async function openSession(ort: typeof Ort): Promise<Ort.InferenceSession> {
try {
const gpu = await ort.InferenceSession.create(MODEL_URL, { executionProviders: ['webgpu'] });
if (await warmsUp(ort, gpu)) return gpu;
await gpu.release();
} catch {
// Whatever went wrong, the wasm provider is the fallback and not a failure.
if (await hasDevice()) {
try {
const gpu = await ort.InferenceSession.create(MODEL_URL, { executionProviders: ['webgpu'] });
if (await warmsUp(ort, gpu)) return gpu;
await gpu.release();
} catch {
// Whatever went wrong, the wasm provider is the fallback and not a failure.
}
}
return ort.InferenceSession.create(MODEL_URL, { executionProviders: ['wasm'] });
}
-3
View File
@@ -28,9 +28,6 @@ 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,7 +39,6 @@ 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ỷ',