UNPKG

@lightningjs/renderer

Version:
293 lines 12.2 kB
/* * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * * Copyright 2024 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 { isProductionEnvironment } from '../utils.js'; import { Texture, TextureType } from './textures/Texture.js'; import { bytesToMb } from './utils.js'; /** * LRU (Least Recently Used) style memory manager for textures * * @remarks * This class is responsible for managing the memory usage of textures * in the Renderer. It keeps track of the memory used by each texture * and triggers a cleanup when the memory usage exceeds a critical * threshold (`criticalThreshold`). * * The cleanup process will free up non-renderable textures until the * memory usage is below a target threshold (`targetThresholdLevel`). * * The memory manager's clean up process will also be triggered when the * scene is idle for a certain amount of time (`cleanupInterval`). */ export class TextureMemoryManager { stage; memUsed = 0; loadedTextures = new Map(); orphanedTextures = []; criticalThreshold = 124e6; targetThreshold = 0.5; cleanupInterval = 5000; debugLogging = false; loggingID = 0; lastCleanupTime = 0; baselineMemoryAllocation = 26e6; criticalCleanupRequested = false; doNotExceedCriticalThreshold = false; originalSetTextureMemUse; /** * The current frame time in milliseconds * * @remarks * This is used to determine when to perform Idle Texture Cleanups. * * Set by stage via `updateFrameTime` method. */ frameTime = 0; constructor(stage, settings) { this.stage = stage; this.originalSetTextureMemUse = this.setTextureMemUse; this.updateSettings(settings); } /** * Add a texture to the orphaned textures list * * @param texture - The texture to add to the orphaned textures list */ addToOrphanedTextures(texture) { // if the texture is already in the orphaned textures list add it at the end if (this.orphanedTextures.includes(texture)) { this.removeFromOrphanedTextures(texture); } // If the texture can be cleaned up, add it to the orphaned textures list if (texture.preventCleanup === false) { this.orphanedTextures.push(texture); } } /** * Remove a texture from the orphaned textures list * * @param texture - The texture to remove from the orphaned textures list */ removeFromOrphanedTextures(texture) { const index = this.orphanedTextures.indexOf(texture); if (index !== -1) { this.orphanedTextures.splice(index, 1); } } /** * Set the memory usage of a texture * * @param texture - The texture to set memory usage for * @param byteSize - The size of the texture in bytes */ setTextureMemUse(texture, byteSize) { if (this.loadedTextures.has(texture)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.memUsed -= this.loadedTextures.get(texture); } if (byteSize === 0) { this.loadedTextures.delete(texture); return; } else { this.memUsed += byteSize; this.loadedTextures.set(texture, byteSize); } if (this.memUsed > this.criticalThreshold) { this.criticalCleanupRequested = true; } } checkCleanup() { return (this.criticalCleanupRequested || (this.memUsed > this.targetThreshold && this.frameTime - this.lastCleanupTime >= this.cleanupInterval)); } checkCriticalCleanup() { return this.memUsed > this.criticalThreshold; } cleanupQuick(critical) { // Free non-renderable textures until we reach the target threshold const platform = this.stage.platform; const memTarget = this.targetThreshold; const timestamp = platform.getTimeStamp(); while (this.memUsed >= memTarget && this.orphanedTextures.length > 0 && (critical || platform.getTimeStamp() - timestamp < 10)) { const texture = this.orphanedTextures.shift(); if (texture === undefined) { continue; } if (texture.renderable === true) { // If the texture is renderable, we can't free it up continue; } // Skip textures that are in transitional states - we only want to clean up // textures that are in a stable state (loaded, failed, or freed) if (texture.state === 'initial' || Texture.TRANSITIONAL_TEXTURE_STATES.includes(texture.state)) { continue; } this.destroyTexture(texture); } } /** * Destroy a texture and remove it from the memory manager * * @param texture - The texture to destroy */ destroyTexture(texture) { if (this.debugLogging === true) { console.log(`[TextureMemoryManager] Destroying texture. State: ${texture.state}`); } const txManager = this.stage.txManager; txManager.removeTextureFromQueue(texture); txManager.removeTextureFromCache(texture); texture.destroy(); this.removeFromOrphanedTextures(texture); this.loadedTextures.delete(texture); } cleanupDeep(critical) { // Free non-renderable textures until we reach the target threshold const memTarget = critical ? this.criticalThreshold : this.targetThreshold; // Filter for textures that are candidates for cleanup // note: This is an expensive operation, so we only do it in deep cleanup const cleanupCandidates = [...this.loadedTextures.keys()].filter((texture) => { return ((texture.type === TextureType.image || texture.type === TextureType.noise || texture.type === TextureType.renderToTexture) && texture.renderable === false && texture.preventCleanup === false && texture.state !== 'initial' && !Texture.TRANSITIONAL_TEXTURE_STATES.includes(texture.state)); }); while (this.memUsed >= memTarget && cleanupCandidates.length > 0) { const texture = cleanupCandidates.shift(); if (texture === undefined) { continue; } this.destroyTexture(texture); } } cleanup(aggressive = false) { const critical = this.criticalCleanupRequested; const criticalThreshold = this.criticalThreshold; const memUsed = this.memUsed; const stage = this.stage; this.lastCleanupTime = this.frameTime; if (critical === true) { stage.queueFrameEvent('criticalCleanup', { memUsed: this.memUsed, criticalThreshold: criticalThreshold, }); } if (this.debugLogging === true) { console.log(`[TextureMemoryManager] Cleaning up textures. Critical: ${critical}. Aggressive: ${aggressive}`); } // Note: We skip textures in transitional states during cleanup: // - 'initial': These textures haven't started loading yet // - 'fetching': These textures are in the process of being fetched // - 'fetched': These textures have been fetched but not yet uploaded to GPU // - 'loading': These textures are being uploaded to the GPU // // For 'failed' and 'freed' states, we only remove them from the tracking // arrays without trying to free GPU resources that don't exist. // try a quick cleanup first this.cleanupQuick(critical); // if we're still above the target threshold, do a deep cleanup if (aggressive === true && memUsed >= criticalThreshold) { this.cleanupDeep(critical); } if (memUsed >= criticalThreshold) { stage.queueFrameEvent('criticalCleanupFailed', { memUsed: memUsed, criticalThreshold: criticalThreshold, }); if (this.debugLogging === true || isProductionEnvironment === false) { console.warn(`[TextureMemoryManager] Memory usage above critical threshold after cleanup: ${memUsed}`); } } else { this.criticalCleanupRequested = false; } } /** * Get the current texture memory usage information * * @remarks * This method is for debugging purposes and returns information about the * current memory usage of the textures in the Renderer. */ getMemoryInfo() { let renderableTexturesLoaded = 0; const renderableMemUsed = [...this.loadedTextures.keys()].reduce((acc, texture) => { renderableTexturesLoaded += texture.renderable ? 1 : 0; // Get the memory used by the texture, defaulting to 0 if not found const textureMemory = this.loadedTextures.get(texture) ?? 0; return acc + (texture.renderable ? textureMemory : 0); }, this.baselineMemoryAllocation); return { criticalThreshold: this.criticalThreshold, targetThreshold: this.targetThreshold, renderableMemUsed, memUsed: this.memUsed, renderableTexturesLoaded, loadedTextures: this.loadedTextures.size, baselineMemoryAllocation: this.baselineMemoryAllocation, }; } updateSettings(settings) { const { criticalThreshold, doNotExceedCriticalThreshold } = settings; this.doNotExceedCriticalThreshold = doNotExceedCriticalThreshold || false; this.criticalThreshold = Math.round(criticalThreshold); if (this.memUsed === 0) { this.memUsed = Math.round(settings.baselineMemoryAllocation); } else { const memUsedExBaseline = this.memUsed - this.baselineMemoryAllocation; this.memUsed = Math.round(settings.baselineMemoryAllocation + memUsedExBaseline); } this.baselineMemoryAllocation = Math.round(settings.baselineMemoryAllocation); const targetFraction = Math.max(0, Math.min(1, settings.targetThresholdLevel)); this.targetThreshold = Math.max(Math.round(criticalThreshold * targetFraction), this.baselineMemoryAllocation); this.cleanupInterval = settings.cleanupInterval; this.debugLogging = settings.debugLogging; if (this.loggingID && !settings.debugLogging) { clearInterval(this.loggingID); this.loggingID = 0; } if (settings.debugLogging && !this.loggingID) { let lastMemUse = 0; this.loggingID = setInterval(() => { if (lastMemUse !== this.memUsed) { lastMemUse = this.memUsed; console.log(`[TextureMemoryManager] Memory used: ${bytesToMb(this.memUsed)} mb / ${bytesToMb(this.criticalThreshold)} mb (${((this.memUsed / this.criticalThreshold) * 100).toFixed(1)}%)`); } }, 1000); } // If the threshold is 0, we disable the memory manager by replacing the // setTextureMemUse method with a no-op function. if (criticalThreshold === 0) { this.setTextureMemUse = () => { }; } else { this.setTextureMemUse = this.originalSetTextureMemUse; } } } //# sourceMappingURL=TextureMemoryManager.js.map