Gathering detailed insights and metrics for simple-compute-shaders
Gathering detailed insights and metrics for simple-compute-shaders
Gathering detailed insights and metrics for simple-compute-shaders
Gathering detailed insights and metrics for simple-compute-shaders
npm install simple-compute-shaders
Typescript
Module System
Node Version
NPM Version
TypeScript (100%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
12 Stars
29 Commits
1 Watchers
2 Branches
1 Contributors
Updated on Jul 12, 2025
Latest Version
0.1.12
Package Id
simple-compute-shaders@0.1.12
Unpacked Size
112.24 kB
Size
18.36 kB
File Count
26
NPM Version
10.2.4
Node Version
20.11.1
Published on
Mar 02, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
1
This library is a simplified wrapper around the WebGPU API, designed to make it easier to create and run shaders without dealing with the complex details of the WebGPU setup. It allows developers to initialize WebGPU, create data buffers, write shaders, and execute compute or render passes, all with a streamlined interface. The library is suitable for both beginners who want to experiment with GPU programming and experienced developers looking to speed up prototyping.
This library simplifies a great deal of the plumbing needed to do rapid prototyping and even build full-scale applications so that you can focus on writing shader code rather than wasting time on repetative boilerplate code. However, it's no substitute for having the right foundational knowledge on the following concepts. If you're new to graphics programming, I'd recommend having at least a basic understanding of the following things:
GPUBuffer
objects that are easier to configure, read, and write compared with the ones provided by the WebGPU API.
NPM Package
npm i simple-compute-shaders
main
.Shader
by calling Shader.initialize()
. This is required once per application.ComputeShader
or RenderShader2d
object. Pass in your shader code as a string. List binding layouts. For compute shaders, provide a workgroupCount
(a 2 or 3 dimensional array), and don't forget to specify a @workgroup_size
inside your shader.requestAnimationFrame
for render shaders. In this function, write all the buffers that need updating, then call shader.pass()
for RenderShader2d
s or shader.dispatch()
for ComputeShader
s, where shader
is your Shader
instance.dispose()
function on your shader and on each buffer.Use await Shader.initialize()
once per applicaiton. This is required for the library to get the system's GPU device which is required for all other operations, like creating and running shaders.
Simple Compute Shaders has a number of helper classes for encapsulating buffers. They are all implementations of the abstract class ShaderBuffer
.
StorageBuffer
: Stores general-purpose data, readable and writable by compute or fragment shaders, suitable for large, dynamic, or read-write data.UniformBuffer
: Stores small, constant data shared across shader invocations, typically for values that change frequently, such as transformation matrices or frame numbers.IndirectBuffer
: Stores parameters for indirect drawing commands, allowing the GPU to control rendering without CPU involvement, useful for dynamic and GPU-driven rendering scenarios.The buffer classes are based on the primary GPUBufferUsage
values. There are two more classes that are not exposed publicly: VertexBuffer
and IndexBuffer
. These are hidden because they are not supported by fragment or compute shaders.
Each buffer wrapper has the same constructor that accepts a props
argument that contains a dataType
, a conditional size
(in elements) an optional initialValue
, and optional buffer usage flags as booleans.
new StorageBuffer(props: BufferProps)
, new UniformBuffer(props: BufferProps)
, new IndirectBuffer(props: BufferProps)
Where BufferProps
contains fields:
array
or texture_2d
data types.u32
, i32
, and f32
types are passed in as an array of length 1.GPUBufferUsage.MAP_READ
flag. This allows CPU access to the buffer data for reading purposes.GPUBufferUsage.MAP_WRITE
flag. This allows CPU access to the buffer data for writing purposes.GPUBufferUsage.COPY_SRC
flag. This allows the buffer data to be copied to other buffers or textures.GPUBufferUsage.COPY_DST
flag. This allows other buffers or textures to copy their data into this buffer.GPUBufferUsage.QUERY_RESOLVE
flag. Typically used for resolving the results of GPU queries.More details on the canCopy-
and canMap-
flags can be found in the Reading and Writing Buffer Data
section.
Supported values for dataType
are:
u32
, f32
, i32
, vec2<u32>
, vec2<f32>
, vec2<i32>
, vec3<u32>
, vec3<f32>
, vec3<i32>
, vec4<u32>
, vec4<f32>
, vec4<i32>
, mat4x4<u32>
, mat4x4<f32>
, mat4x4<i32>
, texture_2d<u32>
, texture_2d<f32>
, texture_2d<i32>
, array<u32>
, array<f32>
, array<i32>
, array<vec2<u32>>
, array<vec2<f32>>
, array<vec2<i32>>
, array<vec3<u32>>
, array<vec3<f32>>
, array<vec3<i32>>
, array<vec4<u32>>
, array<vec4<f32>>
, array<vec4<i32>>
, array<mat4x4<u32>>
, array<mat4x4<f32>>
, array<mat4x4<i32>
If the dataType
is set to an array<T>
or a texture_2d<T>
, you must provide a size
in array elements or texels. For example, each element of a array<mat4x4<f32>>
only contributes 1 to size
, even though it requires 16 float values in the source array, and will occupy 64 bytes of space on the GPU. Simple Compute Shaders will do that conversion for you when setting up the buffer.
There are two distinct ways to read and write data after a shader has been set up: mapping, and copying. These require specific usage flags to be set up. In the buffer's constructor. Here is a guide on how to choose the usage that makes the most sense.
canCopyDst
to true in the buffer's constructor properties.await ShaderBuffer.write()
to write data.write(value: Float32Array | Uint32Array, offset = 0)
Writes data to the buffer using COPY_DST, allowing data to be transferred from CPU to GPU.
canCopySrc
to true in the buffer's constructor properties.await ShaderBuffer.read()
to read data.read(offset:number = 0, length: number = this.sizeElements)
Asynchronously reads data directly from the buffer by mapping it with MAP_READ usage.
Once the Shader
object has been initialized and your buffers are created, you can instantiate a compute shader or a render shader using their respective constructors.
new ComputeShader(props: ComputeShaderProps)
Constructs a pipeline for a compute shader. The ComputeShaderProps
type is used to configure compute shaders. Below is a detailed explanation of each field in ComputeShaderProps
:
code
: string|Array<string>
. The WGSL code for the compute shader. This code should contain the @compute
entry point named main
. The library injects binding layout definitions automatically, so you don't need to declare bindings explicitly. Set code
to an array of strings to modularize your code.
workgroupCount
: [number, number, number]
or [number, number]
. Specifies the number of workgroups to be dispatched. This can be a 2D or 3D array, depending on the desired compute workload.
bindingLayouts
(optional): Array<BindingLayoutDef>
. An array defining the binding layouts used by the compute shader. This includes information such as the type of resource (storage
, uniform
, etc.), the data type (e.g., u32
, f32
), and the binding group configuration.
useExecutionCountBuffer
(optional): boolean
. Adds a uniform to the shader that counts the number of times the shader has been dispatched. Default value is true
.
executionCountBufferName
(optional): string
. Sets the name of the execution count buffer. Default is "execution_count"
.
useTimeBuffer
(optional): boolean
. Adds a uniform to the shader that has the time (in seconds) since very first call to dispatch()
. Default value is true
.
timeBufferName
(optional): string
. Sets the name of the time buffer. Default is "time"
.
1 2await Shader.initialize(); 3 4await Shader.initialize(); 5 6this.dataBuffer = new StorageBuffer({ 7 dataType: "array<f32>", 8 size: 2048, 9 canCopyDst: true, 10 canCopySrc: true 11}); 12 13this.sortComputeShader = new ComputeShader({ 14 code: ` 15 fn bitonic_compare_swap(i: u32, j: u32, dir: bool) { 16 if ((data[i] > data[j]) == dir) { 17 let temp = data[i]; 18 data[i] = data[j]; 19 data[j] = temp; 20 } 21 } 22 23 @compute @workgroup_size(64) 24 fn main(@builtin(global_invocation_id) global_id: vec3<u32>) { 25 let id = global_id.x; 26 27 // Perform bitonic sort using phases 28 for (var k = 2u; k <= 2048; k *= 2) { 29 for (var j = k / 2; j > 0; j /= 2) { 30 let ixj = id ^ j; 31 if (ixj > id) { 32 bitonic_compare_swap(id, ixj, (id & k) == 0); 33 } 34 35 // Synchronize threads within a workgroup. 36 workgroupBarrier(); 37 } 38 } 39 } 40 `, 41 workgroupCount: [32, 1], 42 bindingLayouts: [ 43 { 44 binding: this.dataBuffer, 45 name: "data", 46 type: "storage" 47 } 48 ] 49}); 50 51// Create a random array of floats. 52 53let data = new Float32Array(2048); 54 55for (let i = 0; i < data.length; i++) { 56 data[i] = Math.random() * 1000; 57} 58 59console.log("Unsorted data:", data); 60 61// Write the data to the buffer. 62 63this.dataBuffer.write(data); 64 65// Sort the data. 66 67this.sortComputeShader.dispatch(); 68 69// Read the data back. 70 71let sortedData = await this.dataBuffer.read(); 72 73console.log("Sorted data:", sortedData);
new RenderShader2d(props: RenderShader2dProps)
Constructs a new pipeline for a render shader, containing a built-in vertex stage with a managed quad. The RenderShader2dProps
type is used to configure render shaders. Below is a detailed explanation of each field in RenderShader2dProps
:
code
: string|Array<string>
. The WGSL code for the fragment shader. This code should contain the @fragment
entry point named main
. Bindings are injected automatically by the library. Set code
to an array of strings to modularize your code.
bindingLayouts
(optional): Array<BindingLayoutDef>
. An array defining the binding layouts used by the render shader. This includes information such as the type of resource (uniform
, storage
, etc.), the data type (e.g., f32
, vec4<f32>
), and the binding group configuration.
canvas
: HTMLCanvasElement
. The HTML canvas element that will be used as the rendering target. This canvas is required for rendering the output of the fragment shader to the screen.
sizeBufferStyle
(optional): "floats"|"vector"|"none"
. Sets how the canvas size uniform(s) is/are passed into the fragment shader. When set to "floats"
(default), the canvas size will be passed into two separate float
uniforms for width and height. When set to "vector"
, the canvas size will be passed in as a vec2<float>
uniform. When set to "none"
, the canvas size is not passed in.
canvasWidthName
(optional, only when sizeBufferStyle
is "floats"
): string
. The name of the canvas width identifier that will be injected into the fragment shader.
canvasHeightName
(optional, only when sizeBufferStyle
is "floats"
): string
. The name of the canvas height identifier that will be injected into the fragment shader.
canvasSizeName
(optional, only when sizeBufferStyle
is "vector"
): string
. The name of the canvas size identifier that will be injected into the fragment shader.
useExecutionCountBuffer
(optional): boolean
. Adds a uniform to the shader that counts the number of times the shader has been invoked. Default value is true
.
executionCountBufferName
(optional): string
. Sets the name of the execution count buffer. Default is "execution_count"
.
useTimeBuffer
(optional): boolean
. Adds a uniform to the shader that has the time (in seconds) since very first call to pass()
. Default value is true
.
timeBufferName
(optional): string
. Sets the name of the time buffer. Default is "time"
.
1 2await Shader.initialize(); 3 4let myUniformBuffer = new UniformBuffer({ 5 dataType: "vec4<f32>", 6 canCopyDst: true, 7 initialValue: [1,0,0,1] // Red 8}); 9 10const renderShader = new RenderShader2d({ 11 code: ` 12 @fragment 13 fn main() -> @location(0) vec4<f32> { 14 return color; // value of the color uniform. 15 } 16 `, 17 bindingLayouts: [ 18 { 19 type: "uniform", 20 name: "color", 21 binding: myUniformBuffer 22 } 23 ], 24 canvas: document.getElementById('myCanvas') as HTMLCanvasElement 25}); 26 27function render(){ 28 let now = Date.now() / 1000; 29 myUniformBuffer.write(new Float32Array([ 30 (Math.sin(now) * 0.5 + 0.5), 31 (Math.sin(now * 1.667) * 0.5 + 0.5), 32 (Math.sin(now * 1.333) * 0.5 + 0.5), 33 1 34 ])); 35 36 renderShader.pass(); 37 requestAnimationFrame(()=>{render();}); 38} 39 40requestAnimationFrame(()=>{render();});
All you need to do is resize the canvas. When sizeBufferStyle
in the RenderShader2d
's constructor is set to "floats"
(default) or "vector"
, the uniforms for the canvas size will be updated automatically before the next pass
call.
1window.addEventListener('resize', () => { 2 canvas.width = window.innerWidth; 3 canvas.height = window.innerHeight; 4});
Each binding layout definition in your bindingLayouts
field must satisfy the BindingLayoutDef
type, as given:
type: "storage" | "read-only-storage" | "uniform" | "write-only-texture" | "var"
. The buffer type.
name: string
. The name of the buffer that will be added to the shader code.
binding (required if bindGroups is not provided, otherwise must be omitted): ShaderBuffer
. The GPU buffer to be added to the default bind group. If one buffer is using binding
, they all must.
bindGroups (CURRENTLY NOT SUPPORTED - required if binding is not provided, otherwise must be omitted): Record<string, ShaderBuffer>
. A collection of GPUBuffer
objects with strings representing bind group names. This is useful for setting up buffer swapping for things like double-buffering. If one buffer is using bindGroups
, they all must, and they all must have the same bind group names.
The easiest way to include WGSL code in your shader is to hard-code it as a JavaScript string. I recommend using WGSL-Plus to compile your WGSL files into JS or TS strings. WGSL-Plus supports a modified WGSL syntax that adds linking, allowing you to break up your code, and has an obfuscation function that allows you to protect your WGSL code somewhat. Note that to use the obfuscator correctly, you need to list out your binding names at the top of your code like so:
1#binding data_binding_1 2#binding data_binding_2
These will be compiled as special comments at the top of your obfuscated code. I.e.:
1//#!binding data_binding_1 _x0 2//#!binding data_binding_2 _x1
And Simple Compute Shaders will automatically map whatever binding names you provide to the obfuscated names.
If you're soming a framework like Webpack or Rollup, you can configure it to import wgsl files directly. For instance, in Webpack, you can add the following under your module rules:
1{ 2 test: /\.wgsl$/, 3 type: "asset/source" 4},
Then you can import your shader as follows:
1import fragCode from "./frag.wgsl";
To run a render pass on a RenderShader2d
, simply call shader.pass()
. To dispatch a compute shader, call shader.dispatch()
. Run renders inside of a requestAnimationFrame
callback. Compute dispatches can be run any time and are syncronous.
Building this library to be as robust as possible was challenging, and is an ongoing project. Suggestions, feedback, and bugfixes are welcome. For major changes to the API, speak with me first.
Feel free to send me an email, reach out to me on X, or open an issue.
This project is licensed under the MIT License. See the LICENSE file for more details.
No vulnerabilities found.
No security vulnerabilities found.