Skip to content

Commit 46eac76

Browse files
authored
Merge pull request #8729 from Nixxx19/nityam/tessellation-freeze-fix-2.x
fix: prevent browser freeze when tessellating >50k vertices (dev-2.0)
2 parents 2eee170 + da1c0d6 commit 46eac76

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

src/core/p5.Renderer3D.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ export class Renderer3D extends Renderer {
222222
// Used by beginShape/endShape functions to construct a p5.Geometry
223223
this.shapeBuilder = new ShapeBuilder(this);
224224

225+
this._largeTessellationAcknowledged = false;
226+
225227
this.geometryBufferCache = new GeometryBufferCache(this);
226228

227229
this.curStrokeCap = constants.ROUND;
@@ -2008,6 +2010,10 @@ const webGPUAddonMessage = 'Add the WebGPU add-on to your project and pass WEBGP
20082010
function renderer3D(p5, fn) {
20092011
p5.Renderer3D = Renderer3D;
20102012

2013+
ShapeBuilder.prototype.friendlyErrorsDisabled = function() {
2014+
return Boolean(p5.disableFriendlyErrors);
2015+
};
2016+
20112017
/**
20122018
* Creates a <a href="#/p5/p5.StorageBuffer">`p5.StorageBuffer`</a>, which is
20132019
* a block of data that shaders can read from, and compute shaders

src/webgl/ShapeBuilder.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export class ShapeBuilder {
4545
this.bufferStrides = { ...INITIAL_BUFFER_STRIDES };
4646
}
4747

48+
friendlyErrorsDisabled() {
49+
return false;
50+
}
51+
4852
constructFromContours(shape, contours) {
4953
if (this._useUserVertexProperties){
5054
this._resetUserVertexProperties();
@@ -148,6 +152,27 @@ export class ShapeBuilder {
148152
}
149153

150154
if (this.shapeMode === constants.PATH) {
155+
const vertexCount = this.geometry.vertices.length;
156+
const MAX_SAFE_TESSELLATION_VERTICES = 50000;
157+
158+
if (
159+
vertexCount > MAX_SAFE_TESSELLATION_VERTICES &&
160+
!this.friendlyErrorsDisabled() &&
161+
!this.renderer._largeTessellationAcknowledged
162+
) {
163+
const proceed = window.confirm(
164+
'🌸 p5.js says:\n\n' +
165+
`This shape has ${vertexCount} vertices. Tessellating shapes with this ` +
166+
'many vertices can be very slow and may cause your browser to become ' +
167+
'unresponsive.\n\n' +
168+
'Do you want to continue tessellating this shape?'
169+
);
170+
if (!proceed) {
171+
return;
172+
}
173+
this.renderer._largeTessellationAcknowledged = true;
174+
}
175+
151176
this.isProcessingVertices = true;
152177
this._tesselateShape();
153178
this.isProcessingVertices = false;

test/unit/webgl/p5.RendererGL.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,98 @@ suite('p5.RendererGL', function() {
20322032
[-10, 0, 10]
20332033
);
20342034
});
2035+
2036+
suite('large tessellation guard', function() {
2037+
test('prompts user before tessellating >50k vertices', function() {
2038+
const renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
2039+
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
2040+
const tessSpy = vi.spyOn(
2041+
renderer.shapeBuilder,
2042+
'_tesselateShape'
2043+
).mockImplementation(() => {});
2044+
2045+
myp5.beginShape();
2046+
for (let i = 0; i < 60000; i++) {
2047+
myp5.vertex(i % 100, Math.floor(i / 100), 0);
2048+
}
2049+
myp5.endShape();
2050+
2051+
expect(confirmSpy).toHaveBeenCalled();
2052+
expect(confirmSpy.mock.calls[0][0]).toContain('60000');
2053+
expect(tessSpy).not.toHaveBeenCalled();
2054+
2055+
confirmSpy.mockRestore();
2056+
tessSpy.mockRestore();
2057+
});
2058+
2059+
test('only prompts once when user approves large tessellation', function() {
2060+
const renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
2061+
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
2062+
const tessSpy = vi.spyOn(
2063+
renderer.shapeBuilder,
2064+
'_tesselateShape'
2065+
).mockImplementation(() => {});
2066+
2067+
myp5.beginShape();
2068+
for (let i = 0; i < 60000; i++) {
2069+
myp5.vertex(i % 100, Math.floor(i / 100), 0);
2070+
}
2071+
myp5.endShape();
2072+
2073+
expect(confirmSpy).toHaveBeenCalledTimes(1);
2074+
expect(renderer._largeTessellationAcknowledged).toBe(true);
2075+
2076+
myp5.beginShape();
2077+
for (let i = 0; i < 60000; i++) {
2078+
myp5.vertex(i % 100, Math.floor(i / 100), 0);
2079+
}
2080+
myp5.endShape();
2081+
2082+
expect(confirmSpy).toHaveBeenCalledTimes(1);
2083+
2084+
confirmSpy.mockRestore();
2085+
tessSpy.mockRestore();
2086+
});
2087+
2088+
test('skips prompt when p5.disableFriendlyErrors is true', function() {
2089+
const renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
2090+
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
2091+
const tessSpy = vi.spyOn(
2092+
renderer.shapeBuilder,
2093+
'_tesselateShape'
2094+
).mockImplementation(() => {});
2095+
p5.disableFriendlyErrors = true;
2096+
2097+
myp5.beginShape();
2098+
for (let i = 0; i < 60000; i++) {
2099+
myp5.vertex(i % 100, Math.floor(i / 100), 0);
2100+
}
2101+
myp5.endShape();
2102+
2103+
expect(confirmSpy).not.toHaveBeenCalled();
2104+
expect(tessSpy).toHaveBeenCalled();
2105+
2106+
p5.disableFriendlyErrors = false;
2107+
confirmSpy.mockRestore();
2108+
tessSpy.mockRestore();
2109+
});
2110+
2111+
test('works normally for <50k vertices', function() {
2112+
const renderer = myp5.createCanvas(10, 10, myp5.WEBGL);
2113+
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
2114+
2115+
myp5.beginShape();
2116+
myp5.vertex(-10, -10, 0);
2117+
myp5.vertex(10, -10, 0);
2118+
myp5.vertex(10, 10, 0);
2119+
myp5.vertex(-10, 10, 0);
2120+
myp5.endShape(myp5.CLOSE);
2121+
2122+
expect(confirmSpy).not.toHaveBeenCalled();
2123+
2124+
confirmSpy.mockRestore();
2125+
});
2126+
});
20352127
});
20362128

20372129
suite('color interpolation', function() {

0 commit comments

Comments
 (0)