Skip to content

Commit

Permalink
feat(texture): dispose of textures when no longer in use (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak authored Dec 29, 2023
1 parent ca01329 commit f316e96
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * from './util.js';
export * from './controls/OrbitControls.js';
export * from './AssetManager.js';
export * from './FormatManager.js';
export * from './TextureManager.js';
export * from './texture/TextureManager.js';
export * from './SceneWorker.js';
export * from './terrain/TerrainManager.js';
export * from './terrain/TerrainMesh.js';
Expand Down
2 changes: 1 addition & 1 deletion src/lib/terrain/TerrainManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as THREE from 'three';
import { MapChunk, MapArea } from '@wowserhq/format';
import { createSplatTexture } from './material.js';
import { createTerrainIndexBuffer, createTerrainVertexBuffer } from './geometry.js';
import TextureManager from '../TextureManager.js';
import TextureManager from '../texture/TextureManager.js';
import TerrainMaterial from './TerrainMaterial.js';
import TerrainMesh from './TerrainMesh.js';

Expand Down
51 changes: 51 additions & 0 deletions src/lib/texture/ManagedCompressedTexture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as THREE from 'three';
import TextureManager from './TextureManager.js';

class ManagedCompressedTexture extends THREE.CompressedTexture {
#manager: TextureManager;
#refId: string;

constructor(
manager: TextureManager,
refId: string,
mipmaps: ImageData[],
width: number,
height: number,
format: THREE.CompressedPixelFormat,
type?: THREE.TextureDataType,
mapping?: THREE.Mapping,
wrapS?: THREE.Wrapping,
wrapT?: THREE.Wrapping,
magFilter?: THREE.MagnificationTextureFilter,
minFilter?: THREE.MinificationTextureFilter,
anisotropy?: number,
colorSpace?: THREE.ColorSpace,
) {
super(
mipmaps,
width,
height,
format,
type,
mapping,
wrapS,
wrapT,
magFilter,
minFilter,
anisotropy,
colorSpace,
);

this.#manager = manager;
this.#refId = refId;
}

dispose() {
if (this.#manager.deref(this.#refId) === 0) {
super.dispose();
}
}
}

export default ManagedCompressedTexture;
export { ManagedCompressedTexture };
51 changes: 51 additions & 0 deletions src/lib/texture/ManagedDataTexture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as THREE from 'three';
import TextureManager from './TextureManager.js';

class ManagedDataTexture extends THREE.DataTexture {
#manager: TextureManager;
#refId: string;

constructor(
manager: TextureManager,
refId: string,
data?: BufferSource | null,
width?: number,
height?: number,
format?: THREE.PixelFormat,
type?: THREE.TextureDataType,
mapping?: THREE.Mapping,
wrapS?: THREE.Wrapping,
wrapT?: THREE.Wrapping,
magFilter?: THREE.MagnificationTextureFilter,
minFilter?: THREE.MinificationTextureFilter,
anisotropy?: number,
colorSpace?: THREE.ColorSpace,
) {
super(
data,
width,
height,
format,
type,
mapping,
wrapS,
wrapT,
magFilter,
minFilter,
anisotropy,
colorSpace,
);

this.#manager = manager;
this.#refId = refId;
}

dispose() {
if (this.#manager.deref(this.#refId) === 0) {
super.dispose();
}
}
}

export default ManagedDataTexture;
export { ManagedDataTexture };
78 changes: 61 additions & 17 deletions src/lib/TextureManager.ts → src/lib/texture/TextureManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as THREE from 'three';
import { Blp, BLP_IMAGE_FORMAT } from '@wowserhq/format';
import FormatManager from './FormatManager.js';
import { normalizePath } from './util.js';
import ManagedCompressedTexture from './ManagedCompressedTexture.js';
import ManagedDataTexture from './ManagedDataTexture.js';
import FormatManager from '../FormatManager.js';
import { normalizePath } from '../util.js';

const THREE_TEXTURE_FORMAT: Record<number, THREE.PixelFormat | THREE.CompressedPixelFormat> = {
[BLP_IMAGE_FORMAT.IMAGE_DXT1]: THREE.RGBA_S3TC_DXT1_Format,
Expand All @@ -14,6 +16,7 @@ class TextureManager {
#formatManager: FormatManager;
#loaded = new Map<string, THREE.Texture>();
#loading = new Map<string, Promise<THREE.Texture>>();
#refs = new Map<string, number>();

constructor(formatManager: FormatManager) {
this.#formatManager = formatManager;
Expand All @@ -26,26 +29,65 @@ class TextureManager {
minFilter: THREE.MinificationTextureFilter = THREE.LinearMipmapLinearFilter,
magFilter: THREE.MagnificationTextureFilter = THREE.LinearFilter,
) {
const cacheKey = [normalizePath(path), wrapS, wrapT, minFilter, magFilter].join(':');
const refId = [normalizePath(path), wrapS, wrapT, minFilter, magFilter].join(':');
this.#ref(refId);

const loaded = this.#loaded.get(cacheKey);
const loaded = this.#loaded.get(refId);
if (loaded) {
return Promise.resolve(loaded);
}

const alreadyLoading = this.#loading.get(cacheKey);
const alreadyLoading = this.#loading.get(refId);
if (alreadyLoading) {
return alreadyLoading;
}

const loading = this.#load(cacheKey, path, wrapS, wrapT, minFilter, magFilter);
this.#loading.set(cacheKey, loading);
const loading = this.#load(refId, path, wrapS, wrapT, minFilter, magFilter);
this.#loading.set(refId, loading);

return loading;
}

deref(refId: string) {
let refCount = this.#refs.get(refId);

// Unknown ref

if (refCount === undefined) {
return;
}

// Decrement

refCount--;

if (refCount > 0) {
this.#refs.set(refId, refCount);
return refCount;
}

// Dispose

const texture = this.#loaded.get(refId);
if (texture) {
this.#loaded.delete(refId);
}

this.#refs.delete(refId);

return 0;
}

#ref(refId: string) {
let refCount = this.#refs.get(refId) || 0;

refCount++;

this.#refs.set(refId, refCount);
}

async #load(
cacheKey: string,
refId: string,
path: string,
wrapS: THREE.Wrapping,
wrapT: THREE.Wrapping,
Expand All @@ -56,7 +98,7 @@ class TextureManager {
try {
blp = await this.#formatManager.get(path, Blp);
} catch (error) {
this.#loading.delete(cacheKey);
this.#loading.delete(refId);
throw error;
}

Expand All @@ -66,24 +108,28 @@ class TextureManager {

const threeFormat = THREE_TEXTURE_FORMAT[imageFormat];
if (threeFormat === undefined) {
this.#loading.delete(cacheKey);
this.#loading.delete(refId);
throw new Error(`Unsupported texture format: ${imageFormat}`);
}

let texture: THREE.CompressedTexture | THREE.DataTexture;
let texture: ManagedCompressedTexture | ManagedDataTexture;
if (
imageFormat === BLP_IMAGE_FORMAT.IMAGE_DXT1 ||
imageFormat === BLP_IMAGE_FORMAT.IMAGE_DXT3 ||
imageFormat === BLP_IMAGE_FORMAT.IMAGE_DXT5
) {
texture = new THREE.CompressedTexture(
texture = new ManagedCompressedTexture(
this,
refId,
null,
blp.width,
blp.height,
threeFormat as THREE.CompressedPixelFormat,
);
} else if (imageFormat === BLP_IMAGE_FORMAT.IMAGE_ABGR8888) {
texture = new THREE.DataTexture(
texture = new ManagedDataTexture(
this,
refId,
null,
blp.width,
blp.height,
Expand All @@ -98,18 +144,16 @@ class TextureManager {
texture.magFilter = magFilter;
texture.anisotropy = 16;
texture.name = normalizePath(path).split('/').at(-1);
texture.userData.cacheKey = cacheKey;

// All newly loaded textures need to be flagged for upload to the GPU
texture.needsUpdate = true;

this.#loaded.set(cacheKey, texture);
this.#loading.delete(cacheKey);
this.#loaded.set(refId, texture);
this.#loading.delete(refId);

return texture;
}
}

export default TextureManager;

export { TextureManager };

0 comments on commit f316e96

Please sign in to comment.