UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

341 lines (340 loc) 10.4 kB
/** * A general-purpose 1D block allocator backed by a doubly-linked list with segregated free-list * buckets. Manages a linear address space where contiguous blocks can be allocated and freed. * Supports incremental defragmentation and automatic growth. * * Free blocks are organized into power-of-2 size buckets for best-fit allocation, which reduces * fragmentation compared to a single first-fit free list. * * @ignore */ export class BlockAllocator { /** * Create a new BlockAllocator. * * @param {number} [capacity] - Initial address space capacity. Defaults to 0. * @param {number} [growMultiplier] - Multiplicative growth factor for auto-grow in * {@link BlockAllocator#updateAllocation}. Defaults to 1.1 (10% extra). */ constructor(capacity?: number, growMultiplier?: number); /** * Head of the main list (all blocks, offset-ordered). * * @type {MemBlock|null} * @private */ private _headAll; /** * Tail of the main list. * * @type {MemBlock|null} * @private */ private _tailAll; /** * Segregated free-list bucket heads. Each entry is the head of a doubly-linked list of free * blocks whose size falls in that power-of-2 range. Bucket i covers sizes [2^i, 2^(i+1)). * The array grows dynamically as larger free blocks appear. * * @type {Array<MemBlock|null>} * @private */ private _freeBucketHeads; /** * Pool of recycled MemBlock objects. * * @type {MemBlock[]} * @private */ private _pool; /** * Total address space. * * @type {number} * @private */ private _capacity; /** * Sum of all allocated block sizes. * * @type {number} * @private */ private _usedSize; /** * Sum of all free region sizes. * * @type {number} * @private */ private _freeSize; /** * Number of free regions. Maintained O(1) for the fragmentation metric. * * @type {number} * @private */ private _freeRegionCount; /** * Multiplicative growth factor used by {@link BlockAllocator#updateAllocation}. * When growing, the new capacity is at least `capacity * growMultiplier`. * * @type {number} * @private */ private _growMultiplier; /** * Total address space capacity. * * @type {number} */ get capacity(): number; /** * Total size of all allocated blocks. * * @type {number} */ get usedSize(): number; /** * Total size of all free regions. * * @type {number} */ get freeSize(): number; /** * Fragmentation ratio in the range [0, 1]. Returns 0 when all free space is one contiguous * block (ideal), and approaches 1 when free space is split into many pieces. Computed O(1) * from the internally maintained free region count. * * @type {number} */ get fragmentation(): number; /** * Compute the bucket index for a given block size. Uses floor(log2(size)) via the CLZ * intrinsic for integer math. * * @param {number} size - Block size (must be > 0). * @returns {number} Bucket index. * @private */ private _bucketFor; /** * Add a free block to the appropriate size bucket. Prepends to the bucket list for O(1) * insertion. Grows the bucket array if needed. * * @param {MemBlock} block - The free block to add. * @private */ private _addToBucket; /** * Remove a free block from its current size bucket. * * @param {MemBlock} block - The free block to remove. * @private */ private _removeFromBucket; /** * Move a free block to the correct bucket after its size changed (e.g. due to merging or * splitting). Only performs the remove+add if the bucket actually changed. * * @param {MemBlock} block - The free block whose size has changed. * @private */ private _rebucket; /** * Obtain a MemBlock from the pool or create a new one. * * @param {number} offset - The offset. * @param {number} size - The size. * @param {boolean} free - Whether the block is free. * @returns {MemBlock} The block. * @private */ private _obtain; /** * Return a MemBlock to the pool. * * @param {MemBlock} block - The block to release. * @private */ private _release; /** * Insert a block into the main list after a given node. * * @param {MemBlock} block - The block to insert. * @param {MemBlock|null} after - Insert after this node (null = insert at head). * @private */ private _insertAfterInMainList; /** * Remove a block from the main list. * * @param {MemBlock} block - The block to remove. * @private */ private _removeFromMainList; /** * Find the best-fit free block for the requested size using segregated buckets. Scans the * target bucket for the smallest block >= size (best-fit), then falls through to higher * buckets where any block is guaranteed large enough (first-fit). * * @param {number} size - Minimum size needed. * @returns {MemBlock|null} The best fitting free block, or null. * @private */ private _findFreeBlock; /** * Allocate a contiguous block of the given size. * * @param {number} size - The number of units to allocate. Must be > 0. * @returns {MemBlock|null} A MemBlock handle, or null if no space is available. */ allocate(size: number): MemBlock | null; /** * Free a previously allocated block. Adjacent free regions are merged automatically. * * @param {MemBlock} block - The block to free (must have been returned by * {@link BlockAllocator#allocate}). */ free(block: MemBlock): void; /** * Grow the address space. Only increases capacity, never decreases. * * @param {number} newCapacity - The new capacity. Must be > current capacity. */ grow(newCapacity: number): void; /** * Defragment the allocator by moving allocated blocks to reduce fragmentation. * * When maxMoves is 0, performs a full compaction in a single O(n) pass: all allocated blocks * are packed contiguously from offset 0 and a single free block is placed at the end. * * When maxMoves > 0, performs incremental defragmentation in two phases: * - Phase 1 (up to maxMoves/2): relocates the last allocated block to the first fitting free * gap (maximizes tail free space). * - Phase 2 (up to maxMoves/2): slides allocated blocks left into adjacent free gaps * (cleans up interior fragmentation). * * @param {number} [maxMoves] - Maximum number of block moves. 0 = full compaction. Defaults * to 0. * @param {Set<MemBlock>} [result] - Optional Set to receive moved blocks. Defaults to a new * Set. * @returns {Set<MemBlock>} The set of MemBlocks that were moved. */ defrag(maxMoves?: number, result?: Set<MemBlock>): Set<MemBlock>; /** * Full compaction: single-pass, pack all allocated blocks from offset 0. * * @param {Set<MemBlock>} result - Set to receive moved blocks. * @private */ private _defragFull; /** * Incremental defragmentation with two phases. * * @param {number} maxMoves - Maximum total moves. * @param {Set<MemBlock>} result - Set to receive moved blocks. * @private */ private _defragIncremental; /** * Move an allocated block to a free gap. The block's offset is updated in-place so caller * handles stay valid. * * @param {MemBlock} block - The allocated block to move. * @param {MemBlock} gap - The free gap to move into (must be >= block size). * @private */ private _moveBlock; /** * Batch update: free a set of blocks and allocate new ones. Handles growth and compaction * internally when allocations cannot be satisfied. * * The `toAllocate` array is modified in-place: each numeric size entry is replaced with the * allocated {@link MemBlock}. * * @param {MemBlock[]} toFree - Blocks to release. * @param {Array<number|MemBlock>} toAllocate - Sizes to allocate. Modified in-place: numbers * are replaced with MemBlock instances. * @returns {boolean} True if a full defrag was performed (all existing blocks have new * offsets and must be re-rendered), false if only incremental allocations were made. */ updateAllocation(toFree: MemBlock[], toAllocate: Array<number | MemBlock>): boolean; } /** * A node in the {@link BlockAllocator}'s linked list, representing either an allocated block or a * free region. Callers receive MemBlock instances as handles from {@link BlockAllocator#allocate} * and must not modify any properties directly. * * @ignore */ export class MemBlock { /** * Position in the address space. * * @type {number} * @private */ private _offset; /** * Size of this block. * * @type {number} * @private */ private _size; /** * True if this is a free region, false if allocated. * * @type {boolean} * @private */ private _free; /** * Previous node in the main (all-nodes) list. * * @type {MemBlock|null} * @private */ private _prev; /** * Next node in the main (all-nodes) list. * * @type {MemBlock|null} * @private */ private _next; /** * Previous node in the bucket free-list. * * @type {MemBlock|null} * @private */ private _prevFree; /** * Next node in the bucket free-list. * * @type {MemBlock|null} * @private */ private _nextFree; /** * Index of the size bucket this free block belongs to, or -1 if not in any bucket. * * @type {number} * @private */ private _bucket; /** * The offset of this block in the address space. * * @type {number} */ get offset(): number; /** * The size of this block. * * @type {number} */ get size(): number; }