Skip to content

Commit 7f68c0d

Browse files
authored
feat: Runtime font loading (#2981)
1 parent 1caa854 commit 7f68c0d

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

sources/engine/Stride.Graphics/Font/FontManager.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
33
using System;
44
using System.Collections.Generic;
5+
using System.IO;
56
using System.Threading;
67
using SharpFont;
78
using Stride.Core;
@@ -114,6 +115,45 @@ public void GenerateBitmap(CharacterSpecification characterSpecification, bool s
114115
}
115116
}
116117

118+
/// <summary>
119+
/// Loads a font from the specified file on the file system and adds it to the internal font cache for use with
120+
/// the given font name and style.
121+
/// </summary>
122+
/// <remarks>
123+
/// <para>If a font with the same name and style is already cached, the method does nothing.</para>
124+
/// <para>This method is thread-safe when adding new fonts to the cache.</para>
125+
/// <para><strong>Memory considerations:</strong> The complete font file data is loaded into memory and kept
126+
/// resident for the lifetime of the <see cref="FontManager"/>. Fonts cannot be individually unloaded;
127+
/// they are only released when the <see cref="FontManager"/> is disposed. Consider this when loading
128+
/// large fonts or many font variants, especially on memory-constrained platforms.</para>
129+
/// </remarks>
130+
/// <param name="fontName">The name to associate with the loaded font. This name is used to reference the font in subsequent
131+
/// operations.</param>
132+
/// <param name="filePath">The path to the font file on the file system. The file must exist and be accessible.</param>
133+
/// <param name="style">The style to apply to the loaded font, such as regular, bold, or italic.</param>
134+
/// <exception cref="FileNotFoundException">Thrown if the file specified by <paramref name="filePath"/> does not exist.</exception>
135+
public void LoadFontFromFileSystem(string fontName, string filePath, FontStyle style)
136+
{
137+
var cacheKey = FontHelper.GetFontPath(fontName, style);
138+
139+
// Fast path: Return if the font is already cached (avoids lock contention)
140+
if (cachedFontFaces.ContainsKey(cacheKey))
141+
return;
142+
143+
lock (freetypeLibrary)
144+
{
145+
// Check again inside lock to prevent race condition
146+
if (cachedFontFaces.ContainsKey(cacheKey))
147+
return;
148+
149+
// Load font data into memory
150+
var fontData = File.ReadAllBytes(filePath);
151+
152+
// Create FreeType face and add to cache
153+
cachedFontFaces[cacheKey] = freetypeLibrary.NewMemoryFace(fontData, 0);
154+
}
155+
}
156+
117157
private void GenerateCharacterGlyph(CharacterSpecification character, bool renderBitmap)
118158
{
119159
// first the possible current glyph info

sources/engine/Stride.Graphics/Font/FontSystem.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ public class FontSystem : IFontFactory
2121
internal FontCacheManager FontCacheManager { get; private set; }
2222
internal readonly HashSet<SpriteFont> AllocatedSpriteFonts = new HashSet<SpriteFont>();
2323

24+
/// <summary>
25+
/// Gets the runtime font provider for registering and managing fonts loaded from the file system.
26+
/// </summary>
27+
/// <remarks>
28+
/// <para>Use this to register custom fonts at runtime that are not part of the content pipeline
29+
/// via <see cref="RuntimeFontProvider.RegisterFont"/>.</para>
30+
/// <para>Once registered, fonts can be loaded using the <see cref="LoadRuntimeFont"/> method.</para>
31+
/// </remarks>
32+
public RuntimeFontProvider RuntimeFonts { get; private set; }
33+
2434
/// <summary>
2535
/// Create a new instance of <see cref="FontSystem" /> base on the provided <see cref="Stride.Graphics.GraphicsDevice" />.
2636
/// </summary>
@@ -40,6 +50,23 @@ public void Load(GraphicsDevice graphicsDevice, IDatabaseFileProviderService fil
4050
GraphicsDevice = graphicsDevice;
4151
FontManager = new FontManager(fileProviderService);
4252
FontCacheManager = new FontCacheManager(this);
53+
RuntimeFonts = new RuntimeFontProvider(this);
54+
}
55+
56+
/// <summary>
57+
/// Loads a runtime-registered font by name.
58+
/// This bypasses the content pipeline entirely.
59+
/// </summary>
60+
/// <param name="fontName">The registered font name. If the font is not registered, the method returns <c>null</c>.</param>
61+
/// <param name="defaultSize">The default font size in pixels.</param>
62+
/// <param name="style">The font style.</param>
63+
/// <returns>A <see cref="SpriteFont"/> instance if the font is registered; otherwise, <c>null</c>.</returns>
64+
public SpriteFont? LoadRuntimeFont(string fontName, float defaultSize = 16f, FontStyle style = FontStyle.Regular)
65+
{
66+
if (!RuntimeFonts.IsRegistered(fontName, style))
67+
return null;
68+
69+
return NewDynamic(defaultSize, fontName, style);
4370
}
4471

4572
public void Draw()
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
8+
namespace Stride.Graphics.Font;
9+
10+
/// <summary>
11+
/// Provides runtime registration and loading of fonts from the file system.
12+
/// </summary>
13+
/// <remarks>
14+
/// <para>This provider allows loading custom TrueType fonts (.ttf) at runtime without going through
15+
/// the content pipeline. Fonts registered through this provider are immediately available for use
16+
/// with <see cref="FontSystem.LoadRuntimeFont"/>.</para>
17+
/// <para><strong>Memory Management:</strong> All registered fonts remain in memory for the lifetime
18+
/// of the application. There is no mechanism to unload individual fonts once registered. For applications
19+
/// with dynamic font requirements or memory constraints, consider the total memory footprint of all
20+
/// registered fonts.</para>
21+
/// </remarks>
22+
public class RuntimeFontProvider
23+
{
24+
private readonly FontSystem fontSystem;
25+
private readonly Dictionary<string, RuntimeFontInfo> registeredFonts = [];
26+
27+
internal RuntimeFontProvider(FontSystem fontSystem)
28+
{
29+
this.fontSystem = fontSystem;
30+
}
31+
32+
/// <summary>
33+
/// Registers a font file for runtime loading.
34+
/// </summary>
35+
/// <remarks>
36+
/// <para>Once registered, fonts are loaded into memory and cached for the lifetime of the font system.
37+
/// Individual fonts cannot be unregistered or unloaded - they remain in memory until the application exits
38+
/// or the font system is disposed.</para>
39+
/// <para>Attempting to register the same font name and style with a different file path will throw an exception.</para>
40+
/// </remarks>
41+
/// <param name="fontName">The name to use when loading the font (e.g., "MyFont").</param>
42+
/// <param name="filePath">The absolute or relative path to the .ttf file.</param>
43+
/// <param name="style">The font style.</param>
44+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="fontName"/> is null or empty.</exception>
45+
/// <exception cref="FileNotFoundException">Thrown when the font file does not exist.</exception>
46+
/// <exception cref="InvalidOperationException">Thrown when attempting to register the same font name and style with a different file path.</exception>
47+
public void RegisterFont(string fontName, string filePath, FontStyle style = FontStyle.Regular)
48+
{
49+
ArgumentException.ThrowIfNullOrWhiteSpace(fontName);
50+
51+
if (!File.Exists(filePath))
52+
throw new FileNotFoundException($"Font file not found: {filePath}", filePath);
53+
54+
var key = FontHelper.GetFontPath(fontName, style);
55+
56+
if (registeredFonts.TryGetValue(key, out var existing))
57+
{
58+
if (existing.FilePath == filePath) return;
59+
60+
throw new InvalidOperationException(
61+
$"Font '{fontName}' with style '{style}' is already registered with path '{existing.FilePath}'. " +
62+
$"Cannot register a different path '{filePath}' for the same font name and style.");
63+
}
64+
65+
fontSystem.FontManager.LoadFontFromFileSystem(fontName, filePath, style);
66+
67+
registeredFonts[key] = new RuntimeFontInfo(fontName, filePath, style);
68+
}
69+
70+
/// <summary>
71+
/// Checks if a font is registered for runtime loading.
72+
/// </summary>
73+
/// <param name="fontName">The font name.</param>
74+
/// <param name="style">The font style.</param>
75+
/// <returns>True if registered, false otherwise.</returns>
76+
public bool IsRegistered(string fontName, FontStyle style = FontStyle.Regular)
77+
{
78+
return registeredFonts.ContainsKey(FontHelper.GetFontPath(fontName, style));
79+
}
80+
81+
private record RuntimeFontInfo(string FontName, string FilePath, FontStyle Style);
82+
}

0 commit comments

Comments
 (0)