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 {
|
interface TextureEntry {
|
||||||
texture: Texture;
|
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)
|
* Multiple ImageSprites can share the same assetKey (duplicated images).
|
||||||
* manages loading/unloading based on proximity. This avoids destroying
|
* Assets.unload() destroys the shared texture source, so we must only
|
||||||
* textures that sprites still reference (which crashes PixiJS v8).
|
* 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 {
|
export class TextureManager {
|
||||||
private cache = new Map<string, TextureEntry>();
|
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.
|
* Returns cached texture if available, otherwise fetches and caches.
|
||||||
*/
|
*/
|
||||||
async load(assetKey: string): Promise<Texture> {
|
async load(assetKey: string): Promise<Texture> {
|
||||||
const existing = this.cache.get(assetKey);
|
const existing = this.cache.get(assetKey);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
existing.refCount++;
|
||||||
return existing.texture;
|
return existing.texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +49,21 @@ export class TextureManager {
|
|||||||
return Texture.EMPTY;
|
return Texture.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cache.set(assetKey, { texture, url });
|
this.cache.set(assetKey, { texture, url, refCount: 1 });
|
||||||
return texture;
|
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);
|
const entry = this.cache.get(assetKey);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
|
||||||
|
entry.refCount--;
|
||||||
|
if (entry.refCount > 0) return; // other sprites still using it
|
||||||
|
|
||||||
this.cache.delete(assetKey);
|
this.cache.delete(assetKey);
|
||||||
try {
|
try {
|
||||||
Assets.unload(entry.url);
|
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. */
|
/** Unload all cached textures. */
|
||||||
clear(): void {
|
clear(): void {
|
||||||
for (const [key] of this.cache) {
|
for (const [, entry] of this.cache) {
|
||||||
this.unload(key);
|
try { Assets.unload(entry.url); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
this.cache.clear();
|
this.cache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export class ImageSprite extends Container {
|
|||||||
unloadTexture(): void {
|
unloadTexture(): void {
|
||||||
if (!this.loaded) return;
|
if (!this.loaded) return;
|
||||||
|
|
||||||
this.textures.unload(this.assetKey);
|
this.textures.release(this.assetKey);
|
||||||
|
|
||||||
// Remove sprite from display tree entirely — avoids PixiJS v8 render crash
|
// Remove sprite from display tree entirely — avoids PixiJS v8 render crash
|
||||||
if (this._sprite) {
|
if (this._sprite) {
|
||||||
|
|||||||
Reference in New Issue
Block a user