/** Errors in this module are deliberately typed so `auto` can fall back to CPU * without hiding invalid experiment input or an explicit user cancellation. */ export class WebGpuError extends Error { constructor(message, options = {}) { super(message, options.cause ? { cause: options.cause } : undefined); this.name = new.target.name; this.code = options.code ?? "WEBGPU_ERROR"; } } export class WebGpuUnavailableError extends WebGpuError { constructor(message = "이 브라우저에서 WebGPU를 사용할 수 없습니다.", options = {}) { super(message, { ...options, code: "WEBGPU_UNAVAILABLE" }); } } export class WebGpuLimitError extends WebGpuError { constructor(message, options = {}) { super(message, { ...options, code: "WEBGPU_LIMIT" }); } } export class WebGpuExecutionError extends WebGpuError { constructor(message, options = {}) { super(message, { ...options, code: "WEBGPU_EXECUTION" }); } } export function isRecoverableWebGpuError(error) { return error instanceof WebGpuError; } function describeAdapter(adapter) { const info = adapter?.info; if (!info) return null; return { vendor: info.vendor ?? "", architecture: info.architecture ?? "", device: info.device ?? "", description: info.description ?? "", }; } function normalizeRequestError(error, message) { if (error instanceof WebGpuError) return error; return new WebGpuUnavailableError(message, { cause: error }); } /** * Request one short-lived WebGPU context. A new call always requests a new * adapter so a device-loss retry never reuses an invalid adapter/device pair. */ export async function createWebGpuContext(options = {}) { const gpu = options.gpu ?? globalThis.navigator?.gpu; if (!gpu) throw new WebGpuUnavailableError(); let adapter; try { adapter = await gpu.requestAdapter({ powerPreference: options.powerPreference ?? "high-performance" }); } catch (error) { throw normalizeRequestError(error, "WebGPU 어댑터 요청에 실패했습니다."); } if (!adapter) throw new WebGpuUnavailableError("사용 가능한 WebGPU 어댑터가 없습니다."); let device; try { device = await adapter.requestDevice(); } catch (error) { throw normalizeRequestError(error, "WebGPU 장치 요청에 실패했습니다."); } const state = { lostInfo: null, uncapturedError: null, closedByApplication: false, }; const onUncapturedError = (event) => { state.uncapturedError = event.error ?? new Error("알 수 없는 WebGPU 오류"); }; device.addEventListener?.("uncapturederror", onUncapturedError); const lostPromise = device.lost.then((info) => { state.lostInfo = info; return info; }); return { adapter, device, adapterInfo: describeAdapter(adapter), state, lostPromise, onUncapturedError, }; } export function assertWebGpuContextHealthy(context) { const { state } = context; if (state.uncapturedError) { throw new WebGpuExecutionError(`처리되지 않은 WebGPU 오류: ${state.uncapturedError.message}`, { cause: state.uncapturedError, }); } if (state.lostInfo && !(state.closedByApplication && state.lostInfo.reason === "destroyed")) { throw new WebGpuExecutionError(`WebGPU 장치가 손실되었습니다: ${state.lostInfo.message || "원인 미상"}`); } } export function closeWebGpuContext(context) { if (!context?.device) return; context.state.closedByApplication = true; context.device.removeEventListener?.("uncapturederror", context.onUncapturedError); context.device.destroy(); } export async function createCheckedComputePipeline(device, { code, entryPoint, label }) { let shaderModule; try { shaderModule = device.createShaderModule({ code, label: `${label} shader` }); const compilationInfo = await shaderModule.getCompilationInfo(); const compilationErrors = compilationInfo.messages.filter((message) => message.type === "error"); if (compilationErrors.length > 0) { const details = compilationErrors .slice(0, 4) .map((message) => `${message.lineNum}:${message.linePos} ${message.message}`) .join("\n"); throw new WebGpuExecutionError(`WGSL 컴파일에 실패했습니다.\n${details}`); } return await device.createComputePipelineAsync({ label, layout: "auto", compute: { module: shaderModule, entryPoint }, }); } catch (error) { if (error instanceof WebGpuError) throw error; throw new WebGpuExecutionError("WebGPU compute pipeline 생성에 실패했습니다.", { cause: error }); } } /** Capture validation/OOM/internal errors around predictable GPU work. */ export async function withWebGpuErrorScopes(device, callback) { const filters = ["internal", "out-of-memory", "validation"]; for (const filter of filters) device.pushErrorScope(filter); let result; let callbackError; try { result = await callback(); } catch (error) { callbackError = error; } const captured = []; for (let index = filters.length - 1; index >= 0; index -= 1) { try { const error = await device.popErrorScope(); if (error) captured.push(error); } catch (error) { captured.push(error); } } if (callbackError) { if (callbackError instanceof WebGpuError) throw callbackError; throw new WebGpuExecutionError("WebGPU 명령 실행에 실패했습니다.", { cause: callbackError }); } if (captured.length > 0) { throw new WebGpuExecutionError(`WebGPU 검증 오류: ${captured.map((error) => error.message).join(" / ")}`, { cause: captured[0], }); } return result; }