UNPKG

@lightningjs/renderer

Version:
289 lines 10.4 kB
/* * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * * Copyright 2023 Comcast Cable Communications Management, LLC. * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { ColorTexture } from './textures/ColorTexture.js'; import { ImageTexture } from './textures/ImageTexture.js'; import { NoiseTexture } from './textures/NoiseTexture.js'; import { SubTexture } from './textures/SubTexture.js'; import { RenderTexture } from './textures/RenderTexture.js'; import { Texture, TextureType } from './textures/Texture.js'; import { EventEmitter } from '../common/EventEmitter.js'; import { TextureError, TextureErrorCode } from './TextureError.js'; export class CoreTextureManager extends EventEmitter { /** * Map of textures by cache key */ keyCache = new Map(); /** * Map of cache keys by texture */ inverseKeyCache = new WeakMap(); /** * Map of texture constructors by their type name */ txConstructors = {}; maxRetryCount; uploadTextureQueue = []; stage; platform; /** * Renderer that this texture manager is associated with * * @remarks * This MUST be set before the texture manager is used. Otherwise errors * will occur when using the texture manager. */ renderer; /** * The current frame time in milliseconds * * @remarks * This is used to populate the `lastRenderableChangeTime` property of * {@link Texture} instances when their renderable state changes. * * Set by stage via `updateFrameTime` method. */ frameTime = 0; constructor(stage, settings) { super(); this.stage = stage; this.platform = stage.platform; this.maxRetryCount = settings.maxRetryCount; this.registerTextureType('ImageTexture', ImageTexture); this.registerTextureType('ColorTexture', ColorTexture); this.registerTextureType('NoiseTexture', NoiseTexture); this.registerTextureType('SubTexture', SubTexture); this.registerTextureType('RenderTexture', RenderTexture); } registerTextureType(textureType, textureClass) { this.txConstructors[textureType] = textureClass; } /** * Enqueue a texture for uploading to the GPU. * * @param texture - The texture to upload */ enqueueUploadTexture(texture) { if (this.uploadTextureQueue.includes(texture) === false) { this.uploadTextureQueue.push(texture); } } /** * Create a texture * * @param textureType - The type of texture to create * @param props - The properties to use for the texture */ createTexture(textureType, props) { let texture; const TextureClass = this.txConstructors[textureType]; if (!TextureClass) { throw new TextureError(TextureErrorCode.TEXTURE_TYPE_NOT_REGISTERED, `Texture type "${textureType}" is not registered`); } const resolvedProps = TextureClass.resolveDefaults(props); const cacheKey = TextureClass.makeCacheKey(resolvedProps); if (cacheKey && this.keyCache.has(cacheKey)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion texture = this.keyCache.get(cacheKey); } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any texture = new TextureClass(this, resolvedProps); if (cacheKey) { this.initTextureToCache(texture, cacheKey); } } return texture; } /** * Override loadTexture to use the batched approach. * * @param texture - The texture to load * @param immediate - Whether to prioritize the texture for immediate loading */ async loadTexture(texture, priority) { if (texture.type === TextureType.subTexture) { // ignore subtextures - they get loaded through their parent return; } if (texture.state === 'loaded') { // if the texture is already loaded, just return return; } texture.setState('loading'); // Get texture data - early return on failure const textureDataResult = await texture.getTextureData().catch((err) => { console.error(err); texture.setState('failed'); return null; }); // Early return if texture data fetch failed if (textureDataResult === null || texture.state === 'failed') { return; } // Handle non-image textures: upload immediately const shouldUploadImmediately = texture.type !== TextureType.image || priority === true; if (shouldUploadImmediately === true) { await this.uploadTexture(texture).catch((err) => { console.error(`Failed to upload texture:`, err); texture.setState('failed'); }); return; } // Queue image textures for throttled upload this.enqueueUploadTexture(texture); } /** * Upload a texture to the GPU * * @param texture Texture to upload * @returns Promise that resolves when the texture is fully loaded */ async uploadTexture(texture) { if (this.stage.txMemManager.doNotExceedCriticalThreshold === true && this.stage.txMemManager.criticalCleanupRequested === true) { // we're at a critical memory threshold, don't upload textures texture.setState('failed', new TextureError(TextureErrorCode.MEMORY_THRESHOLD_EXCEEDED)); return; } if (texture.state === 'failed' || texture.state === 'freed') { // don't upload failed or freed textures return; } if (texture.state === 'loaded') { // already loaded return; } if (texture.textureData === null) { texture.setState('failed', new TextureError(TextureErrorCode.TEXTURE_DATA_NULL, 'Texture data is null, cannot upload texture')); return; } const coreContext = texture.loadCtxTexture(); if (coreContext !== null && coreContext.state === 'loaded') { texture.setState('loaded'); return; } await coreContext.load(); } /** * Check if a texture is being processed */ isProcessingTexture(texture) { return this.uploadTextureQueue.includes(texture) === true; } /** * Process a limited number of uploads. * * @param maxProcessingTime - The maximum processing time in milliseconds */ async processSome(maxProcessingTime) { const platform = this.platform; const startTime = platform.getTimeStamp(); // Process uploads - await each upload to prevent GPU overload while (this.uploadTextureQueue.length > 0 && platform.getTimeStamp() - startTime < maxProcessingTime) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.uploadTextureQueue.shift(); try { await this.uploadTexture(texture); } catch (error) { console.error('Failed to upload texture:', error); // Continue with next texture instead of stopping entire queue } } } hasUpdates() { return this.uploadTextureQueue.length > 0; } /** * Initialize a texture to the cache * * @param texture Texture to cache * @param cacheKey Cache key for the texture */ initTextureToCache(texture, cacheKey) { const { keyCache, inverseKeyCache } = this; keyCache.set(cacheKey, texture); inverseKeyCache.set(texture, cacheKey); } /** * Get a texture from the cache * * @param cacheKey */ getTextureFromCache(cacheKey) { return this.keyCache.get(cacheKey); } /** * Remove a texture from the cache * * @remarks * Called by Texture Cleanup when a texture is freed. * * @param texture */ removeTextureFromCache(texture) { const { inverseKeyCache, keyCache } = this; const cacheKey = inverseKeyCache.get(texture); if (cacheKey) { keyCache.delete(cacheKey); } } /** * Remove texture from the upload queue * * @param texture - The texture to remove */ removeTextureFromQueue(texture) { const uploadIndex = this.uploadTextureQueue.indexOf(texture); if (uploadIndex !== -1) { this.uploadTextureQueue.splice(uploadIndex, 1); } } /** * Destroy the CoreTextureManager and release all internal references. * * @remarks * Clears the upload queue and key/inverse-key caches so that queued * textures can be garbage-collected after a renderer teardown. */ destroy() { this.uploadTextureQueue = []; this.keyCache.clear(); // inverseKeyCache is a WeakMap – entries will be GC'd automatically once // the texture objects themselves are no longer referenced. } /** * Resolve a parent texture from the cache or fallback to the provided texture. * * @param texture - The provided texture to resolve. * @returns The cached or provided texture. */ resolveParentTexture(texture) { if (!texture?.props) { return texture; } const cacheKey = ImageTexture.makeCacheKey(texture.props); const cachedTexture = cacheKey ? this.getTextureFromCache(cacheKey) : undefined; return cachedTexture ?? texture; } } //# sourceMappingURL=CoreTextureManager.js.map