Skip to content

Commit 20a3372

Browse files
authored
Merge pull request #8179 from processing/webgpu
Add WIP WebGPU mode
2 parents 815a456 + fd0cebf commit 20a3372

File tree

247 files changed

+12659
-4603
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

247 files changed

+12659
-4603
lines changed

.github/workflows/ci-test.yml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,47 @@ on:
1111

1212
jobs:
1313
test:
14-
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
include:
17+
- os: ubuntu-latest
18+
browser: chrome
19+
# - os: windows-latest
20+
# browser: chrome
21+
22+
runs-on: ${{ matrix.os }}
1523

1624
steps:
17-
- uses: actions/checkout@v1
25+
- uses: actions/checkout@v4
26+
1827
- name: Use Node.js 20.x
19-
uses: actions/setup-node@v1
28+
uses: actions/setup-node@v4
2029
with:
2130
node-version: 20.x
31+
32+
- name: Verify Chrome (Ubuntu)
33+
if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome'
34+
run: |
35+
google-chrome --version
36+
37+
- name: Verify Chrome (Windows)
38+
if: matrix.os == 'windows-latest' && matrix.browser == 'chrome'
39+
run: |
40+
& "C:\Program Files\Google\Chrome\Application\chrome.exe" --version
41+
2242
- name: Get node modules
2343
run: npm ci
2444
env:
2545
CI: true
26-
- name: build and test
46+
47+
- name: Build and test (Ubuntu)
2748
id: test
28-
run: npm test
49+
if: matrix.os == 'ubuntu-latest'
50+
run: npm test -- --project=unit-tests
2951
continue-on-error: true
3052
env:
3153
CI: true
54+
3255
- name: Generate Visual Test Report
3356
if: always()
3457
run: node visual-report.js

contributor_docs/webgpu.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<!-- The goals and aspirations of our experimental WebGPU mode. -->
2+
3+
p5.js has recently added an experimental WebGPU mode. It is a 3D-capable renderer like WebGL mode, and supports all the functions available in WebGL mode, but has been built using different underlying technology that will help p5.js stay up-to-date as browsers evolve.
4+
5+
It's still in the early days, so we would love for people to test it out, give feedback, and get involved!
6+
7+
## Using WebGPU mode
8+
9+
WebGPU mode is currently experimental, so it is not in the standard build of p5.js. Instead, it comes in a separate file, which you can add to your project after the standard p5.js `script` tag like any other addon:
10+
11+
```html
12+
<html>
13+
<head>
14+
<!-- Libraries -->
15+
<script type="text/javascript" src="p5.js" />
16+
<script type="text/javascript" src="p5.webgpu.js" />
17+
18+
<!-- Your code -->
19+
<script type="text/javascript" src="sketch.js" />
20+
</head>
21+
<body>
22+
</body>
23+
</html>
24+
```
25+
26+
In WebGPU, some more things are asynchronous than before. Creating the WebGPU canvas is `async`, and must now be `await`ed:
27+
28+
```js
29+
async function setup() {
30+
await createCanvas(400, 400, WEBGPU);
31+
}
32+
```
33+
34+
Anything that involves loading pixels is also `async`, so `loadPixels()` and `get()` must now also be `await`ed. Consider using shaders for pixel-level drawing and framebuffers for image copying if you need to do these every frame of an animation.
35+
36+
## Contributing
37+
38+
We'd love to have more people involved with WebGPU mode! Here are some ways you can help:
39+
40+
- Test it out! Let us know what bugs you encounter by filing issues on GitHub.
41+
- Help us optimize the new rendering system. The first step is also testing: what parts are faster or slower than the more stable WebGL mode? Based on that, we can decide on changes to the rendering system to address those issues and implement them in the codebase.
42+
- Brainstorm new ideas! There are new capabilities in the WebGPU spec that we can bring to p5, such as compute shaders. Talk to us on Discord about what you'd love your code to look like when creating, for example, a particle system on the GPU, and we can see how we can build an API around that.
43+
44+
## Goals
45+
46+
So far, the renderers in p5.js can be grouped into 2D and 3D-capable renderers. Initially, there was one renderer for each: the default 2D mode and WebGL mode. The initial goal for WebGPU mode is to be an equivalent of WebGL mode: anything it can do, WebGPU mode aims to also be able to do. But that is also just the starting point. **We view WebGPU mode as a way to give new tools to artists as technology advances, and as a way to ensure p5.js stays up-to-date with browser technology for the next decade.**
47+
48+
WebGPU mode is not aiming to be a more efficient renderer. This is similar to WebGL mode: WebGL mode will not automatically be faster than 2D mode; instead, it provides a different set of tools that may be more appropriate for some tasks. WebGL and now WebGPU modes provide tools for drawing using the GPU and for drawing in 3D.
49+
50+
### New computational tools
51+
52+
The underlying WebGPU technology is still relatively new, but it seems like it will grow to support more than the older WebGL technology can. WebGPU mode in p5.js will be a spot for us to give artists and programmers access to these new computing capabilities, in an accessible package. Compute shaders will likely be the first example of this. Both WebGL and WebGPU support shaders, which currently are used to position vertices of shapes and to pick pixel colors within a triangle all in parallel on the GPU. The WebGPU specification additionally has [compute shaders](https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html), which can be used to process arbitrary data in parallel without it being attached to rendering.
53+
54+
WebGPU mode will not try expose everything WebGPU has to offer to programmers. Instead, it should strategically pick what it exposes, balancing the goals of **expanding creative possibilities** and **being easy to learn.** As an example, a p5.js compute shader API does not need to do everything raw WebGPU compute shaders can do as long as it is still helpful for common tasks, and can be easily adopted without a steep learning curve.
55+
56+
### Preparing for the future
57+
58+
At the time of writing (December 2025), WebGPU is not yet turned on by default in all major browsers on all platforms, but all major browsers are actively developing WebGPU support. There is a lot of energy behind WebGPU development and features while browser WebGL APIs, while not going away, seem largely to be in legacy mode, no longer adding new features. Since p5.js aims to be an accessible way to create programmatic art for the web, and new tools for the web are likely to be created in WebGPU but not WebGL, p5's WebGPU mode will take on a greater importance over time.
59+
60+
Currently, though, WebGL is stable, reliable, and widely available. For that reason, p5.js WebGPU mode will be opt-in and experimental for some time.
61+
62+
## Design decisions
63+
64+
### Class structure
65+
66+
With the addition of WebGPU mode, the built-in p5 renderers have the following structure:
67+
68+
```mermaid
69+
---
70+
title: p5.js Renderers
71+
---
72+
classDiagram
73+
class Base["p5.Renderer"] {
74+
}
75+
class P2D["p5.Renderer2D"] {
76+
}
77+
class P3D["p5.Renderer3D"] {
78+
}
79+
class WebGL["p5.RendererGL"] {
80+
}
81+
class WebGPU["p5.RendererWebGPU"] {
82+
}
83+
Base <|-- P2D
84+
Base <|-- P3D
85+
P3D <|-- WebGL
86+
P3D <|-- WebGPU
87+
```
88+
89+
Entities that are shared by all 3D renderers such as `p5.Geometry`, `p5.Framebuffer`, `p5.Texture`, `p5.Camera`, and `p5.Shader`, rather than including code in each entity to handle both WebGL and WebGPU cases, instead call methods on their 3D renderers. These methods are unimplemented on the base `Renderer3D` class, but are implemented in `RendererGL` and `RendererWebGPU` with platform-specific logic. All new platform-specific logic should be added to renderer classes now rather than being on the entities.
90+
91+
### Rendering
92+
93+
While WebGL mode submits all draw commands immediately, WebGPU mode defers submitting until the last possible moment so that it can submit draw commands in batches. Rather than drawing, commands are built up in an array and `_hasPendingDraws` is set to `true`. In `finishDraw`, called at the end of each frame, these are all finally submitted to the GPU as one render pass. There are a few other times where they get submitted early in other render passes. When switching draw targets, such as when drawing to a framebuffer, pending draws are submitted in a render pass too. This makes sure that you can then read from the framebuffer safely in the next render pass. We also submit a render pass when you call `loadPixels` or another function that involves reading back data from the GPU.
94+
95+
Since draws get batched up, this means that buffers used to send shader uniform values to the GPU cannot be shared. If they were shared, they would get rewritten by the next thing getting drawn before the previous one gets to the GPU! Instead, we build up a pool of buffers that we can pull from for shader uniforms and vertex information.

docs/parameterData.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@
410410
[
411411
"Number?",
412412
"Number?",
413-
"P2D|WEBGL|P2DHDR?",
413+
"P2D|WEBGL|P2DHDR|WEBGPU?",
414414
"HTMLCanvasElement?"
415415
],
416416
[
@@ -1938,8 +1938,7 @@
19381938
"overloads": [
19391939
[
19401940
"Object"
1941-
],
1942-
[]
1941+
]
19431942
]
19441943
},
19451944
"vertex": {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"./math": "./dist/math/index.js",
9595
"./utilities": "./dist/utilities/index.js",
9696
"./webgl": "./dist/webgl/index.js",
97+
"./webgpu": "./dist/webgpu/index.js",
9798
"./type": "./dist/type/index.js"
9899
},
99100
"files": [

preview/index.html

Lines changed: 155 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,170 @@
1818
<body>
1919
<script type="module">
2020
import p5 from '../src/app.js';
21+
import rendererWebGPU from '../src/webgpu/p5.RendererWebGPU.js';
22+
23+
p5.registerAddon(rendererWebGPU);
2124

2225
const sketch = function (p) {
2326
let fbo;
27+
let sh;
28+
let ssh;
29+
let tex;
30+
let font;
31+
let redFilter;
32+
let env;
33+
2434
p.setup = async function () {
25-
p.createCanvas(400, 400, p.WEBGL);
26-
fbo = p.createFramebuffer()
35+
await p.createCanvas(400, 400, p.WEBGPU);
36+
env = await p.loadImage('img/spheremap.jpg');
37+
font = await p.loadFont(
38+
'font/PlayfairDisplay.ttf'
39+
);
40+
fbo = p.createFramebuffer();
41+
42+
redFilter = p.baseFilterShader().modify(() => {
43+
p.getColor((inputs, canvasContent) => {
44+
let col = p.getTexture(canvasContent, inputs.texCoord);
45+
col.g = col.r;
46+
col.b = col.r;
47+
return col;
48+
})
49+
}, { p })
50+
51+
tex = p.createImage(100, 100);
52+
tex.loadPixels();
53+
for (let x = 0; x < tex.width; x++) {
54+
for (let y = 0; y < tex.height; y++) {
55+
const off = (x + y * tex.width) * 4;
56+
tex.pixels[off] = p.round((x / tex.width) * 255);
57+
tex.pixels[off + 1] = p.round((y / tex.height) * 255);
58+
tex.pixels[off + 2] = 0;
59+
tex.pixels[off + 3] = 255;
60+
}
61+
}
62+
tex.updatePixels();
63+
fbo.draw(() => {
64+
//p.clear();
65+
//p.background('orange');
66+
p.imageMode(p.CENTER);
67+
p.image(tex, 0, 0, p.width, p.height);
68+
});
69+
70+
/*sh = p.baseMaterialShader().modify({
71+
uniforms: {
72+
'f32 time': () => p.millis(),
73+
},
74+
'Vertex getWorldInputs': `(inputs: Vertex) {
75+
var result = inputs;
76+
result.position.y += 40.0 * sin(uniforms.time * 0.005);
77+
return result;
78+
}`,
79+
})*/
80+
sh = p.baseMaterialShader().modify(() => {
81+
const time = p.uniformFloat(() => p.millis());
82+
p.getWorldInputs((inputs) => {
83+
inputs.position.y += 40 * p.sin(time * 0.005);
84+
return inputs;
85+
});
86+
}, { p })
87+
/*ssh = p.baseStrokeShader().modify({
88+
uniforms: {
89+
'f32 time': () => p.millis(),
90+
},
91+
'StrokeVertex getWorldInputs': `(inputs: StrokeVertex) {
92+
var result = inputs;
93+
result.position.y += 40.0 * sin(uniforms.time * 0.005);
94+
return result;
95+
}`,
96+
})*/
2797
};
2898

2999
p.draw = function () {
100+
p.clear();
30101
p.push();
31-
p.background(200);
32-
fbo.begin()
33-
p.background('blue')
34-
p.strokeWeight(10);
102+
//p.clip(() => p.rect(-50, -50, 200, 200));
103+
/*p.orbitControl();
104+
p.push();
105+
p.textAlign(p.CENTER, p.CENTER);
106+
p.textFont(font);
107+
p.textSize(85)
108+
p.fill('red')
109+
p.noStroke()
110+
p.rect(0, 0, 100, 100);
111+
p.fill(0);
35112
p.push()
36-
p.stroke('red')
37-
p.line(-100, -100, 100, 100);
113+
p.rotate(p.millis() * 0.001)
114+
p.text('Hello!', 0, 0);
38115
p.pop()
39-
p.translate(200, 200)
40-
p.line(-100, -100, 100, 100);
41-
fbo.end()
42-
p.image(fbo, 0, 0)
116+
p.pop();
117+
return;*/
118+
p.orbitControl();
119+
const t = p.millis() * 0.002;
120+
p.background(200);
121+
p.panorama(env);
122+
p.push();
123+
p.imageLight(env);
124+
p.shader(sh);
125+
// p.strokeShader(ssh)
126+
p.ambientLight(10);
127+
//p.directionalLight(100, 100, 100, 0, 1, -1);
128+
//p.pointLight(155, 155, 155, 0, -200, 500);
129+
p.specularMaterial(255);
130+
p.shininess(50);
131+
p.metalness(100);
132+
//p.stroke('white');
133+
p.noStroke();
134+
for (const [i, c] of ['red', 'gray', 'blue'].entries()) {
135+
p.push();
136+
p.fill(c);
137+
p.translate(
138+
p.width/3 * p.sin(t + i * Math.E),
139+
0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2),
140+
p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3),
141+
)
142+
if (i % 2 === 0) {
143+
if (i === 0) {
144+
p.texture(fbo)
145+
}
146+
p.box(30);
147+
} else {
148+
p.sphere(30);
149+
}
150+
p.pop();
151+
}
152+
p.pop();
153+
154+
// Test beginShape/endShape with immediate mode shapes
155+
p.push();
156+
p.translate(0, 100, 0);
157+
p.fill('yellow');
158+
p.noStroke();
159+
160+
// Draw a circle using beginShape/endShape
161+
p.beginShape();
162+
const numPoints = 16;
163+
for (let i = 0; i < numPoints; i++) {
164+
const angle = (i / numPoints) * Math.PI * 2;
165+
const x = Math.cos(angle) * 50;
166+
const y = Math.sin(angle) * 50;
167+
p.vertex(x, y);
168+
}
169+
p.endShape(p.CLOSE);
170+
171+
p.translate(100, 0, 0);
172+
p.fill('purple');
173+
174+
// Draw a square using beginShape/endShape
175+
p.beginShape();
176+
p.vertex(-30, -30);
177+
p.vertex(30, -30);
178+
p.vertex(30, 30);
179+
p.vertex(-30, 30);
180+
p.endShape(p.CLOSE);
181+
182+
p.pop();
183+
184+
// p.filter(p.BLUR, 10)
43185
p.pop();
44186
};
45187
};
@@ -48,4 +190,4 @@
48190
</script>
49191
</body>
50192

51-
</html>
193+
</html>

rollup.config.mjs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const bundleSize = (name, sourcemap) => {
3636
});
3737
};
3838

39-
const modules = ['math'];
39+
const modules = ['webgpu']; // TODO: also generate math build
4040
const generateModuleBuild = () => {
4141
return modules.map(module => {
4242
return {
@@ -195,7 +195,6 @@ export default [
195195
},
196196
external: /node_modules/,
197197
plugins
198-
}
199-
// NOTE: comment to NOT build standalone math module
200-
// ...generateModuleBuild()
198+
},
199+
...generateModuleBuild()
201200
];

src/accessibility/gridOutput.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function gridOutput(p5, fn){
1010

1111
//updates gridOutput
1212
fn._updateGridOutput = function(idT) {
13-
if (this._renderer && this._renderer instanceof p5.RendererGL) {
13+
if (this._renderer && this._renderer.isP3D) {
1414
if (!this._didOutputGridWebGLMessage) {
1515
this._didOutputGridWebGLMessage = true;
1616
console.error('gridOutput() does not yet work in WebGL mode.');

0 commit comments

Comments
 (0)