// @flow // Note: all "sizes" are measured in bytes import assert from 'assert'; import type {Transferable} from '../types/transferable'; const viewTypes = { 'Int8': Int8Array, 'Uint8': Uint8Array, 'Int16': Int16Array, 'Uint16': Uint16Array, 'Int32': Int32Array, 'Uint32': Uint32Array, 'Float32': Float32Array }; export type ViewType = $Keys; /** * @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; /** * @param {StructArray} structArray The StructArray the struct is stored in * @param {number} index The index of the struct in the StructArray. * @private */ constructor(structArray: StructArray, index: number) { (this: any)._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; export type StructArrayMember = { name: string, type: ViewType, components: number, offset: number }; export type StructArrayLayout = { members: Array, size: number, alignment: ?number } export type SerializedStructArray = { length: number, arrayBuffer: ArrayBuffer }; /** * `StructArray` provides an abstraction over `ArrayBuffer` and `TypedArray` * making it behave like an array of typed structs. * * Conceptually, a StructArray is comprised of elements, i.e., instances of its * associated struct type. Each particular struct type, together with an * alignment size, determines the memory layout of a StructArray whose elements * are of that type. Thus, for each such layout that we need, we have * a corrseponding StructArrayLayout class, inheriting from StructArray and * implementing `emplaceBack()` and `_refreshViews()`. * * In some cases, where we need to access particular elements of a StructArray, * we implement a more specific subclass that inherits from one of the * StructArrayLayouts and adds a `get(i): T` accessor that returns a structured * object whose properties are proxies into the underlying memory space for the * i-th element. This affords the convience of working with (seemingly) plain * Javascript objects without the overhead of serializing/deserializing them * into ArrayBuffers for efficient web worker transfer. * * @private */ class StructArray { capacity: number; length: number; isTransferred: boolean; arrayBuffer: ArrayBuffer; uint8: Uint8Array; // The following properties are defined on the prototype. members: Array; bytesPerElement: number; +emplaceBack: Function; +emplace: Function; constructor() { this.isTransferred = false; this.capacity = -1; this.resize(0); } /** * Serialize a StructArray instance. Serializes both the raw data and the * metadata needed to reconstruct the StructArray base class during * deserialization. * @private */ static serialize(array: StructArray, transferables?: Array): SerializedStructArray { assert(!array.isTransferred); array._trim(); if (transferables) { array.isTransferred = true; transferables.push(array.arrayBuffer); } return { length: array.length, arrayBuffer: array.arrayBuffer, }; } static deserialize(input: SerializedStructArray) { const structArray = Object.create(this.prototype); structArray.arrayBuffer = input.arrayBuffer; structArray.length = input.length; structArray.capacity = input.arrayBuffer.byteLength / structArray.bytesPerElement; structArray._refreshViews(); return structArray; } /** * 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(); } } /** * Resets the the length of the array to 0 without de-allocating capcacity. */ clear() { this.length = 0; } /** * 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.reserve(n); this.length = n; } /** * Indicate a planned increase in size, so that any necessary allocation may * be done once, ahead of time. * @param {number} n The expected size of the array. */ reserve(n: number) { 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() { throw new Error('_refreshViews() must be implemented by each concrete StructArray layout'); } } /** * Given a list of member fields, create a full StructArrayLayout, in * particular calculating the correct byte offset for each field. This data * is used at build time to generate StructArrayLayout_*#emplaceBack() and * other accessors, and at runtime for binding vertex buffer attributes. * * @private */ function createLayout( members: Array<{ name: string, type: ViewType, +components?: number, }>, alignment: number = 1 ): StructArrayLayout { let offset = 0; let maxSize = 0; const layoutMembers = members.map((member) => { assert(member.name.length); 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, offset: memberOffset, }; }); const size = align(offset, Math.max(maxSize, alignment)); return { members: layoutMembers, size, alignment }; } function sizeOf(type: ViewType): number { return viewTypes[type].BYTES_PER_ELEMENT; } function align(offset: number, size: number): number { return Math.ceil(offset / size) * size; } export {StructArray, Struct, viewTypes, createLayout};