Skip to content

Commit c004b41

Browse files
authored
Merge pull request #8555 from Nixxx19/nityam/tessellation-freeze-fix
fix: prevent browser freeze when tessellating >50k vertices (#8219 )
2 parents cd6a3d4 + 0b6f376 commit c004b41

3 files changed

Lines changed: 134 additions & 0 deletions

File tree

src/webgl/p5.RendererGL.Immediate.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,25 @@ p5.RendererGL.prototype._processVertices = function(mode) {
313313
this.immediateMode.shapeMode !== constants.LINES;
314314

315315
if (shouldTess) {
316+
const vertexCount = this.immediateMode.geometry.vertices.length;
317+
const MAX_SAFE_TESSELLATION_VERTICES = 50000;
318+
319+
if (vertexCount > MAX_SAFE_TESSELLATION_VERTICES) {
320+
if (!p5.disableFriendlyErrors && !this._largeTessellationAcknowledged) {
321+
const proceed = window.confirm(
322+
'🌸 p5.js says:\n\n' +
323+
`This shape has ${vertexCount} vertices. Tessellating shapes with this ` +
324+
'many vertices can be very slow and may cause your browser to become ' +
325+
'unresponsive.\n\n' +
326+
'Do you want to continue tessellating this shape?'
327+
);
328+
if (!proceed) {
329+
return;
330+
}
331+
this._largeTessellationAcknowledged = true;
332+
}
333+
}
334+
316335
this._tesselateShape();
317336
}
318337
};

src/webgl/p5.RendererGL.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
637637
}
638638
};
639639

640+
this._largeTessellationAcknowledged = false;
641+
640642
this.curStrokeWeight = 1;
641643
this.pointSize = this.curStrokeWeight;
642644
this.curStrokeCap = constants.ROUND;

test/unit/webgl/p5.RendererGL.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,119 @@ suite('p5.RendererGL', function() {
16861686
done();
16871687
});
16881688

1689+
test('TESS mode prompts user before tessellating >50k vertices', function(done) {
1690+
var renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
1691+
var confirmStub = sinon.stub(window, 'confirm').returns(false);
1692+
1693+
renderer.beginShape(myp5.TESS);
1694+
for (let i = 0; i < 60000; i++) {
1695+
renderer.vertex(i % 100, Math.floor(i / 100), 0);
1696+
}
1697+
renderer.endShape();
1698+
1699+
assert.isTrue(
1700+
confirmStub.called,
1701+
'window.confirm should be called when vertex count exceeds threshold'
1702+
);
1703+
assert.isTrue(
1704+
confirmStub.args[0][0].includes('60000'),
1705+
'confirm message should include the actual vertex count'
1706+
);
1707+
// Shape mode must NOT be changed to TRIANGLE_FAN — draw nothing on cancel
1708+
assert.notEqual(
1709+
renderer.immediateMode.shapeMode,
1710+
myp5.TRIANGLE_FAN,
1711+
'Shape mode should not fall back to TRIANGLE_FAN when user cancels'
1712+
);
1713+
1714+
confirmStub.restore();
1715+
done();
1716+
});
1717+
1718+
test('TESS mode only prompts once when user approves large tessellation', function(done) {
1719+
var renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
1720+
var confirmStub = sinon.stub(window, 'confirm').returns(true);
1721+
var tessellateStub = sinon.stub(renderer, '_tesselateShape');
1722+
1723+
renderer.beginShape(myp5.TESS);
1724+
for (let i = 0; i < 60000; i++) {
1725+
renderer.vertex(i % 100, Math.floor(i / 100), 0);
1726+
}
1727+
renderer.endShape();
1728+
1729+
assert.equal(confirmStub.callCount, 1, 'confirm should be called once on first large shape');
1730+
assert.isTrue(
1731+
renderer._largeTessellationAcknowledged,
1732+
'_largeTessellationAcknowledged should be set after user approves'
1733+
);
1734+
1735+
renderer.beginShape(myp5.TESS);
1736+
for (let i = 0; i < 60000; i++) {
1737+
renderer.vertex(i % 100, Math.floor(i / 100), 0);
1738+
}
1739+
renderer.endShape();
1740+
1741+
assert.equal(confirmStub.callCount, 1, 'confirm should not be called again after acknowledgement');
1742+
1743+
confirmStub.restore();
1744+
tessellateStub.restore();
1745+
done();
1746+
});
1747+
1748+
test('TESS mode skips prompt when p5.disableFriendlyErrors is true', function(done) {
1749+
var renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
1750+
var confirmStub = sinon.stub(window, 'confirm').returns(false);
1751+
var tessellateStub = sinon.stub(renderer, '_tesselateShape');
1752+
p5.disableFriendlyErrors = true;
1753+
1754+
renderer.beginShape(myp5.TESS);
1755+
for (let i = 0; i < 60000; i++) {
1756+
renderer.vertex(i % 100, Math.floor(i / 100), 0);
1757+
}
1758+
renderer.endShape();
1759+
1760+
assert.isFalse(
1761+
confirmStub.called,
1762+
'window.confirm should not be called when p5.disableFriendlyErrors is true'
1763+
);
1764+
assert.isTrue(
1765+
tessellateStub.called,
1766+
'tessellation should proceed without prompt when p5.disableFriendlyErrors is true'
1767+
);
1768+
1769+
p5.disableFriendlyErrors = false;
1770+
confirmStub.restore();
1771+
tessellateStub.restore();
1772+
done();
1773+
});
1774+
1775+
test('TESS mode works normally for <50k vertices', function(done) {
1776+
var renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
1777+
var confirmStub = sinon.stub(window, 'confirm').returns(false);
1778+
1779+
// use a simple shape that tessellates quickly
1780+
renderer.beginShape(myp5.TESS);
1781+
renderer.vertex(-10, -10, 0);
1782+
renderer.vertex(10, -10, 0);
1783+
renderer.vertex(10, 10, 0);
1784+
renderer.vertex(-10, 10, 0);
1785+
renderer.endShape(myp5.CLOSE);
1786+
1787+
assert.isFalse(
1788+
confirmStub.called,
1789+
'window.confirm should not be called for shapes with fewer than 50k vertices'
1790+
);
1791+
1792+
assert.equal(
1793+
renderer.immediateMode.shapeMode,
1794+
myp5.TRIANGLES,
1795+
'Shape mode should be TRIANGLES after normal tessellation'
1796+
);
1797+
1798+
confirmStub.restore();
1799+
done();
1800+
});
1801+
16891802
test('TESS does not affect stroke colors', function(done) {
16901803
var renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
16911804

0 commit comments

Comments
 (0)