npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

webgpu-utils

v1.9.3

Published

webgpu utilities

Downloads

2,991

Readme

webgpu-utils

Docs

See here

Random useful things for WebGPU

As I do more WebGPU I find I need more and more helpers to make things less tedious. These are the result. I expect I'll add more over time.

Easily set Uniforms (based on your WGSL structs/types)

Example:

import {
  makeShaderDataDefinitions,
  makeStructuredView,
} from 'webgpu-utils';

const code = `
struct MyUniforms {
   color: vec4f,
   brightness: f32,
   kernel: array<f32, 9>,
   projectionMatrix: mat4x4f,
};
@group(0) @binding(0) var<uniform> myUniforms: MyUniforms;
`;

const defs = makeShaderDataDefinitions(code);
const myUniformValues = makeStructuredView(defs.uniforms.myUniforms);

// create the correct sized buffer
const uniformBuffer = device.createBuffer({
  size: myUniformValues.arrayBuffer.byteLength,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

// Set some values via set
myUniformValues.set({
  color: [1, 0, 1, 1],
  brightness: 0.8,
  kernel: [
     1, 0, -1,
     2, 0, -2,
     1, 0, -1,
  ],
});

// Set a value by passing it to a math library
mat4.perspective(
    degToRad(45),
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    20,
    myUniformValues.views.projectionMatrix);

// Upload the data to the GPU
device.queue.writeBuffer(uniformBuffer, 0, myUniformValues.arrayBuffer);

See makeStructuredView for details.

Load an image URL as a texture (with mips)

import { createTextureFromImage } from 'webgpu-utils';

const texture = await createTextureFromImage(device, 'https://someimage.url', {
  mips: true,
  flipY: true,
});

Load a canvas/video/ImageBitmap as a texture (with mips)

import { createTextureFromSource } from 'webgpu-utils';

const texture = createTextureFromSource(device, someCanvasVideoImageBitmap, {
  mips: true,
  flipY: true,
});

Load 6 images as a cubemap (with mips)

import { createTextureFromImage } from 'webgpu-utils';

const texture = await createTextureFromImages(device, [
  'images/yokohama/posx.jpg',
  'images/yokohama/negx.jpg',
  'images/yokohama/posy.jpg',
  'images/yokohama/negy.jpg',
  'images/yokohama/posz.jpg',
  'images/yokohama/negz.jpg',
], {
  mips: true,
});

Load data as a texture

import { createTextureFromSource } from 'webgpu-utils';

const r = [255,   0,   0, 255];
const g = [  0, 255,   0, 255];
const b = [  0,   0, 255, 255];
const y = [255, 255,   0, 255];

// if no width or height is passed, then assumes data is rgba8unorm
// if sqrt(numPixels) is in then makes a square. Otherwise Nx1
const data2x2 = [ r, g, b, y ].flat();
const texture2x2 = createTextureFromSource(device, data2x2, {
  mips: true,
});
const data4x1 = {
  data: [ r, g, b, y ].flat();
  width: 4,
};
const texture4x1 = createTextureFromSource(device, data2x2, {
  mips: true,
});
const singlePixelWhiteTexture = createTextureFromSource(
    device, [255, 255, 255, 255]);
const rg16sint2x2 = [
  1,2  3,4,
  5,6, 7,8,
];
const rg16Texture2x2 = createTextureFromSource(
  device, rg16sint2x2, { format: 'rg16sint' });

All data above can be a TypedArray

const singlePixelRedTexture = createTextureFromSource(
    device, new Uint8Array[255, 0, 0, 255]);

Generate mips on an existing texture

import { numMipLevels, generateMipmap } from 'webgpu-utils';

const size = [8, 8, 1];
const texture = device.createTexture({
  size,
  mipLevelCount: numMipLevels(size);
  format: 'rgba8unorm',
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
});

... do whatever you do to fill out the mip level 0 ...

generateMipmap(device, texture);

Create Buffers and attributes (interleaved)

import { numMipLevels, generateMipmap } from 'webgpu-utils';

const bi = wgh.createBuffersAndAttributesFromArrays(device, {
  position: [1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1],
  normal: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1],
  texcoord: [1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1],
  indices: [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23],
});

const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module,
    entryPoint: 'myVSMain',
    buffers: bi.bufferLayouts,  // <---
  },
  ...
});

// at render time
passEncoder.setVertexBuffer(0, bi.buffers[0]);
passEncoder.setIndexBuffer(bi.indexBuffer, bi.indexFormat);
passEncoder.drawIndexed(bi.numElements);

Create GPUBindGroupLayoutDescriptors from WGSL code

import {
  makeShaderDataDefinitions,
  makeBindGroupLayoutDescriptors,
} from 'webgpu-utils';

const code = `
@group(0) @binding(0) var<uniform> mat: mat4x4f;

struct MyVSOutput {
  @builtin(position) position: vec4f,
  @location(1) texcoord: vec2f,
};

@vertex
fn myVSMain(v: MyVSInput) -> MyVSOutput {
  var vsOut: MyVSOutput;
  vsOut.position = mat * v.position;
  vsOut.texcoord = v.texcoord;
  return vsOut;
}

@group(0) @binding(2) var diffuseSampler: sampler;
@group(0) @binding(3) var diffuseTexture: texture_2d<f32>;

@fragment
fn myFSMain(v: MyVSOutput) -> @location(0) vec4f {
  return textureSample(diffuseTexture, diffuseSampler, v.texcoord);
}
`;

const module = device.createShaderModule({code});
const defs = wgh.makeShaderDataDefinitions(code);

const pipelineDesc = {
  vertex: {
    module,
    entryPoint: 'myVSMain',
    buffers: bufferLayouts,
  },
  fragment: {
    module,
    entryPoint: 'myFSMain',
    targets: [
      {format: presentationFormat},
    ],
  },
};

const descriptors = wgh.makeBindGroupLayoutDescriptors(defs, pipelineDesc);
const group0Layout = device.createBindGroupLayout(descriptors[0]);
const layout = device.createPipelineLayout({
  bindGroupLayouts: [group0Layout],
});
const pipeline = device.createRenderPipeline({
  layout,
  ...pipelineDesc,
});

Examples:

Notes about structured data

The first level of an array of intrinsic types is flattened by default.

Example:

const code = `
@group(0) @binding(0) var<uniform> uni1: array<vec3f, 4>;
@group(0) @binding(1) var<uniform> uni2: array<array<vec3f, 3>, 4>;
`;
const defs = makeShaderDataDefinitions(code);
const uni1 = makeStructuredView(defs.uniforms.uni1);
const uni2 = makeStructuredView(defs.uniforms.uni2);

uni1.set([
  1, 2, 3, 0,  // uni1[0]
  4, 5, 6, 0,  // uni1[1]
  //...
]);

uni2.set([
  [
    1, 2, 3, 0,  // uni2[0][0],
    4, 5, 6, 0,  // uni2[0][1],
  ],
  ,  // uni2[1]
  [
    7, 8, 9, 0,  // uni2[2][0],
    4, 5, 6, 0,  // uni2[2][1],
  ],
]);

The reason it's this way is it's common to make large arrays of f32, u32, vec2f, vec3f, vec4f etc. We wouldn't want every element of an array to have its own typedarray view.

You can configure this per type by calling setIntrinsicsToView. The configuration is global. Given th example above

const code = `
@group(0) @binding(0) var<uniform> uni1: array<vec3f, 4>;
@group(0) @binding(1) var<uniform> uni2: array<array<vec3f, 3>, 4>;
`;
const defs = makeShaderDataDefinitions(code);
setIntrinsicsToView(['vec3f']);
const uni1 = makeStructuredView(defs.uniforms.uni1);

uni1.set([
  [1, 2, 3],  // uni1[0]
  [4, 5, 6],  // uni1[1]
  ...
]);

Or to put it another way, in the default case, uni1.views is a Float32Array(16). In the 2nd case it's an array of 4 Float32Array each 3 elements big

arrays of intrinsics can be set by arrays of arrays

const code = `
@group(0) @binding(0) var<uniform> uni1: array<vec2f, 4>;
`;
const defs = makeShaderDataDefinitions(code);
const uni1 = makeStructuredView(defs.uniforms.uni1);

uni1.set([
  [1, 2],  // uni1[0]
  [3, 4],  // uni1[1]
]);

Currently this requires the length of each subarray to match the length of the intrinsic. The reason being, there is no type data used in uni1.set so there is nothing to tell it that it's a vec2f. In this case, it just advances where it's writing by the length of the source data sub arrays.

for unsized arrays you must pass in your own arrayBuffer

The reason is an unsized array's size is defined to WebGPU by its buffer binding size. That information is provided at runtime so there's no way for webgpu-utils to know the size. The solution is you pass in an ArrayBuffer.

Example:

const code = `
@group(0) @binding(0) var<storage> buf1: array<vec3f>;  // unsized array
`;
const defs = makeShaderDataDefinitions(code);
const buf1 = makeStructuredView(defs.storages.buf1, new ArrayBuffer(4 * 16));

// buf1.views will be a Float32Array representing 4 vec3fs

Note: If you have a complex array element type you can call getSizeOfUnsizedArrayElement to get its size. Example:

const code = `
struct Light {
  intensity: f32,
  direction: vec3f,
};
@group(0) @binding(7) var<storage> lights: array<Light>;
`;
const defs = makeShaderDataDefinitions(code);
const {size} = getSizeOfUnsizedArrayElement(defs.storages.lights);
const numLights = 4;
const buf1 = makeStructuredView(
    defs.storages.lights, new ArrayBuffer(numLights * size));

Similarly if you are using an unsized array as the last member of a struct you might do this

const code = `
struct Kernel {
  amount: f32,
  entries: array<vec3f>,
};
@group(0) @binding(7) var<storage> conv: Kernel;
`;
const defs = makeShaderDataDefinitions(code);
const {size: elemSize} = getSizeOfUnsizedArrayElement(defs.storages.conv);
const numKernelEntries = 4;
const size = defs.storages.conv.size + numKernelEntries * elemSize;
const buf1 = makeStructuredView(
    defs.storages.conv, new ArrayBuffer(size));
)

Usage

  • include from the net
import { createTextureFromImage } from 'https://greggman.github.io/webgpu-utils/dist/1.x/webgpu-utils.module.js'

...
npm install webgpu-utils
import { createTextureFromImage } from 'webgpu-utils';

...

Examples

Development

git clone https://github.com/greggman/webgpu-utils.git
cd webgpu-utils
npm ci
npm start

This will run rollup in watch mode, building from typescript into dist/1.x/webgpu-utils.js and start a server

Now open http://localhost:8080/test/ to run tests.

Thanks

Super thanks to Brendan Duncan for wgsl-reflect on which much of this is based.

License

MIT