UNPKG

@motion-core/motion-gpu

Version:

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

295 lines (294 loc) 13.4 kB
import { getShaderCompilationDiagnostics } from "./error-diagnostics.js"; import { formatShaderSourceLocation } from "./shader.js"; //#region src/lib/core/error-report.ts /** * Splits multi-line values into trimmed non-empty lines. */ function splitLines(value) { return value.split("\n").map((line) => line.trim()).filter((line) => line.length > 0); } function toDisplayName(path) { const chunks = (path.split(/[?#]/)[0] ?? path).split(/[\\/]/); const last = chunks[chunks.length - 1]; return last && last.length > 0 ? last : path; } function toSnippet(source, line, radius = 3) { const lines = source.replace(/\r\n?/g, "\n").split("\n"); if (lines.length === 0) return []; const targetLine = Math.min(Math.max(1, line), lines.length); const start = Math.max(1, targetLine - radius); const end = Math.min(lines.length, targetLine + radius); const snippet = []; for (let index = start; index <= end; index += 1) snippet.push({ number: index, code: lines[index - 1] ?? "", highlight: index === targetLine }); return snippet; } function buildSourceFromDiagnostics(error) { const diagnostics = getShaderCompilationDiagnostics(error); if (!diagnostics || diagnostics.diagnostics.length === 0) return null; const primary = diagnostics.diagnostics.find((entry) => entry.sourceLocation !== null); if (!primary?.sourceLocation) return null; const location = primary.sourceLocation; const column = primary.linePos && primary.linePos > 0 ? primary.linePos : void 0; if (location.kind === "fragment") { const component = diagnostics.materialSource?.component ?? (diagnostics.materialSource?.file ? toDisplayName(diagnostics.materialSource.file) : "User shader fragment"); return { component, location: `${component} (${formatShaderSourceLocation(location) ?? `fragment line ${location.line}`})`, line: location.line, ...column !== void 0 ? { column } : {}, snippet: toSnippet(diagnostics.fragmentSource, location.line) }; } if (location.kind === "include") { const includeName = location.include ?? "unknown"; const includeSource = diagnostics.includeSources[includeName] ?? ""; const component = `#include <${includeName}>`; return { component, location: `${component} (${formatShaderSourceLocation(location) ?? `include <${includeName}>`})`, line: location.line, ...column !== void 0 ? { column } : {}, snippet: toSnippet(includeSource, location.line) }; } if (location.kind === "compute") { const computeSource = diagnostics.computeSource ?? diagnostics.fragmentSource; const component = "Compute shader"; return { component, location: `${component} (${formatShaderSourceLocation(location) ?? `compute line ${location.line}`})`, line: location.line, ...column !== void 0 ? { column } : {}, snippet: toSnippet(computeSource, location.line) }; } const defineName = location.define ?? "unknown"; const defineLine = Math.max(1, location.line); const component = `#define ${defineName}`; return { component, location: `${component} (${formatShaderSourceLocation(location) ?? `define "${defineName}" line ${defineLine}`})`, line: defineLine, ...column !== void 0 ? { column } : {}, snippet: toSnippet(diagnostics.defineBlockSource ?? "", defineLine, 2) }; } function formatDiagnosticMessage(entry) { const labels = [formatShaderSourceLocation(entry.sourceLocation), entry.generatedLine > 0 ? `generated WGSL line ${entry.generatedLine}` : null].filter((value) => Boolean(value)); if (labels.length === 0) return entry.message; return `[${labels.join(" | ")}] ${entry.message}`; } /** * Maps known WebGPU/WGSL error patterns to a user-facing title and hint. */ function classifyErrorMessage(message) { if (message.includes("WebGPU is not available in this browser")) return { code: "WEBGPU_UNAVAILABLE", severity: "fatal", recoverable: false, title: "WebGPU unavailable", hint: "Use a browser with WebGPU enabled (latest Chrome/Edge/Safari TP) and secure context." }; if (message.includes("Unable to acquire WebGPU adapter")) return { code: "WEBGPU_ADAPTER_UNAVAILABLE", severity: "fatal", recoverable: false, title: "WebGPU adapter unavailable", hint: "GPU adapter request failed. Check browser permissions, flags and device support." }; if (message.includes("Canvas does not support webgpu context")) return { code: "WEBGPU_CONTEXT_UNAVAILABLE", severity: "error", recoverable: true, title: "Canvas cannot create WebGPU context", hint: "Make sure this canvas is attached to DOM and not using an unsupported context option." }; if (message.includes("WGSL compilation failed")) return { code: "WGSL_COMPILATION_FAILED", severity: "error", recoverable: true, title: "WGSL compilation failed", hint: "Check WGSL line numbers below and verify struct/binding/function signatures." }; if (message.includes("Invalid include directive in fragment shader.") || message.includes("Unknown include \"") || message.includes("Circular include detected for \"") || message.includes("Invalid define value for \"") || message.includes("Invalid include \"")) return { code: "MATERIAL_PREPROCESS_FAILED", severity: "error", recoverable: true, title: "Material preprocess failed", hint: "Validate #include keys, define values and include expansion order before retrying." }; if (message.includes("Compute shader compilation failed")) return { code: "COMPUTE_COMPILATION_FAILED", severity: "error", recoverable: true, title: "Compute shader compilation failed", hint: "Check WGSL compute shader sources below and verify storage bindings." }; if (message.includes("Compute shader must declare `@compute @workgroup_size(...) fn compute(...)`.") || message.includes("Compute shader must include a `@builtin(global_invocation_id)` parameter.") || message.includes("Could not extract @workgroup_size from compute shader source.") || message.includes("@workgroup_size dimensions must be integers in range") || message.includes("Unsupported storage buffer access mode \"")) return { code: "COMPUTE_CONTRACT_INVALID", severity: "error", recoverable: true, title: "Compute contract is invalid", hint: "Ensure compute shader contract (@compute, @workgroup_size, global_invocation_id, storage access) is valid." }; if (message.includes("WebGPU device lost") || message.includes("Device Lost")) return { code: "WEBGPU_DEVICE_LOST", severity: "fatal", recoverable: false, title: "WebGPU device lost", hint: "GPU device/context was lost. Recreate the renderer and check OS/GPU stability." }; if (message.includes("Dispatch workgroup count") && message.includes("max compute workgroups per dimension")) return { code: "WEBGPU_UNCAPTURED_ERROR", severity: "error", recoverable: true, title: "Compute dispatch exceeds device limit", hint: "Reduce dispatch counts or split compute work into multiple dispatches/chunks." }; if (message.includes("maximum storage buffer binding size") || message.includes("maxStorageBufferBindingSize")) return { code: "WEBGPU_UNCAPTURED_ERROR", severity: "error", recoverable: true, title: "Storage buffer exceeds binding limit", hint: "Keep each storage buffer binding below adapter limits or shard data across multiple buffers." }; if (message.includes("WebGPU uncaptured error")) return { code: "WEBGPU_UNCAPTURED_ERROR", severity: "error", recoverable: true, title: "WebGPU uncaptured error", hint: "A GPU command failed asynchronously. Review details and validate resource/state usage." }; if (message.includes("CreateBindGroup") || message.includes("bind group layout")) return { code: "BIND_GROUP_MISMATCH", severity: "error", recoverable: true, title: "Bind group mismatch", hint: "Bindings in shader and runtime resources are out of sync. Verify uniforms/textures layout." }; if (message.includes("Storage buffer \"") && message.includes("write out of bounds:")) return { code: "STORAGE_BUFFER_OUT_OF_BOUNDS", severity: "error", recoverable: true, title: "Storage buffer write out of bounds", hint: "Ensure offset + write byte length does not exceed declared storage buffer size." }; if (message.includes("Cannot read storage buffer \"") || message.includes("Cannot read storage buffer: GPU device unavailable.") || message.includes("not allocated on GPU.")) return { code: "STORAGE_BUFFER_READ_FAILED", severity: "error", recoverable: true, title: "Storage buffer read failed", hint: "Readbacks require an initialized renderer, allocated GPU buffer and active device." }; if (message.includes("Unknown uniform \"") || message.includes("Unknown uniform type for \"") || message.includes("Unknown texture \"") || message.includes("Unknown storage buffer \"") || message.includes("Missing definition for storage buffer \"") || message.includes("Missing texture definition for \"") || message.includes("Storage buffer \"") && message.includes("\" not allocated.") || message.includes("Storage texture \"") && message.includes("\" not allocated.")) return { code: "RUNTIME_RESOURCE_MISSING", severity: "error", recoverable: true, title: "Runtime resource binding failed", hint: "Check material declarations and runtime keys for uniforms, textures and storage resources." }; if (message.includes("Uniform ") && message.includes(" value must")) return { code: "UNIFORM_VALUE_INVALID", severity: "error", recoverable: true, title: "Uniform value is invalid", hint: "Provide finite values with tuple/matrix sizes matching the uniform type." }; if (message.includes("Render pass #") || message.includes("Render graph references unknown runtime target")) return { code: "RENDER_GRAPH_INVALID", severity: "error", recoverable: true, title: "Render graph configuration is invalid", hint: "Verify pass inputs/outputs, declared render targets and execution order." }; if (message.includes("PingPongComputePass must provide a target texture key.")) return { code: "PINGPONG_CONFIGURATION_INVALID", severity: "error", recoverable: true, title: "Ping-pong compute pass is misconfigured", hint: "Configure a valid target texture key for PingPongComputePass." }; if (message.includes("Destination texture needs to have CopyDst")) return { code: "TEXTURE_USAGE_INVALID", severity: "error", recoverable: true, title: "Invalid texture usage flags", hint: "Texture used as upload destination must include CopyDst (and often RenderAttachment)." }; if (message.includes("Texture request failed")) return { code: "TEXTURE_REQUEST_FAILED", severity: "error", recoverable: true, title: "Texture request failed", hint: "Verify texture URL, CORS policy and response status before retrying." }; if (message.includes("createImageBitmap is not available in this runtime")) return { code: "TEXTURE_DECODE_UNAVAILABLE", severity: "fatal", recoverable: false, title: "Texture decode unavailable", hint: "Runtime lacks createImageBitmap support. Use a browser/runtime with image bitmap decoding." }; if (message.toLowerCase().includes("texture request was aborted")) return { code: "TEXTURE_REQUEST_ABORTED", severity: "error", recoverable: true, title: "Texture request aborted", hint: "Texture load was cancelled. Retry the request when source inputs stabilize." }; return { code: "MOTIONGPU_RUNTIME_ERROR", severity: "error", recoverable: true, title: "MotionGPU render error", hint: "Review technical details below. If issue persists, isolate shader/uniform/texture changes." }; } /** * Converts unknown errors to a consistent, display-ready error report. * * @param error - Unknown thrown value. * @param phase - Phase during which error occurred. * @returns Normalized error report. */ function toMotionGPUErrorReport(error, phase) { const shaderDiagnostics = getShaderCompilationDiagnostics(error); const rawMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown FragCanvas error"; const rawLines = splitLines(rawMessage); const defaultMessage = rawLines[0] ?? rawMessage; const defaultDetails = rawLines.slice(1); const source = buildSourceFromDiagnostics(error); const context = shaderDiagnostics?.runtimeContext ?? null; const message = shaderDiagnostics && shaderDiagnostics.diagnostics[0] ? formatDiagnosticMessage(shaderDiagnostics.diagnostics[0]) : defaultMessage; const details = shaderDiagnostics ? shaderDiagnostics.diagnostics.slice(1).map((entry) => formatDiagnosticMessage(entry)) : defaultDetails; const stack = error instanceof Error && error.stack ? splitLines(error.stack).filter((line) => line !== message) : []; let classification = classifyErrorMessage(rawMessage); if (shaderDiagnostics?.shaderStage === "compute" && classification.code === "WGSL_COMPILATION_FAILED") classification = { code: "COMPUTE_COMPILATION_FAILED", severity: "error", recoverable: true, title: "Compute shader compilation failed", hint: "Check WGSL compute shader sources below and verify storage bindings." }; return { code: classification.code, severity: classification.severity, recoverable: classification.recoverable, title: classification.title, message, hint: classification.hint, details, stack, rawMessage, phase, source, context }; } //#endregion export { toMotionGPUErrorReport }; //# sourceMappingURL=error-report.js.map