gpu = {
const CHECKERBOARD_BACKGROUND_CSS = `
background-image:
linear-gradient(45deg, #eee 25%, transparent 25%),
linear-gradient(-45deg, #eee 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #eee 75%),
linear-gradient(-45deg, transparent 75%, #eee 75%);
background-size: 32px 32px;
background-position: 0 0, 0 16px, 16px -16px, -16px 0px;
`;
const context = (width = 512, height = 512, contextType = 'webgpu') => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.style.cssText = CHECKERBOARD_BACKGROUND_CSS;
const context = canvas.getContext(contextType);
return context;
};
const format = () => navigator.gpu.getPreferredCanvasFormat();
const device = async () => {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const format = navigator.gpu.getPreferredCanvasFormat();
return { adapter, device, format };
};
const init = async (width = 512, height = 512) => {
const ctx = context(width, height, 'webgpu') ;
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const format = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format, alphaMode: 'premultiplied' });
return { context: ctx, adapter, device, format };
};
const sampler = (device /*: GPUDevice*/, options = {}) => {
return device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
...options,
});
};
// interface BaseTextureOptions {
// format?: GPUTextureFormat;
// usage: number;
// label?: string;
// }
// interface ImageTextureOptions {
// width: number;
// height: number;
// flipY?: boolean;
// }
// Defaults to storage texture
const createTexture = (
device /*: GPUDevice */,
{
width,
height,
format = 'rgba8unorm',
usage = GPUTextureUsage.COPY_DST |
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.TEXTURE_BINDING,
label,
} /*: BaseTextureOptions & ImageTextureOptions,*/
) /*: GPUTexture*/ => {
return device.createTexture({
...(label && { label }),
format,
size: [width, height],
usage,
});
};
const canvasTexture = (
device /*: GPUDevice*/,
canvas /*: HTMLCanvasElement*/,
{
format = 'rgba8unorm',
flipY = true,
usage = GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
label,
} /*: Partial<BaseTextureOptions & ImageTextureOptions>*/ = {},
) /*: GPUTexture*/ => {
const texture = device.createTexture({
...(label && { label }),
format,
size: [canvas.width, canvas.height],
usage,
});
device.queue.copyExternalImageToTexture(
{ source: canvas, flipY },
{ texture },
[canvas.width, canvas.height],
);
return texture;
};
const DefaultUsage = {
STORAGE: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
UNIFORM: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
};
// interface WrappedBuffer {
// write: (next?: any) => void;
// buffer: GPUBuffer;
// }
const buffer = (
device /*: GPUDevice */,
values /*: any */,
{
usage,
offset = 0,
skipInitialWrite = false,
} /*: { usage: BaseTextureOptions['usage']; offset?: number } */,
) /*: WrappedBuffer*/ => {
const buffer = device.createBuffer({
size: values.byteLength,
usage,
});
if (!skipInitialWrite) {
device.queue.writeBuffer(buffer, offset, values);
}
return {
buffer,
write: (next = values) => device.queue.writeBuffer(buffer, offset, next),
};
};
const storageBuffer = (
device /*: GPUDevice*/,
values /*: any*/,
options /*: Partial<BaseTextureOptions>*/ = {},
) => {
return buffer(device, values, {
...options,
usage: options.usage ?? DefaultUsage.STORAGE,
});
};
const uniformBuffer = (
device /*: GPUDevice*/,
values /*: any*/,
options /*: Partial<BaseTextureOptions>*/ = {},
) => {
return buffer(device, values, {
...options,
usage: options.usage ?? DefaultUsage.UNIFORM,
});
};
const contextDimensions = context => ({ width: context.canvas.width, height: context.canvas.height });
// Shaders
// type ShaderInjections = { [key: string]: string | number };
const defaultRegex = (key, value) => '\\$\\{' + key + '\\}';
// TODO compose shaders using a JS API
const injectShader = (
shader /* : string*/,
injections/* : ShaderInjections*/ = {},
regexStr = defaultRegex
) => {
return Object.entries(injections).reduce((shader, [k, v]) => {
const regex = new RegExp(regexStr(k, v), 'g');
return shader.replace(regex, String(v));
}, shader);
};
const prependShader = (shader, header) => header + '\n\n' + shader;
const bindGroupEntry = (
binding /*: number*/,
resource /*: GPUSampler | GPUTexture | GPUBuffer | WrappedBuffer*/,
) => {
let r;
if (resource instanceof GPUSampler) r = resource;
else if (resource instanceof GPUTexture) r = resource.createView();
else if (resource instanceof GPUBuffer) r = { buffer: resource };
// specific to the util above
else if (resource.hasOwnProperty('buffer')) r = { buffer: resource.buffer };
else r = resource;
return { binding, resource: r };
};
const mapBuffer = (device, arr /* : TypedArray*/, bufferOptions = {}) => {
const Constructor = arr.constructor; /* as TypedArrayConstructor */
const buffer = device.createBuffer({
size: arr.byteLength,
mappedAtCreation: true,
...bufferOptions,
});
new Constructor(buffer.getMappedRange()).set(arr);
buffer.unmap();
return buffer;
};
// via https://webgpufundamentals.org/webgpu/lessons/webgpu-transparency.html
const Blending /*: { [name: string]: GPUBlendState }*/ = {
SourceOver: {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
},
DestinationOver: {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'one',
},
alpha: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'one',
},
},
Additive: {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one',
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one',
},
},
SourceIn: {
color: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'zero',
},
alpha: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'zero',
},
},
DestinationIn: {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'src-alpha',
},
},
SourceOut: {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'zero',
},
alpha: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'zero',
},
},
DestinationOut: {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha',
},
},
SourceAtop: {
color: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha',
},
},
DestinationAtop: {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'src-alpha',
},
alpha: {
operation: 'add',
srcFactor: 'one-minus-dst-alpha',
dstFactor: 'src-alpha',
},
},
};
const gpu = {
device,
context,
init,
format,
sampler,
canvasTexture,
createTexture,
storageBuffer,
uniformBuffer,
contextDimensions,
bindGroupEntry,
mapBuffer,
shader: {
inject: injectShader,
prepend: prependShader,
},
Usage: {
StorageTexture:
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT,
CopyTexture:
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT,
},
Blending,
LinearSampler: Object.freeze({
minFilter: 'linear',
magFilter: 'linear',
mipmapFilter: 'linear',
})
};
return gpu
}