core/renderers/shader.renderer.ts
core/renderers/shader.renderer.ts is a file in Bane's Lab Social Share. 92 lines of code and 28 definitions.
import { NEEDS_WEBGPU, SHADER_FAILED, SUBJECT_SEPARATOR } from "#configuration/strings/card.strings";
import type { ShaderFrame, ShaderPass } from "#types/stage.types";
import { UNIFORM_SLOTS } from "#configuration/constants/card.constants";
import prelude from "../shaders/stage.shader.wgsl?raw";
const HEADER_FLOATS = 4;
const FLOAT_BYTES = 4;
const VERTICES = 3;
const VERTEX_ENTRY = "vertexMain";
const FRAGMENT_ENTRY = "fragmentMain";
const CONTEXT_KIND = "webgpu";
const ERROR_KIND = "error";
const VALIDATION_SCOPE = "validation";
const MESSAGE_SEPARATOR = "; ";
const POSITION_SEPARATOR = ":";
const describe = function describe(message: GPUCompilationMessage): string {
return `${String(message.lineNum)}${POSITION_SEPARATOR}${String(message.linePos)} ${message.message}`;
};
const uniformData = function uniformData(frame: ShaderFrame): Float32Array<ArrayBuffer> {
const data = new Float32Array(HEADER_FLOATS + UNIFORM_SLOTS);
data.set([frame.width, frame.height, frame.time, frame.progress]);
data.set(frame.values.slice(0, UNIFORM_SLOTS), HEADER_FLOATS);
return data;
};
const deviceOf = async function deviceOf(): Promise<GPUDevice> {
if (!("gpu" in navigator)) {
throw new Error(NEEDS_WEBGPU);
}
const adapter = await navigator.gpu.requestAdapter();
if (adapter === null) {
throw new Error(NEEDS_WEBGPU);
}
return adapter.requestDevice();
};
export const createShaderPass = async function createShaderPass(
canvas: HTMLCanvasElement,
source: string,
): Promise<ShaderPass> {
const device = await deviceOf();
const context = canvas.getContext(CONTEXT_KIND);
if (context === null) {
throw new Error(NEEDS_WEBGPU);
}
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ alphaMode: "premultiplied", device, format });
const module = device.createShaderModule({ code: prelude + source });
const compiled = await module.getCompilationInfo();
const errors = compiled.messages.filter((message) => message.type === ERROR_KIND);
if (errors.length > 0) {
throw new Error(SHADER_FAILED + SUBJECT_SEPARATOR + errors.map(describe).join(MESSAGE_SEPARATOR));
}
device.pushErrorScope(VALIDATION_SCOPE);
const pipeline = device.createRenderPipeline({
fragment: { entryPoint: FRAGMENT_ENTRY, module, targets: [{ format }] },
layout: "auto",
primitive: { topology: "triangle-list" },
vertex: { entryPoint: VERTEX_ENTRY, module },
});
const failed = await device.popErrorScope();
if (failed !== null) {
throw new Error(SHADER_FAILED + SUBJECT_SEPARATOR + failed.message);
}
const buffer = device.createBuffer({
size: (HEADER_FLOATS + UNIFORM_SLOTS) * FLOAT_BYTES,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindings = device.createBindGroup({
entries: [{ binding: 0, resource: { buffer } }],
layout: pipeline.getBindGroupLayout(0),
});
return {
draw: async (frame: ShaderFrame): Promise<void> => {
device.queue.writeBuffer(buffer, 0, uniformData(frame));
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
clearValue: { a: 0, b: 0, g: 0, r: 0 },
loadOp: "clear",
storeOp: "store",
view: context.getCurrentTexture().createView(),
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindings);
pass.draw(VERTICES);
pass.end();
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
},
};
};