Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/tests/texture-cleanup-critical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ See docs/ManualRegressionTests.md for more information.
cacheId: Math.floor(Math.random() * 100000),
});
screen.textureOptions.preload = true;
}, 10);
}, 100);
}
11 changes: 10 additions & 1 deletion src/core/CoreTextureManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -178,6 +179,7 @@ export class CoreTextureManager extends EventEmitter {
private priorityQueue: Array<Texture> = [];
private uploadTextureQueue: Array<Texture> = [];
private initialized = false;
private stage: Stage;

imageWorkerManager: ImageWorkerManager | null = null;
hasCreateImageBitmap = !!self.createImageBitmap;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -387,13 +390,19 @@ export class CoreTextureManager extends EventEmitter {
return texture as InstanceType<TextureMap[Type]>;
}

orphanTexture(texture: Texture): void {
this.stage.txMemManager.addToOrphanedTextures(texture);
}

/**
* Override loadTexture to use the batched approach.
*
* @param texture - The texture to load
* @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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 37 additions & 40 deletions src/core/TextureMemoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface MemoryInfo {
export class TextureMemoryManager {
private memUsed = 0;
private loadedTextures: Map<Texture, number> = new Map();
private orphanedTextures: Texture[] = [];
private criticalThreshold: number;
private targetThreshold: number;
private cleanupInterval: number;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
5 changes: 1 addition & 4 deletions src/core/textures/Texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -219,8 +216,8 @@ 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);
}
}
}
Expand Down