mapbox-gl
Version:
A WebGL interactive maps library
384 lines (325 loc) • 11.4 kB
JavaScript
;
// @flow
// Note: all "sizes" are measured in bytes
const assert = require('assert');
module.exports = createStructArrayType;
const viewTypes = {
'Int8': Int8Array,
'Uint8': Uint8Array,
'Uint8Clamped': Uint8ClampedArray,
'Int16': Int16Array,
'Uint16': Uint16Array,
'Int32': Int32Array,
'Uint32': Uint32Array,
'Float32': Float32Array,
'Float64': Float64Array
};
/* eslint-disable no-undef */
type ViewType = $Keys<typeof viewTypes>;
/* eslint-enable no-undef */
/**
* @typedef {Object} StructMember
* @private
* @property {string} name
* @property {string} type
* @property {number} components
*/
/**
* @private
*/
class Struct {
_pos1: number;
_pos2: number;
_pos4: number;
_pos8: number;
_structArray: StructArray;
// The following properties are defined on the prototype of sub classes.
size: number;
alignment: number;
/**
* @param {StructArray} structArray The StructArray the struct is stored in
* @param {number} index The index of the struct in the StructArray.
* @private
*/
constructor(structArray, index) {
this._structArray = structArray;
this._pos1 = index * this.size;
this._pos2 = this._pos1 / 2;
this._pos4 = this._pos1 / 4;
this._pos8 = this._pos1 / 8;
}
}
const DEFAULT_CAPACITY = 128;
const RESIZE_MULTIPLIER = 5;
type StructArrayMember = {|
name: string,
type: ViewType,
components: number,
offset: number
|};
/**
* The StructArray class is inherited by the custom StructArrayType classes created with
* `createStructArrayType(members, options)`.
* @private
*/
class StructArray {
capacity: number;
length: number;
isTransferred: boolean;
arrayBuffer: ArrayBuffer;
int8: ?Int8Array;
uint8: Uint8Array;
uint8clamped: ?Uint8ClampedArray;
int16: ?Int16Array;
uint16: ?Uint16Array;
int32: ?Int32Array;
uint32: ?Uint32Array;
float32: ?Float32Array;
float64: ?Float64Array;
// The following properties aer defined on the prototype.
members: Array<StructArrayMember>;
StructType: typeof Struct;
bytesPerElement: number;
_usedTypes: Array<ViewType>;
emplaceBack: Function;
constructor(serialized?: {arrayBuffer: ArrayBuffer, length: number}) {
this.isTransferred = false;
if (serialized !== undefined) {
// Create from an serialized StructArray
this.arrayBuffer = serialized.arrayBuffer;
this.length = serialized.length;
this.capacity = this.arrayBuffer.byteLength / this.bytesPerElement;
this._refreshViews();
// Create a new StructArray
} else {
this.capacity = -1;
this.resize(0);
}
}
/**
* Serialize the StructArray type. This serializes the *type* not an instance of the type.
*/
static serialize() {
return {
members: this.prototype.members,
alignment: this.prototype.StructType.prototype.alignment,
bytesPerElement: this.prototype.bytesPerElement
};
}
/**
* Serialize this StructArray instance
*/
serialize(transferables: ?{push: (buffer: ArrayBuffer) => void}) {
assert(!this.isTransferred);
this._trim();
if (transferables) {
this.isTransferred = true;
transferables.push(this.arrayBuffer);
}
return {
length: this.length,
arrayBuffer: this.arrayBuffer
};
}
/**
* Return the Struct at the given location in the array.
* @param {number} index The index of the element.
*/
get(index: number) {
assert(!this.isTransferred);
return new this.StructType(this, index);
}
/**
* Resize the array to discard unused capacity.
*/
_trim() {
if (this.length !== this.capacity) {
this.capacity = this.length;
this.arrayBuffer = this.arrayBuffer.slice(0, this.length * this.bytesPerElement);
this._refreshViews();
}
}
/**
* Resize the array.
* If `n` is greater than the current length then additional elements with undefined values are added.
* If `n` is less than the current length then the array will be reduced to the first `n` elements.
* @param {number} n The new size of the array.
*/
resize(n: number) {
assert(!this.isTransferred);
this.length = n;
if (n > this.capacity) {
this.capacity = Math.max(n, Math.floor(this.capacity * RESIZE_MULTIPLIER), DEFAULT_CAPACITY);
this.arrayBuffer = new ArrayBuffer(this.capacity * this.bytesPerElement);
const oldUint8Array = this.uint8;
this._refreshViews();
if (oldUint8Array) this.uint8.set(oldUint8Array);
}
}
/**
* Create TypedArray views for the current ArrayBuffer.
*/
_refreshViews() {
for (const type of this._usedTypes) {
// $FlowFixMe
this[getArrayViewName(type)] = new viewTypes[type](this.arrayBuffer);
}
}
/**
* Output the `StructArray` between indices `startIndex` and `endIndex` as an array of `StructTypes` to enable sorting
* @param {number} startIndex
* @param {number} endIndex
*/
toArray(startIndex: number, endIndex: number) {
assert(!this.isTransferred);
const array = [];
for (let i = startIndex; i < endIndex; i++) {
const struct = this.get(i);
array.push(struct);
}
return array;
}
}
const structArrayTypeCache: {[key: string]: typeof StructArray} = {};
/**
* `createStructArrayType` 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
* @param {Object} options
* @param {number} options.alignment Use `4` to align members to 4 byte boundaries. Default is 1.
* @param {Array<StructMember>} options.members
* @example
*
* var PointArrayType = createStructArrayType({
* members: [
* { type: 'Int16', name: 'x' },
* { type: 'Int16', name: 'y' }
* ]});
*
* var pointArray = new PointArrayType();
* pointArray.emplaceBack(10, 15);
* pointArray.emplaceBack(20, 35);
*
* point = pointArray.get(0);
* assert(point.x === 10);
* assert(point.y === 15);
*
* @private
*/
function createStructArrayType(options: {|
members: Array<{type: ViewType, name: string, components?: number}>,
alignment?: number
|}) {
const key = JSON.stringify(options);
if (structArrayTypeCache[key]) {
return structArrayTypeCache[key];
}
const alignment = (options.alignment === undefined) ?
1 : options.alignment;
let offset = 0;
let maxSize = 0;
const usedTypes = ['Uint8'];
const members = options.members.map((member) => {
assert(member.name.length);
assert(member.type in viewTypes);
if (usedTypes.indexOf(member.type) < 0) usedTypes.push(member.type);
const typeSize = sizeOf(member.type);
const memberOffset = offset = align(offset, Math.max(alignment, typeSize));
const components = member.components || 1;
maxSize = Math.max(maxSize, typeSize);
offset += typeSize * components;
return {
name: member.name,
type: member.type,
components: components,
offset: memberOffset
};
});
const size = align(offset, Math.max(maxSize, alignment));
class StructType extends Struct {}
StructType.prototype.alignment = alignment;
StructType.prototype.size = size;
for (const member of members) {
for (let c = 0; c < member.components; c++) {
const name = member.name + (member.components === 1 ? '' : c);
Object.defineProperty(StructType.prototype, name, {
get: createGetter(member, c),
set: createSetter(member, c)
});
}
}
class StructArrayType extends StructArray {}
StructArrayType.prototype.members = members;
StructArrayType.prototype.StructType = StructType;
StructArrayType.prototype.bytesPerElement = size;
StructArrayType.prototype.emplaceBack = createEmplaceBack(members, size);
StructArrayType.prototype._usedTypes = usedTypes;
structArrayTypeCache[key] = StructArrayType;
return StructArrayType;
}
function align(offset: number, size: number): number {
return Math.ceil(offset / size) * size;
}
function sizeOf(type: ViewType): number {
return viewTypes[type].BYTES_PER_ELEMENT;
}
function getArrayViewName(type: ViewType): string {
return type.toLowerCase();
}
/*
* > I saw major perf gains by shortening the source of these generated methods (i.e. renaming
* > elementIndex to i) (likely due to v8 inlining heuristics).
* - lucaswoj
*/
function createEmplaceBack(members, bytesPerElement): Function {
const usedTypeSizes = [];
const argNames = [];
let body =
'var i = this.length;\n' +
'this.resize(this.length + 1);\n';
for (const member of members) {
const size = sizeOf(member.type);
// array offsets to the end of current data for each type size
// var o{SIZE} = i * ROUNDED(bytesPerElement / size);
if (usedTypeSizes.indexOf(size) < 0) {
usedTypeSizes.push(size);
body += `var o${size.toFixed(0)} = i * ${(bytesPerElement / size).toFixed(0)};\n`;
}
for (let c = 0; c < member.components; c++) {
// arguments v0, v1, v2, ... are, in order, the components of
// member 0, then the components of member 1, etc.
const argName = `v${argNames.length}`;
// The index for `member` component `c` into the appropriate type array is:
// this.{TYPE}[o{SIZE} + MEMBER_OFFSET + {c}] = v{X}
// where MEMBER_OFFSET = ROUND(member.offset / size) is the per-element
// offset of this member into the array
const index = `o${size.toFixed(0)} + ${(member.offset / size + c).toFixed(0)}`;
body += `this.${getArrayViewName(member.type)}[${index}] = ${argName};\n`;
argNames.push(argName);
}
}
body += 'return i;';
return new Function(argNames.toString(), body);
}
function createMemberComponentString(member, component) {
const elementOffset = `this._pos${sizeOf(member.type).toFixed(0)}`;
const componentOffset = (member.offset / sizeOf(member.type) + component).toFixed(0);
const index = `${elementOffset} + ${componentOffset}`;
return `this._structArray.${getArrayViewName(member.type)}[${index}]`;
}
function createGetter(member, c) {
return new Function(`return ${createMemberComponentString(member, c)};`);
}
function createSetter(member, c) {
return new Function('x', `${createMemberComponentString(member, c)} = x;`);
}