Files
micarrsim/packages/webgpu-kernels/device.js
T
2026-08-28 11:26:19 +09:00

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;
}