Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6cdfb28
Implement OpenType and TrueType support
mcendu Oct 31, 2025
3f9dd27
Add helper function for handling variable fonts
mcendu Oct 31, 2025
8229b39
Use TrueType Roboto
mcendu Oct 31, 2025
53c5410
Restore files for testing
mcendu Oct 31, 2025
8e310f4
Update Roboto license
mcendu Oct 31, 2025
635dab5
Change documentation
mcendu Nov 1, 2025
eeb0217
Add helper for handling static outline fonts
mcendu Nov 1, 2025
d6b5dd1
Fix visual discrepancies in test browser
mcendu Nov 1, 2025
d9fe6a4
Fix race condition in LoadFontAsync
mcendu Nov 1, 2025
40eba9e
InspectCode pass
mcendu Nov 1, 2025
c42f765
Bump FreeTypeSharp fork for macOS
mcendu Nov 1, 2025
1c6776a
Bump FreeType bindings again
mcendu Nov 1, 2025
768be1f
Fix FreeType bindings again
mcendu Nov 1, 2025
b76e369
add tests for font variations
mcendu Nov 2, 2025
edeee17
Return null in OutlineFont.GetMetrics when metrics failed to load
mcendu Nov 2, 2025
0326684
Add tests for OutlineFont
mcendu Nov 2, 2025
e672c4c
add a test for OutlineGlyphStore
mcendu Nov 2, 2025
3314c2f
Use raw strings
mcendu Nov 2, 2025
340ed5a
Publicize DecodeFontVariation
mcendu Nov 2, 2025
704f95e
Add outline font benchmarks
mcendu Nov 5, 2025
962c89c
Make self-containedness a field in OGS
mcendu Nov 15, 2025
bf0f60a
Apply CodeFactor suggestions
mcendu Nov 16, 2025
cc73a29
Adjust baseline calculation to be closer to BMFont
mcendu Nov 16, 2025
9a96350
Merge font loading benchmark classes
mcendu Nov 19, 2025
90092b8
Apply fixed scale relative to outline fonts
mcendu Nov 19, 2025
5664391
Do not throw if CodePagesEncodingProvider is not registered
mcendu Nov 23, 2025
e9afb98
Improve documentation
mcendu Nov 23, 2025
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
62 changes: 62 additions & 0 deletions osu.Framework.Benchmarks/BenchmarkOutlineFontLoading.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Diagnostics;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Stores;
using SixLabors.ImageSharp.Memory;

namespace osu.Framework.Benchmarks
{
public class BenchmarkOutlineFontLoading : BenchmarkTest
{
private NamespacedResourceStore<byte[]> baseResources = null!;

public override void SetUp()
{
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator = MemoryAllocator.Default;

baseResources = new NamespacedResourceStore<byte[]>(new DllResourceStore(@"osu.Framework.Benchmarks.dll"), @"Resources");
}

[Params(1, 10, 100, 1000, 10000)]
public int FetchCount;

private const string font_name = @"Fonts/FontAwesome5/FontAwesome-Solid";

[Benchmark(Baseline = true)]
public void BenchmarkNoCache()
{
using (var store = new OutlineGlyphStore(baseResources, font_name))
runFor(store);
}

private void runFor(OutlineGlyphStore store)
{
store.LoadFontAsync().WaitSafely();

var props = typeof(FontAwesome.Solid).GetProperties(BindingFlags.Public | BindingFlags.Static);

int remainingCount = FetchCount;

while (true)
{
foreach (var p in props)
{
object? propValue = p.GetValue(null);
Debug.Assert(propValue != null);

var icon = (IconUsage)propValue;
using (var upload = store.Get(icon.Icon.ToString()))
Trace.Assert(upload.Data != null);

if (remainingCount-- == 0)
return;
}
}
}
}
}
Binary file not shown.
3 changes: 1 addition & 2 deletions osu.Framework.Tests/IO/FontStoreTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#nullable disable

using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
Expand All @@ -22,7 +21,7 @@ public class FontStoreTest
public void OneTimeSetUp()
{
storage = new TemporaryNativeStorage("fontstore-test");
fontResourceStore = new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Drawable).Assembly), "Resources.Fonts.Roboto");
fontResourceStore = new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(FontStoreTest).Assembly), "Resources.Fonts.Roboto");
}

[Test]
Expand Down
3 changes: 1 addition & 2 deletions osu.Framework.Tests/IO/TextureStoreTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
Expand All @@ -18,7 +17,7 @@ public class TextureStoreTest
[OneTimeSetUp]
public void OneTimeSetUp()
{
fontResourceStore = new TextureLoaderStore(new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Drawable).Assembly), "Resources/Fonts"));
fontResourceStore = new TextureLoaderStore(new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(TextureStoreTest).Assembly), "Resources/Fonts"));
}

[Test]
Expand Down
125 changes: 125 additions & 0 deletions osu.Framework.Tests/Text/FontVariationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Text;

namespace osu.Framework.Tests.Text
{
[TestFixture]
public class FontVariationTest
{
private OutlineFont outlineFont = null!;

[OneTimeSetUp]
public void OneTimeSetUp()
{
outlineFont = new OutlineFont(
new NamespacedResourceStore<byte[]>(
new DllResourceStore(typeof(Game).Assembly), @"Resources"
),
"Fonts/Roboto/Roboto"
);

outlineFont.LoadAsync().WaitSafely();
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
outlineFont.Dispose();
}

[Test]
public void TestAxes()
{
var variation = new FontVariation
{
Axes = new Dictionary<string, double>
{
{ @"wght", 123 },
{ @"wdth", 78.5 },
},
};

Assert.AreEqual(variation.GenerateInstanceName(@"Roboto"), @"Roboto_123wght_78.5wdth");

var rawVariation = outlineFont.DecodeFontVariation(variation);
Assert.NotNull(rawVariation);
Assert.NotZero(rawVariation!.Axes.Length);
Assert.Zero(rawVariation.NamedInstance);
}

[Test]
public void TestNamedInstance()
{
var variation = new FontVariation
{
NamedInstance = "Roboto-Regular",
};

Assert.AreEqual(variation.GenerateInstanceName(@"Roboto"), @"Roboto-Regular");
Assert.AreEqual(variation.GenerateInstanceName(@"some other font"), @"Roboto-Regular");

var rawVariation = outlineFont.DecodeFontVariation(variation);
Assert.NotNull(rawVariation);
Assert.Zero(rawVariation!.Axes.Length);
Assert.NotZero(rawVariation.NamedInstance);
}

[Test]
public void TestAxesOverrideNamedInstance()
{
var variation = new FontVariation
{
Axes = new Dictionary<string, double> { { @"wght", 123 } },
NamedInstance = "Roboto-Regular",
};

Assert.AreEqual(variation.GenerateInstanceName(@"Roboto"), @"Roboto_123wght");

var rawVariation = outlineFont.DecodeFontVariation(variation);
Assert.NotNull(rawVariation);
Assert.NotZero(rawVariation!.Axes.Length);
Assert.Zero(rawVariation.NamedInstance);
}

[Test]
public void TestNull()
{
var rawVariation = outlineFont.DecodeFontVariation(null);
Assert.Null(rawVariation);
}

[Test]
public void TestManyAxes()
{
var variation = new FontVariation
{
Axes = new Dictionary<string, double>
{
{ @"wght", 1000 },
{ @"wdth", 50.12345 },
{ @"opsz", 12 },
{ @"GRAD", -100 },
{ @"slnt", -10 },
{ @"XTRA", 456.78901 },
{ @"XOPQ", 34.56789 },
{ @"YOPQ", 56.78901 },
{ @"YTLC", 500.12345 },
{ @"YTUC", 600.12345 },
{ @"YTAS", 700.12345 },
{ @"YTDE", -555.55555 },
{ @"YTFI", 600.12345 },
},
};

const string expected = @"RobotoFlex-F8439096C93C09CF75D5DC6A16A20B99E7A65C932EC1E18731F74415AA77DCDB";
Assert.AreEqual(variation.GenerateInstanceName(@"RobotoFlex"), expected);
}
}
}
87 changes: 87 additions & 0 deletions osu.Framework.Tests/Text/OutlineFontTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Text;

namespace osu.Framework.Tests.Text
{
[TestFixture]
public class OutlineFontTest
{
private OutlineFont outlineFont = null!;
private RawFontVariation? variation;

[OneTimeSetUp]
public void OneTimeSetUp()
{
outlineFont = new OutlineFont(
new NamespacedResourceStore<byte[]>(
new DllResourceStore(typeof(Game).Assembly), @"Resources"
),
@"Fonts/Roboto/Roboto"
);
outlineFont.LoadAsync().WaitSafely();

variation = outlineFont.DecodeFontVariation(new FontVariation
{
NamedInstance = @"Roboto-Regular",
});
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
outlineFont.Dispose();
}

[Test]
public void TestGetGlyphIndex()
{
Assert.NotZero(outlineFont.GetGlyphIndex('A'));
Assert.Zero(outlineFont.GetGlyphIndex('\ufffe'));
}

[Test]
public void TestGetMetrics()
{
uint glyph = outlineFont.GetGlyphIndex('A');
var metrics = outlineFont.GetMetrics(glyph, variation);

Assert.NotNull(metrics);
Assert.AreEqual(metrics!.Character, '\uffff');

Assert.Null(outlineFont.GetMetrics(int.MaxValue, variation));
}

[Test]
public void TestGlyphStores()
{
using (var regular = new OutlineGlyphStore(outlineFont, @"Roboto-Regular"))
using (var bold = new OutlineGlyphStore(outlineFont, @"Roboto-Bold"))
{
var regularA = regular.Get('A');
var boldA = bold.Get('A');
Assert.NotNull(regularA);
Assert.NotNull(boldA);

Assert.AreNotEqual(regularA, boldA);

// the same OutlineGlyphStore should return identical
// metrics for the same character (required as a single
// OutlineFont can be shared by multiple OutlineGlyphStores)
var regularA2 = regular.Get('A');
Assert.NotNull(regularA2);

Assert.AreEqual(regularA!.Character, regularA2!.Character);
Assert.AreEqual(regularA.Baseline, regularA2.Baseline);
Assert.AreEqual(regularA.XOffset, regularA2.XOffset);
Assert.AreEqual(regularA.YOffset, regularA2.YOffset);
Assert.AreEqual(regularA.XAdvance, regularA2.XAdvance);
}
}
}
}
2 changes: 1 addition & 1 deletion osu.Framework.Tests/Text/TextBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class TextBuilderTest
public void SetUp()
{
fontStore = new FontStore(new DummyRenderer(), useAtlas: false);
fontStore.AddTextureSource(new GlyphStore(new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Game).Assembly), @"Resources"), "Fonts/Roboto/Roboto-Regular"));
fontStore.AddTextureSource(new GlyphStore(new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(TextBuilderTest).Assembly), @"Resources"), "Fonts/Roboto/Roboto-Regular"));
fontStore.AddTextureSource(new GlyphStore(new NamespacedResourceStore<byte[]>(new DllResourceStore(typeof(Game).Assembly), @"Resources"), "Fonts/FontAwesome5/FontAwesome-Solid"));

glyphA = fontStore.Get(null, 'a').AsNonNull();
Expand Down
45 changes: 38 additions & 7 deletions osu.Framework/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,16 @@ private void load(FrameworkConfigManager config)
Fonts.AddStore(localFonts = new FontStore(Host.Renderer, useAtlas: false));

// Roboto (FrameworkFont.Regular)
addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-Regular");
addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-RegularItalic");
addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-Bold");
addFont(localFonts, Resources, @"Fonts/Roboto/Roboto-BoldItalic");
var roboto = AddVariableFont(Resources, @"Fonts/Roboto/Roboto", localFonts);
roboto.AddInstance(@"Roboto-Regular");
roboto.AddInstance(@"Roboto-Bold");
var robotoItalic = AddVariableFont(Resources, @"Fonts/Roboto/RobotoItalic", localFonts);
robotoItalic.AddInstance(@"Roboto-Italic", @"Roboto-RegularItalic");
robotoItalic.AddInstance(@"Roboto-BoldItalic");

// RobotoCondensed (FrameworkFont.Condensed)
addFont(localFonts, Resources, @"Fonts/RobotoCondensed/RobotoCondensed-Regular");
addFont(localFonts, Resources, @"Fonts/RobotoCondensed/RobotoCondensed-Bold");
roboto.AddInstance(@"Roboto-CondensedRegular", @"RobotoCondensed-Regular");
roboto.AddInstance(@"Roboto-CondensedBold", @"RobotoCondensed-Bold");

addFont(Fonts, Resources, @"Fonts/FontAwesome5/FontAwesome-Solid");
addFont(Fonts, Resources, @"Fonts/FontAwesome5/FontAwesome-Regular");
Expand Down Expand Up @@ -257,7 +259,7 @@ private void load(FrameworkConfigManager config)
protected virtual OnlineStore CreateOnlineStore() => new OnlineStore();

/// <summary>
/// Add a font to be globally accessible to the game.
/// Add a bitmap font to be globally accessible to the game.
/// </summary>
/// <param name="store">The backing store with font resources.</param>
/// <param name="assetName">The base name of the font.</param>
Expand All @@ -268,6 +270,35 @@ public void AddFont(ResourceStore<byte[]> store, string assetName = null, FontSt
private void addFont(FontStore target, ResourceStore<byte[]> store, string assetName = null)
=> target.AddTextureSource(new RawCachingGlyphStore(store, assetName, Host.CreateTextureLoaderStore(store)));

/// <summary>
/// Add an outline font to be globally accessible to the game.
/// </summary>
/// <param name="store">The backing store with font resources.</param>
/// <param name="assetName">The base name of the font.</param>
/// <param name="target">An optional target store to add the font to. If not specified, <see cref="Fonts"/> is used.</param>
/// <returns>The newly added font family from which fonts can be instantiated.</returns>
public void AddOutlineFont(ResourceStore<byte[]> store, string assetName, FontStore target = null)
=> (target ?? Fonts).AddTextureSource(new OutlineGlyphStore(store, assetName));

/// <summary>
/// Add a variable font to be globally accessible to the game.
/// </summary>
/// <remarks>
/// This does not instantiate any glyph stores. Use
/// <see cref="OutlineFontStore.AddInstance(string, string?)"/>
/// on the returned font store to make the font usable.
/// </remarks>
/// <param name="store">The backing store with font resources.</param>
/// <param name="assetName">The base name of the font.</param>
/// <param name="target">An optional target store to add the font to. If not specified, <see cref="Fonts"/> is used.</param>
/// <returns>The newly added font family from which fonts can be instantiated.</returns>
public OutlineFontStore AddVariableFont(ResourceStore<byte[]> store, string assetName, FontStore target = null)
{
var nestedStore = new OutlineFontStore(Host.Renderer, store, assetName);
(target ?? Fonts).AddStore(nestedStore);
return nestedStore;
}

protected override void LoadComplete()
{
base.LoadComplete();
Expand Down
Loading
Loading