Add WIP WebGPU mode
This refactors RendererGL into two classes, Renderer3D, and a subclass RendererGL. It also introduces a new RendererWebGPU that extends Renderer3D as well.
No WebGPU code is bound by default. Currently it can be tested just by importing its addon module and setting it up, e.g. this test sketch using textures and framebuffers:
WebGPU test sketch source code
import p5 from '../src/app.js';
import rendererWebGPU from '../src/webgpu/p5.RendererWebGPU.js';
p5.registerAddon(rendererWebGPU);
const sketch = function (p) {
let fbo;
let sh;
let tex;
p.setup = async function () {
await p.createCanvas(400, 400, p.WEBGPU);
fbo = p.createFramebuffer();
tex = p.createImage(100, 100);
tex.loadPixels();
for (let x = 0; x < tex.width; x++) {
for (let y = 0; y < tex.height; y++) {
const off = (x + y * tex.width) * 4;
tex.pixels[off] = p.round((x / tex.width) * 255);
tex.pixels[off + 1] = p.round((y / tex.height) * 255);
tex.pixels[off + 2] = 0;
tex.pixels[off + 3] = 255;
}
}
tex.updatePixels();
fbo.draw(() => {
p.imageMode(p.CENTER);
p.image(tex, 0, 0, p.width, p.height);
});
sh = p.baseMaterialShader().modify({
uniforms: {
'f32 time': () => p.millis(),
},
'Vertex getWorldInputs': `(inputs: Vertex) {
var result = inputs;
result.position.y += 40.0 * sin(uniforms.time * 0.005);
return result;
}`,
})
};
p.draw = function () {
p.orbitControl();
const t = p.millis() * 0.002;
p.background(200);
p.shader(sh);
p.ambientLight(150);
p.directionalLight(100, 100, 100, 0, 1, -1);
p.pointLight(155, 155, 155, 0, -200, 500);
p.specularMaterial(255);
p.shininess(300);
p.noStroke();
for (const [i, c] of ['red', 'lime', 'blue'].entries()) {
p.push();
p.fill(c);
p.translate(
p.width/3 * p.sin(t + i * Math.E),
0,
p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3),
)
p.texture(fbo)
p.sphere(30);
p.pop();
}
};
};
new p5(sketch);
Notes
- Tests do not currently run on CI, only locally! In a separate PR (https://github.com/davepagurek/p5.js/pull/2) I've separated out the tests that need a GPU to run and have them running on a self-hosted runner. That PR can also be adapted to run those tests on a runner on e.g. Azure if we need too. For now though, they only will be run manually on your own computer.
- In
WEBGPUmode, you need toawait createCanvas(w, h, WEBGPU) - In WebGPU mode, you have to
await loadPixels()andawait get() - The WebGPU implementation is actually pretty usable but is not fully complete. Remaining features include:
imageLight()- ~~filter shaders~~
- ~~font rendering~~
- ~~p5.strands (currently shader hooks work, but only written in WGSL, not in js)~~
- ~~clipping~~
- The current goal is just feature parity. We will definitely want to then optimize performance more.
@ksen0 because this splits tests into two steps, the regular tests and the webgpu tests, even though the webgpu ones aren't run, I think the change in test steps has made the required check not match up. If we go with self-hosted runners or Azure for WebGPU tests, we'll likely need to update the PR requirements. If we want to keep WebGPU tests off, we can either still update the requirements to match the test name here, or I can update this to match the old test step name.
Is there a default addon export file for the WebGPU renderer that package.json can expose in the exports key? Basically something like src/webgl/index.js.
@limzykenneth that'd be this I think! https://github.com/processing/p5.js/blob/d010b450f1215ad06080c9189e1789e5f4d78fb3/src/webgpu/p5.RendererWebGPU.js#L1926-L1938
Actually I'll have to give this a tad more thought, since it needs more than just the webgpu renderer -- it also needs basically everything else included in the webgl export other than its renderer, e.g. primitives3D: https://github.com/processing/p5.js/blob/aa73cea745b20660492c186ed094741633eecba0/src/webgl/index.js#L19-L37
If I make a similar function for WebGPU that sets up all of those, it'd probably work but would double-add those addons if both WebGPU and WebGL are added I think? Do you think that's OK or should we add some way to detect when an addon function has already been called? It'd have to pay attention to what's passed in because p5.Graphics would possibly also need to call the same function but with different parameters.
It would be nice if the call to p5.registerAddon could be idempotent (I don't know for sure if it already is or not, nor if it is a reasonable expectation?) so repeated call to it with the same function (as in same reference) would be the same as only calling it once.
The main thing I'm currently thinking may be a problem is the decoration feature which if the same addon is registered multiple times then it will also create duplicate decoration. The other thing is that since the addon function will be called multiple times, would it affect some of the internal initialization of the addon if that is the case?
Calling p5.registerAddon with multiple different copies (instead of same reference) of the same function might be even harder to make idempotent I think.
@limzykenneth I made an update to make p5.registerAddon stop early if it's been passed in the same function instance. It's not bulletproof but is maybe good enough for now. We can look into this more as we explore modular builds -- this same-instance deduplication is probably good enough if we're creating a new unified build with different modules, but not sufficient if we're creating a bunch of independent single-module files that users then add script tags for.
Update: added a WGSL backend for p5.strands!
This silly little demo is running this shader:
baseMaterialShader().modify(() => {
const time = uniformFloat(() => millis())
p.getWorldInputs((inputs) => {
inputs.position.y += 40 * sin(time * 0.005);
return inputs;
});
})
https://github.com/user-attachments/assets/d248d305-27ef-4c3b-8286-ff26ebf4f347
Update: filter shaders now work in WebGPU! also the base filter implementations are now in p5.strands so that we get both implementations "for free"