UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

328 lines (261 loc) • 8.29 kB
import { AbstractTextureAtlas } from "./AbstractTextureAtlas.js"; class PatchRecord { constructor() { /** * * @type {AtlasPatch|null} */ this.patch = null; /** * When was the patch last used * @type {number} */ this.last_use_time = 0; } } /** * Adding and removing elements to atlas can be costly. It can trigger repack, it may require splatting or cleaning. * This abstraction allows us to bypass that most of the time by caching unused patches and just reusing them in the future * if they are requested again. * * The caching strategy here is a little unusual. The texture atlas has a certain size, and the cache will use as much of * the atlas space as it can. Meaning that if there is not much unused space - the cache will be very small * And if only a small part of the atlas is area is used by "live" patches, the cache can be quite large */ export class CachingTextureAtlas extends AbstractTextureAtlas { /** * * @param {AbstractTextureAtlas} atlas */ constructor({ atlas }) { super(); /** * * @type {PatchRecord[]} * @private */ this.__cached_patches = []; /** * * @type {TextureAtlas} * @private */ this.__atals = atlas; } /** * Finds an entry in the cache to evict * @returns {number} * @private */ __find_eviction_target() { let best_date = Infinity; let best_node = -1; const records = this.__cached_patches; const n = records.length; for (let i = 0; i < n; i++) { const record = records[i]; const date = record.last_use_time; if (date < best_date) { best_node = i; best_date = date; } } return best_node; } /** * Finds an entry in the cache that could be replaced with an incoming element * @private * @returns {number} * @param {number} width * @param {number} height */ __find_replacement_target(width, height) { // find the smallest element in area that is least recently used in cache let best_area = Infinity; let best_date = Infinity; let best_node = -1; const records = this.__cached_patches; const n = records.length; for (let i = 0; i < n; i++) { const record = records[i]; const packing = record.patch.packing; const w = packing.x1 - packing.x0; const h = packing.y1 - packing.y0; if (w < width || h < height) { // too small continue; } const area = w * h; if (area > best_area) { // prefer smaller patches to evict, as they are cheaper to pull back later on continue; } const date = record.last_use_time; if (date >= best_date) { // prefer patches that have been in cache the longest (the oldest records) continue; } // found a better candidate, update best_area = area; best_date = date; best_node = i; } return best_node; } /** * Will evict specified element from cache * @private * @param {number} index */ __evict(index) { const records = this.__cached_patches; const record = records[index]; records.splice(index, 1); this.__atals.remove(record.patch); } /** * Evict all cached elements, purging the cache * @private */ __evict_all() { for (let i = this.__cached_patches.length - 1; i > 0; i--) { this.__evict(i); } } /** * Free up space in cache to accommodate certain area * @private * @param {number} width * @param {number} height * @returns {boolean} true if enough space was freed up to fit specified area, false otherwise */ __evict_for(width, height) { if (this.__cached_patches.length <= 0) { // nothing to evict return false; } const target = this.__find_replacement_target(width, height); if (target !== -1) { // evicted just one element this.__evict(target); return true; } while (!this.__atals.can_pack(width, height)) { const victim = this.__find_eviction_target(); if (victim === -1) { return false; } this.__evict(victim); } return true; } update() { try { this.__atals.update(); } catch (e) { // failed to perform update if (this.__cached_patches.length > 0) { // has some caches patches, lets drop them this.__evict_all(); // try to update again this.__atals.update(); } else { // just re-throw throw e; } } } /** * * @returns {AbstractTextureAtlas} */ get atlas() { return this.__atals; } /** * * @return {Sampler2D} */ get sampler() { return this.__atals.sampler; } reset() { this.__cached_patches.splice(0, this.__cached_patches.length); this.__atals.reset(); } /** * * @param {Sampler2D} sampler * @returns {number} * @private */ __find_cache_record_index(sampler) { const records = this.__cached_patches; const n = records.length; for (let i = 0; i < n; i++) { if (records[i].patch.sampler === sampler) { return i; } } return -1; } /** * * @param {AtlasPatch} patch * @returns {number} * @private */ __find_cache_record_index_by_patch(patch) { const records = this.__cached_patches; const n = records.length; for (let i = 0; i < n; i++) { if (records[i].patch === patch) { return i; } } return -1; } add(sampler, padding = 4) { const existing_patch_index = this.__find_cache_record_index(sampler); const records = this.__cached_patches; if (existing_patch_index !== -1) { // cache hit const record = records[existing_patch_index]; // remove from cache records.splice(existing_patch_index, 1); // return patch return record.patch; } const w = sampler.width + padding * 2; const h = sampler.height + padding * 2; if (records.length > 0 && !this.__atals.can_pack(w, h)) { // new element will not fit, lets try to evict some cache to try and accommodate it // note that eviction will purge the entire cache in case that this patch doesn't fit this.__evict_for(w, h); } return this.__atals.add(sampler, padding); } /** * * @param {AtlasPatch} patch */ remove(patch) { // validate presence of patch in the atlas if (!this.__atals.contains(patch)) { return false; } const index = this.__find_cache_record_index_by_patch(patch); if (index !== -1) { // already in the cache return false; } // push to cache, let eviction logic handle the rest const record = new PatchRecord(); record.last_use_time = performance.now(); record.patch = patch; this.__cached_patches.push(record); return true; } }