@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
JavaScript
//#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