Skip to content

Free font textures when a font asset is unloaded#8969

Merged
mvaligursky merged 1 commit into
mainfrom
mv-font-unload-destroy
Jun 26, 2026
Merged

Free font textures when a font asset is unloaded#8969
mvaligursky merged 1 commit into
mainfrom
mv-font-unload-destroy

Conversation

@mvaligursky

Copy link
Copy Markdown
Contributor

Unloading a font asset (asset.unload()) did not release the GPU memory used by its textures. The Font resource had no destroy() method, so the asset system's generic resource.destroy?.() cleanup was a no-op and the textures stayed resident in VRAM. The textures were also left in the resource loader cache, so a subsequent load could return a stale entry.

Fixes #7033.

Changes:

  • Add Font.destroy(), which destroys the font's textures and removes them from the resource loader cache.
  • FontHandler now gives the created Font a reference to the resource loader so it can clear the cached texture entries on unload.

API Changes:

  • Added Font#destroy(). It is called automatically when the owning font asset is unloaded; existing user workarounds that manually destroy the font's textures remain safe (double-destroy is a no-op).

Add Font.destroy() so unloading a font asset releases its texture VRAM and
clears the textures from the resource loader cache. FontHandler passes the
loader to the Font so it can clear the cache entries.

Fixes #7033.
@github-actions

Copy link
Copy Markdown

Public API report

This PR changes the public API surface (+1 / −0), per the docs' rules (@ignore / @Private / undocumented are excluded).

Show API diff
+Font.destroy(): void

Informational only — this never fails the build.

@github-actions

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2261.2 KB (+0.2 KB, +0.01%) 581.7 KB (+0.1 KB, +0.01%) 452.2 KB (−0.0 KB, −0.00%)
playcanvas.min.mjs 2258.6 KB (+0.2 KB, +0.01%) 580.7 KB (+0.1 KB, +0.01%) 451.7 KB (+0.0 KB, +0.01%)

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated PR review — posted on my behalf by Claude Code (Opus 4.8). Not a human review. The points below are suggestions to weigh as possible improvements, not changes that necessarily need to be addressed.


Fix is correct for the standard load → unload → reload case (I confirmed texture.name reliably equals the load URL = cache key, since every texture parser sets name: url, and clearCache/getFromCache key on the same makeKey(url, type)). Three things to consider, in rough order of likelihood:

  1. Active-use behavior change worth confirming. Previously the texture leak masked it; now asset.unload() actually destroys the GPU textures. If a TextElement (or CanvasFont consumer) still references the font when its asset is unloaded, it will hold destroyed textures and render with dead resources. This is the intended semantics of unload, but worth confirming the text element drops/handles its font reference gracefully on asset unload rather than erroring — the PR doesn't mention that path.

  2. Shared texture, no reference counting. Font.destroy() unconditionally calls texture.destroy() and clearCache(...), and the loader cache (_cache[key]) isn't ref-counted. If the same texture URL is ever resolved for more than one live Font (e.g. two font assets whose derived .png URL collides, or any future path that shares a cached font texture), unloading one font destroys the texture out from under the others. Low probability since each font's atlas PNG is normally unique — but previously the leak hid this, so it's newly reachable. A guard ("only destroy if no other resource holds it") or accepting the limitation with a note would cover it.

  3. Coupling to the parser naming convention. Keying cache removal on texture.name works only because all texture parsers happen to set name: url. That's true today across all six parsers, but it couples Font.destroy to that convention; if a parser ever set a different name, the cache entry would silently leak (texture still freed, but a reload returns a dead entry). Storing the actual loaded URLs on the Font at load time would decouple it. Minor / robustness only.

@mvaligursky mvaligursky merged commit 158aae2 into main Jun 26, 2026
10 checks passed
@mvaligursky mvaligursky deleted the mv-font-unload-destroy branch June 26, 2026 08:27
mvaligursky added a commit that referenced this pull request Jun 26, 2026
Add Font.destroy() so unloading a font asset releases its texture VRAM and
clears the textures from the resource loader cache. FontHandler passes the
loader to the Font so it can clear the cache entries.

Fixes #7033.

Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When a font asset is unloaded the font’s textures are not destroyed and still consume VRAM

1 participant