@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
372 lines (309 loc) • 9.66 kB
JavaScript
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;
}
}