From af5df3043cfe054a89d974e1d4445500f19c524a Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 31 Jan 2025 00:21:43 +0100 Subject: [PATCH 1/2] fix: enhance texture management by adding orphaned texture handling --- examples/tests/texture-cleanup-critical.ts | 2 +- src/core/CoreTextureManager.ts | 11 +++- src/core/Stage.ts | 2 +- src/core/TextureMemoryManager.ts | 77 +++++++++++----------- src/core/textures/Texture.ts | 1 + 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/examples/tests/texture-cleanup-critical.ts b/examples/tests/texture-cleanup-critical.ts index f04d0d6b..7393bc07 100644 --- a/examples/tests/texture-cleanup-critical.ts +++ b/examples/tests/texture-cleanup-critical.ts @@ -78,5 +78,5 @@ See docs/ManualRegressionTests.md for more information. cacheId: Math.floor(Math.random() * 100000), }); screen.textureOptions.preload = true; - }, 10); + }, 100); } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 19c9ae35..af03fc3f 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -27,6 +27,7 @@ import { RenderTexture } from './textures/RenderTexture.js'; import type { Texture } from './textures/Texture.js'; import { EventEmitter } from '../common/EventEmitter.js'; import { getTimeStamp } from './platform.js'; +import type { Stage } from './Stage.js'; /** * Augmentable map of texture class types @@ -178,6 +179,7 @@ export class CoreTextureManager extends EventEmitter { private priorityQueue: Array = []; private uploadTextureQueue: Array = []; private initialized = false; + private stage: Stage; imageWorkerManager: ImageWorkerManager | null = null; hasCreateImageBitmap = !!self.createImageBitmap; @@ -208,8 +210,9 @@ export class CoreTextureManager extends EventEmitter { */ frameTime = 0; - constructor(numImageWorkers: number) { + constructor(stage: Stage, numImageWorkers: number) { super(); + this.stage = stage; this.validateCreateImageBitmap() .then((result) => { this.hasCreateImageBitmap = @@ -387,6 +390,10 @@ export class CoreTextureManager extends EventEmitter { return texture as InstanceType; } + orphanTexture(texture: Texture): void { + this.stage.txMemManager.addToOrphanedTextures(texture); + } + /** * Override loadTexture to use the batched approach. * @@ -394,6 +401,8 @@ export class CoreTextureManager extends EventEmitter { * @param immediate - Whether to prioritize the texture for immediate loading */ loadTexture(texture: Texture, priority?: boolean): void { + this.stage.txMemManager.removeFromOrphanedTextures(texture); + if (texture.state === 'loaded' || texture.state === 'loading') { return; } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index b7bba8bc..8a9009e1 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -151,7 +151,7 @@ export class Stage { } = options; this.eventBus = options.eventBus; - this.txManager = new CoreTextureManager(numImageWorkers); + this.txManager = new CoreTextureManager(this, numImageWorkers); // Wait for the Texture Manager to initialize // once it does, request a render diff --git a/src/core/TextureMemoryManager.ts b/src/core/TextureMemoryManager.ts index 501d2753..7d0018c6 100644 --- a/src/core/TextureMemoryManager.ts +++ b/src/core/TextureMemoryManager.ts @@ -109,6 +109,7 @@ export interface MemoryInfo { export class TextureMemoryManager { private memUsed = 0; private loadedTextures: Map = new Map(); + private orphanedTextures: Texture[] = []; private criticalThreshold: number; private targetThreshold: number; private cleanupInterval: number; @@ -161,6 +162,36 @@ export class TextureMemoryManager { } } + /** + * Add a texture to the orphaned textures list + * + * @param texture - The texture to add to the orphaned textures list + */ + addToOrphanedTextures(texture: 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: 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: Texture, byteSize: number) { if (this.loadedTextures.has(texture)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -193,7 +224,7 @@ export class TextureMemoryManager { this.lastCleanupTime = this.frameTime; this.criticalCleanupRequested = false; - if (critical) { + if (critical === true) { this.stage.queueFrameEvent('criticalCleanup', { memUsed: this.memUsed, criticalThreshold: this.criticalThreshold, @@ -206,48 +237,14 @@ export class TextureMemoryManager { ); } - /** - * Sort the loaded textures by renderability, then by last touch time. - * - * This will ensure that the array is ordered by the following: - * - Non-renderable textures, starting at the least recently rendered - * - Renderable textures, starting at the least recently rendered - */ - const textures = [...this.loadedTextures.keys()].sort( - (textureA, textureB) => { - const txARenderable = textureA.renderable; - const txBRenderable = textureB.renderable; - if (txARenderable === txBRenderable) { - return ( - textureA.lastRenderableChangeTime - - textureB.lastRenderableChangeTime - ); - } else if (txARenderable) { - return 1; - } else if (txBRenderable) { - return -1; - } - return 0; - }, - ); - // Free non-renderable textures until we reach the target threshold const memTarget = this.targetThreshold; const txManager = this.stage.txManager; - for (const texture of textures) { - if (texture.renderable) { - // Stop at the first renderable texture (The rest are renderable because of the sort above) - // We don't want to free renderable textures because they will just likely be reloaded in the next frame - break; - } - if (texture.preventCleanup === false) { - texture.free(); - txManager.removeTextureFromCache(texture); - } - if (this.memUsed <= memTarget) { - // Stop once we've freed enough textures to reach under the target threshold - break; - } + + while (this.memUsed >= memTarget && this.orphanedTextures.length > 0) { + const texture = this.orphanedTextures.shift()!; + texture.free(); + txManager.removeTextureFromCache(texture); } if (this.memUsed >= this.criticalThreshold) { diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 2ce86947..7f10d4bd 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -221,6 +221,7 @@ export abstract class Texture extends EventEmitter { (this.renderable as boolean) = false; (this.lastRenderableChangeTime as number) = this.txManager.frameTime; this.onChangeIsRenderable?.(false); + this.txManager.orphanTexture(this); } } } From c2a11c88a787378d4e4b066b5436b50e357b110f Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 31 Jan 2025 00:34:40 +0100 Subject: [PATCH 2/2] fix: remove unused lastRenderableChangeTime property from Texture class --- src/core/textures/Texture.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 7f10d4bd..d104dacf 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -170,8 +170,6 @@ export abstract class Texture extends EventEmitter { readonly renderable: boolean = false; - readonly lastRenderableChangeTime = 0; - public type: TextureType = TextureType.generic; public preventCleanup = false; @@ -210,7 +208,6 @@ export abstract class Texture extends EventEmitter { const newSize = this.renderableOwners.size; if (newSize > oldSize && newSize === 1) { (this.renderable as boolean) = true; - (this.lastRenderableChangeTime as number) = this.txManager.frameTime; this.onChangeIsRenderable?.(true); this.load(); } @@ -219,7 +216,6 @@ export abstract class Texture extends EventEmitter { const newSize = this.renderableOwners.size; if (newSize < oldSize && newSize === 0) { (this.renderable as boolean) = false; - (this.lastRenderableChangeTime as number) = this.txManager.frameTime; this.onChangeIsRenderable?.(false); this.txManager.orphanTexture(this); }