fix(refboard): remove LRU eviction — let culling system manage textures

LRU eviction was destroying texture sources while sprites still
referenced them, causing PixiJS v8 'alphaMode of null' crash.

The viewport culling system (MAX_LOADED_TEXTURES=60) already controls
what's loaded/unloaded, so TextureManager doesn't need its own eviction.
Simplified to a pure load/unload cache with no budget tracking.
This commit is contained in:
Hiren Kangad
2026-03-10 11:09:17 +05:30
parent 11a44f5271
commit 1ffb7e9d41
+9 -47
View File
@@ -3,18 +3,17 @@ import { Texture, Assets } from "pixi.js";
interface TextureEntry {
texture: Texture;
url: string; // needed for Assets.unload()
lastUsed: number;
memoryEstimate: number;
}
/**
* TextureManager — GPU texture cache with LRU eviction and memory budget.
* Uses Assets.unload() instead of texture.destroy() for proper cleanup.
* TextureManager — GPU texture cache.
*
* No internal LRU eviction — the viewport culling system (PixiCanvas)
* manages loading/unloading based on proximity. This avoids destroying
* textures that sprites still reference (which crashes PixiJS v8).
*/
export class TextureManager {
private cache = new Map<string, TextureEntry>();
private budget = 256 * 1024 * 1024; // 256 MB — keeps headroom for GPU ops
private currentUsage = 0;
/** Build the URL for a given asset. */
urlForAsset(assetKey: string): string {
@@ -31,7 +30,6 @@ export class TextureManager {
async load(assetKey: string): Promise<Texture> {
const existing = this.cache.get(assetKey);
if (existing) {
existing.lastUsed = performance.now();
return existing.texture;
}
@@ -45,23 +43,7 @@ export class TextureManager {
return Texture.EMPTY;
}
const w = texture.source.width ?? 256;
const h = texture.source.height ?? 256;
const memoryEstimate = w * h * 4; // RGBA
this.currentUsage += memoryEstimate;
this.cache.set(assetKey, {
texture,
url,
lastUsed: performance.now(),
memoryEstimate,
});
while (this.currentUsage > this.budget && this.cache.size > 1) {
this.evictLRU();
}
this.cache.set(assetKey, { texture, url });
return texture;
}
@@ -70,39 +52,19 @@ export class TextureManager {
const entry = this.cache.get(assetKey);
if (!entry) return;
this.currentUsage -= entry.memoryEstimate;
this.cache.delete(assetKey);
try {
Assets.unload(entry.url);
} catch {
// Fallback if Assets doesn't know about it
entry.texture.destroy(true);
}
this.cache.delete(assetKey);
}
/** Evict the least-recently-used cache entry. */
private evictLRU(): void {
let oldestKey: string | null = null;
let oldestTime = Infinity;
for (const [key, entry] of this.cache) {
if (entry.lastUsed < oldestTime) {
oldestTime = entry.lastUsed;
oldestKey = key;
// ignore — texture may already be gone
}
}
if (oldestKey) {
this.unload(oldestKey);
}
}
/** Unload all cached textures and reset memory tracking. */
/** Unload all cached textures. */
clear(): void {
for (const [key] of this.cache) {
this.unload(key);
}
this.cache.clear();
this.currentUsage = 0;
}
}