UNPKG

@motion-core/motion-gpu

Version:

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

126 lines (110 loc) 3.49 kB
import { createCurrentWritable as currentWritable, type CurrentReadable } from './current-value.js'; import { toMotionGPUErrorReport, type MotionGPUErrorReport } from './error-report.js'; import { isAbortError, loadTexturesFromUrls, mergeAbortSignals, type LoadedTexture, type TextureLoadOptions } from './texture-loader.js'; interface TextureLoadController { textures: CurrentReadable<LoadedTexture[] | null>; loading: CurrentReadable<boolean>; error: CurrentReadable<Error | null>; errorReport: CurrentReadable<MotionGPUErrorReport | null>; reload: () => Promise<void>; resume: () => void; dispose: () => void; } function disposeTextures(textures: LoadedTexture[] | null): void { for (const texture of textures ?? []) { texture.dispose(); } } function toError(error: unknown): Error { return error instanceof Error ? error : new Error('Unknown texture loading error'); } export function createTextureLoadController({ getUrls, getOptions }: { getUrls: () => string[]; getOptions: () => TextureLoadOptions; }): TextureLoadController { const textures = currentWritable<LoadedTexture[] | null>(null); const loading = currentWritable(true); const error = currentWritable<Error | null>(null); const errorReport = currentWritable<MotionGPUErrorReport | null>(null); let disposed = false; let requestVersion = 0; let activeController: AbortController | null = null; let runningLoad: Promise<void> | null = null; let reloadQueued = false; const executeLoad = async (): Promise<void> => { if (disposed) return; const version = ++requestVersion; const controller = new AbortController(); activeController = controller; loading.set(true); error.set(null); errorReport.set(null); const previous = textures.current; const options = getOptions() ?? {}; const mergedSignal = mergeAbortSignals(controller.signal, options.signal); try { const loaded = await loadTexturesFromUrls(getUrls(), { ...options, signal: mergedSignal.signal }); if (disposed || version !== requestVersion) { disposeTextures(loaded); return; } textures.set(loaded); disposeTextures(previous); } catch (nextError) { if (disposed || version !== requestVersion || isAbortError(nextError)) return; disposeTextures(previous); textures.set(null); const normalizedError = toError(nextError); error.set(normalizedError); errorReport.set(toMotionGPUErrorReport(normalizedError, 'initialization')); } finally { if (!disposed && version === requestVersion) loading.set(false); if (activeController === controller) activeController = null; mergedSignal.dispose(); } }; const runLoadLoop = async (): Promise<void> => { do { reloadQueued = false; await executeLoad(); } while (reloadQueued && !disposed); }; const reload = (): Promise<void> => { activeController?.abort(); if (runningLoad) { reloadQueued = true; return runningLoad; } const pending = runLoadLoop(); const trackedPending = pending.finally(() => { if (runningLoad === trackedPending) runningLoad = null; }); runningLoad = trackedPending; return trackedPending; }; const resume = (): void => { disposed = false; }; const dispose = (): void => { if (disposed) return; disposed = true; requestVersion += 1; activeController?.abort(); disposeTextures(textures.current); textures.set(null); loading.set(false); }; return { textures, loading, error, errorReport, reload, resume, dispose }; }