Skip to content

Commit 613a7bb

Browse files
committed
feat(texture): dispose of textures when no longer in use
1 parent ca01329 commit 613a7bb

File tree

5 files changed

+160
-19
lines changed

5 files changed

+160
-19
lines changed

src/lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export * from './util.js';
22
export * from './controls/OrbitControls.js';
33
export * from './AssetManager.js';
44
export * from './FormatManager.js';
5-
export * from './TextureManager.js';
5+
export * from './texture/TextureManager.js';
66
export * from './SceneWorker.js';
77
export * from './terrain/TerrainManager.js';
88
export * from './terrain/TerrainMesh.js';

src/lib/terrain/TerrainManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as THREE from 'three';
22
import { MapChunk, MapArea } from '@wowserhq/format';
33
import { createSplatTexture } from './material.js';
44
import { createTerrainIndexBuffer, createTerrainVertexBuffer } from './geometry.js';
5-
import TextureManager from '../TextureManager.js';
5+
import TextureManager from '../texture/TextureManager.js';
66
import TerrainMaterial from './TerrainMaterial.js';
77
import TerrainMesh from './TerrainMesh.js';
88

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as THREE from 'three';
2+
import TextureManager from './TextureManager.js';
3+
4+
class ManagedCompressedTexture extends THREE.CompressedTexture {
5+
#manager: TextureManager;
6+
#refId: string;
7+
8+
constructor(
9+
manager: TextureManager,
10+
refId: string,
11+
mipmaps: ImageData[],
12+
width: number,
13+
height: number,
14+
format: THREE.CompressedPixelFormat,
15+
type?: THREE.TextureDataType,
16+
mapping?: THREE.Mapping,
17+
wrapS?: THREE.Wrapping,
18+
wrapT?: THREE.Wrapping,
19+
magFilter?: THREE.MagnificationTextureFilter,
20+
minFilter?: THREE.MinificationTextureFilter,
21+
anisotropy?: number,
22+
colorSpace?: THREE.ColorSpace,
23+
) {
24+
super(
25+
mipmaps,
26+
width,
27+
height,
28+
format,
29+
type,
30+
mapping,
31+
wrapS,
32+
wrapT,
33+
magFilter,
34+
minFilter,
35+
anisotropy,
36+
colorSpace,
37+
);
38+
39+
this.#manager = manager;
40+
this.#refId = refId;
41+
}
42+
43+
dispose() {
44+
this.#manager.deref(this.#refId);
45+
}
46+
}
47+
48+
export default ManagedCompressedTexture;
49+
export { ManagedCompressedTexture };

src/lib/texture/ManagedDataTexture.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as THREE from 'three';
2+
import TextureManager from './TextureManager.js';
3+
4+
class ManagedDataTexture extends THREE.DataTexture {
5+
#manager: TextureManager;
6+
#refId: string;
7+
8+
constructor(
9+
manager: TextureManager,
10+
refId: string,
11+
data?: BufferSource | null,
12+
width?: number,
13+
height?: number,
14+
format?: THREE.PixelFormat,
15+
type?: THREE.TextureDataType,
16+
mapping?: THREE.Mapping,
17+
wrapS?: THREE.Wrapping,
18+
wrapT?: THREE.Wrapping,
19+
magFilter?: THREE.MagnificationTextureFilter,
20+
minFilter?: THREE.MinificationTextureFilter,
21+
anisotropy?: number,
22+
colorSpace?: THREE.ColorSpace,
23+
) {
24+
super(
25+
data,
26+
width,
27+
height,
28+
format,
29+
type,
30+
mapping,
31+
wrapS,
32+
wrapT,
33+
magFilter,
34+
minFilter,
35+
anisotropy,
36+
colorSpace,
37+
);
38+
39+
this.#manager = manager;
40+
this.#refId = refId;
41+
}
42+
43+
dispose() {
44+
this.#manager.deref(this.#refId);
45+
}
46+
}
47+
48+
export default ManagedDataTexture;
49+
export { ManagedDataTexture };

src/lib/TextureManager.ts renamed to src/lib/texture/TextureManager.ts

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as THREE from 'three';
22
import { Blp, BLP_IMAGE_FORMAT } from '@wowserhq/format';
3-
import FormatManager from './FormatManager.js';
4-
import { normalizePath } from './util.js';
3+
import ManagedCompressedTexture from './ManagedCompressedTexture.js';
4+
import ManagedDataTexture from './ManagedDataTexture.js';
5+
import FormatManager from '../FormatManager.js';
6+
import { normalizePath } from '../util.js';
57

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

1821
constructor(formatManager: FormatManager) {
1922
this.#formatManager = formatManager;
@@ -26,26 +29,64 @@ class TextureManager {
2629
minFilter: THREE.MinificationTextureFilter = THREE.LinearMipmapLinearFilter,
2730
magFilter: THREE.MagnificationTextureFilter = THREE.LinearFilter,
2831
) {
29-
const cacheKey = [normalizePath(path), wrapS, wrapT, minFilter, magFilter].join(':');
32+
const refId = [normalizePath(path), wrapS, wrapT, minFilter, magFilter].join(':');
33+
this.#ref(refId);
3034

31-
const loaded = this.#loaded.get(cacheKey);
35+
const loaded = this.#loaded.get(refId);
3236
if (loaded) {
3337
return Promise.resolve(loaded);
3438
}
3539

36-
const alreadyLoading = this.#loading.get(cacheKey);
40+
const alreadyLoading = this.#loading.get(refId);
3741
if (alreadyLoading) {
3842
return alreadyLoading;
3943
}
4044

41-
const loading = this.#load(cacheKey, path, wrapS, wrapT, minFilter, magFilter);
42-
this.#loading.set(cacheKey, loading);
45+
const loading = this.#load(refId, path, wrapS, wrapT, minFilter, magFilter);
46+
this.#loading.set(refId, loading);
4347

4448
return loading;
4549
}
4650

51+
deref(refId: string) {
52+
let refCount = this.#refs.get(refId);
53+
54+
// Unknown ref
55+
56+
if (refCount === undefined) {
57+
return;
58+
}
59+
60+
// Decrement
61+
62+
refCount--;
63+
64+
if (refCount > 0) {
65+
this.#refs.set(refId, refCount);
66+
return;
67+
}
68+
69+
// Dispose
70+
71+
const texture = this.#loaded.get(refId);
72+
if (texture) {
73+
this.#loaded.delete(refId);
74+
texture.dispose();
75+
}
76+
77+
this.#refs.delete(refId);
78+
}
79+
80+
#ref(refId: string) {
81+
let refCount = this.#refs.get(refId) || 0;
82+
83+
refCount++;
84+
85+
this.#refs.set(refId, refCount);
86+
}
87+
4788
async #load(
48-
cacheKey: string,
89+
refId: string,
4990
path: string,
5091
wrapS: THREE.Wrapping,
5192
wrapT: THREE.Wrapping,
@@ -56,7 +97,7 @@ class TextureManager {
5697
try {
5798
blp = await this.#formatManager.get(path, Blp);
5899
} catch (error) {
59-
this.#loading.delete(cacheKey);
100+
this.#loading.delete(refId);
60101
throw error;
61102
}
62103

@@ -66,24 +107,28 @@ class TextureManager {
66107

67108
const threeFormat = THREE_TEXTURE_FORMAT[imageFormat];
68109
if (threeFormat === undefined) {
69-
this.#loading.delete(cacheKey);
110+
this.#loading.delete(refId);
70111
throw new Error(`Unsupported texture format: ${imageFormat}`);
71112
}
72113

73-
let texture: THREE.CompressedTexture | THREE.DataTexture;
114+
let texture: ManagedCompressedTexture | ManagedDataTexture;
74115
if (
75116
imageFormat === BLP_IMAGE_FORMAT.IMAGE_DXT1 ||
76117
imageFormat === BLP_IMAGE_FORMAT.IMAGE_DXT3 ||
77118
imageFormat === BLP_IMAGE_FORMAT.IMAGE_DXT5
78119
) {
79-
texture = new THREE.CompressedTexture(
120+
texture = new ManagedCompressedTexture(
121+
this,
122+
refId,
80123
null,
81124
blp.width,
82125
blp.height,
83126
threeFormat as THREE.CompressedPixelFormat,
84127
);
85128
} else if (imageFormat === BLP_IMAGE_FORMAT.IMAGE_ABGR8888) {
86-
texture = new THREE.DataTexture(
129+
texture = new ManagedDataTexture(
130+
this,
131+
refId,
87132
null,
88133
blp.width,
89134
blp.height,
@@ -98,18 +143,16 @@ class TextureManager {
98143
texture.magFilter = magFilter;
99144
texture.anisotropy = 16;
100145
texture.name = normalizePath(path).split('/').at(-1);
101-
texture.userData.cacheKey = cacheKey;
102146

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

106-
this.#loaded.set(cacheKey, texture);
107-
this.#loading.delete(cacheKey);
150+
this.#loaded.set(refId, texture);
151+
this.#loading.delete(refId);
108152

109153
return texture;
110154
}
111155
}
112156

113157
export default TextureManager;
114-
115158
export { TextureManager };

0 commit comments

Comments
 (0)