From 649209459fd91282b00beb8d00c094abd4180dc3 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Fri, 21 Nov 2025 15:29:07 +0100 Subject: [PATCH] Cache system to reuse same SkeletonData, TextureAtlas, and C3Texture. --- .../spine-construct3-lib/src/AssetLoader.ts | 108 +++++++++++++++--- .../spine-construct3-lib/src/C3Texture.ts | 5 +- .../src/c3runtime/instance.ts | 5 + spine-ts/spine-construct3/src/plugin.ts | 1 - 4 files changed, 101 insertions(+), 18 deletions(-) diff --git a/spine-ts/spine-construct3/spine-construct3-lib/src/AssetLoader.ts b/spine-ts/spine-construct3/spine-construct3-lib/src/AssetLoader.ts index 19374a62e..6439cdce2 100644 --- a/spine-ts/spine-construct3/spine-construct3-lib/src/AssetLoader.ts +++ b/spine-ts/spine-construct3/spine-construct3-lib/src/AssetLoader.ts @@ -27,12 +27,21 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, TextureAtlas } from "@esotericsoftware/spine-core"; +import { AtlasAttachmentLoader, SkeletonBinary, type SkeletonData, SkeletonJson, TextureAtlas, TextureAtlasPage } from "@esotericsoftware/spine-core"; import { C3Texture, C3TextureEditor } from "./C3Texture"; +interface CacheEntry { + data: T; + refCount: number; +} + export class AssetLoader { + private static CacheSkeleton = new Map>(); + private static CacheAtlas = new Map>(); + private static CacheTexture = new Map>(); + public async loadSkeletonEditor (sid: number, textureAtlas: TextureAtlas, scale = 1, instance: SDK.IWorldInstance) { const projectFile = instance.GetProject().GetProjectFileBySID(sid); if (!projectFile) return null; @@ -87,27 +96,43 @@ export class AssetLoader { } public async loadSkeletonRuntime (path: string, textureAtlas: TextureAtlas, scale = 1, instance: IRuntime) { + const cacheKey = `${path}|scale${scale}`; + + const fileInCache = this.getFromCache(AssetLoader.CacheSkeleton, cacheKey); + if (fileInCache) return fileInCache; + const fullPath = await instance.assets.getProjectFileUrl(path); if (!fullPath) return null; const atlasLoader = new AtlasAttachmentLoader(textureAtlas); + let skeletonData: SkeletonData; const isBinary = path.endsWith(".skel"); if (isBinary) { const content = await instance.assets.fetchArrayBuffer(fullPath); if (!content) return null; const skeletonLoader = new SkeletonBinary(atlasLoader); skeletonLoader.scale = scale; - return skeletonLoader.readSkeletonData(content); + skeletonData = skeletonLoader.readSkeletonData(content); + } else { + const content = await instance.assets.fetchJson(fullPath); + if (!content) return null; + const skeletonLoader = new SkeletonJson(atlasLoader); + skeletonLoader.scale = scale; + skeletonData = skeletonLoader.readSkeletonData(content); } - const content = await instance.assets.fetchJson(fullPath); - if (!content) return null; - const skeletonLoader = new SkeletonJson(atlasLoader); - skeletonLoader.scale = scale; - return skeletonLoader.readSkeletonData(content); + + AssetLoader.CacheSkeleton.set(cacheKey, { data: skeletonData, refCount: 1 }); + + return skeletonData; } public async loadAtlasRuntime (path: string, instance: IRuntime, renderer: IRenderer) { + const cacheKey = path; + + const fileInCache = this.getFromCache(AssetLoader.CacheAtlas, cacheKey); + if (fileInCache) return fileInCache; + const fullPath = await instance.assets.getProjectFileUrl(path); if (!fullPath) return null; @@ -117,24 +142,77 @@ export class AssetLoader { const basePath = path.substring(0, path.lastIndexOf("/") + 1); const textureAtlas = new TextureAtlas(content); await Promise.all(textureAtlas.pages.map(async page => { - const texture = await this.loadSpineTextureRuntime(basePath + page.name, page.pma, instance); - if (texture) { - const spineTexture = new C3Texture(texture, renderer, page); - page.setTexture(spineTexture); - } + const texture = await this.loadSpineTextureRuntime(basePath, page, instance, renderer); + if (texture) page.setTexture(texture); return texture; })); + + AssetLoader.CacheAtlas.set(cacheKey, { data: textureAtlas, refCount: 1 }); + return textureAtlas; } - public async loadSpineTextureRuntime (pageName: string, pma = false, instance: IRuntime) { - const fullPath = await instance.assets.getProjectFileUrl(pageName); + public async loadSpineTextureRuntime (basePath: string, page: TextureAtlasPage, instance: IRuntime, renderer: IRenderer) { + const cacheKey = basePath + page.name; + + const fileInCache = this.getFromCache(AssetLoader.CacheTexture, cacheKey); + if (fileInCache) return fileInCache; + + const fullPath = await instance.assets.getProjectFileUrl(cacheKey); if (!fullPath) return null; const content = await instance.assets.fetchBlob(fullPath); if (!content) return null; - return AssetLoader.createImageBitmapFromBlob(content, pma); + const image = await AssetLoader.createImageBitmapFromBlob(content, page.pma); + if (!image) return null; + + const spineTexture = new C3Texture(image, renderer, page); + + this.addToCache(AssetLoader.CacheTexture, cacheKey, spineTexture); + + return spineTexture; + } + + public releaseInstanceResources (skeletonPath: string, atlasPath: string, loaderScale: number) { + this.releaseResource(AssetLoader.CacheSkeleton, `${skeletonPath}|scale${loaderScale}`); + + const atlasEntry = AssetLoader.CacheAtlas.get(atlasPath); + if (atlasEntry) { + this.releaseResource(AssetLoader.CacheAtlas, atlasPath, () => { + const basePath = atlasPath.substring(0, atlasPath.lastIndexOf("/") + 1); + for (const page of atlasEntry.data.pages) { + const textureKey = basePath + page.name; + this.releaseResource(AssetLoader.CacheTexture, textureKey, (texture) => { + texture.dispose(); + }); + } + }); + } + } + + private releaseResource (cache: Map>, key: string, disposer?: (data: T) => void) { + const entry = cache.get(key); + if (!entry) return; + + entry.refCount--; + + if (entry.refCount <= 0) { + if (disposer) disposer(entry.data); + cache.delete(key); + } + } + + private addToCache (cache: Map>, cacheKey: string, data: T) { + cache.set(cacheKey, { data, refCount: 1 }); + } + + private getFromCache (cache: Map>, cacheKey: string) { + const fileInCache = cache.get(cacheKey); + if (!fileInCache) return undefined; + + fileInCache.refCount++; + return fileInCache.data; } static async createImageBitmapFromBlob (blob: Blob, pma: boolean): Promise { diff --git a/spine-ts/spine-construct3/spine-construct3-lib/src/C3Texture.ts b/spine-ts/spine-construct3/spine-construct3-lib/src/C3Texture.ts index fd773c54a..ac8b4d5e1 100644 --- a/spine-ts/spine-construct3/spine-construct3-lib/src/C3Texture.ts +++ b/spine-ts/spine-construct3/spine-construct3-lib/src/C3Texture.ts @@ -63,7 +63,7 @@ export class C3TextureEditor extends Texture { export class C3Texture extends Texture { texture: ITexture; - renderer: IRenderer; + renderer?: IRenderer; constructor (image: HTMLImageElement | ImageBitmap, renderer: IRenderer, page: TextureAtlasPage) { super(image); @@ -88,7 +88,8 @@ export class C3Texture extends Texture { } dispose () { - this.renderer.deleteTexture(this.texture); + this.renderer?.deleteTexture(this.texture); + this.renderer = undefined; } } diff --git a/spine-ts/spine-construct3/src/c3runtime/instance.ts b/spine-ts/spine-construct3/src/c3runtime/instance.ts index 9ec087bd6..ffca4203f 100644 --- a/spine-ts/spine-construct3/src/c3runtime/instance.ts +++ b/spine-ts/spine-construct3/src/c3runtime/instance.ts @@ -451,6 +451,11 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase { _release () { super._release(); + this.assetLoader.releaseInstanceResources(this.propSkel, this.propAtlas, this.propLoaderScale); + this.textureAtlas = undefined; + this.renderer = undefined; + this.skeleton = undefined; + this.state = undefined; } /**********/ diff --git a/spine-ts/spine-construct3/src/plugin.ts b/spine-ts/spine-construct3/src/plugin.ts index de6bd9e3d..361e3093c 100644 --- a/spine-ts/spine-construct3/src/plugin.ts +++ b/spine-ts/spine-construct3/src/plugin.ts @@ -1,4 +1,3 @@ -// biome-ignore lint/correctness/noUnusedImports: necessary to make C3 recognize the plugin import type { SDKEditorInstanceClass } from "./instance.ts"; const SDK = globalThis.SDK;