diff --git a/src/app.js b/src/app.js index 763ed82a15..1bbb7d1be2 100644 --- a/src/app.js +++ b/src/app.js @@ -88,6 +88,7 @@ import './webgl/light'; import './webgl/loading'; import './webgl/material'; import './webgl/p5.Camera'; +import './webgl/p5.DataArray'; import './webgl/p5.Geometry'; import './webgl/p5.Matrix'; import './webgl/p5.RendererGL.Immediate'; diff --git a/src/webgl/p5.DataArray.js b/src/webgl/p5.DataArray.js new file mode 100644 index 0000000000..9ab0c2eaf2 --- /dev/null +++ b/src/webgl/p5.DataArray.js @@ -0,0 +1,110 @@ +import p5 from '../core/main'; + +/** + * An internal class to store data that will be sent to a p5.RenderBuffer. + * Those need to eventually go into a Float32Array, so this class provides a + * variable-length array container backed by a Float32Array so that it can be + * sent to the GPU without allocating a new array each frame. + * + * Like a C++ vector, its fixed-length Float32Array backing its contents will + * double in size when it goes over its capacity. + * + * @example + *
+ * + * // Initialize storage with a capacity of 4 + * const storage = new DataArray(4); + * console.log(storage.data.length); // 4 + * console.log(storage.length); // 0 + * console.log(storage.dataArray()); // Empty Float32Array + * + * storage.push(1, 2, 3, 4, 5, 6); + * console.log(storage.data.length); // 8 + * console.log(storage.length); // 6 + * console.log(storage.dataArray()); // Float32Array{1, 2, 3, 4, 5, 6} + * + *
+ */ +p5.DataArray = class DataArray { + constructor(initialLength = 128) { + this.length = 0; + this.data = new Float32Array(initialLength); + this.initialLength = initialLength; + } + + /** + * Returns a Float32Array window sized to the exact length of the data + */ + dataArray() { + return this.subArray(0, this.length); + } + + /** + * A "soft" clear, which keeps the underlying storage size the same, but + * empties the contents of its dataArray() + */ + clear() { + this.length = 0; + } + + /** + * Can be used to scale a DataArray back down to fit its contents. + */ + rescale() { + if (this.length < this.data.length / 2) { + // Find the power of 2 size that fits the data + const targetLength = 1 << Math.ceil(Math.log2(this.length)); + const newData = new Float32Array(targetLength); + newData.set(this.data.subarray(0, this.length), 0); + this.data = newData; + } + } + + /** + * A full reset, which allocates a new underlying Float32Array at its initial + * length + */ + reset() { + this.clear(); + this.data = new Float32Array(this.initialLength); + } + + /** + * Adds values to the DataArray, expanding its internal storage to + * accommodate the new items. + */ + push(...values) { + this.ensureLength(this.length + values.length); + this.data.set(values, this.length); + this.length += values.length; + } + + /** + * Returns a copy of the data from the index `from`, inclusive, to the index + * `to`, exclusive + */ + slice(from, to) { + return this.data.slice(from, Math.min(to, this.length)); + } + + /** + * Returns a mutable Float32Array window from the index `from`, inclusive, to + * the index `to`, exclusive + */ + subArray(from, to) { + return this.data.subarray(from, Math.min(to, this.length)); + } + + /** + * Expand capacity of the internal storage until it can fit a target size + */ + ensureLength(target) { + while (this.data.length < target) { + const newData = new Float32Array(this.data.length * 2); + newData.set(this.data, 0); + this.data = newData; + } + } +}; + +export default p5.DataArray; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index ef63ea5c96..768a4b2cf7 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -17,28 +17,28 @@ import p5 from '../core/main'; * @param {Integer} [detailY] number of vertices along the y-axis. * @param {function} [callback] function to call upon object instantiation. */ -p5.Geometry = class { +p5.Geometry = class Geometry { constructor(detailX, detailY, callback){ //an array containing every vertex //@type [p5.Vector] this.vertices = []; //an array containing every vertex for stroke drawing - this.lineVertices = []; + this.lineVertices = new p5.DataArray(); // The tangents going into or out of a vertex on a line. Along a straight // line segment, both should be equal. At an endpoint, one or the other // will not exist and will be all 0. In joins between line segments, they // may be different, as they will be the tangents on either side of the join. - this.lineTangentsIn = []; - this.lineTangentsOut = []; + this.lineTangentsIn = new p5.DataArray(); + this.lineTangentsOut = new p5.DataArray(); // When drawing lines with thickness, entries in this buffer represent which // side of the centerline the vertex will be placed. The sign of the number // will represent the side of the centerline, and the absolute value will be // used as an enum to determine which part of the cap or join each vertex // represents. See the doc comments for _addCap and _addJoin for diagrams. - this.lineSides = []; + this.lineSides = new p5.DataArray(); //an array containing 1 normal per vertex //@type [p5.Vector] @@ -60,7 +60,7 @@ p5.Geometry = class { // One color per line vertex, generated automatically based on // vertexStrokeColors in _edgesToVertices() - this.lineVertexColors = []; + this.lineVertexColors = new p5.DataArray(); this.detailX = detailX !== undefined ? detailX : 1; this.detailY = detailY !== undefined ? detailY : 1; this.dirtyFlags = {}; @@ -72,16 +72,16 @@ p5.Geometry = class { } reset() { - this.lineVertices.length = 0; - this.lineTangentsIn.length = 0; - this.lineTangentsOut.length = 0; - this.lineSides.length = 0; + this.lineVertices.clear(); + this.lineTangentsIn.clear(); + this.lineTangentsOut.clear(); + this.lineSides.clear(); this.vertices.length = 0; this.edges.length = 0; this.vertexColors.length = 0; this.vertexStrokeColors.length = 0; - this.lineVertexColors.length = 0; + this.lineVertexColors.clear(); this.vertexNormals.length = 0; this.uvs.length = 0; @@ -261,10 +261,10 @@ p5.Geometry = class { * @chainable */ _edgesToVertices() { - this.lineVertices.length = 0; - this.lineTangentsIn.length = 0; - this.lineTangentsOut.length = 0; - this.lineSides.length = 0; + this.lineVertices.clear(); + this.lineTangentsIn.clear(); + this.lineTangentsOut.clear(); + this.lineSides.clear(); const potentialCaps = new Map(); const connected = new Set(); @@ -409,18 +409,20 @@ p5.Geometry = class { const a = begin.array(); const b = end.array(); const dirArr = dir.array(); - this.lineSides.push(1, -1, 1, 1, -1, -1); + this.lineSides.push(1, 1, -1, 1, -1, -1); for (const tangents of [this.lineTangentsIn, this.lineTangentsOut]) { - tangents.push(dirArr, dirArr, dirArr, dirArr, dirArr, dirArr); + for (let i = 0; i < 6; i++) { + tangents.push(...dirArr); + } } - this.lineVertices.push(a, a, b, b, a, b); + this.lineVertices.push(...a, ...b, ...a, ...b, ...b, ...a); this.lineVertexColors.push( - fromColor, - fromColor, - toColor, - toColor, - fromColor, - toColor + ...fromColor, + ...toColor, + ...fromColor, + ...toColor, + ...toColor, + ...fromColor ); return this; } @@ -446,12 +448,12 @@ p5.Geometry = class { const tanInArray = tangent.array(); const tanOutArray = [0, 0, 0]; for (let i = 0; i < 6; i++) { - this.lineVertices.push(ptArray); - this.lineTangentsIn.push(tanInArray); - this.lineTangentsOut.push(tanOutArray); - this.lineVertexColors.push(color); + this.lineVertices.push(...ptArray); + this.lineTangentsIn.push(...tanInArray); + this.lineTangentsOut.push(...tanOutArray); + this.lineVertexColors.push(...color); } - this.lineSides.push(-1, -2, 2, 2, 1, -1); + this.lineSides.push(-1, 2, -2, 1, 2, -1); return this; } @@ -488,14 +490,13 @@ p5.Geometry = class { const tanInArray = fromTangent.array(); const tanOutArray = toTangent.array(); for (let i = 0; i < 12; i++) { - this.lineVertices.push(ptArray); - this.lineTangentsIn.push(tanInArray); - this.lineTangentsOut.push(tanOutArray); - this.lineVertexColors.push(color); - } - for (const side of [-1, 1]) { - this.lineSides.push(side, 2 * side, 3 * side, side, 3 * side, 0); + this.lineVertices.push(...ptArray); + this.lineTangentsIn.push(...tanInArray); + this.lineTangentsOut.push(...tanOutArray); + this.lineVertexColors.push(...color); } + this.lineSides.push(-1, -3, -2, -1, 0, -3); + this.lineSides.push(3, 1, 2, 3, 0, 1); return this; } diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 246bc8fa29..79851e485f 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -61,11 +61,11 @@ p5.RenderBuffer = class { shader.enableAttrib(attr, this.size); } else { const loc = attr.location; - if (loc === -1 || !this._renderer.registerEnabled[loc]) { return; } + if (loc === -1 || !this._renderer.registerEnabled.has(loc)) { return; } // Disable register corresponding to unused attribute gl.disableVertexAttribArray(loc); // Record register availability - this._renderer.registerEnabled[loc] = false; + this._renderer.registerEnabled.delete(loc); } } }; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index a3d771e549..5508401adc 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -226,7 +226,7 @@ p5.RendererGL.prototype.endShape = function( if (this._doFill) { if ( !this.geometryBuilder && - this.immediateMode.geometry.vertices.length > 1 + this.immediateMode.geometry.vertices.length >= 3 ) { this._drawImmediateFill(); } @@ -234,7 +234,7 @@ p5.RendererGL.prototype.endShape = function( if (this._doStroke) { if ( !this.geometryBuilder && - this.immediateMode.geometry.lineVertices.length > 1 + this.immediateMode.geometry.lineVertices.length >= 1 ) { this._drawImmediateStroke(); } @@ -477,6 +477,7 @@ p5.RendererGL.prototype._drawImmediateFill = function() { for (const buff of this.immediateMode.buffers.fill) { buff._prepareBuffer(this.immediateMode.geometry, shader); } + shader.disableRemainingAttributes(); this._applyColorBlend(this.curFillColor); @@ -499,25 +500,19 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { this._useLineColor = (this.immediateMode.geometry.vertexStrokeColors.length > 0); - const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE); - // Prevent strokes from getting removed by culling - gl.disable(gl.CULL_FACE); - const shader = this._getImmediateStrokeShader(); this._setStrokeUniforms(shader); for (const buff of this.immediateMode.buffers.stroke) { buff._prepareBuffer(this.immediateMode.geometry, shader); } + shader.disableRemainingAttributes(); this._applyColorBlend(this.curStrokeColor); gl.drawArrays( gl.TRIANGLES, 0, - this.immediateMode.geometry.lineVertices.length + this.immediateMode.geometry.lineVertices.length / 3 ); - if (faceCullingEnabled) { - gl.enable(gl.CULL_FACE); - } shader.unbindShader(); }; diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 308b7310ce..d278d51428 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -5,8 +5,6 @@ import './p5.RendererGL'; import './p5.RenderBuffer'; import * as constants from '../core/constants'; -let hashCount = 0; - /** * @param {p5.Geometry} geometry The model whose resources will be freed */ @@ -30,11 +28,9 @@ p5.RendererGL.prototype._initBufferDefaults = function(gId) { this._freeBuffers(gId); //@TODO remove this limit on hashes in retainedMode.geometry - hashCount++; - if (hashCount > 1000) { + if (Object.keys(this.retainedMode.geometry).length > 1000) { const key = Object.keys(this.retainedMode.geometry)[0]; - delete this.retainedMode.geometry[key]; - hashCount--; + this._freeBuffers(key); } //create a new entry in our retainedMode.geometry @@ -48,7 +44,6 @@ p5.RendererGL.prototype._freeBuffers = function(gId) { } delete this.retainedMode.geometry[gId]; - hashCount--; const gl = this.GL; if (buffers.indexBuffer) { @@ -115,7 +110,9 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { buffers.vertexCount = model.vertices ? model.vertices.length : 0; } - buffers.lineVertexCount = model.lineVertices ? model.lineVertices.length : 0; + buffers.lineVertexCount = model.lineVertices + ? model.lineVertices.length / 3 + : 0; return buffers; }; @@ -130,13 +127,18 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { const gl = this.GL; const geometry = this.retainedMode.geometry[gId]; - if (!this.geometryBuilder && this._doFill) { + if ( + !this.geometryBuilder && + this._doFill && + this.retainedMode.geometry[gId].vertexCount > 0 + ) { this._useVertexColor = (geometry.model.vertexColors.length > 0); const fillShader = this._getRetainedFillShader(); this._setFillUniforms(fillShader); for (const buff of this.retainedMode.buffers.fill) { buff._prepareBuffer(geometry, fillShader); } + fillShader.disableRemainingAttributes(); if (geometry.indexBuffer) { //vertex index buffer this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); @@ -148,19 +150,14 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { if (!this.geometryBuilder && this._doStroke && geometry.lineVertexCount > 0) { this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); - const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE); - // Prevent strokes from getting removed by culling - gl.disable(gl.CULL_FACE); const strokeShader = this._getRetainedStrokeShader(); this._setStrokeUniforms(strokeShader); for (const buff of this.retainedMode.buffers.stroke) { buff._prepareBuffer(geometry, strokeShader); } + strokeShader.disableRemainingAttributes(); this._applyColorBlend(this.curStrokeColor); this._drawArrays(gl.TRIANGLES, gId); - if (faceCullingEnabled) { - gl.enable(gl.CULL_FACE); - } strokeShader.unbindShader(); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 35a809c33f..b2d8150332 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -473,7 +473,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this._useLineColor = false; this._useVertexColor = false; - this.registerEnabled = []; + this.registerEnabled = new Set(); this._tint = [255, 255, 255, 255]; @@ -525,10 +525,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer { geometry: {}, buffers: { stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this, this._flatten), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this, this._flatten), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) ], fill: [ @@ -564,10 +564,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer { new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) ], stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this, this._flatten), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this, this._flatten), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) ], point: this.GL.createBuffer() @@ -1823,7 +1823,12 @@ p5.RendererGL = class RendererGL extends p5.Renderer { if (!target) target = this.GL.ARRAY_BUFFER; this.GL.bindBuffer(target, buffer); if (values !== undefined) { - const data = new (type || Float32Array)(values); + let data = values; + if (values instanceof p5.DataArray) { + data = values.dataArray(); + } else if (!(data instanceof (type || Float32Array))) { + data = new (type || Float32Array)(data); + } this.GL.bufferData(target, data, usage || this.GL.STATIC_DRAW); } } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 33072e9616..170c3f9954 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -582,10 +582,10 @@ p5.Shader = class { if (loc !== -1) { const gl = this._renderer.GL; // Enable register even if it is disabled - if (!this._renderer.registerEnabled[loc]) { + if (!this._renderer.registerEnabled.has(loc)) { gl.enableVertexAttribArray(loc); // Record register availability - this._renderer.registerEnabled[loc] = true; + this._renderer.registerEnabled.add(loc); } this._renderer.GL.vertexAttribPointer( loc, @@ -599,6 +599,26 @@ p5.Shader = class { } return this; } + + /** + * Once all buffers have been bound, this checks to see if there are any + * remaining active attributes, likely left over from previous renders, + * and disables them so that they don't affect rendering. + * @method disableRemainingAttributes + * @private + */ + disableRemainingAttributes() { + for (const location of this._renderer.registerEnabled.values()) { + if ( + !Object.keys(this.attributes).some( + key => this.attributes[key].location === location + ) + ) { + this._renderer.GL.disableVertexAttribArray(location); + this._renderer.registerEnabled.delete(location); + } + } + } }; export default p5.Shader; diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index 0b39a4b95e..2dddf8e33e 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -175,7 +175,6 @@ suite('p5.Geometry', function() { // Geometry mode myp5.fill(255); const geom = myp5.buildGeometry(drawGeometry); - console.log(geom); myp5.background(255); myp5.push(); applyLights(); @@ -257,5 +256,40 @@ suite('p5.Geometry', function() { myp5.pop(); }, [checkLights]); }); + + test('freeGeometry() cleans up resources', function() { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.pixelDensity(1); + + const drawShape = () => { + myp5.fill('blue'); + myp5.stroke(0); + myp5.beginShape(myp5.QUAD_STRIP); + myp5.vertex(-5, -5); + myp5.vertex(5, -5); + myp5.vertex(-5, 5); + myp5.vertex(5, 5); + myp5.endShape(); + }; + + const geom = myp5.buildGeometry(drawShape); + + myp5.background('red'); + myp5.fill('blue'); + myp5.stroke(0); + myp5.model(geom); + assert.deepEqual(myp5.get(5, 5), [0, 0, 255, 255]); + + // Using immediate mode after freeing the geometry should work + myp5.freeGeometry(geom); + myp5.background('red'); + drawShape(); + assert.deepEqual(myp5.get(5, 5), [0, 0, 255, 255]); + + // You can still draw the geometry even after freeing it + myp5.background('red'); + myp5.model(geom); + assert.deepEqual(myp5.get(5, 5), [0, 0, 255, 255]); + }); }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 8523e48476..7c78cedfd4 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1847,13 +1847,13 @@ suite('p5.RendererGL', function() { const attributes = renderer._curShader.attributes; const loc = attributes.aTexCoord.location; - assert.equal(renderer.registerEnabled[loc], true); + assert.equal(renderer.registerEnabled.has(loc), true); myp5.model(myGeom); - assert.equal(renderer.registerEnabled[loc], false); + assert.equal(renderer.registerEnabled.has(loc), false); myp5.triangle(-8, -8, 8, 8, -8, 8); - assert.equal(renderer.registerEnabled[loc], true); + assert.equal(renderer.registerEnabled.has(loc), true); done(); });