@lightningjs/renderer
Version:
Lightning 3 Renderer
289 lines • 10.4 kB
JavaScript
/*
* 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