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