fix(refboard): reference-counted textures prevent shared source crash
When duplicated images share the same assetKey, Assets.unload() was destroying the shared texture source — crashing all sprites using it (null addressModeU/alphaMode in WebGL renderer). TextureManager now tracks refCount per asset. Only calls Assets.unload() when the last reference is released. ImageSprite calls release() instead of unload() on texture teardown.
This commit is contained in:
@@ -2,15 +2,20 @@ import { Texture, Assets } from "pixi.js";
|
||||
|
||||
interface TextureEntry {
|
||||
texture: Texture;
|
||||
url: string; // needed for Assets.unload()
|
||||
url: string;
|
||||
refCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* TextureManager — GPU texture cache.
|
||||
* TextureManager — GPU texture cache with reference counting.
|
||||
*
|
||||
* 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).
|
||||
* Multiple ImageSprites can share the same assetKey (duplicated images).
|
||||
* Assets.unload() destroys the shared texture source, so we must only
|
||||
* call it when the LAST reference is released. Otherwise PixiJS v8
|
||||
* crashes with null source errors (alphaMode, addressModeU).
|
||||
*
|
||||
* The viewport culling system (PixiCanvas) calls load/release to
|
||||
* manage which textures consume GPU memory.
|
||||
*/
|
||||
export class TextureManager {
|
||||
private cache = new Map<string, TextureEntry>();
|
||||
@@ -24,12 +29,13 @@ export class TextureManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a texture for the given asset.
|
||||
* Load a texture for the given asset. Increments ref count.
|
||||
* Returns cached texture if available, otherwise fetches and caches.
|
||||
*/
|
||||
async load(assetKey: string): Promise<Texture> {
|
||||
const existing = this.cache.get(assetKey);
|
||||
if (existing) {
|
||||
existing.refCount++;
|
||||
return existing.texture;
|
||||
}
|
||||
|
||||
@@ -43,15 +49,21 @@ export class TextureManager {
|
||||
return Texture.EMPTY;
|
||||
}
|
||||
|
||||
this.cache.set(assetKey, { texture, url });
|
||||
this.cache.set(assetKey, { texture, url, refCount: 1 });
|
||||
return texture;
|
||||
}
|
||||
|
||||
/** Remove a specific texture from the cache via Assets.unload(). */
|
||||
unload(assetKey: string): void {
|
||||
/**
|
||||
* Release a reference to a texture. Only actually unloads from GPU
|
||||
* when the last reference is released.
|
||||
*/
|
||||
release(assetKey: string): void {
|
||||
const entry = this.cache.get(assetKey);
|
||||
if (!entry) return;
|
||||
|
||||
entry.refCount--;
|
||||
if (entry.refCount > 0) return; // other sprites still using it
|
||||
|
||||
this.cache.delete(assetKey);
|
||||
try {
|
||||
Assets.unload(entry.url);
|
||||
@@ -60,10 +72,22 @@ export class TextureManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Force unload (used by clear). */
|
||||
unload(assetKey: string): void {
|
||||
const entry = this.cache.get(assetKey);
|
||||
if (!entry) return;
|
||||
this.cache.delete(assetKey);
|
||||
try {
|
||||
Assets.unload(entry.url);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** Unload all cached textures. */
|
||||
clear(): void {
|
||||
for (const [key] of this.cache) {
|
||||
this.unload(key);
|
||||
for (const [, entry] of this.cache) {
|
||||
try { Assets.unload(entry.url); } catch { /* ignore */ }
|
||||
}
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user