Skip to content
Draft
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d49b9ff
M1: Added Runtime SDF placeholder.
yuechen-li-dev Jan 29, 2026
3275c16
M2. MSDF Cache Scaffold.
yuechen-li-dev Jan 29, 2026
5d9185f
M3: Font works end to end, renders ugly circles.
yuechen-li-dev Jan 29, 2026
75f5369
M4 WIP: Buggy implementation with flickers so far.
yuechen-li-dev Jan 29, 2026
1fa1cd4
It works, but each text spins computer like jet engine.
yuechen-li-dev Jan 29, 2026
2f0b165
Async implementation and various optimizations.
yuechen-li-dev Jan 29, 2026
dd4f8e0
Fixed bad copypaste for SpriteFontAssetCompiler.cs
yuechen-li-dev Jan 30, 2026
0dc668c
Less sloppy Async.
yuechen-li-dev Jan 31, 2026
69e2f15
Revert "Less sloppy Async."
yuechen-li-dev Jan 31, 2026
7ad4caa
Remove runtime SDF bake size plumbing
yuechen-li-dev Feb 1, 2026
b04c959
Merge pull request #4 from yuechen-li-dev/codex/refactor-glyph-sizing…
yuechen-li-dev Feb 1, 2026
d5d9576
Adjusted default bakesize to 64 so SDF is high quality.
yuechen-li-dev Feb 1, 2026
18e227d
Merge branch 'stride3d:master' into Runtime-SDF-Font
yuechen-li-dev Feb 2, 2026
217a8fd
Changed to channel based async design.
yuechen-li-dev Feb 2, 2026
e2e9f09
Channel based refactor for async, introduce interface for easier libr…
yuechen-li-dev Feb 2, 2026
ae2e234
See above message. Wrong commit lol.
yuechen-li-dev Feb 2, 2026
a0058c4
Merge branch 'Runtime-SDF-Font' of https://github.com/yuechen-li-dev/…
yuechen-li-dev Feb 2, 2026
914819d
refactor for future MSDFGeneration.
yuechen-li-dev Feb 3, 2026
048b521
Initial wiring. Need to fix generationPipeline.
yuechen-li-dev Feb 3, 2026
d7d8d5b
Ok, it renders but only as blocks or dots. Debug time.
yuechen-li-dev Feb 3, 2026
dbba408
It works! Mostly. Need to squash some font specific bugs.
yuechen-li-dev Feb 4, 2026
a22eb17
Next try with MsdfGen too.
yuechen-li-dev Feb 4, 2026
e455a49
Comment edits.
yuechen-li-dev Feb 4, 2026
cc65561
clean up gitignore of temp file.
yuechen-li-dev Feb 4, 2026
4d9159c
Move offset logic out of ApplyUploadedGlyph for safety. Change glyphk…
yuechen-li-dev Feb 4, 2026
dd6b53a
Minor changes so VS would have less messages.
yuechen-li-dev Feb 4, 2026
738280e
Cleaned up nullable in FontSystem.
yuechen-li-dev Feb 9, 2026
92e5460
removed unsafe keyword and unneed cast for buffer copy.
yuechen-li-dev Feb 9, 2026
739c69e
Spacing consistancy in pregenerated glyph method.
yuechen-li-dev Feb 9, 2026
0bea15f
Changed the font manager to private.
yuechen-li-dev Feb 9, 2026
6a98305
Remove extra colon.
yuechen-li-dev Feb 9, 2026
a457fd6
Remerged MSDF method into FontManager. Remerged unused overload. Gene…
yuechen-li-dev Feb 10, 2026
3463072
Changed warning from scaffolding to indicate experimental feature. Si…
yuechen-li-dev Feb 10, 2026
113d16d
Moved comment to more accurately depict pipeline for upload step.
yuechen-li-dev Feb 10, 2026
f2930a2
Small refactor for EnsureSdfScheduled to address 0 dimension glyphs a…
yuechen-li-dev Feb 10, 2026
3176a76
Removed debug message from FontCacheManagerMSDF and cleanup for reada…
yuechen-li-dev Feb 10, 2026
dcfae4b
Move oversized glyph dimension check from FontCacheManager to Font fi…
yuechen-li-dev Feb 10, 2026
f8aadd1
Re-added accidentally deleted packer logic.
yuechen-li-dev Feb 10, 2026
0753019
remove unused bool isClosed definition from outline extractor.
yuechen-li-dev Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion sources/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
<PackageVersion Include="Microsoft.Management.Infrastructure" Version="3.0.0-preview.4" />
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="7.0.4" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="MSDF-Sharp.Core" Version="1.0.2" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="Remora.MSDFGen" Version="1.0.0" />
<PackageVersion Include="ServiceWire" Version="5.6.0" />
<PackageVersion Include="SharpDX" Version="4.2.0" />
<PackageVersion Include="SharpDX.D3DCompiler" Version="4.2.0" />
Expand Down Expand Up @@ -123,4 +125,4 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.

using System.ComponentModel;
using Stride.Core;
using Stride.Core.Annotations;
using Stride.Core.Mathematics;

namespace Stride.Assets.SpriteFont
{
[DataContract("RuntimeSignedDistanceFieldSpriteFontType")]
[Display("Runtime SDF")]
public class RuntimeSignedDistanceFieldSpriteFontType : SpriteFontTypeBase
{
/// <inheritdoc/>
[DataMember(30)]
[DataMemberRange(MathUtil.ZeroTolerance, 2)]
[DefaultValue(20)]
[Display("Default Size")]
public override float Size { get; set; } = 64;

/// <summary>
/// Distance field range/spread (in pixels) used during MSDF generation.
/// </summary>
[DataMember(40)]
[DefaultValue(8)]
[DataMemberRange(1, 64, 1, 4, 0)]
[Display("Pixel Range")]
public int PixelRange { get; set; } = 8;

/// <summary>
/// Extra padding around each glyph inside the atlas (in pixels).
/// </summary>
[DataMember(50)]
[DefaultValue(2)]
[DataMemberRange(0, 16, 1, 2, 0)]
[Display("Padding")]
public int Padding { get; set; } = 2;
}
}
110 changes: 80 additions & 30 deletions sources/engine/Stride.Assets/SpriteFont/SpriteFontAssetCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,39 +40,55 @@ protected override void Prepare(AssetCompilerContext context, AssetItem assetIte
result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new SignedDistanceFieldFontCommand(targetUrlInStorage, assetClone, assetItem.Package));
}
else
if (asset.FontType is RuntimeRasterizedSpriteFontType)
else if (asset.FontType is RuntimeRasterizedSpriteFontType)
{
UFile fontPathOnDisk = asset.FontSource.GetFontPath(result);
if (fontPathOnDisk == null)
{
UFile fontPathOnDisk = asset.FontSource.GetFontPath(result);
if (fontPathOnDisk == null)
{
result.Error($"Runtime rasterized font compilation failed. Font {asset.FontSource.GetFontName()} was not found on this machine.");
result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new FailedFontCommand());
return;
}

var fontImportLocation = FontHelper.GetFontPath(asset.FontSource.GetFontName(), asset.FontSource.Style);

result.Error($"Runtime rasterized font compilation failed. Font {asset.FontSource.GetFontName()} was not found on this machine.");
result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new ImportStreamCommand { SourcePath = fontPathOnDisk, Location = fontImportLocation });
result.BuildSteps.Add(new RuntimeRasterizedFontCommand(targetUrlInStorage, asset, assetItem.Package));
result.BuildSteps.Add(new FailedFontCommand());
return;
}
else
{
var fontTypeStatic = asset.FontType as OfflineRasterizedSpriteFontType;
if (fontTypeStatic == null)
throw new ArgumentException("Tried to compile a non-offline rasterized sprite font with the compiler for offline resterized fonts!");

// copy the asset and transform the source and character set file path to absolute paths
var assetClone = AssetCloner.Clone(asset);
var assetDirectory = assetAbsolutePath.GetParent();
assetClone.FontSource = asset.FontSource;
fontTypeStatic.CharacterSet = !string.IsNullOrEmpty(fontTypeStatic.CharacterSet) ? UPath.Combine(assetDirectory, fontTypeStatic.CharacterSet): null;
var fontImportLocation = FontHelper.GetFontPath(asset.FontSource.GetFontName(), asset.FontSource.Style);

result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new ImportStreamCommand { SourcePath = fontPathOnDisk, Location = fontImportLocation });
result.BuildSteps.Add(new RuntimeRasterizedFontCommand(targetUrlInStorage, asset, assetItem.Package));
}
else if (asset.FontType is RuntimeSignedDistanceFieldSpriteFontType)
{
UFile fontPathOnDisk = asset.FontSource.GetFontPath(result);
if (fontPathOnDisk == null)
{
result.Error($"Runtime SDF font compilation failed. Font {asset.FontSource.GetFontName()} was not found on this machine.");
result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new OfflineRasterizedFontCommand(targetUrlInStorage, assetClone, colorSpace, assetItem.Package));
result.BuildSteps.Add(new FailedFontCommand());
return;
}

var fontImportLocation = FontHelper.GetFontPath(asset.FontSource.GetFontName(), asset.FontSource.Style);

result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new ImportStreamCommand { SourcePath = fontPathOnDisk, Location = fontImportLocation });
result.BuildSteps.Add(new RuntimeSignedDistanceFieldFontCommand(targetUrlInStorage, asset, assetItem.Package));
}
else
{
var fontTypeStatic = asset.FontType as OfflineRasterizedSpriteFontType;
if (fontTypeStatic == null)
throw new ArgumentException("Tried to compile a non-offline rasterized sprite font with the compiler for offline resterized fonts!");

// copy the asset and transform the source and character set file path to absolute paths
var assetClone = AssetCloner.Clone(asset);
var assetDirectory = assetAbsolutePath.GetParent();
assetClone.FontSource = asset.FontSource;
fontTypeStatic.CharacterSet = !string.IsNullOrEmpty(fontTypeStatic.CharacterSet) ? UPath.Combine(assetDirectory, fontTypeStatic.CharacterSet) : null;

result.BuildSteps = new AssetBuildStep(assetItem);
result.BuildSteps.Add(new OfflineRasterizedFontCommand(targetUrlInStorage, assetClone, colorSpace, assetItem.Package));
}
}

internal class OfflineRasterizedFontCommand : AssetCommand<SpriteFontAsset>
Expand Down Expand Up @@ -118,7 +134,7 @@ protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandC
{
staticFont = OfflineRasterizedFontCompiler.Compile(FontDataFactory, Parameters, colorspace == ColorSpace.Linear);
}
catch (FontNotFoundException ex)
catch (FontNotFoundException ex)
{
commandContext.Logger.Error($"Font [{ex.FontName}] was not found on this machine.", ex);
return Task.FromResult(ResultStatus.Failed);
Expand Down Expand Up @@ -190,9 +206,9 @@ public RuntimeRasterizedFontCommand(string url, SpriteFontAsset description, IAs
protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandContext)
{
var dynamicFont = FontDataFactory.NewDynamic(
Parameters.FontType.Size, Parameters.FontSource.GetFontName(), Parameters.FontSource.Style,
Parameters.FontType.AntiAlias, useKerning:false, extraSpacing:Parameters.Spacing, extraLineSpacing:Parameters.LineSpacing,
defaultCharacter:Parameters.DefaultCharacter);
Parameters.FontType.Size, Parameters.FontSource.GetFontName(), Parameters.FontSource.Style,
Parameters.FontType.AntiAlias, useKerning: false, extraSpacing: Parameters.Spacing, extraLineSpacing: Parameters.LineSpacing,
defaultCharacter: Parameters.DefaultCharacter);

var assetManager = new ContentManager(MicrothreadLocalDatabases.ProviderService);
assetManager.Save(Url, dynamicFont);
Expand All @@ -201,6 +217,40 @@ protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandC
}
}

internal class RuntimeSignedDistanceFieldFontCommand : AssetCommand<SpriteFontAsset>
{
public RuntimeSignedDistanceFieldFontCommand(string url, SpriteFontAsset description, IAssetFinder assetFinder)
: base(url, description, assetFinder)
{
}

protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandContext)
{
// 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.


var runtimeSdfType = (RuntimeSignedDistanceFieldSpriteFontType)Parameters.FontType;

var sdfFont = FontDataFactory.NewRuntimeSignedDistanceField(
runtimeSdfType.Size,
Parameters.FontSource.GetFontName(),
Parameters.FontSource.Style,
runtimeSdfType.PixelRange,
runtimeSdfType.Padding,
useKerning: false,
Copy link
Copy Markdown
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
Copy Markdown
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.

extraSpacing: Parameters.Spacing,
extraLineSpacing: Parameters.LineSpacing,
defaultCharacter: Parameters.DefaultCharacter);

var assetManager = new ContentManager(MicrothreadLocalDatabases.ProviderService);
assetManager.Save(Url, sdfFont);

return Task.FromResult(ResultStatus.Successful);
}
}

/// <summary>
/// Proxy command which always fails, called when font is compiled with the wrong assets
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static SpriteFontAsset Create()
FontSource = new SystemFontProvider(),
FontType = new OfflineRasterizedSpriteFontType()
{
CharacterRegions = { new CharacterRegion(' ', (char)127) }
CharacterRegions = { new CharacterRegion(' ', (char)127) }
},
};
}
Expand Down Expand Up @@ -42,7 +42,7 @@ public override SpriteFontAsset New()
}
}

public class SignedDistanceFieldSpriteFontFactory: AssetFactory<SpriteFontAsset>
public class SignedDistanceFieldSpriteFontFactory : AssetFactory<SpriteFontAsset>
{
public static SpriteFontAsset Create()
{
Expand All @@ -61,4 +61,21 @@ public override SpriteFontAsset New()
return Create();
}
}

public class RuntimeSignedDistanceFieldSpriteFontFactory : AssetFactory<SpriteFontAsset>
{
public static SpriteFontAsset Create()
{
return new SpriteFontAsset
{
FontSource = new SystemFontProvider(),
FontType = new RuntimeSignedDistanceFieldSpriteFontType(),
};
}

public override SpriteFontAsset New()
{
return Create();
}
}
}
135 changes: 135 additions & 0 deletions sources/engine/Stride.Graphics/Font/CharacterBitmapRgba.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.

using System;
using System.Data;
using Stride.Core;

namespace Stride.Graphics.Font
{
/// <summary>
/// An RGBA bitmap representing a glyph (4 bytes per pixel).
/// Intended for runtime MSDF font (stored in RGB, alpha optional).
/// </summary>
internal sealed class CharacterBitmapRgba : IDisposable
{
private readonly int width;
private readonly int rows;
private readonly int pitch;
private readonly IntPtr buffer;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This doesn't have to be an IntPtr, or have the class as disposable.
You should replace this with a Color[], the only non native usage is in CommandList.UpdateSubResource which has two signature for ReadOnlySpan<T>

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 made another branch to use Color[] without the dispose, however, it doesn't seem to display anything. This one is probably one of these changes that requires a lot of debugging and I suggest that we leave it until later.


private bool disposed;

/// <summary>
/// Initializes a null bitmap.
/// </summary>
public CharacterBitmapRgba()
{
}

/// <summary>
/// Allocates an RGBA bitmap (uninitialized).
/// </summary>
public CharacterBitmapRgba(int width, int rows)
{
if (width < 0) throw new ArgumentOutOfRangeException(nameof(width));
if (rows < 0) throw new ArgumentOutOfRangeException(nameof(rows));

this.width = width;
this.rows = rows;
pitch = checked(width * 4);

if (width != 0 && rows != 0)
{
buffer = MemoryUtilities.Allocate(checked(pitch * rows), 1);
}
}

/// <summary>
/// Allocates an RGBA bitmap and copies data from a source buffer with the given pitch.
/// </summary>
public unsafe CharacterBitmapRgba(IntPtr srcRgba, int width, int rows, int srcPitchBytes)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unused

: this(width, rows)
{
if (srcRgba == IntPtr.Zero && (width != 0 || rows != 0))
throw new ArgumentNullException(nameof(srcRgba));
if (srcPitchBytes < 0) throw new ArgumentOutOfRangeException(nameof(srcPitchBytes));

if (buffer == IntPtr.Zero)
return;

var src = (byte*)srcRgba;
var dst = (byte*)buffer;

// Copy row-by-row to handle pitch differences.
var copyBytesPerRow = Math.Min(srcPitchBytes, pitch);
for (int y = 0; y < rows; y++)
{
var srcRow = src + y * srcPitchBytes;
var dstRow = dst + y * pitch;

MemoryUtilities.CopyWithAlignmentFallback(dstRow, srcRow, (uint)copyBytesPerRow);

if (copyBytesPerRow < pitch)
{
MemoryUtilities.Clear(dstRow + copyBytesPerRow, (uint)(pitch - copyBytesPerRow));
}
}
}

public bool IsDisposed => disposed;

public int Width
{
get
{
ThrowIfDisposed();
return width;
}
}

public int Rows
{
get
{
ThrowIfDisposed();
return rows;
}
}

public int Pitch
{
get
{
ThrowIfDisposed();
return pitch;
}
}

public IntPtr Buffer
{
get
{
ThrowIfDisposed();
return buffer;
}
}

public void Dispose()
{
if (disposed)
return;

if (buffer != IntPtr.Zero)
MemoryUtilities.Free(buffer);

disposed = true;
}

private void ThrowIfDisposed()
{
if (disposed)
throw new ObjectDisposedException(nameof(CharacterBitmapRgba), "Cannot access a disposed object.");
}
}
}
Loading