UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

372 lines (309 loc) • 9.66 kB
import { assert } from "../../../../../assert.js"; import { align_4 } from "../../../../../binary/align_4.js"; import { makeArrayBuffer } from "../../../../../binary/makeArrayBuffer.js"; import { typed_array_copy } from "../../../../../collection/array/typed/typed_array_copy.js"; import { max3 } from "../../../../../math/max3.js"; /** * How many items to reserve by default * @readonly * @type {number} */ const INITIAL_CAPACITY = 128; /** * @readonly * @type {number} */ const CAPACITY_GROW_MULTIPLIER = 1.2; /** * @readonly * @type {number} */ const CAPACITY_GROW_MIN_STEP = 32; /** * @see https://github.com/blender/blender/blob/master/source/blender/blenlib/intern/BLI_mempool.c */ export class BinaryElementPool { /** * Unused slots * @type {number[]} * @private */ __free = []; /** * Tracks last unallocated item in the list, * this separate cursor is necessary to prevent re-allocation of the 'free' array * @type {number} * @private */ __free_pointer = 0; /** * * @type {number} * @private */ __size = 0; /** * * @param {number} item_size in bytes * @param {number} [initial_capacity] how many items to reverse in the newly created pool * @param {boolean} [use_shared_buffer] */ constructor(item_size, initial_capacity = INITIAL_CAPACITY, use_shared_buffer = false) { assert.isNonNegativeInteger(item_size, 'item_size'); assert.greaterThan(item_size, 0, 'item_size must be greater than 0'); assert.isNonNegativeInteger(initial_capacity, 'initial_capacity'); assert.isBoolean(use_shared_buffer, 'use_shared_buffer'); /** * Size of a single pool item in bytes * @type {number} * @private */ this.__item_size = item_size; /** * * @type {ArrayBuffer} * @private */ this.__data_buffer = makeArrayBuffer(align_4(initial_capacity * item_size), use_shared_buffer); /** * * @type {Uint8Array} * @private */ this.__data_uint8 = new Uint8Array(this.__data_buffer); /** * * @type {Uint32Array} * @private */ this.__data_uint32 = new Uint32Array(this.__data_buffer); /** * * @type {Float32Array} * @private */ this.__data_float32 = new Float32Array(this.__data_buffer); this.data_view = new DataView(this.__data_buffer); /** * * @type {number} * @private */ this.__capacity = initial_capacity; } /** * * @param {ArrayBuffer} buffer * @param {number} allocated_record_count */ fromArrayBuffer(buffer, allocated_record_count = 0) { assert.defined(buffer, 'buffer'); assert.notNull(buffer, 'buffer'); const capacity = Math.floor(buffer.byteLength / this.__item_size); assert.isNonNegativeInteger(allocated_record_count, 'allocated_record_count'); assert.lessThanOrEqual(allocated_record_count, capacity, 'allocated_record_count is higher than capacity'); this.__data_buffer = buffer; this.__data_uint8 = new Uint8Array(buffer); this.__data_uint32 = new Uint32Array(buffer); this.__data_float32 = new Float32Array(buffer); this.data_view = new DataView(buffer); this.__capacity = capacity; // drop free slots this.__free_pointer = 0; this.__size = allocated_record_count; } get arrayBuffer() { return this.__data_buffer; } /** * Size of a single record in bytes * @return {number} */ get item_size() { return this.__item_size; } /** * Returns size of used region, this includes both elements that are allocated and those that aren't * Please note that this value does not represent number of currently active elements, if you need that - you'll need to use something else * @return {number} */ get size() { return this.__size; } /** * * @return {number} */ get byteSize() { return this.__capacity * this.__item_size; } /** * Number of records that the pool can currently contain * @return {number} */ get capacity() { return this.__capacity; } /** * * @return {Uint32Array} */ get data_uint32() { return this.__data_uint32; } /** * * @return {Float32Array} */ get data_float32() { return this.__data_float32; } /** * Get rid of excess capacity */ trim() { this.__set_capacity(this.__size); } /** * * @param {number} id * @return {number} */ element_address(id) { assert.isNonNegativeInteger(id, 'id'); return this.__item_size * id; } /** * Returns word-offset of element * Word size is 4, so this is the same as `element_address(id) / 4` * @param {number} id * @return {number} */ element_word(id) { return this.element_address(id) >> 2; } /** * Used alongside iterators to check if element is actually allocated or not * @param {number} id * @return {boolean} */ is_allocated(id) { assert.isNonNegativeInteger(id, 'id'); if (id >= this.__size) { // ID is past allocated region return false; } const pointer = this.__free_pointer; for (let i = 0; i < pointer; i++) { const _id = this.__free[i]; if (id === _id) { // found in unallocated set return false; } } return true; } /** * * @param {number} new_capacity * @private */ __set_capacity(new_capacity) { if (this.__capacity === new_capacity) { // no point return; } const old_data_uint8 = this.__data_uint8; const aligned_byte_size = align_4(new_capacity * this.__item_size); const ArrayBufferType = this.__data_buffer.constructor; const new_data_buffer = new ArrayBufferType(aligned_byte_size); this.__data_buffer = new_data_buffer; this.__data_uint8 = new Uint8Array(new_data_buffer); this.__data_uint32 = new Uint32Array(this.__data_buffer); this.__data_float32 = new Float32Array(this.__data_buffer); this.data_view = new DataView(new_data_buffer); // copy old data typed_array_copy(old_data_uint8, this.__data_uint8); this.__capacity = new_capacity; } /** * * @param {number} min_capacity * @private */ __grow_capacity(min_capacity) { const new_capacity = Math.ceil(max3( min_capacity, this.__capacity * CAPACITY_GROW_MULTIPLIER, this.__capacity + CAPACITY_GROW_MIN_STEP )); this.__set_capacity(new_capacity); } /** * * @param {number} capacity */ ensure_capacity(capacity) { if (this.__capacity < capacity) { this.__grow_capacity(capacity); } } /** * * @return {number} ID of the allocated element */ allocate() { if (this.__free_pointer > 0) { // get unused slot this.__free_pointer--; return this.__free[this.__free_pointer]; } // allocate new let result = this.__size; this.__size++; if (this.__size >= this.__capacity) { // grow if necessary this.__grow_capacity(this.__size); } // assert.greaterThan(this.__data_buffer.byteLength, result * this.__item_size, 'memory underflow'); return result; } /** * Allocate a continuous range of IDs in bulk * @param {number} count * @return {number} offset where the range starts, this is your first ID basically */ allocate_continuous(count) { const offset = this.__size; this.__size += count; if (this.__size >= this.__capacity) { this.__grow_capacity(this.__size); } // assert.greaterThanOrEqual(this.__data_buffer.byteLength, (offset + count) * this.__item_size, 'memory underflow'); return offset; } /** * Please note that this method does not perform any checks at all. * You have to make sure that the item is actually unneeded and no duplicate calls are made * @param {number} id */ release(id) { if (id === this.__size - 1) { // releasing item at the end of the reserved region, don't have to push it into free set this.__size--; } else if (id >= this.__size) { // do nothing, just throw it away // this should never happen } else { this.__free[this.__free_pointer++] = id; } } /** * Removed all data from the pool * Note that initial allocation pointer is set to 0 */ clear() { this.__size = 0; this.__free_pointer = 0; } }