Skip to content

Conversation

davepagurek
Copy link
Contributor

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 (Run WebGPU tests on self-hosted runner davepagurek/p5.js#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 WEBGPU mode, you need to await createCanvas(w, h, WEBGPU)
  • In WebGPU mode, you have to await loadPixels() and await 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)
  • The current goal is just feature parity. We will definitely want to then optimize performance more.

@davepagurek
Copy link
Contributor Author

@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.

@ksen0 ksen0 added this to the 2.2 milestone Oct 21, 2025
@limzykenneth
Copy link
Member

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.

@davepagurek
Copy link
Contributor Author

@limzykenneth that'd be this I think!

function rendererWebGPU(p5, fn) {
p5.RendererWebGPU = RendererWebGPU;
p5.renderers[constants.WEBGPU] = p5.RendererWebGPU;
fn.ensureTexture = function(source) {
return this._renderer.ensureTexture(source);
}
// TODO: move this and the duplicate in the WebGL renderer to another file
fn.setAttributes = async function (key, value) {
return this._renderer._setAttributes(key, value);
}
}

@davepagurek
Copy link
Contributor Author

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:

p5.js/src/webgl/index.js

Lines 19 to 37 in aa73cea

export default function(p5){
rendererGL(p5, p5.prototype);
primitives3D(p5, p5.prototype);
interaction(p5, p5.prototype);
light(p5, p5.prototype);
loading(p5, p5.prototype);
material(p5, p5.prototype);
text(p5, p5.prototype);
renderBuffer(p5, p5.prototype);
quat(p5, p5.prototype);
matrix(p5, p5.prototype);
geometry(p5, p5.prototype);
camera(p5, p5.prototype);
framebuffer(p5, p5.prototype);
dataArray(p5, p5.prototype);
shader(p5, p5.prototype);
texture(p5, p5.prototype);
strands(p5, p5.prototype);
}

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.

@limzykenneth
Copy link
Member

limzykenneth commented Oct 22, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants