fix(studio): an iPhone keeps the repair off WebGPU, off the worker pool and at 384 pixels, the three things its page did not survive
This commit is contained in:
@@ -1455,7 +1455,7 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
||||
const my0 = Math.min(...ys) - widest;
|
||||
const mx1 = Math.max(...xs) + widest;
|
||||
const my1 = Math.max(...ys) + widest;
|
||||
const margin = Math.max(16, 0.5 * Math.max(mx1 - mx0, my1 - my0));
|
||||
const margin = Math.max(16, Math.min(64, Math.round(Math.max(widest * 1.5, 0.25 * Math.max(mx1 - mx0, my1 - my0)))));
|
||||
const srcLeft = Math.max(boxLeft, Math.floor(mx0 - margin));
|
||||
const srcTop = Math.max(boxTop, Math.floor(my0 - margin));
|
||||
const srcRight = Math.min(boxRight, Math.ceil(mx1 + margin));
|
||||
|
||||
@@ -50,6 +50,15 @@ interface Loaded {
|
||||
|
||||
let loaded: Promise<Loaded> | null = null;
|
||||
|
||||
function isIOS(): boolean {
|
||||
if (typeof navigator === 'undefined') return false;
|
||||
const ua = navigator.userAgent || '';
|
||||
return (
|
||||
/iPad|iPhone|iPod/.test(ua) ||
|
||||
(navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1)
|
||||
);
|
||||
}
|
||||
|
||||
// One warm-up run through the graph, answering whether the session it was given
|
||||
// can actually execute the model. WebGPU is worth having — a repair lands in
|
||||
// well under a second instead of a second and a half — but a provider the graph
|
||||
@@ -57,27 +66,33 @@ let loaded: Promise<Loaded> | null = null;
|
||||
// the trial is a real run and not a `create` that returned.
|
||||
async function warmsUp(ort: typeof Ort, session: Ort.InferenceSession): Promise<boolean> {
|
||||
try {
|
||||
await session.run({
|
||||
[session.inputNames[0]]: new ort.Tensor('uint8', new Uint8Array(3 * WARM_SIDE * WARM_SIDE), [
|
||||
1, 3, WARM_SIDE, WARM_SIDE,
|
||||
]),
|
||||
[session.inputNames[1]]: new ort.Tensor('uint8', new Uint8Array(WARM_SIDE * WARM_SIDE).fill(255), [
|
||||
1, 1, WARM_SIDE, WARM_SIDE,
|
||||
]),
|
||||
const in0 = new ort.Tensor('uint8', new Uint8Array(3 * WARM_SIDE * WARM_SIDE), [
|
||||
1, 3, WARM_SIDE, WARM_SIDE,
|
||||
]);
|
||||
const in1 = new ort.Tensor('uint8', new Uint8Array(WARM_SIDE * WARM_SIDE).fill(255), [
|
||||
1, 1, WARM_SIDE, WARM_SIDE,
|
||||
]);
|
||||
const out = await session.run({
|
||||
[session.inputNames[0]]: in0,
|
||||
[session.inputNames[1]]: in1,
|
||||
});
|
||||
try {
|
||||
in0.dispose?.();
|
||||
in1.dispose?.();
|
||||
out[session.outputNames[0]]?.dispose?.();
|
||||
} catch {}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Whether the browser has a device at all.
|
||||
// WebGPU on iOS (iPhone / iPad / WKWebView) is experimental and unstable;
|
||||
// compiling WGSL or dispatching ONNX pipelines crashes WebKit's GPUProcess,
|
||||
// causing iOS to silently reload the page. iOS is therefore excluded from WebGPU.
|
||||
async function hasDevice(): Promise<boolean> {
|
||||
if (isIOS()) return false;
|
||||
try {
|
||||
return Boolean(await navigator.gpu?.requestAdapter());
|
||||
} catch {
|
||||
@@ -108,36 +123,16 @@ function load(): Promise<Loaded> {
|
||||
loaded = (async () => {
|
||||
const ort = await import('onnxruntime-web');
|
||||
ort.env.wasm.wasmPaths = WASM_DIR;
|
||||
// Same rule as the upscaler's: the threads need COOP+COEP for their
|
||||
// SharedArrayBuffer, and asking for more than one without isolation is a
|
||||
// throw rather than a fallback. Four, because at 512 pixels square the
|
||||
// graph is done before a wider pool has paid for its own start-up.
|
||||
ort.env.wasm.numThreads = self.crossOriginIsolated
|
||||
const ios = isIOS();
|
||||
// On iOS WebKit, multi-threading or worker proxying triggers memory limits
|
||||
// and WebKit crashes. Single thread direct WASM execution is rock-solid and
|
||||
// fast on Apple Silicon / A-series chips.
|
||||
ort.env.wasm.numThreads = ios
|
||||
? 1
|
||||
: self.crossOriginIsolated
|
||||
? Math.min(4, navigator.hardwareConcurrency || 1)
|
||||
: 1;
|
||||
// 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.wasm.proxy = !ios;
|
||||
ort.env.webgpu.powerPreference = 'high-performance';
|
||||
const session = await openSession(ort);
|
||||
return { ort, session };
|
||||
@@ -204,9 +199,10 @@ export async function inpaintCrop(
|
||||
height: number,
|
||||
hole: Uint8Array
|
||||
): Promise<Uint8Array | null> {
|
||||
const maxSide = isIOS() ? 384 : INPAINT_MAX_SIDE;
|
||||
let w = Math.max(8, Math.round(width));
|
||||
let h = Math.max(8, Math.round(height));
|
||||
const scale = Math.min(1, INPAINT_MAX_SIDE / Math.max(w, h));
|
||||
const scale = Math.min(1, maxSide / Math.max(w, h));
|
||||
// Whole multiples of the graph's own stride, so the last block of the encoder
|
||||
// is a block and not a remainder: rounded down, since the renderer's crop is
|
||||
// wider than the mask by its own margin and losing a sliver of that margin
|
||||
@@ -239,9 +235,11 @@ export async function inpaintCrop(
|
||||
|
||||
try {
|
||||
const { ort, session } = await load();
|
||||
const in0 = new ort.Tensor('uint8', src, [1, 3, h, w]);
|
||||
const in1 = new ort.Tensor('uint8', mask, [1, 1, h, w]);
|
||||
const out = await session.run({
|
||||
[session.inputNames[0]]: new ort.Tensor('uint8', src, [1, 3, h, w]),
|
||||
[session.inputNames[1]]: new ort.Tensor('uint8', mask, [1, 1, h, w]),
|
||||
[session.inputNames[0]]: in0,
|
||||
[session.inputNames[1]]: in1,
|
||||
});
|
||||
const tensor = out[session.outputNames[0]];
|
||||
const oplane = w * h;
|
||||
@@ -255,6 +253,11 @@ export async function inpaintCrop(
|
||||
patch[i * 4 + 2] = data[2 * oplane + i];
|
||||
patch[i * 4 + 3] = 255;
|
||||
}
|
||||
try {
|
||||
in0.dispose?.();
|
||||
in1.dispose?.();
|
||||
tensor.dispose?.();
|
||||
} catch {}
|
||||
return patch;
|
||||
} catch (err) {
|
||||
// A model that cannot run is a soft repair, not a broken one — and this one
|
||||
@@ -270,7 +273,8 @@ export async function inpaintCrop(
|
||||
// uses it to cut the crop it hands over: a 3000px box read back at 3000px and
|
||||
// scaled here would be 27MB of pixels moved for a repair that ends up 512 wide.
|
||||
export function inpaintCropSize(width: number, height: number): { w: number; h: number } {
|
||||
const scale = Math.min(1, INPAINT_MAX_SIDE / Math.max(1, Math.max(width, height)));
|
||||
const maxSide = isIOS() ? 384 : INPAINT_MAX_SIDE;
|
||||
const scale = Math.min(1, maxSide / Math.max(1, Math.max(width, height)));
|
||||
return {
|
||||
w: Math.max(8, Math.floor((Math.max(1, Math.round(width)) * scale) / 8) * 8),
|
||||
h: Math.max(8, Math.floor((Math.max(1, Math.round(height)) * scale) / 8) * 8),
|
||||
|
||||
Reference in New Issue
Block a user