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