feat: Dynamic Runtime Rasterized MSDF Sprite Font#3055
feat: Dynamic Runtime Rasterized MSDF Sprite Font#3055yuechen-li-dev wants to merge 55 commits intostride3d:masterfrom
Conversation
|
@dotnet-policy-service agree |
|
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 |
|
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. |
| using (var fontStream = contentManager.OpenAsStream(fontPath)) | ||
| { |
There was a problem hiding this comment.
This change is not needed. Also, it adds a brace that is never closed?
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
| /// <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) |
There was a problem hiding this comment.
The XML docs still says otherwise, <c>null</c>. Why was the return type changed from SpriteFont? to non-null SpriteFont?
There was a problem hiding this comment.
Good catch. I think I removed it too hastily without looking at it too carefully. File should be properly nullable now.
| 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 |
There was a problem hiding this comment.
Blank line between attributes and class, Comments on the same line as the brace
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
Try to not use abbreviations: GetDfParams() -> GetDistanceFieldParams() or something alike. Reads better in callstacks and is apparent what it does at a quick glance
There was a problem hiding this comment.
Fixed after partial class split.
@Ethereal77, not sure if the license is ok here https://github.com/SixLabors/Fonts/blob/main/LICENSE, and how it would affect commercial games. |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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 |
I believe you are right. |
…y forcing warm-up before async.
…agnostic-test Add bounded runtime SDF glyph upload convergence diagnostic
…now uses rasterized preview in asset thumbnails for editor stability/readability.
…classes from the RT-SDF font file.
…ests-for-runtimesigneddistancefieldspri Finalize Runtime SDF diagnostics into focused regression tests
PR Details
This PR adds an experimental runtime MSDF font path to Stride’s sprite font system.
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.
Rasterizer backends
Two managed, cross-platform MSDFGen ports are included (no P/Invoke):
Also included:
Notes / limitations
Testing
Core files:
RuntimeSignedDistanceFieldSpriteFont.csSharpFontOutlineExtractor.csMsdfGenCoreRasterizer.cs(MSDFGen-Sharp backend)RemoraMsdfRasterizer.csThanks. This took a lot longer than I thought it would.
Comparison
Existing Runtime Rasterized Font:


New Runtime MSDF Font:
Related Issue
#2584
Types of changes
Checklist