From 051df1e1cac7a4492e1fb6493634e3eee22fb2a6 Mon Sep 17 00:00:00 2001 From: Nixxx19 Date: Sun, 22 Feb 2026 17:45:38 +0530 Subject: [PATCH 1/2] fix: prevent browser freeze when tessellating >50k vertices --- src/webgl/p5.RendererGL.Immediate.js | 17 +++++++ test/unit/webgl/p5.RendererGL.js | 67 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 31ce48f630..915db04537 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -313,6 +313,23 @@ p5.RendererGL.prototype._processVertices = function(mode) { this.immediateMode.shapeMode !== constants.LINES; if (shouldTess) { + // libtess can't handle >65k vertices and will freeze the browser + const vertexCount = this.immediateMode.geometry.vertices.length; + const MAX_SAFE_TESSELATION_VERTICES = 50000; + + if (vertexCount > MAX_SAFE_TESSELATION_VERTICES) { + p5._friendlyError( + 'p5.js WebGL: Tessellation warning', + `Attempting to tessellate a shape with ${vertexCount} vertices. ` + + `Tessellation of shapes with more than ${MAX_SAFE_TESSELATION_VERTICES} vertices ` + + 'may cause the browser to freeze. Consider reducing the number of vertices ' + + 'or using a different shape mode (e.g., TRIANGLES, TRIANGLE_STRIP) instead of TESS.' + ); + // skip tessellation to prevent freeze, use TRIANGLE_FAN as fallback + this.immediateMode.shapeMode = constants.TRIANGLE_FAN; + return; + } + this._tesselateShape(); } }; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 3c7e3df1a2..26e0d24b84 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1686,6 +1686,73 @@ suite('p5.RendererGL', function() { done(); }); + test('TESS mode warns and prevents freeze for >50k vertices', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + var friendlyErrorSpy = sinon.spy(p5, '_friendlyError'); + + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex( + Math.random() * 10 - 5, + Math.random() * 10 - 5, + Math.random() * 10 - 5 + ); + } + renderer.endShape(); + + assert.isTrue( + friendlyErrorSpy.called, + 'p5._friendlyError should be called for large vertex count' + ); + + const warningCall = friendlyErrorSpy.getCalls().find(call => + call.args[0] && call.args[0].includes('Tessellation warning') + ); + assert.isDefined( + warningCall, + 'Tessellation warning should be shown' + ); + + assert.equal( + renderer.immediateMode.shapeMode, + myp5.TRIANGLE_FAN, + 'Shape mode should be changed to TRIANGLE_FAN when tessellation is skipped' + ); + + friendlyErrorSpy.restore(); + done(); + }); + + test('TESS mode works normally for <50k vertices', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + var friendlyErrorSpy = sinon.spy(p5, '_friendlyError'); + + // use a simple shape that tessellates quickly + renderer.beginShape(myp5.TESS); + renderer.vertex(-10, -10, 0); + renderer.vertex(10, -10, 0); + renderer.vertex(10, 10, 0); + renderer.vertex(-10, 10, 0); + renderer.endShape(myp5.CLOSE); + + const warningCall = friendlyErrorSpy.getCalls().find(call => + call.args[0] && call.args[0].includes('Tessellation warning') + ); + assert.isUndefined( + warningCall, + 'No tessellation warning should be shown for <50k vertices' + ); + + assert.equal( + renderer.immediateMode.shapeMode, + myp5.TRIANGLES, + 'Shape mode should be TRIANGLES after normal tessellation' + ); + + friendlyErrorSpy.restore(); + done(); + }); + test('TESS does not affect stroke colors', function(done) { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); From 61d9d172bcee5a090572eb23eb1df65afbca157d Mon Sep 17 00:00:00 2001 From: Nixxx19 Date: Tue, 17 Mar 2026 23:14:27 +0530 Subject: [PATCH 2/2] feat(webgl): prompt before tessellating very large shapes --- src/webgl/p5.RendererGL.Immediate.js | 34 ++++++---- src/webgl/p5.RendererGL.js | 5 ++ test/unit/webgl/p5.RendererGL.js | 98 ++++++++++++++++++++-------- 3 files changed, 95 insertions(+), 42 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 915db04537..6c443c87df 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -313,21 +313,27 @@ p5.RendererGL.prototype._processVertices = function(mode) { this.immediateMode.shapeMode !== constants.LINES; if (shouldTess) { - // libtess can't handle >65k vertices and will freeze the browser const vertexCount = this.immediateMode.geometry.vertices.length; - const MAX_SAFE_TESSELATION_VERTICES = 50000; - - if (vertexCount > MAX_SAFE_TESSELATION_VERTICES) { - p5._friendlyError( - 'p5.js WebGL: Tessellation warning', - `Attempting to tessellate a shape with ${vertexCount} vertices. ` + - `Tessellation of shapes with more than ${MAX_SAFE_TESSELATION_VERTICES} vertices ` + - 'may cause the browser to freeze. Consider reducing the number of vertices ' + - 'or using a different shape mode (e.g., TRIANGLES, TRIANGLE_STRIP) instead of TESS.' - ); - // skip tessellation to prevent freeze, use TRIANGLE_FAN as fallback - this.immediateMode.shapeMode = constants.TRIANGLE_FAN; - return; + const MAX_SAFE_TESSELLATION_VERTICES = 50000; + + if (vertexCount > MAX_SAFE_TESSELLATION_VERTICES) { + // If FES is disabled (or minified build), just run tessellation as-is. + // Otherwise, prompt the user once to decide whether to continue. + if (!p5.disableFriendlyErrors && !this._largeTessellationAcknowledged) { + const proceed = window.confirm( + '🌸 p5.js says:\n\n' + + `This shape has ${vertexCount} vertices. Tessellating shapes with this ` + + 'many vertices can be very slow and may cause your browser to become ' + + 'unresponsive.\n\n' + + 'Do you want to continue tessellating this shape?' + ); + if (!proceed) { + // User cancelled — draw nothing for this shape. + return; + } + // User approved — skip this prompt for the rest of the session. + this._largeTessellationAcknowledged = true; + } } this._tesselateShape(); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 27583e036a..ae6cdfb885 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -637,6 +637,11 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } }; + // Flag set to true once the user approves tessellating a large shape via + // the confirm() prompt. Once set, we skip the prompt for the rest of the + // session so the user is only interrupted once. + this._largeTessellationAcknowledged = false; + this.curStrokeWeight = 1; this.pointSize = this.curStrokeWeight; this.curStrokeCap = constants.ROUND; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 26e0d24b84..47d340d421 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1686,46 +1686,91 @@ suite('p5.RendererGL', function() { done(); }); - test('TESS mode warns and prevents freeze for >50k vertices', function(done) { + test('TESS mode prompts user before tessellating >50k vertices', function(done) { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); - var friendlyErrorSpy = sinon.spy(p5, '_friendlyError'); + // Stub confirm() so the user "cancels" — shape should draw nothing + var confirmStub = sinon.stub(window, 'confirm').returns(false); renderer.beginShape(myp5.TESS); for (let i = 0; i < 60000; i++) { - renderer.vertex( - Math.random() * 10 - 5, - Math.random() * 10 - 5, - Math.random() * 10 - 5 - ); + renderer.vertex(i % 100, Math.floor(i / 100), 0); } renderer.endShape(); assert.isTrue( - friendlyErrorSpy.called, - 'p5._friendlyError should be called for large vertex count' - ); - - const warningCall = friendlyErrorSpy.getCalls().find(call => - call.args[0] && call.args[0].includes('Tessellation warning') + confirmStub.called, + 'window.confirm should be called when vertex count exceeds threshold' ); - assert.isDefined( - warningCall, - 'Tessellation warning should be shown' + assert.isTrue( + confirmStub.args[0][0].includes('60000'), + 'confirm message should include the actual vertex count' ); - - assert.equal( + // Shape mode must NOT be changed to TRIANGLE_FAN — draw nothing on cancel + assert.notEqual( renderer.immediateMode.shapeMode, myp5.TRIANGLE_FAN, - 'Shape mode should be changed to TRIANGLE_FAN when tessellation is skipped' + 'Shape mode should not fall back to TRIANGLE_FAN when user cancels' ); - friendlyErrorSpy.restore(); + confirmStub.restore(); + done(); + }); + + test('TESS mode only prompts once when user approves large tessellation', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + // User approves on the first prompt + var confirmStub = sinon.stub(window, 'confirm').returns(true); + + // First large shape — should prompt + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.equal(confirmStub.callCount, 1, 'confirm should be called once on first large shape'); + assert.isTrue( + renderer._largeTessellationAcknowledged, + '_largeTessellationAcknowledged should be set after user approves' + ); + + // Second large shape — should NOT prompt again + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.equal(confirmStub.callCount, 1, 'confirm should not be called again after acknowledgement'); + + confirmStub.restore(); + done(); + }); + + test('TESS mode skips prompt when p5.disableFriendlyErrors is true', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + var confirmStub = sinon.stub(window, 'confirm').returns(false); + p5.disableFriendlyErrors = true; + + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.isFalse( + confirmStub.called, + 'window.confirm should not be called when p5.disableFriendlyErrors is true' + ); + + p5.disableFriendlyErrors = false; + confirmStub.restore(); done(); }); test('TESS mode works normally for <50k vertices', function(done) { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); - var friendlyErrorSpy = sinon.spy(p5, '_friendlyError'); + var confirmStub = sinon.stub(window, 'confirm').returns(false); // use a simple shape that tessellates quickly renderer.beginShape(myp5.TESS); @@ -1735,12 +1780,9 @@ suite('p5.RendererGL', function() { renderer.vertex(-10, 10, 0); renderer.endShape(myp5.CLOSE); - const warningCall = friendlyErrorSpy.getCalls().find(call => - call.args[0] && call.args[0].includes('Tessellation warning') - ); - assert.isUndefined( - warningCall, - 'No tessellation warning should be shown for <50k vertices' + assert.isFalse( + confirmStub.called, + 'window.confirm should not be called for shapes with fewer than 50k vertices' ); assert.equal( @@ -1749,7 +1791,7 @@ suite('p5.RendererGL', function() { 'Shape mode should be TRIANGLES after normal tessellation' ); - friendlyErrorSpy.restore(); + confirmStub.restore(); done(); });