171 lines
5.5 KiB
JavaScript
171 lines
5.5 KiB
JavaScript
/** 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;
|
|
}
|