@motion-core/motion-gpu
Version:
Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.
174 lines (158 loc) • 4.52 kB
text/typescript
import type {
RenderPass,
RenderPassContext,
RenderPassFlags,
RenderPassInputSlot,
RenderPassOutputSlot
} from '../core/types.js';
export interface FullscreenPassOptions extends RenderPassFlags {
enabled?: boolean;
needsSwap?: boolean;
input?: RenderPassInputSlot;
output?: RenderPassOutputSlot;
filter?: GPUFilterMode;
}
/**
* Shared base for fullscreen texture sampling passes.
*/
export abstract class FullscreenPass implements RenderPass {
enabled: boolean;
needsSwap: boolean;
input: RenderPassInputSlot;
output: RenderPassOutputSlot;
clear: boolean;
clearColor: [number, number, number, number];
preserve: boolean;
private readonly filter: GPUFilterMode;
private device: GPUDevice | null = null;
private sampler: GPUSampler | null = null;
private bindGroupLayout: GPUBindGroupLayout | null = null;
private shaderModule: GPUShaderModule | null = null;
private readonly pipelineByFormat = new Map<GPUTextureFormat, GPURenderPipeline>();
private bindGroupByView = new WeakMap<GPUTextureView, GPUBindGroup>();
protected constructor(options: FullscreenPassOptions = {}) {
this.enabled = options.enabled ?? true;
this.needsSwap = options.needsSwap ?? true;
this.input = options.input ?? 'source';
this.output = options.output ?? (this.needsSwap ? 'target' : 'source');
this.clear = options.clear ?? false;
this.clearColor = options.clearColor ?? [0, 0, 0, 1];
this.preserve = options.preserve ?? true;
this.filter = options.filter ?? 'linear';
}
protected abstract getProgram(): string;
protected abstract getVertexEntryPoint(): string;
protected abstract getFragmentEntryPoint(): string;
protected invalidateFullscreenCache(): void {
this.shaderModule = null;
this.pipelineByFormat.clear();
this.bindGroupByView = new WeakMap();
}
private ensureResources(
device: GPUDevice,
format: GPUTextureFormat
): {
sampler: GPUSampler;
bindGroupLayout: GPUBindGroupLayout;
pipeline: GPURenderPipeline;
} {
if (this.device !== device) {
this.device = device;
this.sampler = null;
this.bindGroupLayout = null;
this.invalidateFullscreenCache();
}
if (!this.sampler) {
this.sampler = device.createSampler({
magFilter: this.filter,
minFilter: this.filter,
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge'
});
}
if (!this.bindGroupLayout) {
this.bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
sampler: { type: 'filtering' }
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: 'float',
viewDimension: '2d',
multisampled: false
}
}
]
});
}
if (!this.shaderModule) {
this.shaderModule = device.createShaderModule({ code: this.getProgram() });
}
let pipeline = this.pipelineByFormat.get(format);
if (!pipeline) {
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [this.bindGroupLayout]
});
pipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: this.shaderModule,
entryPoint: this.getVertexEntryPoint()
},
fragment: {
module: this.shaderModule,
entryPoint: this.getFragmentEntryPoint(),
targets: [{ format }]
},
primitive: { topology: 'triangle-list' }
});
this.pipelineByFormat.set(format, pipeline);
}
return {
sampler: this.sampler,
bindGroupLayout: this.bindGroupLayout,
pipeline
};
}
setSize(width: number, height: number): void {
void width;
void height;
}
protected renderFullscreen(context: RenderPassContext): void {
const { sampler, bindGroupLayout, pipeline } = this.ensureResources(
context.device,
context.output.format
);
const inputView = context.input.view;
let bindGroup = this.bindGroupByView.get(inputView);
if (!bindGroup) {
bindGroup = context.device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: inputView }
]
});
this.bindGroupByView.set(inputView, bindGroup);
}
const pass = context.beginRenderPass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
}
render(context: RenderPassContext): void {
this.renderFullscreen(context);
}
dispose(): void {
this.device = null;
this.sampler = null;
this.bindGroupLayout = null;
this.invalidateFullscreenCache();
}
}