Skip to content

feat: Dynamic Runtime Rasterized MSDF Sprite Font#3055

Draft
yuechen-li-dev wants to merge 55 commits intostride3d:masterfrom
yuechen-li-dev:runtime-msdf-font
Draft

feat: Dynamic Runtime Rasterized MSDF Sprite Font#3055
yuechen-li-dev wants to merge 55 commits intostride3d:masterfrom
yuechen-li-dev:runtime-msdf-font

Conversation

@yuechen-li-dev
Copy link
Copy Markdown

PR Details

This PR adds an experimental runtime MSDF font path to Stride’s sprite font system.

Screenshot 2026-02-04 003111

Introduction

Stride’s current sprite font rasterization options each have trade-offs. Dynamically generating MSDF glyphs on-demand is the common approach in modern engines (e.g., Unity/Unreal): it combines the benefits of Stride’s runtime rasterized fonts (large character sets like CJK) with offline SDF/MSDF advantages (smooth edges at high resolution, easy scaling) with minimal downsides.

Implementation

Dynamic MSDF generation differs substantially from Stride’s existing static SDF pipeline.

  • Uses SharpFont (already a Stride dependency) to extract glyph outlines on-demand when glyphs are requested at runtime.
  • Generates MSDF textures asynchronously via a bounded Channel + worker pool, then uploads into the font atlas.
  • The MSDF rasterizer is intentionally modular / swappable to keep the pipeline future-proof.

Rasterizer backends

Two managed, cross-platform MSDFGen ports are included (no P/Invoke):

Also included:

  • Two debug rasterizers:
    • a simple pattern output backend
    • an outline diagnostic backend

Notes / limitations

  • "Default Size" parameter is used for baking, keep it relatively large (64+) for best results.
  • Some fonts (especially those with overlapping / self-intersecting contours) can produce repeatable MSDF artifacts (notably in punctuation/CJK). Current workaround: preprocess fonts in FontForge (Remove Overlap / Simplify / Correct Direction). This keeps runtime lightweight and avoids bundling heavy geometry cleanup.
  • Game Studio “Play” vs running from Visual Studio can differ in dependency deployment. Ensure the MSDF backend assemblies are present in the runtime output; if needed, run the game via VS at least once so dependencies get copied/resolved. Otherwise, fonts may render in the editor but not in-game.
  • Thumbnail preview for runtime MSDF fonts is currently blank; I wasn’t able to find a clean fix yet.

Testing

  • Verified glyph generation + atlas upload in the editor.
  • Verified runtime behavior when launched from Visual Studio.
  • Used debug backends to validate atlas upload/sampling independently of MSDF generation.

Core files:

  • RuntimeSignedDistanceFieldSpriteFont.cs
  • SharpFontOutlineExtractor.cs
  • MsdfGenCoreRasterizer.cs (MSDFGen-Sharp backend)
  • RemoraMsdfRasterizer.cs
  • Pipeline glue (MsdfGenerationPipeline.cs, etc.)

Thanks. This took a lot longer than I thought it would.

Comparison

Existing Runtime Rasterized Font:
image
New Runtime MSDF Font:
image

Related Issue

#2584

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

yuechen-li-dev and others added 26 commits January 28, 2026 20:09
This reverts commit 0dc668c.
…-in-runtimesigneddistancefieldspritefon

Remove runtime SDF bake size plumbing and use SpriteFont.Size for glyph generation.
…ey cache into ConcurretDictionaries to future-proof multithreaded calls in the future.
@yuechen-li-dev
Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@Eideren
Copy link
Copy Markdown
Collaborator

Eideren commented Feb 4, 2026

We already have an offline SDF option - but I understand that the reason you implemented this feature is because non-latin character sets tend to be very large and require a high resolution. Offline SDF we currently have wouldn't work because it would have to cover multiple thousands of characters with a fairly high resolution, potentially blowing out build sizes.

Definitely cool to see, now, is there a particular reason why this PR specifically sidesteps using most of the logic already setup for offline SDF ?

Also, as it stands, the limitations seems a bit like a show stopper. Our engine already has enough quirk as it is, best to fix those before we merge this in

@Eideren Eideren added the area-UI label Feb 4, 2026
@Eideren Eideren changed the title Add Dynamic Runtime Rasterized MSDF Sprite Font feat: Dynamic Runtime Rasterized MSDF Sprite Font Feb 4, 2026
@Eideren
Copy link
Copy Markdown
Collaborator

Eideren commented Apr 5, 2026

Changing this to draft while you work on it, let me know when you're ready for another review :)

@Eideren Eideren marked this pull request as draft April 5, 2026 17:48
@yuechen-li-dev
Copy link
Copy Markdown
Author

Changing this to draft while you work on it, let me know when you're ready for another review :)

Yeah, sorry, I don't think I will be able to work on this for quite some time as the direct results from my work on Stride had led to some major projects that are taking priority right now, so unfortunately, I would not be able to work on this for a while. Feel free to poke around the code and continue my work, the main concern from me that I'm honestly unsure what to do about is that the thumbnail generation is not async aware, and I don't want to rewrite the entire font pipeline to fit async in when it doesn't really need it since async kinda infects everything, and of course, harden the entire MSDF pipeline via unit tests, which should be fairly simple with LLMs.

Thanks for the support, I'll circle back to this when I have time.

Comment on lines +310 to +311
using (var fontStream = contentManager.OpenAsStream(fontPath))
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change is not needed. Also, it adds a brace that is never closed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Weird. I just merged master into this feature branch from VS and took incoming change to get this branch sync'd since I have a little bit of time to work on this. Give me a min, let me look it over.

}
}

internal class RuntimeSignedDistanceFieldFontCommand : AssetCommand<SpriteFontAsset>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a little thought: Is not this file a bit full of classes already? Would not make more sense to split the internal classes to their own files to ease discovery?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I've added much to this file other than what is needed to implement runtime MSDF, and I'd like to keep the scope of this PR narrow to just feature implementation cleanly right now. Refactoring/cleaning up existing file will probably have to be another pass.

DequeueRequest:

// update the generated cached data
// update the generated cached data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Indentation

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed.

/// <param name="style">The font style.</param>
/// <returns>A <see cref="SpriteFont"/> instance if the font is registered; otherwise, <c>null</c>.</returns>
public SpriteFont? LoadRuntimeFont(string fontName, float defaultSize = 16f, FontStyle style = FontStyle.Regular)
public SpriteFont LoadRuntimeFont(string fontName, float defaultSize = 16f, FontStyle style = FontStyle.Regular)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The XML docs still says otherwise, <c>null</c>. Why was the return type changed from SpriteFont? to non-null SpriteFont?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch. I think I removed it too hastily without looking at it too carefully. File should be properly nullable now.

Comment on lines +14 to +22
namespace Stride.Graphics.Font
{ /// <summary>
/// A dynamic font that asynchronously generates multi-channel signed distance mapping for glyphs as needed, enabling sharp, smooth edges and resizability.
/// </summary>
[ReferenceSerializer, DataSerializerGlobal(typeof(ReferenceSerializer<RuntimeSignedDistanceFieldSpriteFont>), Profile = "Content")]
[ContentSerializer(typeof(RuntimeSignedDistanceFieldSpriteFontContentSerializer))]
[DataSerializer(typeof(RuntimeSignedDistanceFieldSpriteFontSerializer))]

internal sealed class RuntimeSignedDistanceFieldSpriteFont : SpriteFont
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blank line between attributes and class, Comments on the same line as the brace

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Should be fixed after the partial class split.

// Keep today’s encoding behavior explicit and centralized.
private static readonly DistanceEncodeParams DefaultEncode = new(Bias: 0.4f, Scale: 0.5f);

private DistanceFieldParams GetDfParams()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try to not use abbreviations: GetDfParams() -> GetDistanceFieldParams() or something alike. Reads better in callstacks and is apparent what it does at a quick glance

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed after partial class split.

@VaclavElias
Copy link
Copy Markdown
Contributor

Another alternative worth considering is SixLabors Fonts (from the creators of ImageSharp). Btw, it also does text layouting

@Ethereal77, not sure if the license is ok here https://github.com/SixLabors/Fonts/blob/main/LICENSE, and how it would affect commercial games.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this file has too many responsibilities and is too long. For contrast, SignedDistanceFieldSpriteFont is just the serializable definition of a SDF sprite font. This file is not only that, but also has infrastructure for asynchronous atlas building, EDT transform computation, and maybe other things.

I would split those parts to the systems in charge of managing fonts, or to components specialized in each of those tasks that can as an added benefit be tested in isolation (and maybe even used in some other part where they may be beneficial)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

To avoid a major architecture rewrite, I've split the file into 3 partial classes: the font file, the generator/rasterizer seams, and the fallback bitmap based SDF builder. If there is need, I think there could be a bigger split later on, but the seams are already pretty clean.

@Ethereal77
Copy link
Copy Markdown
Contributor

Another alternative worth considering is SixLabors Fonts (from the creators of ImageSharp). Btw, it also does text layouting

@Ethereal77, not sure if the license is ok here https://github.com/SixLabors/Fonts/blob/main/LICENSE, and how it would affect commercial games.

I think they changed the license some time ago to allow free use for open-source projects, and to be also free if the library is consumed transitively (i.e., through Stride). The paid license would only apply if your project is closed-source and you are using the library directly. Correct me if I'm wrong

@VaclavElias
Copy link
Copy Markdown
Contributor

..and to be also free if the library is consumed transitively (i.e., through Stride). The paid license would only apply if your project is closed-source and you are using the library directly. Correct me if I'm wrong

I believe you are right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants