diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js new file mode 100644 index 0000000000..406ac8c7f2 --- /dev/null +++ b/src/ol/render/webgl/BatchRenderer.js @@ -0,0 +1,197 @@ +/** + * @module ol/render/webgl/BatchRenderer + */ +import GeometryType from '../../geom/GeometryType.js'; +import { + create as createTransform, + makeInverse as makeInverseTransform, + multiply as multiplyTransform, +} from '../../transform.js'; +import {abstract} from '../../util.js'; +import {WebGLWorkerMessageType} from './constants.js'; + +/** + * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different + * for each feature. + * @property {string} name Attribute name. + * @property {function(import("../../Feature").default, Object):number} callback This callback computes the numerical value of the + * attribute for a given feature (properties are available as 2nd arg for quicker access). + */ + +let workerMessageCounter = 0; + +/** + * @classdesc Abstract class for batch renderers. + * Batch renderers are meant to render the geometries contained in a {@link module:ol/render/webgl/GeometryBatch} + * instance. They are responsible for generating render instructions and transforming them into WebGL buffers. + */ +class AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + constructor(helper, worker, vertexShader, fragmentShader, customAttributes) { + /** + * @type {import("../../webgl/Helper.js").default} + * @protected + */ + this.helper_ = helper; + + /** + * @type {Worker} + * @protected + */ + this.worker_ = worker; + + /** + * @type {WebGLProgram} + * @protected + */ + this.program_ = this.helper_.getProgram(fragmentShader, vertexShader); + + /** + * A list of attributes used by the renderer. + * @type {Array} + * @protected + */ + this.attributes_ = []; + + /** + * @type {Array} + * @protected + */ + this.customAttributes_ = customAttributes; + } + + /** + * Rebuild rendering instructions and webgl buffers based on the provided frame state + * Note: this is a costly operation. + * @param {import("./MixedGeometryBatch.js").AbstractGeometryBatch} batch + * @param {import("../../PluggableMap").FrameState} frameState Frame state. + * @param {import("../../geom/GeometryType.js").default} geometryType + */ + rebuild(batch, frameState, geometryType) { + // store transform for rendering instructions + batch.renderInstructionsTransform = this.helper_.makeProjectionTransform( + frameState, + createTransform() + ); + this.generateRenderInstructions_(batch); + this.generateBuffers_(batch, geometryType); + } + + /** + * Render the geometries in the batch. This will also update the current transform used for rendering according to + * the invert transform of the webgl buffers + * @param {import("./MixedGeometryBatch.js").AbstractGeometryBatch} batch + * @param {import("../../transform.js").Transform} currentTransform + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + */ + render(batch, currentTransform, frameState) { + // multiply the current projection transform with the invert of the one used to fill buffers + this.helper_.makeProjectionTransform(frameState, currentTransform); + multiplyTransform(currentTransform, batch.invertVerticesBufferTransform); + + // enable program, buffers and attributes + this.helper_.useProgram(this.program_, frameState); + this.helper_.bindBuffer(batch.verticesBuffer); + this.helper_.bindBuffer(batch.indicesBuffer); + this.helper_.enableAttributes(this.attributes_); + + const renderCount = batch.indicesBuffer.getSize(); + this.helper_.drawElements(0, renderCount); + } + + /** + * Rebuild rendering instructions based on the provided frame state + * This is specific to the geometry type and has to be implemented by subclasses. + * @param {import("./MixedGeometryBatch.js").default} batch + * @protected + */ + generateRenderInstructions_(batch) { + abstract(); + } + + /** + * Rebuild internal webgl buffers for rendering based on the current rendering instructions; + * This is asynchronous: webgl buffers wil _not_ be updated right away + * @param {import("./MixedGeometryBatch.js").AbstractGeometryBatch} batch + * @param {import("../../geom/GeometryType.js").default} geometryType + * @protected + */ + generateBuffers_(batch, geometryType) { + const messageId = workerMessageCounter++; + + let messageType; + switch (geometryType) { + case GeometryType.POLYGON: + messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS; + break; + case GeometryType.POINT: + messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS; + break; + case GeometryType.LINE_STRING: + messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS; + break; + } + + /** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */ + const message = { + id: messageId, + type: messageType, + renderInstructions: batch.renderInstructions.buffer, + renderInstructionsTransform: batch.renderInstructionsTransform, + customAttributesCount: this.customAttributes_.length, + }; + // additional properties will be sent back as-is by the worker + message['projectionTransform'] = batch.renderInstructionsTransform; + this.worker_.postMessage(message, [batch.renderInstructions.buffer]); + + // leave ownership of render instructions + batch.renderInstructions = null; + + const handleMessage = + /** + * @param {*} event Event. + * @this {AbstractBatchRenderer} + */ + function (event) { + const received = event.data; + + // this is not the response to our request: skip + if (received.id !== messageId) { + return; + } + + // we've received our response: stop listening + this.worker_.removeEventListener('message', handleMessage); + + // store transform & invert transform for webgl buffers + batch.verticesBufferTransform = received.projectionTransform; + makeInverseTransform( + batch.invertVerticesBufferTransform, + batch.verticesBufferTransform + ); + + // copy & flush received buffers to GPU + batch.verticesBuffer.fromArrayBuffer(received.vertexBuffer); + this.helper_.flushBufferData(batch.verticesBuffer); + batch.indicesBuffer.fromArrayBuffer(received.indexBuffer); + this.helper_.flushBufferData(batch.indicesBuffer); + + // take back ownership of the render instructions for further use + batch.renderInstructions = new Float32Array( + received.renderInstructions + ); + + // TODO: call layer.changed somehow for the layer to rerender!!!1 + }.bind(this); + + this.worker_.addEventListener('message', handleMessage); + } +} + +export default AbstractBatchRenderer; diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js new file mode 100644 index 0000000000..31ef0e4e6a --- /dev/null +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -0,0 +1,108 @@ +/** + * @module ol/render/webgl/LineStringBatchRenderer + */ +import {AttributeType} from '../../webgl/Helper.js'; +import {transform2D} from '../../geom/flat/transform.js'; +import AbstractBatchRenderer from './BatchRenderer.js'; + +class LineStringBatchRenderer extends AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + constructor(helper, worker, vertexShader, fragmentShader, customAttributes) { + super(helper, worker, vertexShader, fragmentShader, customAttributes); + + // vertices for lines must hold both a position (x,y) and an offset (dx,dy) + this.attributes_ = [ + { + name: 'a_segmentStart', + size: 2, + type: AttributeType.FLOAT, + }, + { + name: 'a_segmentEnd', + size: 2, + type: AttributeType.FLOAT, + }, + { + name: 'a_parameters', + size: 1, + type: AttributeType.FLOAT, + }, + ].concat( + customAttributes.map(function (attribute) { + return { + name: 'a_' + attribute.name, + size: 1, + type: AttributeType.FLOAT, + }; + }) + ); + } + + /** + * Render instructions for lines are structured like so: + * [ customAttr0, ... , customAttrN, numberOfVertices0, x0, y0, ... , xN, yN, numberOfVertices1, ... ] + * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch + * @override + */ + generateRenderInstructions_(batch) { + // here we anticipate the amount of render instructions for lines: + // 2 instructions per vertex for position (x and y) + // + 1 instruction per line per custom attributes + // + 1 instruction per line (for vertices count) + const totalInstructionsCount = + 2 * batch.verticesCount + + (1 + this.customAttributes_.length) * batch.geometriesCount; + if ( + !batch.renderInstructions || + batch.renderInstructions.length !== totalInstructionsCount + ) { + batch.renderInstructions = new Float32Array(totalInstructionsCount); + } + + // loop on features to fill the render instructions + let batchEntry; + const flatCoords = []; + let renderIndex = 0; + let value; + for (const featureUid in batch.entries) { + batchEntry = batch.entries[featureUid]; + for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) { + flatCoords.length = batchEntry.flatCoordss[i].length; + transform2D( + batchEntry.flatCoordss[i], + 0, + flatCoords.length, + 2, + batch.renderInstructionsTransform, + flatCoords + ); + + // custom attributes + for (let k = 0, kk = this.customAttributes_.length; k < kk; k++) { + value = this.customAttributes_[k].callback( + batchEntry.feature, + batchEntry.properties + ); + batch.renderInstructions[renderIndex++] = value; + } + + // vertices count + batch.renderInstructions[renderIndex++] = flatCoords.length / 2; + + // looping on points for positions + for (let j = 0, jj = flatCoords.length; j < jj; j += 2) { + batch.renderInstructions[renderIndex++] = flatCoords[j]; + batch.renderInstructions[renderIndex++] = flatCoords[j + 1]; + } + } + } + } +} + +export default LineStringBatchRenderer; diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js new file mode 100644 index 0000000000..cc7ddb3318 --- /dev/null +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -0,0 +1,90 @@ +/** + * @module ol/render/webgl/PointBatchRenderer + */ + +import {apply as applyTransform} from '../../transform.js'; +import {AttributeType} from '../../webgl/Helper.js'; +import AbstractBatchRenderer from './BatchRenderer.js'; + +class PointBatchRenderer extends AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + constructor(helper, worker, vertexShader, fragmentShader, customAttributes) { + super(helper, worker, vertexShader, fragmentShader, customAttributes); + + // vertices for point must hold both a position (x,y) and an index (their position in the quad) + this.attributes_ = [ + { + name: 'a_position', + size: 2, + type: AttributeType.FLOAT, + }, + { + name: 'a_index', + size: 1, + type: AttributeType.FLOAT, + }, + ].concat( + customAttributes.map(function (attribute) { + return { + name: 'a_' + attribute.name, + size: 1, + type: AttributeType.FLOAT, + }; + }) + ); + } + + /** + * Render instructions for lines are structured like so: + * [ x0, y0, customAttr0, ... , xN, yN, customAttrN ] + * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch + * @override + */ + generateRenderInstructions_(batch) { + // here we anticipate the amount of render instructions for points: + // 2 instructions per vertex for position (x and y) + // + 1 instruction per vertex per custom attributes + const totalInstructionsCount = + (2 + this.customAttributes_.length) * batch.geometriesCount; + if ( + !batch.renderInstructions || + batch.renderInstructions.length !== totalInstructionsCount + ) { + batch.renderInstructions = new Float32Array(totalInstructionsCount); + } + + // loop on features to fill the render instructions + let batchEntry; + const tmpCoords = []; + let renderIndex = 0; + let value; + for (const featureUid in batch.entries) { + batchEntry = batch.entries[featureUid]; + for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) { + tmpCoords[0] = batchEntry.flatCoordss[i][0]; + tmpCoords[1] = batchEntry.flatCoordss[i][1]; + applyTransform(batch.renderInstructionsTransform, tmpCoords); + + batch.renderInstructions[renderIndex++] = tmpCoords[0]; + batch.renderInstructions[renderIndex++] = tmpCoords[1]; + + // pushing custom attributes + for (let j = 0, jj = this.customAttributes_.length; j < jj; j++) { + value = this.customAttributes_[j].callback( + batchEntry.feature, + batchEntry.properties + ); + batch.renderInstructions[renderIndex++] = value; + } + } + } + } +} + +export default PointBatchRenderer; diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js new file mode 100644 index 0000000000..b99f22659d --- /dev/null +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -0,0 +1,111 @@ +/** + * @module ol/render/webgl/PolygonBatchRenderer + */ +import {AttributeType} from '../../webgl/Helper.js'; +import {transform2D} from '../../geom/flat/transform.js'; +import AbstractBatchRenderer from './BatchRenderer.js'; + +class PolygonBatchRenderer extends AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + constructor(helper, worker, vertexShader, fragmentShader, customAttributes) { + super(helper, worker, vertexShader, fragmentShader, customAttributes); + + // By default only a position attribute is required to render polygons + this.attributes_ = [ + { + name: 'a_position', + size: 2, + type: AttributeType.FLOAT, + }, + ].concat( + customAttributes.map(function (attribute) { + return { + name: 'a_' + attribute.name, + size: 1, + type: AttributeType.FLOAT, + }; + }) + ); + } + + /** + * Render instructions for polygons are structured like so: + * [ customAttr0, ..., customAttrN, numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, ..., xN, yN, numberOfRings,... ] + * @param {import("./MixedGeometryBatch.js").PolygonGeometryBatch} batch + * @override + */ + generateRenderInstructions_(batch) { + // here we anticipate the amount of render instructions for polygons: + // 2 instructions per vertex for position (x and y) + // + 1 instruction per polygon per custom attributes + // + 1 instruction per polygon (for vertices count in polygon) + // + 1 instruction per ring (for vertices count in ring) + const totalInstructionsCount = + 2 * batch.verticesCount + + (1 + this.customAttributes_.length) * batch.geometriesCount + + batch.ringsCount; + if ( + !batch.renderInstructions || + batch.renderInstructions.length !== totalInstructionsCount + ) { + batch.renderInstructions = new Float32Array(totalInstructionsCount); + } + + // loop on features to fill the render instructions + let batchEntry; + const flatCoords = []; + let renderIndex = 0; + let value; + for (const featureUid in batch.entries) { + batchEntry = batch.entries[featureUid]; + for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) { + flatCoords.length = batchEntry.flatCoordss[i].length; + transform2D( + batchEntry.flatCoordss[i], + 0, + flatCoords.length, + 2, + batch.renderInstructionsTransform, + flatCoords + ); + + // custom attributes + for (let k = 0, kk = this.customAttributes_.length; k < kk; k++) { + value = this.customAttributes_[k].callback( + batchEntry.feature, + batchEntry.properties + ); + batch.renderInstructions[renderIndex++] = value; + } + + // ring count + batch.renderInstructions[renderIndex++] = + batchEntry.ringsVerticesCounts[i].length; + + // vertices count in each ring + for ( + let j = 0, jj = batchEntry.ringsVerticesCounts[i].length; + j < jj; + j++ + ) { + batch.renderInstructions[renderIndex++] = + batchEntry.ringsVerticesCounts[i][j]; + } + + // looping on points for positions + for (let j = 0, jj = flatCoords.length; j < jj; j += 2) { + batch.renderInstructions[renderIndex++] = flatCoords[j]; + batch.renderInstructions[renderIndex++] = flatCoords[j + 1]; + } + } + } + } +} + +export default PolygonBatchRenderer; diff --git a/test/browser/spec/ol/render/webgl/batchrenderer.test.js b/test/browser/spec/ol/render/webgl/batchrenderer.test.js new file mode 100644 index 0000000000..ef0c4e37cf --- /dev/null +++ b/test/browser/spec/ol/render/webgl/batchrenderer.test.js @@ -0,0 +1,276 @@ +import PointBatchRenderer from '../../../../../../src/ol/render/webgl/PointBatchRenderer.js'; +import WebGLHelper from '../../../../../../src/ol/webgl/Helper.js'; +import {create as createWebGLWorker} from '../../../../../../src/ol/worker/webgl.js'; +import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js'; +import Feature from '../../../../../../src/ol/Feature.js'; +import Point from '../../../../../../src/ol/geom/Point.js'; +import Polygon from '../../../../../../src/ol/geom/Polygon.js'; +import LineString from '../../../../../../src/ol/geom/LineString.js'; +import GeometryType from '../../../../../../src/ol/geom/GeometryType.js'; +import {create as createTransform} from '../../../../../../src/ol/transform.js'; +import {WebGLWorkerMessageType} from '../../../../../../src/ol/render/webgl/constants.js'; +import LineStringBatchRenderer from '../../../../../../src/ol/render/webgl/LineStringBatchRenderer.js'; +import {FLOAT} from '../../../../../../src/ol/webgl.js'; +import PolygonBatchRenderer from '../../../../../../src/ol/render/webgl/PolygonBatchRenderer.js'; + +const POINT_VERTEX_SHADER = `precision mediump float; +void main(void) {}`; +const POINT_FRAGMENT_SHADER = `precision mediump float; +void main(void) {}`; + +const SAMPLE_FRAMESTATE = { + viewState: { + center: [0, 10], + resolution: 1, + rotation: 0, + }, + size: [10, 10], +}; + +describe('Batch renderers', function () { + let batchRenderer, helper, mixedBatch, worker, attributes; + + beforeEach(function () { + helper = new WebGLHelper(); + worker = createWebGLWorker(); + attributes = [ + { + name: 'test', + callback: function (feature, properties) { + return properties.test; + }, + }, + ]; + + mixedBatch = new MixedGeometryBatch(); + mixedBatch.addFeatures([ + new Feature({ + test: 1000, + geometry: new Point([10, 20]), + }), + new Feature({ + test: 2000, + geometry: new Point([30, 40]), + }), + new Feature({ + test: 3000, + geometry: new Polygon([ + [ + [10, 10], + [20, 10], + [30, 20], + [20, 40], + [10, 10], + ], + ]), + }), + new Feature({ + test: 4000, + geometry: new LineString([ + [100, 200], + [300, 400], + [500, 600], + ]), + }), + ]); + }); + + describe('PointBatchRenderer', function () { + beforeEach(function () { + batchRenderer = new PointBatchRenderer( + helper, + worker, + POINT_VERTEX_SHADER, + POINT_FRAGMENT_SHADER, + attributes + ); + }); + describe('constructor', function () { + it('generates the attributes list', function () { + expect(batchRenderer.attributes_).to.eql([ + {name: 'a_position', size: 2, type: FLOAT}, + {name: 'a_index', size: 1, type: FLOAT}, + {name: 'a_test', size: 1, type: FLOAT}, + ]); + }); + }); + describe('#rebuild', function () { + beforeEach(function (done) { + sinon.spy(helper, 'flushBufferData'); + batchRenderer.rebuild( + mixedBatch.pointBatch, + SAMPLE_FRAMESTATE, + GeometryType.POINT + ); + // wait for worker response for our specific message + worker.addEventListener('message', function (event) { + if ( + event.data.type === WebGLWorkerMessageType.GENERATE_POINT_BUFFERS && + event.data.renderInstructions.byteLength > 0 + ) { + done(); + } + }); + }); + it('generates render instructions and updates buffers from the worker response', function () { + expect(Array.from(mixedBatch.pointBatch.renderInstructions)).to.eql([ + 2, 2, 1000, 6, 6, 2000, + ]); + }); + it('updates buffers', function () { + expect( + mixedBatch.pointBatch.verticesBuffer.getArray().length + ).to.be.greaterThan(0); + expect( + mixedBatch.pointBatch.indicesBuffer.getArray().length + ).to.be.greaterThan(0); + expect(helper.flushBufferData.calledTwice).to.be(true); + }); + it('updates the instructions transform', function () { + expect(mixedBatch.pointBatch.renderInstructionsTransform).to.eql([ + 0.2, 0, 0, 0.2, 0, -2, + ]); + }); + }); + describe('#render (from parent)', function () { + beforeEach(function () { + sinon.spy(helper, 'makeProjectionTransform'); + sinon.spy(helper, 'useProgram'); + sinon.spy(helper, 'bindBuffer'); + sinon.spy(helper, 'enableAttributes'); + sinon.spy(helper, 'drawElements'); + + const transform = createTransform(); + batchRenderer.render( + mixedBatch.pointBatch, + transform, + SAMPLE_FRAMESTATE + ); + }); + it('computes current transform', function () { + expect(helper.makeProjectionTransform.calledOnce).to.be(true); + }); + it('computes sets up render parameters', function () { + expect(helper.useProgram.calledOnce).to.be(true); + expect(helper.enableAttributes.calledOnce).to.be(true); + expect(helper.bindBuffer.calledTwice).to.be(true); + }); + it('renders elements', function () { + expect(helper.drawElements.calledOnce).to.be(true); + }); + }); + }); + + describe('LineStringBatchRenderer', function () { + beforeEach(function () { + batchRenderer = new LineStringBatchRenderer( + helper, + worker, + POINT_VERTEX_SHADER, + POINT_FRAGMENT_SHADER, + attributes + ); + }); + describe('constructor', function () { + it('generates the attributes list', function () { + expect(batchRenderer.attributes_).to.eql([ + {name: 'a_segmentStart', size: 2, type: FLOAT}, + {name: 'a_segmentEnd', size: 2, type: FLOAT}, + {name: 'a_parameters', size: 1, type: FLOAT}, + {name: 'a_test', size: 1, type: FLOAT}, + ]); + }); + }); + describe('#rebuild', function () { + beforeEach(function (done) { + sinon.spy(helper, 'flushBufferData'); + batchRenderer.rebuild( + mixedBatch.lineStringBatch, + SAMPLE_FRAMESTATE, + GeometryType.LINE_STRING + ); + // wait for worker response for our specific message + worker.addEventListener('message', function (event) { + if ( + event.data.type === + WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS && + event.data.renderInstructions.byteLength > 0 + ) { + done(); + } + }); + }); + it('generates render instructions and updates buffers from the worker response', function () { + expect( + Array.from(mixedBatch.lineStringBatch.renderInstructions) + ).to.eql([ + 3000, 5, 2, 0, 4, 0, 6, 2, 4, 6, 2, 0, 4000, 3, 20, 38, 60, 78, 100, + 118, + ]); + }); + it('updates buffers', function () { + expect( + mixedBatch.lineStringBatch.verticesBuffer.getArray().length + ).to.be.greaterThan(0); + expect( + mixedBatch.lineStringBatch.indicesBuffer.getArray().length + ).to.be.greaterThan(0); + expect(helper.flushBufferData.calledTwice).to.be(true); + }); + }); + }); + + describe('PolygonBatchRenderer', function () { + beforeEach(function () { + batchRenderer = new PolygonBatchRenderer( + helper, + worker, + POINT_VERTEX_SHADER, + POINT_FRAGMENT_SHADER, + attributes + ); + }); + describe('constructor', function () { + it('generates the attributes list', function () { + expect(batchRenderer.attributes_).to.eql([ + {name: 'a_position', size: 2, type: FLOAT}, + {name: 'a_test', size: 1, type: FLOAT}, + ]); + }); + }); + describe('#rebuild', function () { + beforeEach(function (done) { + sinon.spy(helper, 'flushBufferData'); + batchRenderer.rebuild( + mixedBatch.polygonBatch, + SAMPLE_FRAMESTATE, + GeometryType.POLYGON + ); + // wait for worker response for our specific message + worker.addEventListener('message', function (event) { + if ( + event.data.type === + WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS && + event.data.renderInstructions.byteLength > 0 + ) { + done(); + } + }); + }); + it('generates render instructions and updates buffers from the worker response', function () { + expect(Array.from(mixedBatch.polygonBatch.renderInstructions)).to.eql([ + 3000, 1, 5, 2, 0, 4, 0, 6, 2, 4, 6, 2, 0, + ]); + }); + it('updates buffers', function () { + expect( + mixedBatch.polygonBatch.verticesBuffer.getArray().length + ).to.be.greaterThan(0); + expect( + mixedBatch.polygonBatch.indicesBuffer.getArray().length + ).to.be.greaterThan(0); + expect(helper.flushBufferData.calledTwice).to.be(true); + }); + }); + }); +});