UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

290 lines (289 loc) 9.94 kB
//#region src/lib/core/texture-loader.ts var resourceCache = /* @__PURE__ */ new Map(); function createAbortError() { try { return new DOMException("Texture request was aborted", "AbortError"); } catch { const error = /* @__PURE__ */ new Error("Texture request was aborted"); error.name = "AbortError"; return error; } } /** * Checks whether error represents abort cancellation. */ function isAbortError(error) { return error instanceof Error && (error.name === "AbortError" || error.message.toLowerCase().includes("aborted")); } function mergeAbortSignals(primary, ...secondarySignals) { const signals = [primary, ...secondarySignals].filter((signal) => signal != null); if (signals.length === 1) return { signal: primary, dispose: () => {} }; if (typeof AbortSignal.any === "function") return { signal: AbortSignal.any(signals), dispose: () => {} }; const fallback = new AbortController(); let disposed = false; const cleanup = () => { if (disposed) return; disposed = true; for (const signal of signals) signal.removeEventListener("abort", abort); }; const abort = () => { if (!fallback.signal.aborted) fallback.abort(); cleanup(); }; if (signals.some((signal) => signal.aborted)) { fallback.abort(); return { signal: fallback.signal, dispose: () => {} }; } for (const signal of signals) signal.addEventListener("abort", abort, { once: true }); return { signal: fallback.signal, dispose: cleanup }; } function canShareTextureRequest(requestInit) { const method = (requestInit?.method ?? "GET").toUpperCase(); return (method === "GET" || method === "HEAD") && requestInit?.body == null; } function normalizeRequestInit(requestInit) { const headers = new Headers(requestInit?.headers); const headerEntries = Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b)); const normalized = {}; normalized.method = (requestInit?.method ?? "GET").toUpperCase(); normalized.mode = requestInit?.mode ?? null; normalized.cache = requestInit?.cache ?? null; normalized.credentials = requestInit?.credentials ?? null; normalized.redirect = requestInit?.redirect ?? null; normalized.referrer = requestInit?.referrer ?? null; normalized.referrerPolicy = requestInit?.referrerPolicy ?? null; normalized.integrity = requestInit?.integrity ?? null; normalized.keepalive = requestInit?.keepalive ?? false; normalized.priority = requestInit?.priority ?? null; normalized.headers = headerEntries; return normalized; } function withoutRequestSignal(requestInit) { if (!requestInit || requestInit.signal == null) return requestInit; const rest = { ...requestInit }; delete rest.signal; return rest; } function mergeTextureClientSignals(options) { const primary = options.signal ?? options.requestInit?.signal; if (!primary) return null; return mergeAbortSignals(primary, options.signal === void 0 ? void 0 : options.requestInit?.signal); } function normalizeTextureLoadOptions(options) { const colorSpace = options.colorSpace ?? "srgb"; const normalized = { colorSpace, decode: { colorSpaceConversion: options.decode?.colorSpaceConversion ?? (colorSpace === "linear" ? "none" : "default"), premultiplyAlpha: options.decode?.premultiplyAlpha ?? "default", imageOrientation: options.decode?.imageOrientation ?? "none" } }; if (options.requestInit !== void 0) normalized.requestInit = options.requestInit; if (options.signal !== void 0) normalized.signal = options.signal; if (options.update !== void 0) normalized.update = options.update; if (options.flipY !== void 0) normalized.flipY = options.flipY; if (options.premultipliedAlpha !== void 0) normalized.premultipliedAlpha = options.premultipliedAlpha; if (options.generateMipmaps !== void 0) normalized.generateMipmaps = options.generateMipmaps; return normalized; } /** * Builds a deterministic resource cache key for cache-eligible URL IO config. */ function buildTextureResourceCacheKey(url, options = {}) { const normalized = normalizeTextureLoadOptions(options); return JSON.stringify({ url, colorSpace: normalized.colorSpace, requestInit: normalizeRequestInit(normalized.requestInit), decode: normalized.decode }); } /** * Clears the internal texture resource cache. */ function clearTextureBlobCache() { for (const entry of resourceCache.values()) if (!entry.settled) entry.controller.abort(); resourceCache.clear(); } function acquireTextureBlob(url, options) { const key = canShareTextureRequest(options.requestInit) ? buildTextureResourceCacheKey(url, options) : null; const existing = key === null ? void 0 : resourceCache.get(key); if (existing && key !== null) { existing.refs += 1; let released = false; return { entry: existing, release: () => { if (released) return; released = true; existing.refs = Math.max(0, existing.refs - 1); if (existing.refs === 0) { if (!existing.settled) existing.controller.abort(); if (resourceCache.get(key) === existing) resourceCache.delete(key); } } }; } const normalized = normalizeTextureLoadOptions(options); const controller = new AbortController(); const requestInit = { ...normalized.requestInit ?? {}, signal: controller.signal }; const entry = { key, refs: 1, controller, settled: false, blobPromise: fetch(url, requestInit).then(async (response) => { if (!response.ok) throw new Error(`Texture request failed (${response.status}) for ${url}`); return response.blob(); }).then((blob) => { entry.settled = true; return blob; }).catch((error) => { if (key !== null && resourceCache.get(key) === entry) resourceCache.delete(key); throw error; }) }; if (key !== null) resourceCache.set(key, entry); let released = false; return { entry, release: () => { if (released) return; released = true; entry.refs = Math.max(0, entry.refs - 1); if (entry.refs === 0) { if (!entry.settled) entry.controller.abort(); if (key !== null && resourceCache.get(key) === entry) resourceCache.delete(key); } } }; } async function awaitWithAbort(promise, signal) { if (!signal) return promise; if (signal.aborted) throw createAbortError(); return new Promise((resolve, reject) => { const onAbort = () => { reject(createAbortError()); }; signal.addEventListener("abort", onAbort, { once: true }); promise.then(resolve, reject).finally(() => { signal.removeEventListener("abort", onAbort); }); }); } /** * Loads a single texture from URL and converts it to an `ImageBitmap`. * * @param url - Texture URL. * @param options - Loading options. * @returns Loaded texture object. * @throws {Error} When runtime does not support `createImageBitmap` or request fails. */ async function loadTextureFromUrl(url, options = {}) { if (typeof createImageBitmap !== "function") throw new Error("createImageBitmap is not available in this runtime"); const normalized = normalizeTextureLoadOptions(options); const clientSignal = mergeTextureClientSignals(normalized); let release = null; let bitmap = null; try { if (clientSignal?.signal.aborted) throw createAbortError(); const acquired = acquireTextureBlob(url, options); release = acquired.release; const blob = await awaitWithAbort(acquired.entry.blobPromise, clientSignal?.signal); const bitmapOptions = { colorSpaceConversion: normalized.decode.colorSpaceConversion, premultiplyAlpha: normalized.decode.premultiplyAlpha, imageOrientation: normalized.decode.imageOrientation }; bitmap = bitmapOptions.colorSpaceConversion === "default" && bitmapOptions.premultiplyAlpha === "default" && bitmapOptions.imageOrientation === "none" ? await createImageBitmap(blob) : await createImageBitmap(blob, bitmapOptions); if (clientSignal?.signal.aborted) { bitmap.close(); bitmap = null; throw createAbortError(); } let disposed = false; const loaded = { url, source: bitmap, width: bitmap.width, height: bitmap.height, colorSpace: normalized.colorSpace, dispose: () => { if (disposed) return; disposed = true; bitmap?.close(); bitmap = null; } }; if (normalized.update !== void 0) loaded.update = normalized.update; if (normalized.flipY !== void 0) loaded.flipY = normalized.flipY; if (normalized.premultipliedAlpha !== void 0) loaded.premultipliedAlpha = normalized.premultipliedAlpha; if (normalized.generateMipmaps !== void 0) loaded.generateMipmaps = normalized.generateMipmaps; return loaded; } catch (error) { if (bitmap) bitmap.close(); throw error; } finally { release?.(); clientSignal?.dispose(); } } /** * Loads many textures in parallel from URLs. * * @param urls - Texture URLs. * @param options - Shared loading options. * @returns Promise resolving to loaded textures in input order. */ async function loadTexturesFromUrls(urls, options = {}) { const loaded = []; const batchController = new AbortController(); const mergedSignal = mergeAbortSignals(batchController.signal, options.signal, options.requestInit?.signal); const requestInit = withoutRequestSignal(options.requestInit); const abortBatch = () => { if (!batchController.signal.aborted) batchController.abort(); }; let failed = false; try { const loadPromises = urls.map(async (url) => { const texture = await loadTextureFromUrl(url, { ...options, ...requestInit !== void 0 ? { requestInit } : {}, signal: mergedSignal.signal }); if (failed) { texture.dispose(); throw createAbortError(); } loaded.push(texture); return texture; }); return await Promise.all(loadPromises); } catch (error) { failed = true; abortBatch(); for (const texture of loaded) texture.dispose(); throw error; } finally { mergedSignal.dispose(); } } //#endregion export { buildTextureResourceCacheKey, clearTextureBlobCache, isAbortError, loadTextureFromUrl, loadTexturesFromUrls, mergeAbortSignals }; //# sourceMappingURL=texture-loader.js.map