Skip to content
Closed
69 changes: 56 additions & 13 deletions src/core/object-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ class ObjectPool {
*/
_count = 0;

/**
* A map from object references to their index in `_pool`. This is used to determine
* whether an object is actually allocated from this pool (and at which index).
*
* @type {WeakMap<InstanceType<T>, number>}
* @private
*/
_objToIndexMap = new WeakMap();

/**
* @param {T} constructorFunc - The constructor function for the
* objects in the pool.
Expand All @@ -40,18 +49,6 @@ class ObjectPool {
this._resize(size);
}

/**
* @param {number} size - The number of object instances to allocate.
* @private
*/
_resize(size) {
if (size > this._pool.length) {
for (let i = this._pool.length; i < size; i++) {
this._pool[i] = new this._constructor();
}
}
}

/**
* Returns an object instance from the pool. If no instances are available, the pool will be
* doubled in size and a new instance will be returned.
Expand All @@ -60,18 +57,64 @@ class ObjectPool {
*/
allocate() {
if (this._count >= this._pool.length) {
this._resize(this._pool.length * 2);
this._resize(Math.max(1, this._pool.length * 2));
}
return this._pool[this._count++];
}

/**
* Attempts to free the given object back into the pool. This only works if the object
* was previously allocated and is still in use.
*
* @param {InstanceType<T>} obj - The object instance to be freed back into the pool.
* @returns {boolean} Whether freeing succeeded.
*/
free(obj) {
const index = this._objToIndexMap.get(obj);
if (index === undefined) {
return false;
}

if (index >= this._count) {
return false;
}

// Swap this object with the last allocated object, then decrement `_count`
const lastIndex = this._count - 1;
const lastObj = this._pool[lastIndex];

this._pool[index] = lastObj;
this._pool[lastIndex] = obj;

this._objToIndexMap.set(lastObj, index);
this._objToIndexMap.set(obj, lastIndex);

this._count -= 1;
return true;
}

/**
* All object instances in the pool will be available again. The pool itself will not be
* resized.
*/
freeAll() {
this._count = 0;
}

/**
* @param {number} size - The number of object instances to allocate.
* @private
*/
_resize(size) {
if (size > this._pool.length) {
for (let i = this._pool.length; i < size; i++) {
const obj = new this._constructor();
this._pool[i] = obj;

this._objToIndexMap.set(obj, i);
}
}
}
}

export { ObjectPool };
131 changes: 131 additions & 0 deletions src/core/queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* A circular queue that automatically extends its capacity when full.
* This implementation uses a fixed-size array to store elements and
* supports efficient enqueue and dequeue operations.
* It is recommended to use `initialCapacity` that is close to **real-world** usage.
* @template T
*/
class Queue {
/**
* Create a new queue.
* @param {number} [initialCapacity] - The initial capacity of the queue.
*/
constructor(initialCapacity = 8) {
/**
* Underlying storage for the queue.
* @type {Array<T|undefined>}
* @private
*/
this._storage = new Array(initialCapacity);

/**
* The head (front) index.
* @type {number}
* @private
*/
this._head = 0;

/**
* The current number of elements in the queue.
* @type {number}
* @private
*/
this._length = 0;
}

/**
* The current number of elements in the queue.
* @type {number}
* @readonly
*/
get length() {
return this._length;
}

/**
* Change the capacity of the underlying storage.
* Does not shrink capacity if new capacity is less than or equal to the current length.
* @param {number} capacity - The new capacity for the queue.
*/
set capacity(capacity) {
if (capacity <= this._length) {
return;
}

const oldCapacity = this._storage.length;
this._storage.length = capacity;

// Handle wrap-around scenario by moving elements.
if (this._head + this._length > oldCapacity) {
const endLength = oldCapacity - this._head;
for (let i = 0; i < endLength; i++) {
this._storage[capacity - endLength + i] = this._storage[this._head + i];
}
this._head = capacity - endLength;
}
}

/**
* The capacity of the queue.
* @type {number}
* @readonly
*/
get capacity() {
return this._storage.length;
}

/**
* Enqueue (push) a value to the back of the queue.
* Automatically extends capacity if the queue is full.
* @param {T} value - The value to enqueue.
* @returns {number} The new length of the queue.
*/
enqueue(value) {
if (this._length === this._storage.length) {
this.capacity = this._storage.length * 2;
}

const tailIndex = (this._head + this._length) % this._storage.length;
this._storage[tailIndex] = value;
this._length++;
return this._length;
}

/**
* Dequeue (pop) a value from the front of the queue.
* @returns {T|undefined} The dequeued value, or `undefined` if the queue is empty.
*/
dequeue() {
if (this.isEmpty()) {
return undefined;
}

const value = this._storage[this._head];
this._storage[this._head] = undefined;
this._head = (this._head + 1) % this._storage.length;
this._length--;

return value;
}

/**
* Returns the value at the front of the queue without removing it.
* @returns {T|undefined} The front value, or `undefined` if the queue is empty.
*/
peek() {
if (this.isEmpty()) {
return undefined;
}
return this._storage[this._head];
}

/**
* Determines whether the queue is empty.
* @returns {boolean} True if the queue is empty, false otherwise.
*/
isEmpty() {
return this._length === 0;
}
}

export { Queue };
2 changes: 1 addition & 1 deletion src/framework/lightmapper/lightmapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ class Lightmapper {

const meshInstances = bakeNode.meshInstances;
for (let i = 0; i < meshInstances.length; i++) {
if (meshInstances[i]._isVisible(shadowCam)) {
if (meshInstances[i]._isVisible(shadowCam, this.renderer._aabbUpdateIndex)) {
nodeVisible = true;
break;
}
Expand Down
Loading
Loading