UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

530 lines (399 loc) • 14.6 kB
import { ClampToEdgeWrapping, DataTexture, LinearFilter, NearestFilter, REVISION as THREE_VERSION, RGBAFormat, UnsignedByteType, Vector2 } from "three"; import { assert } from "../../../../core/assert.js"; import { BitSet } from "../../../../core/binary/BitSet.js"; import { Cache } from "../../../../core/cache/Cache.js"; import { array_copy } from "../../../../core/collection/array/array_copy.js"; import { HashMap } from "../../../../core/collection/map/HashMap.js"; import { passThrough } from "../../../../core/function/passThrough.js"; import { strictEquals } from "../../../../core/function/strictEquals.js"; import { max2 } from "../../../../core/math/max2.js"; import { min2 } from "../../../../core/math/min2.js"; import { Sampler2D } from "../sampler/Sampler2D.js"; import { writeSample2DDataToDataTexture } from "../sampler/writeSampler2DDataToDataTexture.js"; import { tile_address_to_finger_print } from "./tile/tile_address_to_finger_print.js"; import { VirtualTextureTileLoader } from "./VirtualTextureTileLoader.js"; import { VT_DEFAULT_PAGE_RESOLUTION } from "./VT_DEFAULT_PAGE_RESOLUTION.js"; /** * How much extra data to store in cache (in bytes) * IMPORTANT: MAKE SURE THAT THE CACHE CAN HOLD AT LEAST 1 TILE! * @type {number} */ const DEFAULT_CACHE_SIZE = 256 * 1024 * 1024; export class VirtualTexturePage { /** * @readonly * @type {DataTexture} */ #page_texture = new DataTexture( new Uint8Array(VT_DEFAULT_PAGE_RESOLUTION * VT_DEFAULT_PAGE_RESOLUTION * 4), VT_DEFAULT_PAGE_RESOLUTION, VT_DEFAULT_PAGE_RESOLUTION, RGBAFormat, UnsignedByteType, ); #tile_copy_sampler = Sampler2D.uint8(4, 1, 1); #tile_copy_texture = new DataTexture(this.#tile_copy_sampler.data, 1, 1); #page_texture_size = [VT_DEFAULT_PAGE_RESOLUTION, VT_DEFAULT_PAGE_RESOLUTION]; /** * * @type {number[]} */ #page_texture_resolution_in_tiles = [0, 0]; get page_texture_resolution_in_tiles() { return this.#page_texture_resolution_in_tiles; } /** * Used internally to track number of times `update` method was called * @type {number} */ update_count = 0; /** * Track updates to page content, whenever a tile is made resident or is removed - this number goes up by 1 * @type {number} */ version = 0; /** * Maximum number of new page assignments per single update cycle * This helps prevent thrashing and keep performance overhead of updating pages predictable * @type {number} */ #cycle_assignment_limit = 4; /** * * @type {THREE.WebGLRenderer|null} */ #renderer = null; set renderer(v) { this.#renderer = v; } get page_texture_resolution() { return this.#page_texture_size; } get texture() { return this.#page_texture; } /** * * @type {VirtualTextureTile[]} */ #resident_tiles = []; /** * * @returns {VirtualTextureTile[]} */ get resident_tiles() { return this.#resident_tiles; } /** * * @type {HashMap<number, VirtualTextureTile>} */ #resident_tile_lookup = new HashMap({ keyHashFunction: passThrough, keyEqualityFunction: strictEquals, }); #page_slot_occupancy = new BitSet(); #tile_resolution = 128; get tile_resolution() { return this.#tile_resolution; } /** * * @param {number} v */ set tile_resolution(v) { this.#tile_resolution = v; this.#update_tile_slot_resolution(); this.clear_resident(); } /** * Extra padding on repeated pixels around each tile on each side (top, left, right, bottom) * Used to enable texture filtering * @type {number} */ #tile_margin = 4; get tile_margin() { return this.#tile_margin; } #tile_slot_resolution = this.#tile_resolution + this.#tile_margin * 2; #update_tile_slot_resolution() { this.#tile_slot_resolution = this.#tile_resolution + this.#tile_margin * 2; } #residency_tile_capacity = 0; /** * Must be >=0 * When 0 - tiles that would not fit onto the page will not be loaded * When >0 - same fraction of extra tiles will be fetching and put into cache, resulting in prefetching * @type {number} */ #prefetch_factor = 0.33; #loader = new VirtualTextureTileLoader(); /** * * @type {AssetManager|null} */ #asset_manager = null; #page_texture_set_parameters() { const texture = this.#page_texture; texture.unpackAlignment = 4; texture.type = UnsignedByteType; texture.format = RGBAFormat; texture.internalFormat = "RGBA8"; texture.generateMipmaps = false; texture.minFilter = NearestFilter; texture.magFilter = LinearFilter; texture.wrapS = ClampToEdgeWrapping; texture.wrapT = ClampToEdgeWrapping; texture.anisotropy = this.#tile_margin * 2; } constructor() { this.#page_texture.name = "Virtual Texture / Page"; this.#page_texture_set_parameters(); this.#page_texture.needsUpdate = true; this.#tile_copy_texture.name = "Virtual Texture / Copy Tile"; this.#tile_copy_texture.wrapT = ClampToEdgeWrapping; this.#tile_copy_texture.wrapS = ClampToEdgeWrapping; this.#tile_copy_texture.generateMipmaps = false; this.#loader.on.loaded.add(this.#handle_tile_loaded, this); this.page_texture_size = [VT_DEFAULT_PAGE_RESOLUTION, VT_DEFAULT_PAGE_RESOLUTION]; } /** * * @param {VirtualTextureTile} tile */ #handle_tile_loaded(tile) { const finderPrint = tile.finger_print; assert.notNull(tile.data, 'tile.data'); assert.equal(tile.data.width, this.#tile_slot_resolution); assert.equal(tile.data.height, this.#tile_slot_resolution); if (this.#tile_cache.has(finderPrint)) { // already loaded return; } this.#tile_cache.put(finderPrint, tile); } /** * * @param {string} v */ set path(v) { this.#loader.path = v; } /** * * @param {AssetManager} v */ set asset_manager(v) { this.#asset_manager = v this.#loader.asset_manager = v; } #update_residency_capacity() { const slot_resolution = this.#tile_slot_resolution; const size = this.#page_texture_size; const x = Math.floor(size[0] / slot_resolution); const y = Math.floor(size[1] / slot_resolution); this.#page_texture_resolution_in_tiles[0] = x; this.#page_texture_resolution_in_tiles[1] = y; this.#residency_tile_capacity = x * y; // Limit the queue to be able to load no more than an entire page, trying to load more would be wasteful as we wouldn't be able to use that data immediately anyway this.#loader.queue_limit = max2(1, this.#residency_tile_capacity); } /** * Tiles that are not currently resident live here, this is essentially a second level cache * key is the tile's "fingerprint" * @type {Cache<number,VirtualTextureTile>} */ #tile_cache = new Cache({ keyHashFunction: passThrough, keyEqualityFunction: strictEquals, maxWeight: DEFAULT_CACHE_SIZE, valueWeigher: () => this.#tile_resolution * this.#tile_resolution * 4 }); #find_eviction_target_index() { // find least-recently used const tiles = this.#resident_tiles; const count = tiles.length; let min = Infinity; let min_index = -1; for (let i = 0; i < count; i++) { const tile = tiles[i]; if (tile.last_used_time < min) { min = tile.last_used_time; min_index = i; } } return min_index; } #evict_one() { const index = this.#find_eviction_target_index(); if (index === -1) { return false; } return this.#remove_resident_tile(index); } /** * * @param {number} index * @returns {boolean} */ #remove_resident_tile(index) { const [removed] = this.#resident_tiles.splice(index, 1); const pageSlot = removed.page_slot; // mark slot as empty for next tile this.#page_slot_occupancy.clear(pageSlot); removed.page_slot = -1; const fingerprint = removed.finger_print; this.#resident_tile_lookup.delete(fingerprint); // push into cache this.#tile_cache.put(fingerprint, removed); this.version++; return true; } /** * * @param {VirtualTextureTile} tile */ #make_resident(tile) { if (this.#residency_tile_capacity === 0) { throw new Error('Capacity is 0'); } let slot = this.#page_slot_occupancy.nextClearBit(0); while (slot >= this.#residency_tile_capacity) { this.#evict_one(); slot = this.#page_slot_occupancy.nextClearBit(0); } tile.page_slot = slot; tile.last_used_time = this.update_count; this.#page_slot_occupancy.set(slot, true); this.#resident_tiles.push(tile); this.#resident_tile_lookup.set(tile.finger_print, tile); const res_x = this.#page_texture_resolution_in_tiles[0]; const position_tile_x = slot % res_x; const position_tile_y = (slot - position_tile_x) / res_x; const slot_resolution = this.#tile_slot_resolution; const copy_sampler = this.#tile_copy_sampler; const tile_texture = this.#tile_copy_texture; if (copy_sampler.width !== slot_resolution || copy_sampler.height !== slot_resolution) { copy_sampler.resize(slot_resolution, slot_resolution); writeSample2DDataToDataTexture(copy_sampler, tile_texture); } tile_texture.image.data.set(tile.data.data); tile_texture.needsUpdate = true; const write_position = new Vector2( position_tile_x * slot_resolution, position_tile_y * slot_resolution, ); const renderer = this.#renderer; if (THREE_VERSION < 165) { renderer.copyTextureToTexture( write_position, tile_texture, this.#page_texture ); } else { // API changed since version 165 renderer.copyTextureToTexture( tile_texture, this.#page_texture, // srcRegion { min: { x: 0, y: 0 }, max: { x: slot_resolution, y: slot_resolution }, }, // dstPosition write_position ); } this.version++; return true; } /** * * @param {number[]} v */ set page_texture_size(v) { const current_resolution = this.#page_texture_size; if (v[0] !== current_resolution[0] || v[1] !== current_resolution[1]) { array_copy( v, 0, current_resolution, 0, 2 ); this.clear_resident(); const page_texture = this.#page_texture; const page_image = page_texture.image; page_texture.dispose(); const width = current_resolution[0]; const height = current_resolution[1]; page_image.data = new Uint8Array(width * height * 4); page_image.width = width; page_image.height = height; page_texture.needsUpdate = true; } this.#update_residency_capacity(); } get page_texture_size() { return this.#page_texture_size; } /** * * @param {VirtualTextureUsage} usage */ update_usage(usage) { const usage_occupancy = usage.occupancy; const usage_occupancy_count = usage.occupancy_count; // Usage occupancy is already sorted by priority, so there's no point is trying to fetch more than t let fetch_limit = this.#residency_tile_capacity * (1 + this.#prefetch_factor); let writes_remaining = min2( max2(1, this.#cycle_assignment_limit), this.#residency_tile_capacity ); let remaining_slots = this.#residency_tile_capacity; for (let i = 0; i < usage_occupancy_count && fetch_limit > 0; i++) { const tile_address = usage_occupancy[i]; const fingerPrint = tile_address_to_finger_print(tile_address); let tile = this.#resident_tile_lookup.get(fingerPrint); if (tile !== undefined) { // already resident // touch to prevent eviction tile.last_used_time = this.update_count; remaining_slots--; fetch_limit--; continue; } tile = this.#tile_cache.get(fingerPrint); if (tile === null) { this.#loader.enqueue(fingerPrint); } else if (remaining_slots > 0) { // found in cache, and we have some slots remaining to write into if (writes_remaining > 0) { // tile is not active, and we can write it writes_remaining--; this.#make_resident(tile); } fetch_limit--; remaining_slots--; } } this.#loader.update_usage(usage); this.update_count++; } clear_resident() { while (this.#resident_tiles.length > 0) { this.#evict_one(); } } dispose() { this.clear_resident(); this.#page_texture.dispose(); this.#tile_cache.clear(); this.#page_slot_occupancy.reset(); } }