Skip to content
23 changes: 23 additions & 0 deletions src/core/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,29 @@ function environment(p5, fn, lifecycles) {
*/
fn.webglVersion = C.P2D;

/**
* A `String` variable with the renderer in use.
*
* `rendererType`'s value equals one of the following string constants:
*
* - `P2D` whose value is `'p2d'`. This is the default for 2D sketches.
* - `WEBGL` whose value is `'webgl'`.
* - `WEBGPU` whose value is `'webgpu'`.
*
* @property {(P2D|WEBGL|WEBGPU)} rendererType
* @readOnly
* @example
* function setup() {
* background(200);
*
* // Display the current renderer type.
* text(rendererType, 42, 54);
*
* describe('The text "p2d" written in black on a gray background.');
* }
*/
fn.rendererType = C.P2D;

/**
* A `Number` variable that stores the width of the screen display.
*
Expand Down
3 changes: 3 additions & 0 deletions src/core/p5.Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ class Renderer {
// constructor state when getSupportedIndividualVertexProperties()
// is first consulted.
this._supportsIndividualTextureCoordinates = undefined;

this.rendererType = constants.P2D;
this._pInst.rendererType = this.rendererType;
}

get currentShape() {
Expand Down
30 changes: 10 additions & 20 deletions src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -2351,11 +2351,9 @@ function renderer3D(p5, fn) {
*/
fn.createStorage = function (dataOrCount) {
if (!this._renderer.createStorage) {
p5._friendlyError(
`createStorage() is only available with the WebGPU renderer. ${webGPUAddonMessage}`,
'createStorage'
throw new Error(
`createStorage() is only available with the WebGPU renderer. ${webGPUAddonMessage}`
);
return;
}
return this._renderer.createStorage(dataOrCount);
};
Expand Down Expand Up @@ -2532,11 +2530,9 @@ function renderer3D(p5, fn) {
*/
fn.createStorageList = function (maxCapacity, schemaOrData) {
if (!this._renderer.createStorageList) {
p5._friendlyError(
`createStorageList() is only available with the WebGPU renderer. ${webGPUAddonMessage}`,
'createStorageList'
throw new Error(
`createStorageList() is only available with the WebGPU renderer. ${webGPUAddonMessage}`
);
return;
}
return this._renderer.createStorageList(maxCapacity, schemaOrData);
};
Expand All @@ -2560,11 +2556,9 @@ function renderer3D(p5, fn) {
*/
fn.baseComputeShader = function () {
if (!this._renderer.baseComputeShader) {
p5._friendlyError(
`baseComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`,
'baseComputeShader'
throw new Error(
`baseComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`
);
return;
}
return this._renderer.baseComputeShader();
};
Expand Down Expand Up @@ -2728,11 +2722,9 @@ function renderer3D(p5, fn) {
*/
fn.buildComputeShader = function (cb, context) {
if (!this._renderer.baseComputeShader) {
p5._friendlyError(
`buildComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`,
'buildComputeShader'
throw new Error(
`buildComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`
);
return;
}
return this.baseComputeShader().modify(cb, context, { hook: 'iteration' });
};
Expand Down Expand Up @@ -2907,11 +2899,9 @@ function renderer3D(p5, fn) {
*/
fn.compute = function (shader, x, y, z) {
if (!this._renderer.compute) {
p5._friendlyError(
`compute() is only available with the WebGPU renderer. ${webGPUAddonMessage}`,
'compute'
throw new Error(
`compute() is only available with the WebGPU renderer. ${webGPUAddonMessage}`
);
return;
}
this._renderer.compute(shader, x, y, z);
};
Expand Down
30 changes: 23 additions & 7 deletions src/core/rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,28 @@ function rendering(p5, fn) {
args.unshift(renderer);
}

if (!renderers[selectedRenderer]) {
if (selectedRenderer === constants.WEBGPU) {
p5.FES
.log`To create a WEBGPU canvas, remember to add the WebGPU add-on to your project.`();
if (!renderers[renderer] && typeof renderer === 'string') {
if (renderer === constants.WEBGPU) {
p5.FES.log`To create a WEBGPU canvas, add the WebGPU add-on to your project.`();
if (renderers[constants.WEBGL]) {
p5.FES.log`Falling back to WebGL renderer.`();
selectedRenderer = constants.WEBGL;
}
args.shift();
} else {
p5.FES
.log`We weren't able to find a renderer called ${selectedRenderer}.`();
.log`We weren't able to find a renderer called ${renderer}.`();
}
}

// Init our graphics renderer
if (this._renderer) this._renderer.remove();
if (this._renderer) {
this._renderer.remove();
const index = this._elements.indexOf(this._renderer);
if (index !== -1) {
this._elements.splice(index, 1);
}
}
this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args);
this._defaultGraphicsCreated = true;
this._elements.push(this._renderer);
Expand All @@ -162,7 +172,13 @@ function rendering(p5, fn) {
}

if (this._renderer.contextReady) {
return this._renderer.contextReady.then(() => this._renderer);
return this._renderer.contextReady.then(() => this._renderer).catch((e) => {
if (selectedRenderer !== constants.WEBGPU) {
throw new Error('Failed to create canvas.');
}
p5.FES.log`${e.message} Falling back to WebGL renderer.`();
return this.createCanvas(w, h, constants.WEBGL, ...args);
});
} else {
return this._renderer;
}
Expand Down
3 changes: 3 additions & 0 deletions src/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ class RendererGL extends Renderer3D {
constructor(pInst, w, h, isMainCanvas, elt) {
super(pInst, w, h, isMainCanvas, elt);

this.rendererType = constants.WEBGL;
this._pInst.rendererType = this.rendererType;

if (this.webglVersion === constants.WEBGL2) {
this.blendExt = this.GL;
} else {
Expand Down
32 changes: 26 additions & 6 deletions src/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,9 @@ function rendererWebGPU(p5, fn) {
constructor(pInst, w, h, isMainCanvas, elt) {
super(pInst, w, h, isMainCanvas, elt);

this.rendererType = constants.WEBGPU;
this._pInst.rendererType = this.rendererType;

warnExperimental(p5, pInst, 'webgpu');

// Used to group draws into one big render pass
Expand Down Expand Up @@ -855,18 +858,35 @@ function rendererWebGPU(p5, fn) {
}

async _initContext() {
this.adapter = await navigator.gpu?.requestAdapter(
if (!navigator.gpu) {
throw new Error(
'WebGPU is not supported by this browser.'
);
}
this.adapter = await navigator.gpu.requestAdapter(
this._webgpuAttributes
);
this.device = await this.adapter?.requestDevice({
// Todo: check support
if (!this.adapter) {
throw new Error(
'No compatible GPU could be found for WebGPU on this device.'
);
}
this.device = await this.adapter.requestDevice({
requiredFeatures: ['depth32float-stencil8']
});
}).catch(() => null);
if (!this.device) {
throw new Error('Your browser does not support WebGPU.');
throw new Error(
'WebGPU is available, but a WebGPU device could not be created on this system.'
);
}
this.queue = this.device.queue;
this.drawingContext = this.canvas.getContext('webgpu');
if (!this.drawingContext) {
throw new Error(
'WebGPU is available, but the drawing context could not be created for this canvas.'
);
}

this.queue = this.device.queue;
this.presentationFormat = navigator.gpu.getPreferredCanvasFormat();
this.drawingContext.configure({
device: this.device,
Expand Down
97 changes: 97 additions & 0 deletions test/unit/core/rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,101 @@ suite('Rendering', function () {
assert.equal(g.pixelDensity(), expectedDensity);
});
});

suite('WebGPU to WebGL fallback', function () {
function fesMessages(fesSpy) {
return fesSpy.mock.calls.map(args => {
const [strings, ...values] = args;
return strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), '');
});
}

test('rendererType defaults to P2D and tracks WEBGL', function () {
myp5.createCanvas(10, 10);
assert.equal(myp5.rendererType, myp5.P2D);
myp5.createCanvas(10, 10, myp5.WEBGL);
assert.equal(myp5.rendererType, myp5.WEBGL);
assert.equal(myp5._renderer.rendererType, myp5.WEBGL);
});

test('falls back to WEBGL when the WEBGPU addon is missing', function () {
const saved = p5.renderers[myp5.WEBGPU];
delete p5.renderers[myp5.WEBGPU];
const fesSpy = vi.spyOn(p5.FES, 'log');
try {
const renderer = myp5.createCanvas(10, 10, myp5.WEBGPU);
assert.equal(renderer.rendererType, myp5.WEBGL);
assert.equal(myp5.rendererType, myp5.WEBGL);
const messages = fesMessages(fesSpy).join('\n');
assert.isTrue(messages.includes('add-on'));
assert.isTrue(messages.includes('Falling back to WebGL'));
} finally {
if (saved) {
p5.renderers[myp5.WEBGPU] = saved;
}
fesSpy.mockRestore();
}
});

test('falls back to WEBGL when WEBGPU context creation fails', async function () {
const failureReason =
'No compatible GPU could be found for WebGPU on this device, so p5 cannot start it here.';
class FailingWebGPU {
constructor(pInst) {
this._pInst = pInst;
this.canvas = document.createElement('canvas');
this.elt = this.canvas;
this.contextReady = Promise.reject(new Error(failureReason));
}
remove() {}
_applyDefaults() {}
}
const saved = p5.renderers[myp5.WEBGPU];
p5.renderers[myp5.WEBGPU] = FailingWebGPU;
const fesSpy = vi.spyOn(p5.FES, 'log');
try {
const renderer = await myp5.createCanvas(10, 10, myp5.WEBGPU);
assert.instanceOf(renderer, p5.RendererGL);
assert.equal(myp5.rendererType, myp5.WEBGL);
assert.isFalse(
myp5._elements.some(element => element instanceof FailingWebGPU)
);
const messages = fesMessages(fesSpy).join('\n');
assert.isTrue(messages.includes('Falling back to WebGL'));
assert.isTrue(messages.includes('No compatible GPU'));
} finally {
if (saved) {
p5.renderers[myp5.WEBGPU] = saved;
} else {
delete p5.renderers[myp5.WEBGPU];
}
fesSpy.mockRestore();
}
});

test('non-WEBGPU context failure still throws', async function () {
class FailingFake {
constructor() {
this.canvas = document.createElement('canvas');
this.elt = this.canvas;
this.contextReady = Promise.reject(new Error('boom'));
}
remove() {}
_applyDefaults() {}
}
p5.renderers['fake-fail'] = FailingFake;
try {
let error = null;
try {
await myp5.createCanvas(10, 10, 'fake-fail');
} catch (e) {
error = e;
}
expect(error).to.exist;
expect(error.message).to.match(/Failed to create canvas/);
} finally {
delete p5.renderers['fake-fail'];
}
});
});
});
Loading
Loading