⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@yuechen-li-dev
Copy link

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
Author

@dotnet-policy-service agree

@Eideren
Copy link
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
@yuechen-li-dev
Copy link
Author

yuechen-li-dev commented Feb 4, 2026

Thanks for the thoughtful review - I agree with the motivation you outlined (CJK + large character sets + resolution/build-size constraints is the core reason for doing this at runtime).

Why this approach doesn’t reuse most of the offline SDF pipeline

Stride’s current offline SDF path is essentially a font compiler wrapper around an external tool (msdfgen.exe) to generate bitmap plus some atlas bookkeeping called through DirectWrite. In other words, the “real” work (outline extraction, contour cleanup/orientation, MSDF generation, shape normalization) happens inside the external binary, not inside Stride’s runtime code, as we can see in SignedDistanceFieldFontImporter.cs here.

For runtime MSDF, we need a very different shape of solution:

  • no external exe (shipping an msdfgen tool / invoking processes at runtime isn’t viable, especially cross-platform)
  • async generation + caching, because glyph generation is heavy and needs to happen incrementally under gameplay timing constraints
  • incremental atlas updates while the game is running

So there isn’t much logic to reuse without effectively rewriting the offline pipeline into a reusable in-process library anyway, which is what this PR does, but oriented around runtime constraints.

This approach lets us keep the pipeline cross-platform and embeddable (no msdfgen.exe / no DirectWrite-only path), and it would cleanly resolve #3021 (because no platform specific font compilation is required, as atlas generation happens at runtime using managed portable C# libraries, rather than an external Windows-specific toolchain).

Finally, because of the modular architecture, this approach unlocks future extensions, such as MTSDF alpha map based effects (outlines, glow, rich text effects) which can be done by evolving just the rasterizer/shader instead of reworking the full asset pipeline again.

To address notes and limitations I listed:

  • This implementation is fully functional. The only real issue is where the font thumbnail for Runtime SDF fonts in the Game Editor do not render correctly. Attempts to isolate a fix for this specific UI element currently impact standard font thumbnails; given it is purely cosmetic, it has been left as-is to preserve the integrity of the runtime rendering.
  • For optimal quality and performance, it is recommended to set a relatively large default size in the asset properties. This defines the "bake" resolution; however, the font remains fully resizable within TextBlocks without loss of clarity.
  • Like the existing offline MSDF implementation, certain fonts with overlapping or self-intersecting contours may display artifacts. This is a characteristic of the MSDF algorithm needing a clearly defined "inside" and "outside" and not an issue with this implementation. If artifacts appear, preprocessing the font file to simplify contours once before import is an easy solution. Integrating a SkiaSharp based preprocessing step was considered but omitted to avoid unnecessary project complexity for a niche edge case.

This is from release version of stride to demonstrate these artifacts on offline SDF fonts:
Font used: https://fonts.google.com/noto/specimen/Noto+Sans+SC, the static semi-bold version from the zip file.
image
Fixed after preprocessing:
image

@Eideren
Copy link
Collaborator

Eideren commented Feb 5, 2026

Thank you for the clarification, the source for msdfgen isn't part of this repository, but we do maintain our own version through https://github.com/stride3d/msdfgen - which could have been used as the base for a c# port or integration, but doesn't matter too much now, even more so with the fixes you've introduced.

I'll try to find the time to take a look at it this week end, thanks for the work.

Copy link
Collaborator

@Eideren Eideren left a comment

Choose a reason for hiding this comment

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

Looks really good, I was able to repro the missing characters when building a game, haven't had time to look into it yet, but we can't quite merge before this is fixed.
Still, I have a fair amount of things to run by you.

Parameters.FontSource.Style,
runtimeSdfType.PixelRange,
runtimeSdfType.Padding,
useKerning: false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason why kerning is always off, on this call site and all other usage I could find

Copy link
Author

Choose a reason for hiding this comment

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

The initial basis of this project is an exact copy of the runtime rasterized font that was already in the system.

https://github.com/stride3d/stride/blob/master/sources/engine/Stride.Assets/SpriteFont/SpriteFontAssetCompiler.cs

And kerning was set as off for it, so I just copied.

Comment on lines 229 to 232
// M1 NOTE:
// This is a scaffolding step. We serialize a functional font so the pipeline works end-to-end.
// In M3/M4, this will be replaced by a real Runtime MSDF font object.
commandContext.Logger.Warning("Runtime SDF font is currently scaffolded in M1 (temporary). It will behave like a runtime raster font until the MSDF runtime generator is implemented.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems either unrelated or too loosely documented, what do you mean by M1 and co. ?

Copy link
Author

Choose a reason for hiding this comment

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

These are personal milestones for me to keep track of project development. Since MVP Is complete, this can and will be removed.

Copy link
Author

Choose a reason for hiding this comment

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

I just had the warning to label it as an experimental feature.

/// Dummy MSDF rasterizer that generates simple test patterns.
/// Use this to isolate pipeline issues from MSDF generation issues.
/// </summary>
public sealed class DummyTestRasterizer : IGlyphMsdfRasterizer
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you set it up as a test in Stride.Graphics.Tests ?

Copy link
Author

Choose a reason for hiding this comment

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

These two debug rasterizers are for visual debug feedbacks. I can set up some unit test to test outline extraction/MSDF generation functionality without having to manually switch out rasterizers.

{
if (contour.Edges.Count == 0) continue;

EdgeColor[] colors = { EdgeColor.CYAN, EdgeColor.MAGENTA, EdgeColor.YELLOW };
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be a static array, or a stack allocated one

Comment on lines +177 to +184
foreach (var c in colors)
{
if (c != prevColor && c != nextColor)
{
chosen = c;
break;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

You only need to test the two other colors afaict ?

Comment on lines 388 to 391
// Prefer outline-based MSDF generation when available.
// We still rely on the bitmap path to populate glyph metrics today, but MSDF uses the outline.
if (FontManager != null &&
FontManager.TryGetGlyphOutline(FontName, Style, Size, key.C, out var outline, out _))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't we warn users somewhere that the font they chose does not provide glyph outline, maybe as part of the build steps for a Runtime SDF asset ?

Copy link
Author

Choose a reason for hiding this comment

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

That was something I was unsure about when coding this, but it really looks like all fonts would always have a glyph outline and the extraction process is fairly direct. I will set up some unit tests for this, but ideally, I would like to see if it is possible to include an open source .ttf font file (I would suggest Noto Sans CJK Regular for coverage) so the unit tests aren't reliant on checking what font does the tester have installed on their computer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good, we have a test setup for japanese over here
https://github.com/stride3d/stride/blob/master/sources/engine/Stride.Graphics.Tests/TestDynamicSpriteFontJapanese.cs
Which uses a font bundled with the repo:
https://github.com/stride3d/stride/blob/master/sources/data/tests/Fonts/SourceHanSans-Light.otf
Feel free to look at how that one is setup to build yours, as long as the font is compatible with MIT it'll be fine.

@yuechen-li-dev
Copy link
Author

Looks really good, I was able to repro the missing characters when building a game, haven't had time to look into it yet, but we can't quite merge before this is fixed.

Yeah, my findings on this issue are that the atlas generation/upload for runtime SDF is async, and the existing font thumbnail compiler is not async aware and since it is only called once to generate the thumbnail, and since the fonts aren't ready when it is called, it returns nothing. I'm not sure what the clean way to resolve it would be, and I've experimented a lot on this.

I might be a little busy this weekend, so I'll address these gradually over the next week. Thanks for the thorough review.

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.

2 participants