Skip to content

Commit 9167263

Browse files
committed
Fix multiple resources allocation for same instances during startup.
1 parent 1afafb9 commit 9167263

File tree

1 file changed

+80
-75
lines changed

1 file changed

+80
-75
lines changed

spine-ts/spine-construct3/spine-construct3-lib/src/AssetLoader.ts

Lines changed: 80 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ import { C3TextureEditor, C3TextureRuntime } from "./C3Texture";
3232

3333

3434
interface CacheEntry<T> {
35-
data: T;
35+
data?: T;
36+
promise: Promise<T>;
3637
refCount: number;
3738
}
3839

40+
type ResourceCache<T> = Map<string, CacheEntry<T>>;
41+
3942
export class AssetLoader {
4043

41-
private static CacheSkeleton = new Map<string, CacheEntry<SkeletonData>>();
42-
private static CacheAtlas = new Map<string, CacheEntry<TextureAtlas>>();
43-
private static CacheTexture = new Map<string, CacheEntry<C3TextureRuntime>>();
44+
private static CacheSkeleton: ResourceCache<SkeletonData> = new Map();
45+
private static CacheAtlas: ResourceCache<TextureAtlas> = new Map();
46+
private static CacheTexture: ResourceCache<C3TextureRuntime> = new Map();
4447

4548
public async loadSkeletonEditor (sid: number, textureAtlas: TextureAtlas, scale = 1, instance: SDK.IWorldInstance) {
4649
const projectFile = instance.GetProject().GetProjectFileBySID(sid);
@@ -96,102 +99,94 @@ export class AssetLoader {
9699
}
97100

98101
public async loadSkeletonRuntime (path: string, textureAtlas: TextureAtlas, scale = 1, instance: IRuntime) {
99-
const cacheKey = `${path}|scale${scale}`;
100-
101-
const fileInCache = this.getFromCache(AssetLoader.CacheSkeleton, cacheKey);
102-
if (fileInCache) return fileInCache;
103-
104-
const fullPath = await instance.assets.getProjectFileUrl(path);
105-
if (!fullPath) return null;
106-
107-
const atlasLoader = new AtlasAttachmentLoader(textureAtlas);
108-
109-
let skeletonData: SkeletonData;
110-
const isBinary = path.endsWith(".skel");
111-
if (isBinary) {
112-
const content = await instance.assets.fetchArrayBuffer(fullPath);
113-
if (!content) return null;
114-
const skeletonLoader = new SkeletonBinary(atlasLoader);
115-
skeletonLoader.scale = scale;
116-
skeletonData = skeletonLoader.readSkeletonData(content);
117-
} else {
118-
const content = await instance.assets.fetchJson(fullPath);
119-
if (!content) return null;
120-
const skeletonLoader = new SkeletonJson(atlasLoader);
121-
skeletonLoader.scale = scale;
122-
skeletonData = skeletonLoader.readSkeletonData(content);
123-
}
124-
125-
AssetLoader.CacheSkeleton.set(cacheKey, { data: skeletonData, refCount: 1 });
102+
const loadPromise = (async () => {
103+
const fullPath = await instance.assets.getProjectFileUrl(path);
104+
if (!fullPath) throw new Error(`Cannot find project file url for: ${path}`);
105+
106+
const atlasLoader = new AtlasAttachmentLoader(textureAtlas);
107+
108+
let skeletonData: SkeletonData;
109+
const isBinary = path.endsWith(".skel");
110+
if (isBinary) {
111+
const content = await instance.assets.fetchArrayBuffer(fullPath);
112+
if (!content) throw new Error(`Cannot fetch array buffer for: ${fullPath}`);
113+
114+
const skeletonLoader = new SkeletonBinary(atlasLoader);
115+
skeletonLoader.scale = scale;
116+
skeletonData = skeletonLoader.readSkeletonData(content);
117+
} else {
118+
const content = await instance.assets.fetchJson(fullPath);
119+
if (!content) throw new Error(`Cannot fetch json for: ${fullPath}`);
120+
121+
const skeletonLoader = new SkeletonJson(atlasLoader);
122+
skeletonLoader.scale = scale;
123+
skeletonData = skeletonLoader.readSkeletonData(content);
124+
}
125+
return skeletonData;
126+
});
126127

127-
return skeletonData;
128+
return this.loadRuntimeResource(`${path}|scale${scale}`, AssetLoader.CacheSkeleton, loadPromise);
128129
}
129130

130131
public async loadAtlasRuntime (path: string, instance: IRuntime, renderer: IRenderer) {
131-
const cacheKey = path;
132-
133-
const fileInCache = this.getFromCache(AssetLoader.CacheAtlas, cacheKey);
134-
if (fileInCache) return fileInCache;
135-
136-
const fullPath = await instance.assets.getProjectFileUrl(path);
137-
if (!fullPath) return null;
132+
const loadPromise = (async () => {
133+
const fullPath = await instance.assets.getProjectFileUrl(path);
134+
if (!fullPath) throw new Error(`Cannot find project file url for: ${path}`);
138135

139-
const content = await instance.assets.fetchText(fullPath);
140-
if (!content) return null;
136+
const content = await instance.assets.fetchText(fullPath);
137+
if (!content) throw new Error(`Cannot fetch text for: ${fullPath}`);
141138

142-
const basePath = path.substring(0, path.lastIndexOf("/") + 1);
143-
const textureAtlas = new TextureAtlas(content);
144-
await Promise.all(textureAtlas.pages.map(async page => {
145-
const texture = await this.loadSpineTextureRuntime(basePath, page, instance, renderer);
146-
if (texture) page.setTexture(texture);
147-
return texture;
148-
}));
139+
const basePath = path.substring(0, path.lastIndexOf("/") + 1);
140+
const textureAtlas = new TextureAtlas(content);
141+
await Promise.all(textureAtlas.pages.map(async page => {
142+
const texture = await this.loadSpineTextureRuntime(basePath, page, instance, renderer);
143+
if (texture) page.setTexture(texture);
144+
return texture;
145+
}));
149146

150-
AssetLoader.CacheAtlas.set(cacheKey, { data: textureAtlas, refCount: 1 });
147+
return textureAtlas;
148+
});
151149

152-
return textureAtlas;
150+
return this.loadRuntimeResource(path, AssetLoader.CacheAtlas, loadPromise);
153151
}
154152

155153
public async loadSpineTextureRuntime (basePath: string, page: TextureAtlasPage, instance: IRuntime, renderer: IRenderer) {
156154
const cacheKey = basePath + page.name;
157155

158-
const fileInCache = this.getFromCache(AssetLoader.CacheTexture, cacheKey);
159-
if (fileInCache) return fileInCache;
160-
161-
const fullPath = await instance.assets.getProjectFileUrl(cacheKey);
162-
if (!fullPath) return null;
156+
const loadPromise = (async () => {
157+
const fullPath = await instance.assets.getProjectFileUrl(cacheKey);
158+
if (!fullPath) throw new Error(`Cannot find project file url for: ${cacheKey}`);
163159

164-
const content = await instance.assets.fetchBlob(fullPath);
165-
if (!content) return null;
160+
const content = await instance.assets.fetchBlob(fullPath);
161+
if (!content) throw new Error(`Cannot fetch blob for: ${fullPath}`);
166162

167-
const image = await AssetLoader.createImageBitmapFromBlob(content, page.pma);
168-
if (!image) return null;
163+
const image = await AssetLoader.createImageBitmapFromBlob(content, page.pma);
164+
if (!image) throw new Error(`Cannot create image bitmap for: ${fullPath}`);
169165

170-
const spineTexture = new C3TextureRuntime(image, renderer, page);
166+
return new C3TextureRuntime(image, renderer, page);
167+
});
171168

172-
this.addToCache(AssetLoader.CacheTexture, cacheKey, spineTexture);
173-
174-
return spineTexture;
169+
return this.loadRuntimeResource(cacheKey, AssetLoader.CacheTexture, loadPromise);
175170
}
176171

177172
public releaseInstanceResources (skeletonPath: string, atlasPath: string, loaderScale: number) {
178173
this.releaseResource(AssetLoader.CacheSkeleton, `${skeletonPath}|scale${loaderScale}`);
179174

180175
const atlasEntry = AssetLoader.CacheAtlas.get(atlasPath);
181176
if (atlasEntry) {
182-
this.releaseResource(AssetLoader.CacheAtlas, atlasPath, () => {
177+
this.releaseResource(AssetLoader.CacheAtlas, atlasPath, async () => {
183178
const basePath = atlasPath.substring(0, atlasPath.lastIndexOf("/") + 1);
184-
for (const page of atlasEntry.data.pages) {
179+
for (const page of (await atlasEntry.promise).pages) {
185180
const textureKey = basePath + page.name;
186181
this.releaseResource(AssetLoader.CacheTexture, textureKey, (texture) => {
187-
texture.dispose();
182+
texture?.dispose();
188183
});
189184
}
190185
});
191186
}
192187
}
193188

194-
private releaseResource<T> (cache: Map<string, CacheEntry<T>>, key: string, disposer?: (data: T) => void) {
189+
private releaseResource<T> (cache: ResourceCache<T>, key: string, disposer?: (data?: T) => void) {
195190
const entry = cache.get(key);
196191
if (!entry) return;
197192

@@ -203,24 +198,34 @@ export class AssetLoader {
203198
}
204199
}
205200

206-
private addToCache<T> (cache: Map<string, CacheEntry<T>>, cacheKey: string, data: T) {
207-
cache.set(cacheKey, { data, refCount: 1 });
201+
private async loadRuntimeResource<T> (cacheKey: string, resourceCache: ResourceCache<T>, loader: () => Promise<T>): Promise<T> {
202+
const entry = this.getFromCache(resourceCache, cacheKey);
203+
if (entry) return entry.promise;
204+
const result = loader();
205+
this.addToCache(resourceCache, cacheKey, result);
206+
return result;
207+
}
208+
209+
private async addToCache<T> (cache: ResourceCache<T>, cacheKey: string, promise: Promise<T>) {
210+
const cachEntry: CacheEntry<T> = { promise, refCount: 1 };
211+
cache.set(cacheKey, cachEntry);
212+
cachEntry.data = await promise;
208213
}
209214

210-
private getFromCache<T> (cache: Map<string, CacheEntry<T>>, cacheKey: string) {
211-
const fileInCache = cache.get(cacheKey);
212-
if (!fileInCache) return undefined;
215+
private getFromCache<T> (cache: ResourceCache<T>, cacheKey: string) {
216+
const entry = cache.get(cacheKey);
217+
if (!entry) return undefined;
213218

214-
fileInCache.refCount++;
215-
return fileInCache.data;
219+
entry.refCount++;
220+
return entry;
216221
}
217222

218223
static async createImageBitmapFromBlob (blob: Blob, pma: boolean): Promise<ImageBitmap | null> {
219224
try {
220225
return createImageBitmap(blob, { premultiplyAlpha: pma ? "none" : "premultiply" });
221226
} catch (e) {
222227
console.error("Failed to create ImageBitmap from blob:", e);
223-
return null;
228+
throw e;
224229
}
225230
}
226231

0 commit comments

Comments
 (0)