From d40a059ef0f94b10a93f0578658213ac5a21229c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 9 Mar 2016 15:31:41 -0800 Subject: [PATCH] combine duplicate parts of Buffer and StructArray --- js/data/bucket.js | 81 +++++---- js/data/bucket/symbol_bucket.js | 4 +- js/data/buffer.js | 248 +++----------------------- js/data/feature_tree.js | 10 +- js/source/worker.js | 4 +- js/source/worker_tile.js | 12 +- js/symbol/collision_box.js | 9 +- js/util/struct_array.js | 302 ++++++++++++++++++++++++++------ 8 files changed, 334 insertions(+), 336 deletions(-) diff --git a/js/data/bucket.js b/js/data/bucket.js index ac1191a4350..41f75fdc174 100644 --- a/js/data/bucket.js +++ b/js/data/bucket.js @@ -4,6 +4,7 @@ var featureFilter = require('feature-filter'); var Buffer = require('./buffer'); var StyleLayer = require('../style/style_layer'); var util = require('../util/util'); +var StructArrayType = require('../util/struct_array'); module.exports = Bucket; @@ -79,8 +80,10 @@ function Bucket(options) { if (options.elementGroups) { this.elementGroups = options.elementGroups; - this.buffers = util.mapObject(options.buffers, function(options) { - return new Buffer(options); + this.buffers = util.mapObject(options.structArrays, function(structArray, bufferName) { + var structArrayType = options.structArrayTypes[bufferName]; + var type = (structArrayType.members[0].name === 'vertices' ? Buffer.BufferType.ELEMENT : Buffer.BufferType.VERTEX); + return new Buffer(structArray, structArrayType, type); }); } } @@ -91,13 +94,13 @@ function Bucket(options) { */ Bucket.prototype.populateBuffers = function() { this.createStyleLayer(); - this.createBuffers(); + this.createStructArrays(); for (var i = 0; i < this.features.length; i++) { this.addFeature(this.features[i]); } - this.trimBuffers(); + this.trimArrays(); }; /** @@ -113,14 +116,14 @@ Bucket.prototype.makeRoomFor = function(shaderName, numVertices) { var currentGroup = groups.length && groups[groups.length - 1]; if (!currentGroup || currentGroup.vertexLength + numVertices > 65535) { - var vertexBuffer = this.buffers[this.getBufferName(shaderName, 'vertex')]; - var elementBuffer = this.buffers[this.getBufferName(shaderName, 'element')]; - var secondElementBuffer = this.buffers[this.getBufferName(shaderName, 'secondElement')]; + var vertexArray = this.structArrays[this.getBufferName(shaderName, 'vertex')]; + var elementArray = this.structArrays[this.getBufferName(shaderName, 'element')]; + var secondElementArray = this.structArrays[this.getBufferName(shaderName, 'secondElement')]; currentGroup = new ElementGroup( - vertexBuffer.length, - elementBuffer && elementBuffer.length, - secondElementBuffer && secondElementBuffer.length + vertexArray.length, + elementArray && elementArray.length, + secondElementArray && secondElementArray.length ); groups.push(currentGroup); } @@ -133,9 +136,10 @@ Bucket.prototype.makeRoomFor = function(shaderName, numVertices) { * as necessary. * @private */ -Bucket.prototype.createBuffers = function() { +Bucket.prototype.createStructArrays = function() { var elementGroups = this.elementGroups = {}; - var buffers = this.buffers = {}; + var structArrays = this.structArrays = {}; + var structArrayTypes = this.structArrayTypes = {}; for (var shaderName in this.shaderInterfaces) { var shaderInterface = this.shaderInterfaces[shaderName]; @@ -144,10 +148,10 @@ Bucket.prototype.createBuffers = function() { var vertexBufferName = this.getBufferName(shaderName, 'vertex'); var vertexAddMethodName = this.getAddMethodName(shaderName, 'vertex'); - buffers[vertexBufferName] = new Buffer({ - type: Buffer.BufferType.VERTEX, - attributes: shaderInterface.attributes - }); + var VertexArrayType = new StructArrayType(shaderInterface.attributes, { alignment: Buffer.VERTEX_ATTRIBUTE_ALIGNMENT }); + + structArrays[vertexBufferName] = new VertexArrayType(); + structArrayTypes[vertexBufferName] = VertexArrayType.serialize(); this[vertexAddMethodName] = this[vertexAddMethodName] || createVertexAddMethod( shaderName, @@ -156,16 +160,21 @@ Bucket.prototype.createBuffers = function() { ); } + if (shaderInterface.elementBuffer) { var elementBufferName = this.getBufferName(shaderName, 'element'); - buffers[elementBufferName] = createElementBuffer(shaderInterface.elementBufferComponents); - this[this.getAddMethodName(shaderName, 'element')] = createElementAddMethod(this.buffers[elementBufferName]); + var ElementArrayType = createElementBufferType(shaderInterface.elementBufferComponents); + structArrays[elementBufferName] = new ElementArrayType(); + structArrayTypes[elementBufferName] = ElementArrayType.serialize(); + this[this.getAddMethodName(shaderName, 'element')] = createElementAddMethod(this.structArrays[elementBufferName]); } if (shaderInterface.secondElementBuffer) { var secondElementBufferName = this.getBufferName(shaderName, 'secondElement'); - buffers[secondElementBufferName] = createElementBuffer(shaderInterface.secondElementBufferComponents); - this[this.getAddMethodName(shaderName, 'secondElement')] = createElementAddMethod(this.buffers[secondElementBufferName]); + var SecondElementArrayType = createElementBufferType(shaderInterface.secondElementBufferComponents); + structArrays[secondElementBufferName] = new SecondElementArrayType(); + structArrayTypes[secondElementBufferName] = SecondElementArrayType.serialize(); + this[this.getAddMethodName(shaderName, 'secondElement')] = createElementAddMethod(this.structArrays[secondElementBufferName]); } elementGroups[shaderName] = []; @@ -178,9 +187,9 @@ Bucket.prototype.destroy = function(gl) { } }; -Bucket.prototype.trimBuffers = function() { - for (var bufferName in this.buffers) { - this.buffers[bufferName].trim(); +Bucket.prototype.trimArrays = function() { + for (var bufferName in this.structArrays) { + this.structArrays[bufferName].trim(); } }; @@ -212,9 +221,10 @@ Bucket.prototype.serialize = function() { }, zoom: this.zoom, elementGroups: this.elementGroups, - buffers: util.mapObject(this.buffers, function(buffer) { - return buffer.serialize(); - }) + structArrays: util.mapObject(this.structArrays, function(structArray) { + return structArray.serialize(); + }), + structArrayTypes: this.structArrayTypes }; }; @@ -239,7 +249,7 @@ function createVertexAddMethod(shaderName, shaderInterface, bufferName) { pushArgs = pushArgs.concat(shaderInterface.attributes[i].value); } - var body = 'return this.buffers.' + bufferName + '.push(' + pushArgs.join(', ') + ');'; + var body = 'return this.structArrays.' + bufferName + '.emplaceBack(' + pushArgs.join(', ') + ');'; if (!createVertexAddMethodCache[body]) { createVertexAddMethodCache[body] = new Function(shaderInterface.attributeArgs, body); @@ -250,19 +260,16 @@ function createVertexAddMethod(shaderName, shaderInterface, bufferName) { function createElementAddMethod(buffer) { return function(one, two, three) { - return buffer.push(one, two, three); + return buffer.emplaceBack(one, two, three); }; } -function createElementBuffer(components) { - return new Buffer({ - type: Buffer.BufferType.ELEMENT, - attributes: [{ - name: 'vertices', - components: components || 3, - type: Buffer.ELEMENT_ATTRIBUTE_TYPE - }] - }); +function createElementBufferType(components) { + return new StructArrayType([{ + type: Buffer.ELEMENT_ATTRIBUTE_TYPE, + name: 'vertices', + components: components || 3 + }]); } function capitalize(string) { diff --git a/js/data/bucket/symbol_bucket.js b/js/data/bucket/symbol_bucket.js index 60802b84a46..c74462b9d3a 100644 --- a/js/data/bucket/symbol_bucket.js +++ b/js/data/bucket/symbol_bucket.js @@ -116,7 +116,7 @@ SymbolBucket.prototype.shaderInterfaces = { SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) { this.createStyleLayer(); - this.createBuffers(); + this.createStructArrays(); // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. @@ -231,7 +231,7 @@ SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) this.placeFeatures(collisionTile, this.collisionDebug); - this.trimBuffers(); + this.trimArrays(); }; SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, featureIndex) { diff --git a/js/data/buffer.js b/js/data/buffer.js index 7f8d26364ab..5221a004cdf 100644 --- a/js/data/buffer.js +++ b/js/data/buffer.js @@ -1,90 +1,25 @@ 'use strict'; -// Note: all "sizes" are measured in bytes - -var assert = require('assert'); +module.exports = Buffer; /** - * 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. - * - * Though the buffers are intended for WebGL, this class should have no formal code dependencies - * on WebGL. Though the buffers are populated by vector tile features, this class should have - * no domain knowledge about vector tiles, coordinate systems, etc. + * The `Buffer` class turns a `StructArray` into a WebGL buffer. Each member of the StructArray's + * Struct type is converted to a WebGL atribute. * * @class Buffer * @private - * @param options - * @param {BufferType} options.type - * @param {Array.} options.attributes + * @param {object} structArray A serialized StructArray. + * @param {object} structArrayType A serialized StructArrayType. + * @param {BufferType} type */ -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(); - } +function Buffer(structArray, structArrayType, type) { + this.arrayBuffer = structArray.arrayBuffer; + this.length = structArray.length; + this.attributes = structArrayType.members; + this.itemSize = structArrayType.BYTES_PER_ELEMENT; + this.type = type; } -Buffer.prototype.serialize = function() { - return { - type: this.type, - capacity: this.capacity, - arrayBuffer: this.arrayBuffer, - attributes: this.attributes, - itemSize: this.itemSize, - length: this.length - }; -}; - /** * Bind this buffer to a WebGL context. * @private @@ -116,6 +51,13 @@ Buffer.prototype.destroy = function(gl) { } }; +var convertType = { + Int8: 'BYTE', + Uint8: 'UNSIGNED_BYTE', + Int16: 'SHORT', + Uint16: 'UNSIGNED_SHORT' +}; + /** * Set the attribute pointers in a WebGL context according to the buffer's attribute layout * @private @@ -128,120 +70,11 @@ Buffer.prototype.setAttribPointers = function(gl, shader, offset) { var attrib = this.attributes[i]; gl.vertexAttribPointer( - shader['a_' + attrib.name], attrib.components, gl[attrib.type.name], + shader['a_' + attrib.name], attrib.components, gl[convertType[attrib.type]], false, this.itemSize, offset + attrib.offset); } }; -/** - * Resize the buffer to discard unused capacity. - * @private - */ -Buffer.prototype.trim = function() { - this.capacity = align(this.itemSize * this.length, Buffer.CAPACITY_ALIGNMENT); - this.arrayBuffer = this.arrayBuffer.slice(0, this.capacity); - this._refreshViews(); -}; - -/** - * Get an item from the `ArrayBuffer`. Only used for debugging. - * @private - * @param {number} index The index of the item to get - * @returns {Object.>} - */ -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 oldUByteView = this.views.UNSIGNED_BYTE; - this.capacity = align(capacity, Buffer.CAPACITY_ALIGNMENT); - this.arrayBuffer = new ArrayBuffer(this.capacity); - this._refreshViews(); - this.views.UNSIGNED_BYTE.set(oldUByteView); -}; - -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 * ' + Buffer.CAPACITY_RESIZE_MULTIPLIER + '); }\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 @@ -258,10 +91,10 @@ Buffer.BufferType = { * @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' } + BYTE: 'Int8', + UNSIGNED_BYTE: 'Uint8', + SHORT: 'Int16', + UNSIGNED_SHORT: 'Uint16' }; /** @@ -274,28 +107,6 @@ Buffer.AttributeType = { */ Buffer.ELEMENT_ATTRIBUTE_TYPE = Buffer.AttributeType.UNSIGNED_SHORT; -/** - * @property {number} - * @private - * @readonly - */ -Buffer.CAPACITY_DEFAULT = 1024; - -/** - * @property {number} - * @private - * @readonly - */ -Buffer.CAPACITY_RESIZE_MULTIPLIER = 5; - -/** - * 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} @@ -303,14 +114,3 @@ Buffer.CAPACITY_ALIGNMENT = 2; * @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; diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index dd74c53e9f3..851c23a1d5d 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -4,14 +4,14 @@ var Point = require('point-geometry'); var loadGeometry = require('./load_geometry'); var EXTENT = require('./bucket').EXTENT; var featureFilter = require('feature-filter'); -var createStructArrayType = require('../util/struct_array'); +var StructArrayType = require('../util/struct_array'); var Grid = require('../util/grid'); var StringNumberMapping = require('../util/string_number_mapping'); var vt = require('vector-tile'); var Protobuf = require('pbf'); var GeoJSONFeature = require('../util/vectortile_to_geojson'); -var FeatureIndexArray = createStructArrayType([ +var FeatureIndexArray = new StructArrayType([ // the index of the feature in the original vectortile { type: 'Uint32', name: 'featureIndex' }, // the source layer the feature appears in @@ -20,7 +20,7 @@ var FeatureIndexArray = createStructArrayType([ { type: 'Uint16', name: 'bucketIndex' } ]); -var FilteredFeatureIndexArray = createStructArrayType([ +var FilteredFeatureIndexArray = new StructArrayType([ // the index of the feature in the original vectortile { type: 'Uint32', name: 'featureIndex' }, // the source layer the feature appears in @@ -87,12 +87,12 @@ FeatureTree.prototype.serialize = function() { coord: this.coord, overscaling: this.overscaling, grid: this.grid.toArrayBuffer(), - featureIndexArray: this.featureIndexArray.arrayBuffer, + featureIndexArray: this.featureIndexArray.serialize(), numberToLayerIDs: this.numberToLayerIDs }; return { data: data, - transferables: [data.grid, data.featureIndexArray] + transferables: [data.grid, data.featureIndexArray.arrayBuffer] }; }; diff --git a/js/source/worker.js b/js/source/worker.js index fac9619119f..f9b2b2558c6 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -213,8 +213,8 @@ util.extend(Worker.prototype, { var collisionTile = new CollisionTile(params.collisionTile, tile.collisionBoxArray); var featureTree = new FeatureTree(params.featureTree, params.rawTileData, collisionTile); - var featureArrayBuffer = featureTree.query(params, this.styleLayersByID, false).arrayBuffer; - callback(null, featureArrayBuffer, [featureArrayBuffer]); + var featureArray = featureTree.query(params, this.styleLayersByID, false).serialize(); + callback(null, featureArray, [featureArray.arrayBuffer]); } else { callback(null, []); } diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 29fb9861c76..b8e92734172 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -204,8 +204,8 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData var featureTree_ = featureTree.serialize(); var collisionTile_ = collisionTile.serialize(); - var collisionBoxArray = tile.collisionBoxArray.arrayBuffer.slice(0); - var transferables = [rawTileData, collisionBoxArray].concat(featureTree.transferables).concat(collisionTile_.transferables); + var collisionBoxArray = tile.collisionBoxArray.serialize(); + var transferables = [rawTileData].concat(featureTree.transferables).concat(collisionTile_.transferables); buckets = filterEmptyBuckets(buckets); @@ -250,8 +250,8 @@ WorkerTile.prototype.redoPlacement = function(angle, pitch, collisionDebug) { function filterEmptyBuckets(buckets) { return buckets.filter(function(bucket) { - for (var bufferName in bucket.buffers) { - if (bucket.buffers[bufferName].length > 0) return true; + for (var bufferName in bucket.structArrays) { + if (bucket.structArrays[bufferName].length > 0) return true; } return false; }); @@ -260,8 +260,8 @@ function filterEmptyBuckets(buckets) { function getTransferables(buckets) { var transferables = []; for (var i in buckets) { - for (var j in buckets.buffers) { - transferables.push(buckets[i].buffers[j].arrayBuffer); + for (var j in buckets[i].structArrays) { + transferables.push(buckets[i].structArrays[j].arrayBuffer); } } return transferables; diff --git a/js/symbol/collision_box.js b/js/symbol/collision_box.js index 66d84139070..d5a0682446f 100644 --- a/js/symbol/collision_box.js +++ b/js/symbol/collision_box.js @@ -1,6 +1,7 @@ 'use strict'; -var createStructArrayType = require('../util/struct_array'); +var StructArrayType = require('../util/struct_array'); +var util = require('../util/util'); var Point = require('point-geometry'); /** @@ -39,7 +40,7 @@ var Point = require('point-geometry'); * @private */ -module.exports = createStructArrayType([ +var CollisionBoxArray = module.exports = new StructArrayType([ // the box is centered around the anchor point { type: 'Int16', name: 'anchorPointX' }, { type: 'Int16', name: 'anchorPointY' }, @@ -68,7 +69,9 @@ module.exports = createStructArrayType([ { type: 'Int16', name: 'bbox3' }, { type: 'Float32', name: 'placementScale' } -], { +]); + +util.extendAll(CollisionBoxArray.prototype.StructType.prototype, { get anchorPoint() { return new Point(this.anchorPointX, this.anchorPointY); } diff --git a/js/util/struct_array.js b/js/util/struct_array.js index eb8f734fa32..b86512f98f0 100644 --- a/js/util/struct_array.js +++ b/js/util/struct_array.js @@ -1,8 +1,10 @@ 'use strict'; -var inherit = require('./util').inherit; +// Note: all "sizes" are measured in bytes -module.exports = createStructArrayType; +var assert = require('assert'); + +module.exports = StructArrayType; var viewTypes = { 'Int8': Int8Array, @@ -16,38 +18,101 @@ var viewTypes = { 'Float64': Float64Array }; -function createStructArrayType(members, methods) { - if (methods === undefined) methods = {}; +/** + * @typedef StructMember + * @private + * @property {string} name + * @property {string} type + * @property {number} components + */ + +var structArrayTypeCache = {}; + +/** + * `StructArrayType` is used to create new `StructArray` types. + * + * `StructArray` provides an abstraction over `ArrayBuffer` and `TypedArray` making it behave like + * an array of typed structs. A StructArray is comprised of elements. Each element has a set of + * members that are defined when the `StructArrayType` is created. + * + * StructArrays useful for creating large arrays that: + * - can be transferred from workers as a Transferable object + * - can be copied cheaply + * - use less memory for lower-precision members + * - can be used as buffers in WebGL. + * + * @class StructArrayType + * @param {Array.} + * @param options + * @param {number} options.alignment Use `4` to align members to 4 byte boundaries. Default is 1. + * + * @example + * + * var PointArrayType = new StructArrayType([ + * { type: 'Int16', name: 'x' }, + * { type: 'Int16', name: 'y' } + * ]); + * + * var pointArray = new PointArrayType(); + * pointArray.emplaceBack(10, 15); + * pointArray.emplaceBack(20, 35); + * + * point = pointArray.at(0); + * assert(point.x === 10); + * assert(point.y === 15); + * point._setIndex(1); + * assert(point.x === 20); + * assert(point.y === 35); + * + * @private + */ +function StructArrayType(members, options) { + + var key = JSON.stringify({ members: members, options: options }); + if (structArrayTypeCache[key]) { + return structArrayTypeCache[key]; + } + + if (options === undefined) options = {}; + if (options.alignment === undefined) options.alignment = 1; function StructType() { Struct.apply(this, arguments); } - StructType.prototype = inherit(Struct, methods); + StructType.prototype = Object.create(Struct.prototype); var offset = 0; var maxSize = 0; + var usedTypes = ['Uint8']; - for (var m = 0; m < members.length; m++) { - var member = members[m]; + StructType.prototype.members = members.map(function(member) { + member = { + name: member.name, + type: member.type, + components: member.components || 1 + }; - if (!viewTypes[member.type]) { - throw new Error(JSON.stringify(member.type) + ' is not a valid type'); - } + assert(member.name.length); + assert(member.type in viewTypes); - var size = sizeOf(member.type); - maxSize = Math.max(maxSize, size); - offset = member.offset = align(offset, size); + if (usedTypes.indexOf(member.type) < 0) usedTypes.push(member.type); + + var typeSize = sizeOf(member.type); + maxSize = Math.max(maxSize, typeSize); + member.offset = offset = align(offset, Math.max(options.alignment, typeSize)); Object.defineProperty(StructType.prototype, member.name, { - get: createGetter(member.type, offset), - set: createSetter(member.type, offset) + get: member.components === 1 ? createGetter(member) : createArrayGetter(member), + set: member.components === 1 ? createSetter(member) : createArraySetter(member) }); - offset += size; - } + offset += typeSize * member.components; + + return member; + }); - StructType.prototype.BYTE_SIZE = align(offset, maxSize); + StructType.prototype.BYTE_SIZE = align(offset, Math.max(maxSize, options.alignment)); function StructArrayType() { StructArray.apply(this, arguments); @@ -56,7 +121,12 @@ function createStructArrayType(members, methods) { StructArrayType.prototype = Object.create(StructArray.prototype); StructArrayType.prototype.StructType = StructType; StructArrayType.prototype.BYTES_PER_ELEMENT = StructType.prototype.BYTE_SIZE; - StructArrayType.prototype.emplaceBack = createEmplaceBack(members, StructType.prototype.BYTE_SIZE); + StructArrayType.prototype.emplaceBack = createEmplaceBack(StructType.prototype.members, StructType.prototype.BYTE_SIZE); + StructArrayType.prototype._usedTypes = usedTypes; + + StructArrayType.serialize = StructArray.serialize; + + structArrayTypeCache[key] = StructArrayType; return StructArrayType; } @@ -75,83 +145,201 @@ function getArrayViewName(type) { function createEmplaceBack(members, BYTES_PER_ELEMENT) { + var usedTypeSizes = []; var argNames = []; var body = '' + - 'var pos1 = this.length * ' + BYTES_PER_ELEMENT.toFixed(0) + ';\n' + - 'var pos2 = pos1 / 2;\n' + - 'var pos4 = pos1 / 4;\n' + + 'var elementIndex = this.length;\n' + 'this.length++;\n' + - 'this.metadataArray[0]++;\n' + - 'if (this.length > this.allocatedLength) this.resize(this.length);\n'; + 'if (this.length > this.allocatedLength) this._resize(this.length);\n'; + for (var m = 0; m < members.length; m++) { var member = members[m]; - var argName = 'arg_' + m; - var index = 'pos' + sizeOf(member.type).toFixed(0) + ' + ' + (member.offset / sizeOf(member.type)).toFixed(0); - body += 'this.' + getArrayViewName(member.type) + '[' + index + '] = ' + argName + ';\n'; - argNames.push(argName); + var size = sizeOf(member.type); + + if (usedTypeSizes.indexOf(size) < 0) { + usedTypeSizes.push(size); + body += 'var pos' + size.toFixed(0) + ' = elementIndex * ' + (BYTES_PER_ELEMENT / size).toFixed(0) + ';\n'; + } + + for (var c = 0; c < member.components; c++) { + var argName = 'arg' + argNames.length; + var index = 'pos' + size.toFixed(0) + ' + ' + (member.offset / size + c).toFixed(0); + body += 'this.' + getArrayViewName(member.type) + '[' + index + '] = ' + argName + ';\n'; + argNames.push(argName); + } } + + body += 'return elementIndex;'; + return new Function(argNames, body); } -function createGetter(type, offset) { - var index = 'this._pos' + sizeOf(type).toFixed(0) + ' + ' + (offset / sizeOf(type)).toFixed(0); - return new Function([], 'return this._structArray.' + getArrayViewName(type) + '[' + index + '];'); +function getMemberComponentString(member, component) { + var elementOffset = 'this._pos' + sizeOf(member.type).toFixed(0); + var componentOffset = (member.offset / sizeOf(member.type) + component).toFixed(0); + var index = elementOffset + ' + ' + componentOffset; + return 'this._structArray.' + getArrayViewName(member.type) + '[' + index + ']'; + } -function createSetter(type, offset) { - var index = 'this._pos' + sizeOf(type).toFixed(0) + ' + ' + (offset / sizeOf(type)).toFixed(0); - return new Function(['x'], 'this._structArray.' + getArrayViewName(type) + '[' + index + '] = x;'); +function createGetter(member) { + return new Function([], 'return ' + getMemberComponentString(member, 0) + ';'); } +function createSetter(member) { + return new Function(['x'], getMemberComponentString(member, 0) + ' = x;'); +} +function createArrayGetter(member) { + var body = 'return [\n'; + for (var c = 0; c < member.components; c++) { + body += getMemberComponentString(member, c) + ',\n'; + } + body += '];'; + return new Function([], body); +} + +function createArraySetter(member) { + var argNames = []; + var body = ''; + for (var c = 0; c < member.components; c++) { + var argName = 'c' + argNames.length; + body += getMemberComponentString(member, c) + ' = ' + argName + ';\n'; + argNames.push(argName); + } + return new Function(argNames, body); +} + + +/** + * @class Struct + * @param {StructArray} structArray The StructArray the struct is stored in + * @param {number} index The index of the struct in the StructArray. + * @private + */ function Struct(structArray, index) { this._structArray = structArray; this._setIndex(index); } +/** + * Make this Struct object point to a different instance in the same array. + * It can be cheaper to use .setIndex to re-use an existing Struct than to + * create a new one. + * @param {number} index The index of the struct in the StructArray; + * @private + */ Struct.prototype._setIndex = function(index) { this._pos1 = index * this.BYTE_SIZE; this._pos2 = this._pos1 / 2; this._pos4 = this._pos1 / 4; + this._pos8 = this._pos1 / 8; }; -function StructArray(initialAllocatedLength) { - if (initialAllocatedLength instanceof ArrayBuffer) { - this.arrayBuffer = initialAllocatedLength; +/** + * @class StructArray + * The StructArray class is inherited by the custom StructArrayType classes created with + * `new StructArrayType(members, options)`. + * @private + */ +function StructArray(lengthOrSerialized) { + // Create from an serialized StructArray + if (typeof lengthOrSerialized === 'object') { + var serialized = lengthOrSerialized; + this.arrayBuffer = serialized.arrayBuffer; + this.length = serialized.length; + this.allocatedLength = this.arrayBuffer.byteLength / this.BYTES_PER_ELEMENT; this._refreshViews(); - this.length = this.metadataArray[0]; - this.allocatedLength = this.uint8Array.length / this.BYTES_PER_ELEMENT; + + // Create a new StructArray } else { - if (initialAllocatedLength === undefined) { - initialAllocatedLength = this.DEFAULT_ALLOCATED_LENGTH; - } - this.resize(initialAllocatedLength); + this.length = 0; + this.allocatedLength = 0; + this._resize(lengthOrSerialized === undefined ? this.DEFAULT_ALLOCATED_LENGTH : lengthOrSerialized); } } -StructArray.prototype.DEFAULT_ALLOCATED_LENGTH = 100; -StructArray.prototype.RESIZE_FACTOR = 1.5; -StructArray.prototype.allocatedLength = 0; -StructArray.prototype.length = 0; -var METADATA_BYTES = align(4, 8); +/** + * Serialize the StructArray type. This serializes the *type* not an instance of the type. + * @private + */ +StructArray.serialize = function() { + return { + members: this.prototype.StructType.prototype.members, + BYTES_PER_ELEMENT: this.prototype.BYTES_PER_ELEMENT + }; +}; + +/** + * @property {number} + * @private + * @readonly + */ +StructArray.prototype.DEFAULT_ALLOCATED_LENGTH = 128; + +/** + * @property {number} + * @private + * @readonly + */ +StructArray.prototype.RESIZE_MULTIPLIER = 5; + +/** + * Serialize this StructArray instance + * @private + */ +StructArray.prototype.serialize = function() { + if (this.length !== this.allocatedLength) { + this.trim(); + } + return { + length: this.length, + arrayBuffer: this.arrayBuffer + }; +}; + +/** + * Return the Struct at the given location in the array. + * @private + * @param {number} index The index of the element. + */ +StructArray.prototype.at = function(index) { + return new this.StructType(this, index); +}; -StructArray.prototype.resize = function(n) { - this.allocatedLength = Math.max(n, Math.floor(this.allocatedLength * this.RESIZE_FACTOR)); - this.arrayBuffer = new ArrayBuffer(METADATA_BYTES + align(this.allocatedLength * this.BYTES_PER_ELEMENT, 8)); +/** + * Resize the buffer to discard unused capacity. + * @private + */ +StructArray.prototype.trim = function() { + this.allocatedLength = this.length; + this.arrayBuffer = this.arrayBuffer.slice(0, this.length * this.BYTES_PER_ELEMENT); + this._refreshViews(); +}; + +/** + * Resize the array so that it fits at least `n` elements. + * @private + * @param {number} n The number of elements that must fit in the array after the resize. + */ +StructArray.prototype._resize = function(n) { + this.allocatedLength = Math.max(n, Math.floor(this.allocatedLength * this.RESIZE_MULTIPLIER)); + this.arrayBuffer = new ArrayBuffer(this.allocatedLength * this.BYTES_PER_ELEMENT); var oldUint8Array = this.uint8Array; this._refreshViews(); if (oldUint8Array) this.uint8Array.set(oldUint8Array); }; +/** + * Create TypedArray views for the current ArrayBuffer. + * @private + */ StructArray.prototype._refreshViews = function() { - for (var t in viewTypes) { - this[getArrayViewName(t)] = new viewTypes[t](this.arrayBuffer, METADATA_BYTES); + for (var t = 0; t < this._usedTypes.length; t++) { + var type = this._usedTypes[t]; + this[getArrayViewName(type)] = new viewTypes[type](this.arrayBuffer); } - this.metadataArray = new Uint32Array(this.arrayBuffer, 0, 1); }; -StructArray.prototype.at = function(index) { - return new this.StructType(this, index); -};