UNPKG

@motion-core/motion-gpu

Version:

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

426 lines (425 loc) 15.1 kB
import { assertUniformValueForType } from "./uniforms.js"; import { resolveMaterial } from "./material.js"; import { toMotionGPUErrorReport } from "./error-report.js"; import { createRenderer } from "./renderer.js"; import { buildRendererPipelineSignature } from "./recompile-policy.js"; //#region src/lib/core/runtime-loop.ts function getRendererRetryDelayMs(attempt) { return Math.min(8e3, 250 * 2 ** Math.max(0, attempt - 1)); } var ERROR_CLEAR_GRACE_MS = 750; function createMotionGPURuntimeLoop(options) { const { canvas: canvasElement, registry, size } = options; let frameId = null; let renderer = null; let isDisposed = false; let observedCssWidth = -1; let observedCssHeight = -1; let resizeObserver = null; try { resizeObserver = new ResizeObserver((entries) => { const entry = entries[entries.length - 1]; if (!entry) return; const boxSize = entry.contentBoxSize?.[0]; if (boxSize) { observedCssWidth = Math.max(0, Math.floor(boxSize.inlineSize)); observedCssHeight = Math.max(0, Math.floor(boxSize.blockSize)); } else { observedCssWidth = Math.max(0, Math.floor(entry.contentRect.width)); observedCssHeight = Math.max(0, Math.floor(entry.contentRect.height)); } if (!isDisposed) scheduleFrame(); }); resizeObserver.observe(canvasElement); } catch { resizeObserver = null; } let previousTime = performance.now() / 1e3; let activeRendererSignature = ""; let failedRendererSignature = null; let failedRendererAttempts = 0; let nextRendererRetryAt = 0; let rendererRebuildPromise = null; const runtimeUniforms = {}; const runtimeTextures = {}; let activeUniforms = {}; let activeTextures = {}; let uniformKeys = []; let uniformKeySet = /* @__PURE__ */ new Set(); let uniformTypes = /* @__PURE__ */ new Map(); let textureKeys = []; let textureKeySet = /* @__PURE__ */ new Set(); let activeMaterialSignature = ""; let currentCssWidth = -1; let currentCssHeight = -1; const renderUniforms = {}; const renderTextures = {}; const canvasSize = { width: 0, height: 0 }; let storageBufferKeys = []; let storageBufferKeySet = /* @__PURE__ */ new Set(); let storageBufferDefinitions = {}; const pendingStorageWrites = []; let shouldContinueAfterFrame = false; let activeErrorKey = null; let errorHistory = []; let errorClearReadyAtMs = 0; let lastFrameTimestampMs = performance.now(); const resolveNowMs = (nowMs) => { if (typeof nowMs === "number" && Number.isFinite(nowMs)) return nowMs; return lastFrameTimestampMs; }; const getHistoryLimit = () => { const value = options.getErrorHistoryLimit?.() ?? 0; if (!Number.isFinite(value) || value <= 0) return 0; return Math.floor(value); }; const publishErrorHistory = () => { options.reportErrorHistory?.(errorHistory); const onErrorHistory = options.getOnErrorHistory?.(); if (!onErrorHistory) return; try { onErrorHistory(errorHistory); } catch {} }; const syncErrorHistory = () => { const limit = getHistoryLimit(); if (limit <= 0) { if (errorHistory.length === 0) return; errorHistory = []; publishErrorHistory(); return; } if (errorHistory.length <= limit) return; errorHistory.splice(0, errorHistory.length - limit); publishErrorHistory(); }; const setError = (error, phase, nowMs) => { const report = toMotionGPUErrorReport(error, phase); errorClearReadyAtMs = resolveNowMs(nowMs) + ERROR_CLEAR_GRACE_MS; const reportKey = JSON.stringify({ phase: report.phase, title: report.title, message: report.message, rawMessage: report.rawMessage }); if (activeErrorKey === reportKey) return; activeErrorKey = reportKey; const historyLimit = getHistoryLimit(); if (historyLimit > 0) { errorHistory.push(report); if (errorHistory.length > historyLimit) errorHistory.splice(0, errorHistory.length - historyLimit); publishErrorHistory(); } options.reportError(report); const onError = options.getOnError(); if (!onError) return; try { onError(report); } catch {} }; const maybeClearError = (nowMs) => { if (activeErrorKey === null) return; if (resolveNowMs(nowMs) < errorClearReadyAtMs) return; activeErrorKey = null; errorClearReadyAtMs = 0; options.reportError(null); }; const shouldRecreateRendererAfterError = (error) => { return toMotionGPUErrorReport(error, "render").code === "WEBGPU_DEVICE_LOST"; }; const scheduleFrame = () => { if (isDisposed || frameId !== null) return; frameId = requestAnimationFrame(renderFrame); }; const requestFrame = () => { scheduleFrame(); }; const invalidate = (token) => { registry.invalidate(token); requestFrame(); }; const advance = () => { registry.advance(); requestFrame(); }; const resetRuntimeMaps = () => { for (const key of Object.keys(runtimeUniforms)) if (!uniformKeySet.has(key)) delete runtimeUniforms[key]; for (const key of Object.keys(runtimeTextures)) if (!textureKeySet.has(key)) delete runtimeTextures[key]; }; const resetRenderPayloadMaps = () => { for (const key of Object.keys(renderUniforms)) if (!uniformKeySet.has(key)) delete renderUniforms[key]; for (const key of Object.keys(renderTextures)) if (!textureKeySet.has(key)) delete renderTextures[key]; }; const syncMaterialRuntimeState = (materialState) => { const signatureChanged = activeMaterialSignature !== materialState.signature; const defaultsChanged = activeUniforms !== materialState.uniforms || activeTextures !== materialState.textures; if (!signatureChanged && !defaultsChanged) return; activeUniforms = materialState.uniforms; activeTextures = materialState.textures; if (!signatureChanged) return; const layoutEntries = materialState.uniformLayout.entries; const nextUniformKeys = []; const nextUniformTypes = /* @__PURE__ */ new Map(); for (const entry of layoutEntries) { nextUniformKeys.push(entry.name); nextUniformTypes.set(entry.name, entry.type); } uniformKeys = nextUniformKeys; uniformTypes = nextUniformTypes; textureKeys = materialState.textureKeys; uniformKeySet = new Set(uniformKeys); textureKeySet = new Set(textureKeys); storageBufferKeys = materialState.storageBufferKeys; storageBufferKeySet = new Set(storageBufferKeys); storageBufferDefinitions = options.getMaterial().storageBuffers ?? {}; resetRuntimeMaps(); resetRenderPayloadMaps(); activeMaterialSignature = materialState.signature; }; const resolveActiveMaterial = () => { return resolveMaterial(options.getMaterial()); }; const setUniform = (name, value) => { if (!uniformKeySet.has(name)) throw new Error(`Unknown uniform "${name}". Declare it in material.uniforms first.`); const expectedType = uniformTypes.get(name); if (!expectedType) throw new Error(`Unknown uniform type for "${name}"`); assertUniformValueForType(expectedType, value); runtimeUniforms[name] = value; }; const setTexture = (name, value) => { if (!textureKeySet.has(name)) throw new Error(`Unknown texture "${name}". Declare it in material.textures first.`); runtimeTextures[name] = value; }; const writeStorageBuffer = (name, data, writeOptions) => { if (!storageBufferKeySet.has(name)) throw new Error(`Unknown storage buffer "${name}". Declare it in material.storageBuffers first.`); const definition = storageBufferDefinitions[name]; if (!definition) throw new Error(`Missing definition for storage buffer "${name}".`); const offset = writeOptions?.offset ?? 0; if (offset < 0 || offset + data.byteLength > definition.size) throw new Error(`Storage buffer "${name}" write out of bounds: offset=${offset}, dataSize=${data.byteLength}, bufferSize=${definition.size}.`); pendingStorageWrites.push({ name, data, offset }); }; const readStorageBuffer = (name) => { if (!storageBufferKeySet.has(name)) throw new Error(`Unknown storage buffer "${name}". Declare it in material.storageBuffers first.`); if (!renderer) return Promise.reject(/* @__PURE__ */ new Error(`Cannot read storage buffer "${name}": renderer not initialized.`)); const gpuBuffer = renderer.getStorageBuffer?.(name); if (!gpuBuffer) return Promise.reject(/* @__PURE__ */ new Error(`Storage buffer "${name}" not allocated on GPU.`)); const device = renderer.getDevice?.(); if (!device) return Promise.reject(/* @__PURE__ */ new Error("Cannot read storage buffer: GPU device unavailable.")); const definition = storageBufferDefinitions[name]; if (!definition) return Promise.reject(/* @__PURE__ */ new Error(`Missing definition for storage buffer "${name}".`)); const stagingBuffer = device.createBuffer({ size: definition.size, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }); const commandEncoder = device.createCommandEncoder(); commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, definition.size); device.queue.submit([commandEncoder.finish()]); return stagingBuffer.mapAsync(GPUMapMode.READ).then(() => { try { return stagingBuffer.getMappedRange().slice(0); } finally { stagingBuffer.unmap(); stagingBuffer.destroy(); } }, (error) => { stagingBuffer.destroy(); throw error; }); }; const renderFrame = (timestamp) => { frameId = null; if (isDisposed) return; lastFrameTimestampMs = timestamp; syncErrorHistory(); let materialState; try { materialState = resolveActiveMaterial(); } catch (error) { setError(error, "initialization", timestamp); scheduleFrame(); return; } shouldContinueAfterFrame = false; const color = options.getColor?.(); const rendererSignature = buildRendererPipelineSignature({ materialSignature: materialState.signature, ...color !== void 0 ? { color } : {} }); syncMaterialRuntimeState(materialState); if (failedRendererSignature && failedRendererSignature !== rendererSignature) { failedRendererSignature = null; failedRendererAttempts = 0; nextRendererRetryAt = 0; } if (!renderer || activeRendererSignature !== rendererSignature) { if (failedRendererSignature === rendererSignature && performance.now() < nextRendererRetryAt) { scheduleFrame(); return; } if (!rendererRebuildPromise) rendererRebuildPromise = (async () => { try { const nextRenderer = await createRenderer({ canvas: canvasElement, fragmentWgsl: materialState.fragmentWgsl, fragmentLineMap: materialState.fragmentLineMap, fragmentSource: materialState.fragmentSource, includeSources: materialState.includeSources, defineBlockSource: materialState.defineBlockSource, materialSource: materialState.source, materialSignature: materialState.signature, uniformLayout: materialState.uniformLayout, textureKeys: materialState.textureKeys, textureDefinitions: materialState.textures, storageBufferKeys: materialState.storageBufferKeys, storageBufferDefinitions, storageTextureKeys: materialState.storageTextureKeys, getRenderTargets: options.getRenderTargets, getPasses: options.getPasses, ...color !== void 0 ? { color } : {}, getClearColor: options.getClearColor, getDpr: () => options.dpr.current, adapterOptions: options.getAdapterOptions(), deviceDescriptor: options.getDeviceDescriptor(), requestRender: scheduleFrame }); if (isDisposed) { nextRenderer.destroy(); return; } renderer?.destroy(); renderer = nextRenderer; activeRendererSignature = rendererSignature; failedRendererSignature = null; failedRendererAttempts = 0; nextRendererRetryAt = 0; maybeClearError(performance.now()); } catch (error) { failedRendererSignature = rendererSignature; failedRendererAttempts += 1; const retryDelayMs = getRendererRetryDelayMs(failedRendererAttempts); nextRendererRetryAt = performance.now() + retryDelayMs; setError(error, "initialization"); } finally { rendererRebuildPromise = null; scheduleFrame(); } })(); return; } const time = timestamp / 1e3; const rawDelta = Math.max(0, time - previousTime); const delta = Math.min(rawDelta, options.maxDelta.current); previousTime = time; let width; let height; if (observedCssWidth >= 0) { width = observedCssWidth; height = observedCssHeight; } else { const rect = canvasElement.getBoundingClientRect(); width = Math.max(0, Math.floor(rect.width)); height = Math.max(0, Math.floor(rect.height)); } if (width !== currentCssWidth || height !== currentCssHeight) { currentCssWidth = width; currentCssHeight = height; size.set({ width, height }); } try { registry.run({ time, delta, setUniform, setTexture, writeStorageBuffer, readStorageBuffer, invalidate, advance, renderMode: registry.getRenderMode(), autoRender: registry.getAutoRender(), canvas: canvasElement }); const shouldRenderFrame = registry.shouldRender(); shouldContinueAfterFrame = registry.getRenderMode() === "always" || registry.getRenderMode() === "on-demand" && shouldRenderFrame; if (shouldRenderFrame) { for (const key of uniformKeys) { const runtimeValue = runtimeUniforms[key]; renderUniforms[key] = runtimeValue === void 0 ? activeUniforms[key] : runtimeValue; } for (const key of textureKeys) { const runtimeValue = runtimeTextures[key]; renderTextures[key] = runtimeValue === void 0 ? activeTextures[key]?.source ?? null : runtimeValue; } canvasSize.width = width; canvasSize.height = height; renderer.render({ time, delta, renderMode: registry.getRenderMode(), uniforms: renderUniforms, textures: renderTextures, canvasSize, pendingStorageWrites: pendingStorageWrites.length > 0 ? pendingStorageWrites : void 0 }); if (pendingStorageWrites.length > 0) pendingStorageWrites.length = 0; } else if (pendingStorageWrites.length > 0) { renderer.flushStorageWrites(pendingStorageWrites); pendingStorageWrites.length = 0; } maybeClearError(timestamp); } catch (error) { setError(error, "render", timestamp); if (renderer && shouldRecreateRendererAfterError(error)) { renderer.destroy(); renderer = null; activeRendererSignature = ""; failedRendererSignature = null; failedRendererAttempts = 0; nextRendererRetryAt = 0; shouldContinueAfterFrame = true; } } finally { registry.endFrame(); } if (shouldContinueAfterFrame) scheduleFrame(); }; (async () => { try { syncMaterialRuntimeState(resolveActiveMaterial()); activeRendererSignature = ""; scheduleFrame(); } catch (error) { setError(error, "initialization"); scheduleFrame(); } })(); return { requestFrame, invalidate, advance, destroy: () => { isDisposed = true; resizeObserver?.disconnect(); resizeObserver = null; if (frameId !== null) { cancelAnimationFrame(frameId); frameId = null; } renderer?.destroy(); registry.clear(); } }; } //#endregion export { createMotionGPURuntimeLoop }; //# sourceMappingURL=runtime-loop.js.map