Skip to content

fix(fonts): msdf atlas settings, and give the font ownership of mirror preload - #2194

Merged
kpal81xd merged 2 commits into
mainfrom
fix/referenced-font-mirror-settings
Aug 4, 2026
Merged

fix(fonts): msdf atlas settings, and give the font ownership of mirror preload#2194
kpal81xd merged 2 commits into
mainfrom
fix/referenced-font-mirror-settings

Conversation

@kpal81xd

@kpal81xd kpal81xd commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #2190. Two problems with the json/texture assets a client-imported font references.

The atlas was created with colour-texture settings

An atlas page was created as a plain texture asset with no data of its own, so it inherited the defaults for colour data: mipmaps: true, srgb: true, minfilter: linear_mip_linear. All three are wrong for a distance field. Mip levels box-average the r/g/b channels and median(average) != average(median), so minified text samples a corrupted median; sRGB gamma corrupts it the same way. This is the same reasoning as playcanvas/engine#8997.

ReferencedFontHandler already forced the right values on the loaded texture, so output was correct — but it was correcting rather than preventing. The texture was built with an 11-level mip chain and an sRGB format and then changed, and on WebGPU changing mipmaps re-creates the texture, because the mip count is baked into the immutable descriptor (playcanvas/engine#9003). The asset data also misreported what the font actually uses, so the inspector showed settings that had no effect and a user toggling them changed nothing.

Pages are now created with mipmaps: false, srgb: false, minfilter: linear. The handler keeps forcing them, now as a no-op for generated pages and a safety net for an atlas repointed at a texture of the user's own.

The font did not own its mirrors' preload

A legacy font was a single asset with a single Preload checkbox, and it governed the descriptor and atlas because those were files inside it. Unpacking them into real assets split that into three independent checkboxes, two of which a user has no way to reason about: the font's own load resolves and loads both mirrors, so their flags only ever agreed with the font by luck. Unticking the font while a page stayed ticked left the atlas downloading at boot for a font nobody preloaded, with no feedback.

The font's preload is now written through to the descriptor and every atlas page, on change and on repoint, and their own field is disabled in the inspector while they serve it. It writes only on a real difference, so it is a no-op once they agree and self-heals a repoint or a merge.

Ownership is keyed on live references rather than on how the asset was created, so it ends when the link does:

  • clear a picker, and the asset gets its field back
  • a page also used by something else, a material map say, keeps its field, because that use has its own reason to preload

The field is disabled rather than hidden so the inherited value stays visible.

Verification

Driven against a local backend with the editor served locally.

Atlas settings, on WebGL2 and WebGPU: the atlas reports mipLevelCount: 1 with zero mipmaps transitions and a single recreateImpl (the constructor's own), where before it needed one re-create for the mip count and another for the format switch. No engine warnings.

Action font / json / atlas mirror field
Fresh import true / true / true disabled
Untick the font false / false / false disabled
Re-tick true / true / true disabled
Clear the json reference json keeps its value enabled
Untick font while unlinked json untouched enabled
Atlas also used by a material enabled

Legacy conversion: a server-pipeline font set to preload: false converts to mirrors that are also preload: false, with the atlas settings applied, 95 glyphs, and numLevels: 1 at runtime. meta.chars preserved.

Also confirmed the timing assumption behind all of this: the descriptor and atlas requests start 0.2 ms apart, and the font's own placeholder file is never fetched, so nothing serialises behind it.

A matching backend change, so that a texture uploaded with noConvert still gets thumbnails generated, ships separately.

An atlas page was created as a plain texture asset with no data of its own, so it
inherited the colour-texture defaults: mipmaps on, srgb on, minfilter
linear_mip_linear. All three are wrong for a distance field. Mip levels
box-average the r/g/b channels and median(average) != average(median), so
minified text samples a corrupted median; srgb gamma corrupts it the same way.

ReferencedFontHandler already forced the right values on the loaded texture, so
output was correct, but it was correcting rather than preventing: the texture was
built with an 11-level mip chain and an sRGB format and then changed, and on
WebGPU changing mipmaps re-creates the texture because the mip count is baked
into the immutable descriptor. The asset data also misreported what the font
actually uses, so the inspector showed settings that had no effect.

Create the page with mipmaps false, srgb false and minfilter linear instead. The
handler keeps forcing them, now as a no-op for generated pages and a safety net
for an atlas repointed at the user's own texture.

Verified on a local environment, on WebGL2 and WebGPU: the atlas reports
mipLevelCount 1 with zero mipmaps transitions and a single recreateImpl (the
constructor's own), where before it needed one re-create for mipmaps and another
for the format switch. Holds on the legacy-font conversion path too.
A legacy font was a single asset with a single Preload checkbox, and it governed
the descriptor and atlas because those were files inside it. Unpacking them into
real assets split that into three independent checkboxes, two of which the user
has no way to reason about: the font's own load resolves and loads both mirrors,
so their flags only ever agreed with the font by luck. Unticking the font while a
page stayed ticked left the atlas downloading at boot for a font nobody
preloaded, silently.

Put the font back in charge. Its preload is written through to the descriptor and
every atlas page, on change and on repoint, and their own field is disabled in
the inspector while they serve it. Writes only on a real difference, so it is a
no-op once they agree and self-heals a repoint or a merge.

Ownership is keyed on live references rather than on how the asset was created,
so it ends when the link does: clear a picker and the asset gets its field back,
and a page also used by something else — a material map, say — keeps it, because
that use has its own reason to preload. The field is disabled rather than hidden
so the inherited value stays visible.

Verified on a local environment: toggling the font drives both mirrors; clearing
the json reference re-enables its field and further font toggles no longer reach
it; a page shared with a material stays editable. A converted legacy font that
was not preloaded produces mirrors that are not preloaded either.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
editor Ready Ready Preview Aug 4, 2026 2:41pm

Request Review

@kpal81xd kpal81xd self-assigned this Aug 4, 2026
@kpal81xd kpal81xd added the bug Something isn't working label Aug 4, 2026
@kpal81xd
kpal81xd merged commit 3d1034c into main Aug 4, 2026
8 checks passed
@kpal81xd
kpal81xd deleted the fix/referenced-font-mirror-settings branch August 4, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant