UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

264 lines (205 loc) • 6.47 kB
import { assert } from "../../../../core/assert.js"; import { array_remove_first } from "../../../../core/collection/array/array_remove_first.js"; import { array_sort_quick } from "../../../../core/collection/array/array_sort_quick.js"; import Signal from "../../../../core/events/signal/Signal.js"; import { AssetManager } from "../../../asset/AssetManager.js"; import { GameAssetType } from "../../../asset/GameAssetType.js"; import { Sampler2D } from "../sampler/Sampler2D.js"; import { sampler2d_to_uint8_RGBA } from "../sampler/sampler2d_to_uint8_RGBA.js"; import { compose_finger_print } from "./tile/compose_finger_print.js"; import { decompose_finger_print } from "./tile/decompose_finger_print.js"; import { VirtualTextureTile } from "./tile/VirtualTextureTile.js"; /** * Facilitates prioritized queueing and loading of individual texture tiles */ export class VirtualTextureTileLoader { /** * Where the tiles are stored * @type {string} */ #root_path = ""; /** * Lets user specify a custom file name template * @type {function(mip:number, x:number, y:number):string} */ #file_name_builder = (mip, x, y) => { assert.isNonNegativeInteger(mip, 'mip'); assert.isNonNegativeInteger(x, 'x'); assert.isNonNegativeInteger(y, 'y'); return `${mip}-${x}-${y}.png`; } /** * * @return {function(number, number, number): string} */ get file_name_builder() { return this.#file_name_builder; } /** * * @param {function(mip:number, x:number, y:number):string} builder */ set file_name_builder(builder) { assert.isFunction(builder, 'builder'); this.#file_name_builder = builder; } set path(v) { this.#root_path = v; } /** * When queue gets larger than this, we start discarding elements * @type {number} */ queue_limit = Infinity; /** * * @type {AssetManager} */ #asset_manager = null; set asset_manager(v) { this.#asset_manager = v; } /** * How many tiles can be loaded at the same time * @type {number} */ #concurrency = 4; /** * Tiles that are currently being loaded * @readonly * @type {number[]} */ #in_flight = []; /** * Contains fingerprints * @readonly * @type {number[]} */ #queue = []; /** * @readonly */ on = { /** * @type {Signal<VirtualTextureTile>} */ loaded: new Signal() }; /** * * @param {number} fingerprint * @returns {boolean} */ #queue_remove(fingerprint) { assert.isNonNegativeInteger(fingerprint, 'fingerprint'); const i = this.#queue.indexOf(fingerprint); if (i === -1) { return false; } this.#queue.splice(i, 1); return true; } #queue_add(fingerprint) { assert.isNonNegativeInteger(fingerprint, 'fingerprint'); if (this.is_queued(fingerprint)) { return false; } this.#queue.push(fingerprint); return true; } /** * * @param {number} fingerprint */ enqueue(fingerprint) { if (this.#in_flight.indexOf(fingerprint) !== -1) { return false; } if (this.#queue_add(fingerprint) === false) { return false; } this.#prod(); return true; } /** * * @param {number} fingerprint * @returns {boolean} */ is_queued(fingerprint) { assert.isNonNegativeInteger(fingerprint, 'fingerprint'); return this.#queue.indexOf(fingerprint) !== -1; } /** * * @param {VirtualTextureUsage} usage */ #sort_queue_by_usage(usage) { /** * * @param {number} fingerprint */ const score = (fingerprint) => -usage.getCountByFingerprint(fingerprint); array_sort_quick( this.#queue, score, null, 0, this.#queue.length - 1 ); } /** * * @param {VirtualTextureUsage} usage */ update_usage(usage) { this.#sort_queue_by_usage(usage); // truncate the queue if necessary const queue_overflow = this.#queue.length - this.queue_limit; if (queue_overflow > 0) { this.#queue.splice(this.queue_limit, queue_overflow); } } /** * Initializes load of the next element in the queue if there is capacity */ #prod() { while (this.#in_flight.length < this.#concurrency && this.#queue.length > 0) { const fingerprint = this.#queue[0]; const { mip, x, y } = decompose_finger_print(fingerprint); this.#load(mip, x, y); } } /** * * @param {number} mip * @param {number} x * @param {number} y * @returns {Promise<VirtualTextureTile>} */ #load(mip, x, y) { const file_name = this.#file_name_builder(mip, x, y); assert.isString(file_name, 'file_name'); const finger_print = compose_finger_print(mip, x, y); this.#queue_remove(finger_print); this.#in_flight.push(finger_print); return this.#asset_manager.promise(`${this.#root_path}/${file_name}`, GameAssetType.Image) .then(asset => { const sampler = asset.create(); const tile = new VirtualTextureTile(); let rgba = sampler; if (rgba.itemSize !== 4) { rgba = Sampler2D.uint8(4, sampler.width, sampler.height); sampler2d_to_uint8_RGBA(rgba, sampler); } tile.data = rgba; tile.finger_print = finger_print; this.on.loaded.send1(tile); return tile; }, (reason) => { console.error(`Failed to load tile ${file_name}, reason:${reason}`); }) .finally(() => { array_remove_first(this.#in_flight,finger_print); this.#prod(); }) } }