UNPKG

mapbox-gl

Version:
301 lines (254 loc) 9.07 kB
'use strict'; // Note: all "sizes" are measured in bytes var assert = require('assert'); /** * The `Buffer` class is responsible for managing one instance of `ArrayBuffer`. `ArrayBuffer`s * provide low-level read/write access to a chunk of memory. `ArrayBuffer`s are populated with * per-vertex data, uploaded to the GPU, and used in rendering. * * `Buffer` provides an abstraction over `ArrayBuffer`, making it behave like an array of * statically typed structs. A buffer is comprised of items. An item is comprised of a set of * attributes. Attributes are defined when the class is constructed. * * @class Buffer * @private * @param options * @param {BufferType} options.type * @param {Array.<BufferAttribute>} options.attributes */ function Buffer(options) { this.type = options.type; // Clone an existing Buffer if (options.arrayBuffer) { this.capacity = options.capacity; this.arrayBuffer = options.arrayBuffer; this.attributes = options.attributes; this.itemSize = options.itemSize; this.length = options.length; // Create a new Buffer } else { this.capacity = align(Buffer.CAPACITY_DEFAULT, Buffer.CAPACITY_ALIGNMENT); this.arrayBuffer = new ArrayBuffer(this.capacity); this.attributes = []; this.itemSize = 0; this.length = 0; // Vertex buffer attributes must be aligned to word boundaries but // element buffer attributes do not need to be aligned. var attributeAlignment = this.type === Buffer.BufferType.VERTEX ? Buffer.VERTEX_ATTRIBUTE_ALIGNMENT : 1; this.attributes = options.attributes.map(function(attributeOptions) { var attribute = {}; attribute.name = attributeOptions.name; attribute.components = attributeOptions.components || 1; attribute.type = attributeOptions.type || Buffer.AttributeType.UNSIGNED_BYTE; attribute.size = attribute.type.size * attribute.components; attribute.offset = this.itemSize; this.itemSize = align(attribute.offset + attribute.size, attributeAlignment); assert(!isNaN(this.itemSize)); assert(!isNaN(attribute.size)); assert(attribute.type.name in Buffer.AttributeType); return attribute; }, this); // These are expensive calls. Because we only push things to buffers in // the worker thread, we can skip in the "clone an existing buffer" case. this._createPushMethod(); this._refreshViews(); } } /** * Bind this buffer to a WebGL context. * @private * @param gl The WebGL context */ Buffer.prototype.bind = function(gl) { var type = gl[this.type]; if (!this.buffer) { this.buffer = gl.createBuffer(); gl.bindBuffer(type, this.buffer); gl.bufferData(type, this.arrayBuffer.slice(0, this.length * this.itemSize), gl.STATIC_DRAW); // dump array buffer once it's bound to gl this.arrayBuffer = null; } else { gl.bindBuffer(type, this.buffer); } }; /** * Destroy the GL buffer bound to the given WebGL context * @private * @param gl The WebGL context */ Buffer.prototype.destroy = function(gl) { if (this.buffer) { gl.deleteBuffer(this.buffer); } }; /** * Set the attribute pointers in a WebGL context according to the buffer's attribute layout * @private * @param gl The WebGL context * @param shader The active WebGL shader * @param {number} offset The offset of the attribute data in the currently bound GL buffer. */ Buffer.prototype.setAttribPointers = function(gl, shader, offset) { for (var i = 0; i < this.attributes.length; i++) { var attrib = this.attributes[i]; gl.vertexAttribPointer( shader['a_' + attrib.name], attrib.components, gl[attrib.type.name], false, this.itemSize, offset + attrib.offset); } }; /** * Get an item from the `ArrayBuffer`. Only used for debugging. * @private * @param {number} index The index of the item to get * @returns {Object.<string, Array.<number>>} */ Buffer.prototype.get = function(index) { this._refreshViews(); var item = {}; var offset = index * this.itemSize; for (var i = 0; i < this.attributes.length; i++) { var attribute = this.attributes[i]; var values = item[attribute.name] = []; for (var j = 0; j < attribute.components; j++) { var componentOffset = ((offset + attribute.offset) / attribute.type.size) + j; values.push(this.views[attribute.type.name][componentOffset]); } } return item; }; /** * Check that a buffer item is well formed and throw an error if not. Only * used for debugging. * @private * @param {number} args The "arguments" object from Buffer::push */ Buffer.prototype.validate = function(args) { var argIndex = 0; for (var i = 0; i < this.attributes.length; i++) { for (var j = 0; j < this.attributes[i].components; j++) { assert(!isNaN(args[argIndex++])); } } assert(argIndex === args.length); }; Buffer.prototype._resize = function(capacity) { var old = this.views.UNSIGNED_BYTE; this.capacity = align(capacity, Buffer.CAPACITY_ALIGNMENT); this.arrayBuffer = new ArrayBuffer(this.capacity); this._refreshViews(); this.views.UNSIGNED_BYTE.set(old); }; Buffer.prototype._refreshViews = function() { this.views = { UNSIGNED_BYTE: new Uint8Array(this.arrayBuffer), BYTE: new Int8Array(this.arrayBuffer), UNSIGNED_SHORT: new Uint16Array(this.arrayBuffer), SHORT: new Int16Array(this.arrayBuffer) }; }; var createPushMethodCache = {}; Buffer.prototype._createPushMethod = function() { var body = ''; var argNames = []; body += 'var i = this.length++;\n'; body += 'var o = i * ' + this.itemSize + ';\n'; body += 'if (o + ' + this.itemSize + ' > this.capacity) { this._resize(this.capacity * 1.5); }\n'; for (var i = 0; i < this.attributes.length; i++) { var attribute = this.attributes[i]; var offsetId = 'o' + i; body += '\nvar ' + offsetId + ' = (o + ' + attribute.offset + ') / ' + attribute.type.size + ';\n'; for (var j = 0; j < attribute.components; j++) { var rvalue = 'v' + argNames.length; var lvalue = 'this.views.' + attribute.type.name + '[' + offsetId + ' + ' + j + ']'; body += lvalue + ' = ' + rvalue + ';\n'; argNames.push(rvalue); } } body += '\nreturn i;\n'; if (!createPushMethodCache[body]) { createPushMethodCache[body] = new Function(argNames, body); } this.push = createPushMethodCache[body]; }; /** * @typedef BufferAttribute * @private * @property {string} name * @property {number} components * @property {BufferAttributeType} type * @property {number} size * @property {number} offset */ /** * @enum {string} BufferType * @private * @readonly */ Buffer.BufferType = { VERTEX: 'ARRAY_BUFFER', ELEMENT: 'ELEMENT_ARRAY_BUFFER' }; /** * @enum {{size: number, name: string}} BufferAttributeType * @private * @readonly */ Buffer.AttributeType = { BYTE: { size: 1, name: 'BYTE' }, UNSIGNED_BYTE: { size: 1, name: 'UNSIGNED_BYTE' }, SHORT: { size: 2, name: 'SHORT' }, UNSIGNED_SHORT: { size: 2, name: 'UNSIGNED_SHORT' } }; /** * An `BufferType.ELEMENT` buffer holds indicies of a corresponding `BufferType.VERTEX` buffer. * These indicies are stored in the `BufferType.ELEMENT` buffer as `UNSIGNED_SHORT`s. * * @property {BufferAttributeType} * @private * @readonly */ Buffer.ELEMENT_ATTRIBUTE_TYPE = Buffer.AttributeType.UNSIGNED_SHORT; /** * The maximum extent of a feature that can be safely stored in the buffer. * In practice, all features are converted to this extent before being added. * * Positions are stored as signed 16bit integers. * One bit is lost for signedness to support featuers extending past the left edge of the tile. * One bit is lost because the line vertex buffer packs 1 bit of other data into the int. * One bit is lost to support features extending past the extent on the right edge of the tile. * This leaves us with 2^13 = 8192 * * @property {number} * @private * @readonly */ Buffer.EXTENT = 8192; /** * @property {number} * @private * @readonly */ Buffer.CAPACITY_DEFAULT = 8192; /** * WebGL performs best if buffer sizes are aligned to 2 byte boundaries. * @property {number} * @private * @readonly */ Buffer.CAPACITY_ALIGNMENT = 2; /** * WebGL performs best if vertex attribute offsets are aligned to 4 byte boundaries. * @property {number} * @private * @readonly */ Buffer.VERTEX_ATTRIBUTE_ALIGNMENT = 4; function align(value, alignment) { alignment = alignment || 1; var remainder = value % alignment; if (alignment !== 1 && remainder !== 0) { value += (alignment - remainder); } return value; } module.exports = Buffer;