System font API (#5393)

* System font API

This is a new API that allows operating system fonts to be loaded by the engine and used by content.

Fonts are provided in a flat list exposing all the relevant metadata. They are loaded from disk with a Load call.

Initial implementation is only for Windows DirectWrite.

* Load system fonts as memory mapped files if possible.

This allows sharing the font file memory with other processes which is always good.

* Use ArrayPool to reduce char array allocations

* Disable verbose logging

* Implement system font support on Linux via Fontconfig

* Implement macOS support

* Add "FREEDESKTOP" define constant

This is basically LINUX || FREEBSD. Though FreeBSD currently gets detected as LINUX too. Oh well.

* Compile out Fontconfig and CoreText system font backends when not on those platforms

* Don't add Fontconfig package dep on Mac/Windows

* Allow disabling system font support via CVar

Cuz why not.
This commit is contained in:
Pieter-Jan Briers
2025-10-28 22:07:55 +01:00
committed by GitHub
parent 8b7fbfa646
commit f3a3f564e1
24 changed files with 1841 additions and 12 deletions

View File

@@ -62,6 +62,7 @@
<PackageVersion Include="SpaceWizards.Sdl" Version="1.0.0" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.1.0" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="SpaceWizards.Fontconfig.Interop" Version="1.0.0" />
<PackageVersion Include="libsodium" Version="1.0.20.1" />
<PackageVersion Include="System.Management" Version="9.0.8" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

@@ -13,7 +13,7 @@
</When>
<Otherwise>
<PropertyGroup>
<DefineConstants>$(DefineConstants);LINUX;UNIX</DefineConstants>
<DefineConstants>$(DefineConstants);LINUX;UNIX;FREEDESKTOP</DefineConstants>
</PropertyGroup>
</Otherwise>
</Choose>

View File

@@ -31,5 +31,6 @@
<Python>python3</Python>
<Python Condition="'$(ActualOS)' == 'Windows'">py -3</Python>
<UseSystemSqlite Condition="'$(TargetOS)' == 'FreeBSD'">True</UseSystemSqlite>
<IsFreedesktop Condition="'$(TargetOS)' == 'FreeBSD' Or '$(TargetOS)' == 'Linux'">True</IsFreedesktop>
</PropertyGroup>
</Project>

View File

@@ -47,6 +47,9 @@ END TEMPLATE-->
* Sandbox:
* Exposed `System.Reflection.Metadata.MetadataUpdateHandlerAttribute`.
* Exposed more overloads on `StringBuilder`.
* The engine can now load system fonts.
* At the moment only available on Windows.
* See `ISystemFontManager` for API.
### Bugfixes

View File

@@ -8,6 +8,7 @@ using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.Graphics.FontManagement;
using Robust.Client.HWId;
using Robust.Client.Input;
using Robust.Client.Localization;
@@ -121,6 +122,8 @@ namespace Robust.Client
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
deps.Register<IUriOpener, UriOpenerDummy>();
deps.Register<ISystemFontManager, SystemFontManagerFallback>();
deps.Register<ISystemFontManagerInternal, SystemFontManagerFallback>();
break;
case GameController.DisplayMode.Clyde:
deps.Register<IClyde, Clyde>();
@@ -131,6 +134,8 @@ namespace Robust.Client
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();
deps.Register<IUriOpener, UriOpener>();
deps.Register<ISystemFontManager, SystemFontManager>();
deps.Register<ISystemFontManagerInternal, SystemFontManager>();
break;
default:
throw new ArgumentOutOfRangeException();

View File

@@ -96,6 +96,7 @@ namespace Robust.Client
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
private IWebViewManagerHook? _webViewHook;
@@ -143,6 +144,7 @@ namespace Robust.Client
_taskManager.Initialize();
_parallelMgr.Initialize();
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
_systemFontManager.Initialize();
// Load optional Robust modules.
LoadOptionalRobustModules(displayMode, _resourceManifest!);

View File

@@ -104,6 +104,12 @@ namespace Robust.Client.Graphics
Handle = IoCManager.Resolve<IFontManagerInternal>().MakeInstance(res.FontFaceHandle, size);
}
internal VectorFont(IFontInstanceHandle handle, int size)
{
Size = size;
Handle = handle;
}
public override int GetAscent(float scale) => Handle.GetAscent(scale);
public override int GetHeight(float scale) => Handle.GetHeight(scale);
public override int GetDescent(float scale) => Handle.GetDescent(scale);
@@ -222,4 +228,74 @@ namespace Robust.Client.Graphics
return null;
}
}
/// <summary>
/// Possible values for font weights. Larger values have thicker font strokes.
/// </summary>
/// <remarks>
/// <para>
/// These values are based on the <c>usWeightClass</c> property of the OpenType specification:
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass
/// </para>
/// </remarks>
/// <seealso cref="ISystemFontFace.Weight"/>
public enum FontWeight : ushort
{
Thin = 100,
ExtraLight = 200,
UltraLight = ExtraLight,
Light = 300,
SemiLight = 350,
Normal = 400,
Regular = Normal,
Medium = 500,
SemiBold = 600,
DemiBold = SemiBold,
Bold = 700,
ExtraBold = 800,
UltraBold = ExtraBold,
Black = 900,
Heavy = Black,
ExtraBlack = 950,
UltraBlack = ExtraBlack,
}
/// <summary>
/// Possible slant values for fonts.
/// </summary>
/// <seealso cref="ISystemFontFace.Slant"/>
public enum FontSlant : byte
{
// NOTE: Enum values correspond to DWRITE_FONT_STYLE.
Normal = 0,
Oblique = 1,
// FUN FACT: they're called "italics" because they look like the Leaning Tower of Pisa.
// Don't fact-check that.
Italic = 2
}
/// <summary>
/// Possible values for font widths. Larger values are proportionally wider.
/// </summary>
/// <remarks>
/// <para>
/// These values are based on the <c>usWidthClass</c> property of the OpenType specification:
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
/// </para>
/// </remarks>
/// <seealso cref="ISystemFontFace.Width"/>
public enum FontWidth : ushort
{
UltraCondensed = 1,
ExtraCondensed = 2,
Condensed = 3,
SemiCondensed = 4,
Normal = 5,
Medium = Normal,
SemiExpanded = 6,
Expanded = 7,
ExtraExpanded = 8,
UltraExpanded = 9,
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Console;
namespace Robust.Client.Graphics.FontManagement;
internal sealed class SystemFontDebugCommand : IConsoleCommand
{
public string Command => "system_font_debug";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
new SystemFontDebugWindow().OpenCentered();
}
}

View File

@@ -0,0 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="System font debug">
<SplitContainer Orientation="Horizontal" MinSize="800 600">
<ScrollContainer HScrollEnabled="False">
<BoxContainer Name="SelectorContainer" Orientation="Vertical" />
</ScrollContainer>
<ScrollContainer HScrollEnabled="False">
<BoxContainer Orientation="Vertical">
<Label Name="FamilyLabel" />
<BoxContainer Orientation="Vertical" Name="FaceContainer" />
</BoxContainer>
</ScrollContainer>
</SplitContainer>
</DefaultWindow>

View File

@@ -0,0 +1,98 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.FontManagement;
[GenerateTypedNameReferences]
internal sealed partial class SystemFontDebugWindow : DefaultWindow
{
private static readonly int[] ExampleFontSizes = [8, 12, 16, 24, 36];
private const string ExampleString = "The quick brown fox jumps over the lazy dog";
[Dependency] private readonly ISystemFontManager _systemFontManager = default!;
public SystemFontDebugWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
var buttonGroup = new ButtonGroup();
foreach (var group in _systemFontManager.SystemFontFaces.GroupBy(k => k.FamilyName).OrderBy(k => k.Key))
{
var fonts = group.ToArray();
SelectorContainer.AddChild(new Selector(this, buttonGroup, group.Key, fonts));
}
}
private void SelectFontFamily(ISystemFontFace[] fonts)
{
FamilyLabel.Text = fonts[0].FamilyName;
FaceContainer.RemoveAllChildren();
foreach (var font in fonts)
{
var exampleContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Margin = new Thickness(8)
};
foreach (var size in ExampleFontSizes)
{
var fontInstance = font.Load(size);
var richTextLabel = new RichTextLabel
{
Stylesheet = new Stylesheet([
StylesheetHelpers.Element<RichTextLabel>().Prop("font", fontInstance)
]),
};
richTextLabel.SetMessage(FormattedMessage.FromUnformatted(ExampleString));
exampleContainer.AddChild(richTextLabel);
}
FaceContainer.AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
new RichTextLabel
{
Text = $"""
{font.FullName}
Family: "{font.FamilyName}", face: "{font.FaceName}", PostScript = "{font.PostscriptName}"
Weight: {font.Weight} ({(int) font.Weight}), slant: {font.Slant} ({(int) font.Slant}), width: {font.Width} ({(int) font.Width})
""",
},
exampleContainer
},
Margin = new Thickness(0, 0, 0, 8)
});
}
}
private sealed class Selector : Control
{
public Selector(SystemFontDebugWindow window, ButtonGroup group, string family, ISystemFontFace[] fonts)
{
var button = new Button
{
Text = family,
Group = group,
ToggleMode = true
};
AddChild(button);
button.OnPressed += _ => window.SelectFontFamily(fonts);
}
}
}

View File

@@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.FontManagement;
internal abstract class SystemFontManagerBase
{
/// <summary>
/// The "standard" locale used when looking up the PostScript name of a font face.
/// </summary>
/// <remarks>
/// <para>
/// Font files allow the PostScript name to be localized, however in practice
/// we would really like to have a language-unambiguous identifier to refer to a font file.
/// We use this locale (en-US) to look up teh PostScript font name, if there are multiple provided.
/// This matches the behavior of the Local Font Access web API:
/// https://wicg.github.io/local-font-access/#concept-font-representation
/// </para>
/// </remarks>
protected static readonly CultureInfo StandardLocale = new("en-US", false);
protected readonly IFontManagerInternal FontManager;
protected readonly ISawmill Sawmill;
protected readonly Lock Lock = new();
protected readonly List<BaseHandle> Fonts = [];
public IEnumerable<ISystemFontFace> SystemFontFaces { get; }
public SystemFontManagerBase(ILogManager logManager, IFontManagerInternal fontManager)
{
FontManager = fontManager;
Sawmill = logManager.GetSawmill("font.system");
SystemFontFaces = Fonts.AsReadOnly();
}
protected abstract IFontFaceHandle LoadFontFace(BaseHandle handle);
protected static string GetLocalizedForLocaleOrFirst(LocalizedStringSet set, CultureInfo culture)
{
var matchCulture = culture;
while (!Equals(matchCulture, CultureInfo.InvariantCulture))
{
if (set.Values.TryGetValue(culture.Name, out var value))
return value;
matchCulture = matchCulture.Parent;
}
return set.Values[set.Primary];
}
protected abstract class BaseHandle(SystemFontManagerBase parent) : ISystemFontFace
{
private IFontFaceHandle? _cachedFont;
public required string PostscriptName { get; init; }
public required LocalizedStringSet FullNames;
public required LocalizedStringSet FamilyNames;
public required LocalizedStringSet FaceNames;
public required FontWeight Weight { get; init; }
public required FontSlant Slant { get; init; }
public required FontWidth Width { get; init; }
public string FullName => GetLocalizedFullName(CultureInfo.CurrentCulture);
public string FamilyName => GetLocalizedFamilyName(CultureInfo.CurrentCulture);
public string FaceName => GetLocalizedFaceName(CultureInfo.CurrentCulture);
public string GetLocalizedFullName(CultureInfo culture)
{
return GetLocalizedForLocaleOrFirst(FullNames, culture);
}
public string GetLocalizedFamilyName(CultureInfo culture)
{
return GetLocalizedForLocaleOrFirst(FamilyNames, culture);
}
public string GetLocalizedFaceName(CultureInfo culture)
{
return GetLocalizedForLocaleOrFirst(FaceNames, culture);
}
public Font Load(int size)
{
var handle = GetFaceHandle();
var instance = parent.FontManager.MakeInstance(handle, size);
return new VectorFont(instance, size);
}
private IFontFaceHandle GetFaceHandle()
{
lock (parent.Lock)
{
if (_cachedFont != null)
return _cachedFont;
parent.Sawmill.Verbose($"Loading system font face: {PostscriptName}");
return _cachedFont = parent.LoadFontFace(this);
}
}
}
protected struct LocalizedStringSet
{
public static readonly LocalizedStringSet Empty = FromSingle("");
/// <summary>
/// The first locale to appear in the list of localized strings.
/// Used as fallback if the desired locale is not provided.
/// </summary>
public required string Primary;
public required Dictionary<string, string> Values;
public static LocalizedStringSet FromSingle(string value, string language = "en")
{
return new LocalizedStringSet
{
Primary = language,
Values = new Dictionary<string, string> { { language, value } }
};
}
}
protected sealed class MemoryMappedFontMemoryHandle : IFontMemoryHandle
{
private readonly MemoryMappedFile _mappedFile;
private readonly MemoryMappedViewAccessor _accessor;
public MemoryMappedFontMemoryHandle(string filePath)
{
_mappedFile = MemoryMappedFile.CreateFromFile(
filePath,
FileMode.Open,
null,
0,
MemoryMappedFileAccess.Read);
_accessor = _mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
}
public unsafe byte* GetData()
{
byte* pointer = null;
_accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
return pointer;
}
public nint GetDataSize()
{
return (nint)_accessor.Capacity;
}
public void Dispose()
{
_accessor.Dispose();
_mappedFile.Dispose();
}
}
}

View File

@@ -0,0 +1,195 @@
#if MACOS
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Robust.Client.Interop.MacOS;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using CF = Robust.Client.Interop.MacOS.CoreFoundation;
using CT = Robust.Client.Interop.MacOS.CoreText;
namespace Robust.Client.Graphics.FontManagement;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that uses CoreText on macOS.
/// </summary>
internal sealed class SystemFontManagerCoreText : SystemFontManagerBase, ISystemFontManagerInternal
{
private static readonly FontWidth[] FontWidths = Enum.GetValues<FontWidth>();
public bool IsSupported => true;
public SystemFontManagerCoreText(ILogManager logManager, IFontManagerInternal fontManager) : base(logManager,
fontManager)
{
}
public unsafe void Initialize()
{
Sawmill.Verbose("Getting CTFontCollection...");
var collection = CT.CTFontCollectionCreateFromAvailableFonts(null);
var array = CT.CTFontCollectionCreateMatchingFontDescriptors(collection);
var count = CF.CFArrayGetCount(array);
Sawmill.Verbose($"Have {count} descriptors...");
for (nint i = 0; i < count.Value; i++)
{
var item = (__CTFontDescriptor*)CF.CFRetain(CF.CFArrayGetValueAtIndex(array, new CLong(i)));
try
{
LoadFontDescriptor(item);
}
catch (Exception ex)
{
Sawmill.Error($"Failed to load font descriptor: {ex}");
}
finally
{
CF.CFRelease(item);
}
}
CF.CFRelease(array);
CF.CFRelease(collection);
}
private unsafe void LoadFontDescriptor(__CTFontDescriptor* descriptor)
{
var displayName = GetFontAttributeManaged(descriptor, CT.kCTFontDisplayNameAttribute);
var postscriptName = GetFontAttributeManaged(descriptor, CT.kCTFontNameAttribute);
var familyName = GetFontAttributeManaged(descriptor, CT.kCTFontFamilyNameAttribute);
var styleName = GetFontAttributeManaged(descriptor, CT.kCTFontStyleNameAttribute);
var url = (__CFURL*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontURLAttribute);
const int maxPath = 1024;
var buf = stackalloc byte[maxPath];
var result = CF.CFURLGetFileSystemRepresentation(url, 1, buf, new CLong(maxPath));
if (result == 0)
throw new Exception("CFURLGetFileSystemRepresentation failed!");
// Sawmill.Verbose(CF.CFStringToManaged(CF.CFURLGetString(url)));
CF.CFRelease(url);
var traits = (__CFDictionary*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontTraitsAttribute);
var (weight, slant, width) = ParseTraits(traits);
CF.CFRelease(traits);
var path = Marshal.PtrToStringUTF8((nint)buf)!;
Fonts.Add(new Handle(this)
{
PostscriptName = postscriptName,
FullNames = LocalizedStringSet.FromSingle(displayName),
FamilyNames = LocalizedStringSet.FromSingle(familyName),
FaceNames = LocalizedStringSet.FromSingle(styleName),
Weight = weight,
Slant = slant,
Width = width,
Path = path
});
}
private static unsafe (FontWeight, FontSlant, FontWidth) ParseTraits(__CFDictionary* dictionary)
{
var weight = FontWeight.Normal;
var slant = FontSlant.Normal;
var width = FontWidth.Normal;
var weightVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWeightTrait);
if (weightVal != null)
weight = ConvertWeight(weightVal);
var slantVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontSlantTrait);
if (slantVal != null)
slant = ConvertSlant(slantVal);
var widthVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWidthTrait);
if (widthVal != null)
width = ConvertWidth(widthVal);
return (weight, slant, width);
}
private static readonly (float, FontWeight)[] FontWeightTable =
[
((float) AppKit.NSFontWeightUltraLight, FontWeight.UltraLight),
((float) AppKit.NSFontWeightThin, FontWeight.Thin),
((float) AppKit.NSFontWeightLight, FontWeight.Light),
((float) AppKit.NSFontWeightRegular, FontWeight.Regular),
((float) AppKit.NSFontWeightMedium, FontWeight.Medium),
((float) AppKit.NSFontWeightSemiBold, FontWeight.SemiBold),
((float) AppKit.NSFontWeightBold, FontWeight.Bold),
((float) AppKit.NSFontWeightHeavy, FontWeight.Heavy),
((float) AppKit.NSFontWeightBlack, FontWeight.Black)
];
private static unsafe FontWeight ConvertWeight(__CFNumber* number)
{
float val;
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
var valCopy = val;
return FontWeightTable.MinBy(tup => Math.Abs(tup.Item1 - valCopy)).Item2;
}
private static unsafe FontWidth ConvertWidth(__CFNumber* number)
{
float val;
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
// Normalize to 0-1 range
val = (val + 1) / 2;
var lerped = MathHelper.Lerp((float)FontWidths[0], (float)FontWidths[^1], val);
return FontWidths.MinBy(x => Math.Abs((float)x - lerped));
}
private static unsafe FontSlant ConvertSlant(__CFNumber* number)
{
float val;
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
// Normalize to 0-1 range
return val == 0 ? FontSlant.Normal : FontSlant.Italic;
}
private static unsafe string GetFontAttributeManaged(__CTFontDescriptor* descriptor, __CFString* key)
{
var str = (__CFString*)CT.CTFontDescriptorCopyAttribute(descriptor, key);
try
{
return CF.CFStringToManaged(str);
}
finally
{
CF.CFRelease(str);
}
}
public void Shutdown()
{
// Nothing to do.
}
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
{
var path = ((Handle)handle).Path;
Sawmill.Verbose(path);
// CTFontDescriptor does not seem to have any way to identify *which* index in the font file should be accessed.
// So we have to just load every one until the postscript name matches.
return FontManager.LoadWithPostscriptName(new MemoryMappedFontMemoryHandle(path), handle.PostscriptName);
}
private sealed class Handle(SystemFontManagerCoreText parent) : BaseHandle(parent)
{
public required string Path;
}
}
#endif

View File

@@ -0,0 +1,503 @@
#if WINDOWS
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.DirectX.DWRITE_FACTORY_TYPE;
using static TerraFX.Interop.DirectX.DWRITE_FONT_PROPERTY_ID;
using static TerraFX.Interop.Windows.Windows;
namespace Robust.Client.Graphics.FontManagement;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
/// </summary>
internal sealed unsafe class SystemFontManagerDirectWrite : SystemFontManagerBase, ISystemFontManagerInternal
{
// For future implementors of other platforms:
// a significant amount of code in this file will be shareable with that of other platforms,
// so some refactoring is warranted.
private readonly IConfigurationManager _cfg;
private IDWriteFactory3* _dWriteFactory;
private IDWriteFontSet* _systemFontSet;
public bool IsSupported => true;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
/// </summary>
public SystemFontManagerDirectWrite(
ILogManager logManager,
IConfigurationManager cfg,
IFontManagerInternal fontManager)
: base(logManager, fontManager)
{
_cfg = cfg;
}
public void Initialize()
{
CreateDWriteFactory();
_systemFontSet = GetSystemFontSet(_dWriteFactory);
lock (Lock)
{
var fontCount = _systemFontSet->GetFontCount();
for (var i = 0u; i < fontCount; i++)
{
LoadSingleFontFromSet(_systemFontSet, i);
}
}
Sawmill.Verbose($"Loaded {Fonts.Count} fonts");
}
public void Shutdown()
{
_systemFontSet->Release();
_systemFontSet = null;
_dWriteFactory->Release();
_dWriteFactory = null;
lock (Lock)
{
foreach (var systemFont in Fonts)
{
((Handle)systemFont).FontFace->Release();
}
Fonts.Clear();
}
}
private void LoadSingleFontFromSet(IDWriteFontSet* set, uint fontIndex)
{
// Get basic parameters that every font should probably have?
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_POSTSCRIPT_NAME, out var postscriptNames))
return;
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FULL_NAME, out var fullNames))
return;
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FAMILY_NAME, out var familyNames))
return;
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FACE_NAME, out var faceNames))
return;
// I assume these parameters can't be missing in practice, but better safe than sorry.
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_WEIGHT, out var weight);
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STYLE, out var style);
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STRETCH, out var stretch);
var parsedWeight = ParseFontWeight(weight);
var parsedSlant = ParseFontSlant(style);
var parsedWidth = ParseFontWidth(stretch);
IDWriteFontFaceReference* reference = null;
var result = set->GetFontFaceReference(fontIndex, &reference);
ThrowIfFailed(result);
var handle = new Handle(this, reference)
{
PostscriptName = GetLocalizedForLocaleOrFirst(postscriptNames, StandardLocale),
FullNames = fullNames,
FamilyNames = familyNames,
FaceNames = faceNames,
Weight = parsedWeight,
Slant = parsedSlant,
Width = parsedWidth
};
Fonts.Add(handle);
}
private static FontWeight ParseFontWeight(DWriteLocalizedString[]? strings)
{
if (strings == null)
return FontWeight.Regular;
return (FontWeight)Parse.Int32(strings[0].Value);
}
private static FontSlant ParseFontSlant(DWriteLocalizedString[]? strings)
{
if (strings == null)
return FontSlant.Normal;
return (FontSlant)Parse.Int32(strings[0].Value);
}
private static FontWidth ParseFontWidth(DWriteLocalizedString[]? strings)
{
if (strings == null)
return FontWidth.Normal;
return (FontWidth)Parse.Int32(strings[0].Value);
}
private void CreateDWriteFactory()
{
fixed (IDWriteFactory3** pFactory = &_dWriteFactory)
{
var result = DirectX.DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof<IDWriteFactory3>(),
(IUnknown**)pFactory);
ThrowIfFailed(result);
}
}
private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory)
{
IDWriteFactory6* factory6;
IDWriteFontSet* fontSet;
var result = factory->QueryInterface(__uuidof<IDWriteFactory6>(), (void**)&factory6);
if (result.SUCCEEDED)
{
Sawmill.Verbose("IDWriteFactory6 available, using newer GetSystemFontSet");
result = factory6->GetSystemFontSet(
_cfg.GetCVar(CVars.FontWindowsDownloadable),
(IDWriteFontSet1**)(&fontSet));
factory6->Release();
}
else
{
Sawmill.Verbose("IDWriteFactory6 not available");
result = factory->GetSystemFontSet(&fontSet);
}
ThrowIfFailed(result, "GetSystemFontSet");
return fontSet;
}
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
{
var fontFace = ((Handle)handle).FontFace;
IDWriteFontFile* file = null;
IDWriteFontFileLoader* loader = null;
try
{
var result = fontFace->GetFontFile(&file);
ThrowIfFailed(result, "IDWriteFontFaceReference::GetFontFile");
result = file->GetLoader(&loader);
ThrowIfFailed(result, "IDWriteFontFile::GetLoader");
void* referenceKey;
uint referenceKeyLength;
result = file->GetReferenceKey(&referenceKey, &referenceKeyLength);
ThrowIfFailed(result, "IDWriteFontFile::GetReferenceKey");
IDWriteLocalFontFileLoader* localLoader;
result = loader->QueryInterface(__uuidof<IDWriteLocalFontFileLoader>(), (void**)&localLoader);
if (result.SUCCEEDED)
{
Sawmill.Verbose("Loading font face via memory mapped file...");
// We can get the local file path on disk. This means we can directly load it via mmap.
uint filePathLength;
ThrowIfFailed(
localLoader->GetFilePathLengthFromKey(referenceKey, referenceKeyLength, &filePathLength),
"IDWriteLocalFontFileLoader::GetFilePathLengthFromKey");
var filePath = new char[filePathLength + 1];
fixed (char* pFilePath = filePath)
{
ThrowIfFailed(
localLoader->GetFilePathFromKey(
referenceKey,
referenceKeyLength,
pFilePath,
(uint)filePath.Length),
"IDWriteLocalFontFileLoader::GetFilePathFromKey");
}
var path = new string(filePath, 0, (int)filePathLength);
localLoader->Release();
return FontManager.Load(new MemoryMappedFontMemoryHandle(path));
}
else
{
Sawmill.Verbose("Loading font face via stream...");
// DirectWrite doesn't give us anything to go with for this file, read it into regular memory.
// If the font file has multiple faces, which is possible, then this approach will duplicate memory.
// That sucks, but I'm really not sure whether there's any way around this short of
// comparing the memory contents by hashing to check equality.
// As I'm pretty sure we can't like reference equality check the font objects somehow.
IDWriteFontFileStream* stream;
result = loader->CreateStreamFromKey(referenceKey, referenceKeyLength, &stream);
ThrowIfFailed(result, "IDWriteFontFileLoader::CreateStreamFromKey");
using var streamObject = new DirectWriteStream(stream);
return FontManager.Load(streamObject, (int)fontFace->GetFontFaceIndex());
}
}
finally
{
if (file != null)
file->Release();
if (loader != null)
loader->Release();
}
}
private static bool TryGetStrings(
IDWriteFontSet* set,
uint listIndex,
DWRITE_FONT_PROPERTY_ID property,
[NotNullWhen(true)] out DWriteLocalizedString[]? strings)
{
BOOL exists;
IDWriteLocalizedStrings* dWriteStrings = null;
var result = set->GetPropertyValues(
listIndex,
property,
&exists,
&dWriteStrings);
ThrowIfFailed(result, "IDWriteFontSet::GetPropertyValues");
if (!exists)
{
strings = null;
return false;
}
try
{
strings = GetStrings(dWriteStrings);
return true;
}
finally
{
dWriteStrings->Release();
}
}
private static bool TryGetStringsSet(
IDWriteFontSet* set,
uint listIndex,
DWRITE_FONT_PROPERTY_ID property,
out LocalizedStringSet strings)
{
if (!TryGetStrings(set, listIndex, property, out var stringsArray))
{
strings = default;
return false;
}
strings = StringsToSet(stringsArray);
return true;
}
private static DWriteLocalizedString[] GetStrings(IDWriteLocalizedStrings* localizedStrings)
{
IDWriteStringList* list;
ThrowIfFailed(localizedStrings->QueryInterface(__uuidof<IDWriteStringList>(), (void**)&list));
try
{
return GetStrings(list);
}
finally
{
list->Release();
}
}
private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList)
{
var array = new DWriteLocalizedString[stringList->GetCount()];
var stringPool = ArrayPool<char>.Shared.Rent(256);
for (var i = 0; i < array.Length; i++)
{
uint length;
ThrowIfFailed(stringList->GetStringLength((uint)i, &length), "IDWriteStringList::GetStringLength");
ExpandIfNecessary(ref stringPool, length + 1);
fixed (char* pArr = stringPool)
{
ThrowIfFailed(
stringList->GetString((uint)i, pArr, (uint)stringPool.Length),
"IDWriteStringList::GetString");
}
var value = new string(stringPool, 0, (int)length);
ThrowIfFailed(stringList->GetLocaleNameLength((uint)i, &length), "IDWriteStringList::GetLocaleNameLength");
ExpandIfNecessary(ref stringPool, length + 1);
fixed (char* pArr = stringPool)
{
ThrowIfFailed(
stringList->GetLocaleName((uint)i, pArr, (uint)stringPool.Length),
"IDWriteStringList::GetLocaleName");
}
var localeName = new string(stringPool, 0, (int)length);
array[i] = new DWriteLocalizedString(value, localeName);
}
ArrayPool<char>.Shared.Return(stringPool);
return array;
}
private static void ExpandIfNecessary(ref char[] array, uint requiredLength)
{
if (requiredLength < array.Length)
return;
ArrayPool<char>.Shared.Return(array);
array = ArrayPool<char>.Shared.Rent(checked((int)requiredLength));
}
private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings)
{
var dict = new Dictionary<string, string>();
foreach (var (value, localeName) in strings)
{
dict[localeName] = value;
}
return new LocalizedStringSet { Primary = strings[0].LocaleName, Values = dict };
}
private sealed class Handle(SystemFontManagerDirectWrite parent, IDWriteFontFaceReference* fontFace) : BaseHandle(parent)
{
public readonly IDWriteFontFaceReference* FontFace = fontFace;
}
/// <summary>
/// A simple implementation of a .NET Stream over a IDWriteFontFileStream.
/// </summary>
private sealed class DirectWriteStream : Stream
{
private readonly IDWriteFontFileStream* _stream;
private readonly ulong _size;
private ulong _position;
private bool _disposed;
public DirectWriteStream(IDWriteFontFileStream* stream)
{
_stream = stream;
fixed (ulong* pSize = &_size)
{
var result = _stream->GetFileSize(pSize);
ThrowIfFailed(result, "IDWriteFontFileStream::GetFileSize");
}
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
return Read(buffer.AsSpan(offset, count));
}
public override int Read(Span<byte> buffer)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DirectWriteStream));
var readLength = (uint)buffer.Length;
if (readLength + _position > _size)
readLength = (uint)(_size - _position);
void* fragmentStart;
void* fragmentContext;
var result = _stream->ReadFileFragment(&fragmentStart, _position, readLength, &fragmentContext);
ThrowIfFailed(result);
var data = new ReadOnlySpan<byte>(fragmentStart, (int)readLength);
data.CopyTo(buffer);
_stream->ReleaseFileFragment(fragmentContext);
_position += readLength;
return (int)readLength;
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = offset;
break;
case SeekOrigin.Current:
Position += offset;
break;
case SeekOrigin.End:
Position = Length + offset;
break;
default:
throw new ArgumentOutOfRangeException(nameof(origin), origin, null);
}
return Position;
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => (long)_size;
public override long Position
{
get => (long)_position;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, _size);
_position = (ulong)value;
}
}
protected override void Dispose(bool disposing)
{
_stream->Release();
_disposed = true;
}
}
private record struct DWriteLocalizedString(string Value, string LocaleName);
}
#endif

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace Robust.Client.Graphics.FontManagement;
/// <summary>
/// A fallback implementation of <see cref="ISystemFontManager"/> that just loads no fonts.
/// </summary>
internal sealed class SystemFontManagerFallback : ISystemFontManagerInternal
{
public void Initialize()
{
}
public void Shutdown()
{
}
public bool IsSupported => false;
public IEnumerable<ISystemFontFace> SystemFontFaces => [];
}

View File

@@ -0,0 +1,235 @@
#if FREEDESKTOP
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Log;
using SpaceWizards.Fontconfig.Interop;
namespace Robust.Client.Graphics.FontManagement;
internal sealed unsafe class SystemFontManagerFontconfig : SystemFontManagerBase, ISystemFontManagerInternal
{
private static readonly (int Fc, FontWidth Width)[] WidthTable = [
(Fontconfig.FC_WIDTH_ULTRACONDENSED, FontWidth.UltraCondensed),
(Fontconfig.FC_WIDTH_EXTRACONDENSED, FontWidth.ExtraCondensed),
(Fontconfig.FC_WIDTH_CONDENSED, FontWidth.Condensed),
(Fontconfig.FC_WIDTH_SEMICONDENSED, FontWidth.SemiCondensed),
(Fontconfig.FC_WIDTH_NORMAL, FontWidth.Normal),
(Fontconfig.FC_WIDTH_SEMIEXPANDED, FontWidth.SemiExpanded),
(Fontconfig.FC_WIDTH_EXPANDED, FontWidth.Expanded),
(Fontconfig.FC_WIDTH_EXTRAEXPANDED, FontWidth.ExtraExpanded),
(Fontconfig.FC_WIDTH_ULTRAEXPANDED, FontWidth.UltraExpanded),
];
public bool IsSupported => true;
public SystemFontManagerFontconfig(ILogManager logManager, IFontManagerInternal fontManager)
: base(logManager, fontManager)
{
}
public void Initialize()
{
Sawmill.Verbose("Initializing Fontconfig...");
var result = Fontconfig.FcInit();
if (result == Fontconfig.FcFalse)
throw new InvalidOperationException("Failed to initialize fontconfig!");
Sawmill.Verbose("Listing fonts...");
var os = Fontconfig.FcObjectSetCreate();
AddToObjectSet(os, Fontconfig.FC_FAMILY);
AddToObjectSet(os, Fontconfig.FC_FAMILYLANG);
AddToObjectSet(os, Fontconfig.FC_STYLE);
AddToObjectSet(os, Fontconfig.FC_STYLELANG);
AddToObjectSet(os, Fontconfig.FC_FULLNAME);
AddToObjectSet(os, Fontconfig.FC_FULLNAMELANG);
AddToObjectSet(os, Fontconfig.FC_POSTSCRIPT_NAME);
AddToObjectSet(os, Fontconfig.FC_SLANT);
AddToObjectSet(os, Fontconfig.FC_WEIGHT);
AddToObjectSet(os, Fontconfig.FC_WIDTH);
AddToObjectSet(os, Fontconfig.FC_FILE);
AddToObjectSet(os, Fontconfig.FC_INDEX);
var allPattern = Fontconfig.FcPatternCreate();
var set = Fontconfig.FcFontList(null, allPattern, os);
for (var i = 0; i < set->nfont; i++)
{
var pattern = set->fonts[i];
try
{
LoadPattern(pattern);
}
catch (Exception e)
{
Sawmill.Error($"Error while loading pattern: {e}");
}
}
Fontconfig.FcPatternDestroy(allPattern);
Fontconfig.FcObjectSetDestroy(os);
Fontconfig.FcFontSetDestroy(set);
}
public void Shutdown()
{
// Nada.
}
private void LoadPattern(FcPattern* pattern)
{
var path = PatternGetStrings(pattern, Fontconfig.FC_FILE)![0];
var idx = PatternGetInts(pattern, Fontconfig.FC_INDEX)![0];
var family = PatternToLocalized(pattern, Fontconfig.FC_FAMILY, Fontconfig.FC_FAMILYLANG);
var style = PatternToLocalized(pattern, Fontconfig.FC_STYLE, Fontconfig.FC_STYLELANG);
var fullName = PatternToLocalized(pattern, Fontconfig.FC_FULLNAME, Fontconfig.FC_FULLNAMELANG);
var psName = PatternGetStrings(pattern, Fontconfig.FC_POSTSCRIPT_NAME);
if (psName == null)
return;
var slant = PatternGetInts(pattern, Fontconfig.FC_SLANT) ?? [Fontconfig.FC_SLANT_ROMAN];
var weight = PatternGetInts(pattern, Fontconfig.FC_WEIGHT) ?? [Fontconfig.FC_WEIGHT_REGULAR];
var width = PatternGetInts(pattern, Fontconfig.FC_WIDTH) ?? [Fontconfig.FC_WIDTH_NORMAL];
Fonts.Add(new Handle(this)
{
FilePath = path,
FileIndex = idx,
FaceNames = style ?? LocalizedStringSet.Empty,
FullNames = fullName ?? LocalizedStringSet.Empty,
FamilyNames = family ?? LocalizedStringSet.Empty,
PostscriptName = psName[0],
Slant = SlantFromFontconfig(slant[0]),
Weight = WeightFromFontconfig(weight[0]),
Width = WidthFromFontconfig(width[0])
});
}
private static FontWeight WeightFromFontconfig(int value)
{
return (FontWeight)Fontconfig.FcWeightToOpenType(value);
}
private static FontSlant SlantFromFontconfig(int value)
{
return value switch
{
Fontconfig.FC_SLANT_ITALIC => FontSlant.Italic,
Fontconfig.FC_SLANT_OBLIQUE => FontSlant.Italic,
_ => FontSlant.Normal,
};
}
private static FontWidth WidthFromFontconfig(int value)
{
return WidthTable.MinBy(t => Math.Abs(t.Fc - value)).Width;
}
private static unsafe void AddToObjectSet(FcObjectSet* os, ReadOnlySpan<byte> value)
{
var result = Fontconfig.FcObjectSetAdd(os, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(value)));
if (result == Fontconfig.FcFalse)
throw new InvalidOperationException("Failed to add to object set!");
}
private static unsafe string[]? PatternGetStrings(FcPattern* pattern, ReadOnlySpan<byte> @object)
{
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out string value) =>
{
byte* str = null;
var res = Fontconfig.FcPatternGetString(p, o, i, &str);
value = Marshal.PtrToStringUTF8((nint)str)!;
return res;
});
}
private static unsafe int[]? PatternGetInts(FcPattern* pattern, ReadOnlySpan<byte> @object)
{
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out int value) =>
{
FcResult res;
fixed (int* pValue = &value)
{
res = Fontconfig.FcPatternGetInteger(p, o, i, pValue);
}
return res;
});
}
private delegate FcResult GetValue<T>(FcPattern* p, sbyte* o, int i, out T value);
private static unsafe T[]? PatternGetValues<T>(FcPattern* pattern, ReadOnlySpan<byte> @object, GetValue<T> getValue)
{
var list = new List<T>();
var i = 0;
while (true)
{
var result = getValue(pattern, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(@object)), i++, out var value);
if (result == FcResult.FcResultMatch)
{
list.Add(value);
}
else if (result == FcResult.FcResultNoMatch)
{
return null;
}
else if (result == FcResult.FcResultNoId)
{
break;
}
else
{
throw new Exception($"FcPatternGetString gave error: {result}");
}
}
return list.ToArray();
}
private static LocalizedStringSet? PatternToLocalized(FcPattern* pattern, ReadOnlySpan<byte> @object, ReadOnlySpan<byte> objectLang)
{
var values = PatternGetStrings(pattern, @object);
var languages = PatternGetStrings(pattern, objectLang);
if (values == null || languages == null || values.Length == 0 || languages.Length != values.Length)
return null;
var dict = new Dictionary<string, string>();
for (var i = 0; i < values.Length; i++)
{
var val = values[i];
var lang = languages[i];
dict.TryAdd(lang, val);
}
return new LocalizedStringSet
{
Primary = languages[0],
Values = dict
};
}
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
{
var cast = (Handle)handle;
return FontManager.Load(new MemoryMappedFontMemoryHandle(cast.FilePath), cast.FileIndex);
}
private sealed class Handle(SystemFontManagerFontconfig parent) : BaseHandle(parent)
{
public required string FilePath;
public required int FileIndex;
}
}
#endif

View File

@@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SharpFont;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics
{
@@ -20,6 +20,7 @@ namespace Robust.Client.Graphics
private const int SheetHeight = 256;
private readonly IClyde _clyde;
private readonly ISawmill _sawmill;
private uint _baseFontDpi = 96;
@@ -28,22 +29,56 @@ namespace Robust.Client.Graphics
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
new();
public FontManager(IClyde clyde)
public FontManager(IClyde clyde, ILogManager logManager)
{
_clyde = clyde;
_library = new Library();
_sawmill = logManager.GetSawmill("font");
}
public IFontFaceHandle Load(Stream stream)
public IFontFaceHandle Load(Stream stream, int index = 0)
{
// Freetype directly operates on the font memory managed by us.
// As such, the font data should be pinned in POH.
var fontData = stream.CopyToPinnedArray();
var face = new Face(_library, fontData, 0);
var handle = new FontFaceHandle(face);
return Load(new ArrayMemoryHandle(fontData), index);
}
public IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0)
{
var face = FaceLoad(memory, index);
var handle = new FontFaceHandle(face, memory);
return handle;
}
public IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName)
{
var numFaces = 1;
for (var i = 0; i < numFaces; i++)
{
var face = FaceLoad(memory, i);
numFaces = face.FaceCount;
if (face.GetPostscriptName() == postscriptName)
return new FontFaceHandle(face, memory);
face.Dispose();
}
// Fallback, load SOMETHING.
_sawmill.Warning($"Failed to load correct font via postscript name! {postscriptName}");
return new FontFaceHandle(FaceLoad(memory, 0), memory);
}
private unsafe Face FaceLoad(IFontMemoryHandle memory, int index)
{
return new Face(_library,
(nint)memory.GetData(),
checked((int)memory.GetDataSize()),
index);
}
void IFontManagerInternal.SetFontDpi(uint fontDpi)
{
_baseFontDpi = fontDpi;
@@ -235,10 +270,13 @@ namespace Robust.Client.Graphics
private sealed class FontFaceHandle : IFontFaceHandle
{
// Keep this alive to avoid it being GC'd.
private readonly IFontMemoryHandle _memoryHandle;
public Face Face { get; }
public FontFaceHandle(Face face)
public FontFaceHandle(Face face, IFontMemoryHandle memoryHandle)
{
_memoryHandle = memoryHandle;
Face = face;
}
}
@@ -377,5 +415,32 @@ namespace Robust.Client.Graphics
public CharMetrics Metrics;
public AtlasTexture? Texture;
}
private sealed class ArrayMemoryHandle(byte[] array) : IFontMemoryHandle
{
private GCHandle _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
public unsafe byte* GetData()
{
return (byte*) _gcHandle.AddrOfPinnedObject();
}
public IntPtr GetDataSize()
{
return array.Length;
}
public void Dispose()
{
_gcHandle.Free();
_gcHandle = default;
GC.SuppressFinalize(this);
}
~ArrayMemoryHandle()
{
Dispose();
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System.IO;
using System.Text;
using Robust.Shared.Graphics;
namespace Robust.Client.Graphics
{
@@ -10,7 +10,15 @@ namespace Robust.Client.Graphics
}
internal interface IFontManagerInternal : IFontManager
{
IFontFaceHandle Load(Stream stream);
IFontFaceHandle Load(Stream stream, int index = 0);
IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0);
/// <summary>
/// Load a specified font in a font collection.
/// </summary>
/// <param name="memory">Memory for the entire font collection.</param>
/// <param name="postscriptName">The postscript name of the font to load.</param>
IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName);
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
void SetFontDpi(uint fontDpi);
}
@@ -22,8 +30,6 @@ namespace Robust.Client.Graphics
internal interface IFontInstanceHandle
{
Texture? GetCharTexture(Rune codePoint, float scale);
Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale);
CharMetrics? GetCharMetrics(Rune codePoint, float scale);
@@ -35,6 +41,12 @@ namespace Robust.Client.Graphics
int GetLineHeight(float scale);
}
internal unsafe interface IFontMemoryHandle : IDisposable
{
byte* GetData();
nint GetDataSize();
}
/// <summary>
/// Metrics for a single glyph in a font.
/// Refer to https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html for more information.

View File

@@ -0,0 +1,127 @@
using System.Collections.Generic;
using System.Globalization;
namespace Robust.Client.Graphics;
/// <summary>
/// Provides access to fonts installed on the user's operating system.
/// </summary>
/// <remarks>
/// <para>
/// Different operating systems ship different fonts, so you should generally not rely on any one
/// specific font being available. This system is primarily provided for allowing user preference.
/// </para>
/// </remarks>
/// <seealso cref="ISystemFontFace"/>
public interface ISystemFontManager
{
/// <summary>
/// Whether access to system fonts is currently supported on this platform.
/// </summary>
bool IsSupported { get; }
/// <summary>
/// The list of font face available from the operating system.
/// </summary>
IEnumerable<ISystemFontFace> SystemFontFaces { get; }
}
/// <summary>
/// A single font face, provided by the user's operating system.
/// </summary>
/// <seealso cref="ISystemFontManager"/>
public interface ISystemFontFace
{
/// <summary>
/// The PostScript name of the font face.
/// This is generally the closest to an unambiguous unique identifier as you're going to get.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Arial-ItalicMT"
/// </para>
/// </remarks>
string PostscriptName { get; }
/// <summary>
/// The full name of the font face, localized to the current locale.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Arial Cursiva"
/// </para>
/// </remarks>
/// <seealso cref="GetLocalizedFullName"/>
string FullName { get; }
/// <summary>
/// The family name of the font face, localized to the current locale.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Arial"
/// </para>
/// </remarks>
/// <seealso cref="GetLocalizedFamilyName"/>
string FamilyName { get; }
/// <summary>
/// The face name (or "style name") of the font face, localized to the current locale.
/// </summary>
/// <remarks>
/// <para>
/// For example, "Cursiva"
/// </para>
/// </remarks>
/// <seealso cref="GetLocalizedFaceName"/>
string FaceName { get; }
/// <summary>
/// Get the <see cref="FullName"/>, localized to a specific locale.
/// </summary>
/// <param name="culture">The locale to fetch the localized string for.</param>
string GetLocalizedFullName(CultureInfo culture);
/// <summary>
/// Get the <see cref="FamilyName"/>, localized to a specific locale.
/// </summary>
/// <param name="culture">The locale to fetch the localized string for.</param>
string GetLocalizedFamilyName(CultureInfo culture);
/// <summary>
/// Get the <see cref="FaceName"/>, localized to a specific locale.
/// </summary>
/// <param name="culture">The locale to fetch the localized string for.</param>
string GetLocalizedFaceName(CultureInfo culture);
/// <summary>
/// The weight of the font face.
/// </summary>
FontWeight Weight { get; }
/// <summary>
/// The slant of the font face.
/// </summary>
FontSlant Slant { get; }
/// <summary>
/// The width of the font face.
/// </summary>
FontWidth Width { get; }
/// <summary>
/// Load the font face so that it can be used in-engine.
/// </summary>
/// <param name="size">The size to load the font at.</param>
/// <returns>A font object that can be used to render text.</returns>
Font Load(int size);
}
/// <summary>
/// Engine-internal API for <see cref="ISystemFontManager"/>.
/// </summary>
internal interface ISystemFontManagerInternal : ISystemFontManager
{
void Initialize();
void Shutdown();
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics.FontManagement;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics;
/// <summary>
/// Implementation of <see cref="ISystemFontManager"/> that proxies to platform-specific implementations,
/// and adds additional logging.
/// </summary>
internal sealed class SystemFontManager : ISystemFontManagerInternal, IPostInjectInit
{
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private ISawmill _sawmill = default!;
private ISystemFontManagerInternal _implementation = default!;
public bool IsSupported => _implementation.IsSupported;
public IEnumerable<ISystemFontFace> SystemFontFaces => _implementation.SystemFontFaces;
public void Initialize()
{
_implementation = GetImplementation();
_sawmill.Verbose($"Using {_implementation.GetType()}");
_sawmill.Debug("Initializing system font manager implementation");
try
{
var sw = RStopwatch.StartNew();
_implementation.Initialize();
_sawmill.Debug($"Done initializing system font manager in {sw.Elapsed}");
}
catch (Exception e)
{
// This is a non-critical engine system that has to parse significant amounts of external data.
// Best to fail gracefully to avoid full startup failures.
_sawmill.Error($"Error while initializing system font manager, resorting to fallback: {e}");
_implementation = new SystemFontManagerFallback();
}
}
public void Shutdown()
{
_sawmill.Verbose("Shutting down system font manager");
try
{
_implementation.Shutdown();
}
catch (Exception e)
{
_sawmill.Error($"Exception shutting down system font manager: {e}");
return;
}
_sawmill.Verbose("Successfully shut down system font manager");
}
private ISystemFontManagerInternal GetImplementation()
{
if (!_cfg.GetCVar(CVars.FontSystem))
return new SystemFontManagerFallback();
#if WINDOWS
return new SystemFontManagerDirectWrite(_logManager, _cfg, _fontManager);
#elif FREEDESKTOP
return new SystemFontManagerFontconfig(_logManager, _fontManager);
#elif MACOS
return new SystemFontManagerCoreText(_logManager, _fontManager);
#else
return new SystemFontManagerFallback();
#endif
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("font.system");
// _sawmill.Level = LogLevel.Verbose;
}
}

View File

@@ -0,0 +1,24 @@
// ReSharper disable InconsistentNaming
#if MACOS
namespace Robust.Client.Interop.MacOS;
/// <summary>
/// Binding to macOS AppKit.
/// </summary>
internal static class AppKit
{
// Values pulled from here:
// https://chromium.googlesource.com/chromium/src/+/b5019b491932dfa597acb3a13a9e7780fb6525a9/ui/gfx/platform_font_mac.mm#53
public const double NSFontWeightUltraLight = -0.8;
public const double NSFontWeightThin = -0.6;
public const double NSFontWeightLight = -0.4;
public const double NSFontWeightRegular = 0;
public const double NSFontWeightMedium = 0.23;
public const double NSFontWeightSemiBold = 0.30;
public const double NSFontWeightBold = 0.40;
public const double NSFontWeightHeavy = 0.56;
public const double NSFontWeightBlack = 0.62;
}
#endif

View File

@@ -0,0 +1,97 @@
#if MACOS
using System.Runtime.InteropServices;
using CFIndex = System.Runtime.InteropServices.CLong;
using Boolean = byte;
namespace Robust.Client.Interop.MacOS;
// ReSharper disable InconsistentNaming
/// <summary>
/// Binding to macOS Core Foundation.
/// </summary>
internal static unsafe class CoreFoundation
{
private const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
public const int kCFNumberFloat32Type = 5;
public static string CFStringToManaged(__CFString* str)
{
var length = CFStringGetLength(str);
return string.Create(
checked((int)length.Value),
(nint)str,
static (span, arg) =>
{
fixed (char* pBuffer = span)
{
CFStringGetCharacters((__CFString*)arg,
new CFRange
{
location = new CFIndex(0),
length = new CFIndex(span.Length),
},
pBuffer);
}
});
}
[DllImport(CoreFoundationLibrary)]
internal static extern void* CFRetain(void* cf);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFRelease(void* cf);
[DllImport(CoreFoundationLibrary)]
internal static extern CFIndex CFArrayGetCount(__CFArray* array);
[DllImport(CoreFoundationLibrary)]
internal static extern void* CFArrayGetValueAtIndex(__CFArray* array, CFIndex index);
[DllImport(CoreFoundationLibrary)]
internal static extern CFIndex CFStringGetLength(__CFString* str);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFStringGetCharacters(__CFString* str, CFRange range, char* buffer);
[DllImport(CoreFoundationLibrary)]
internal static extern Boolean CFURLGetFileSystemRepresentation(
__CFURL* url,
Boolean resolveAgainstBase,
byte* buffer,
CFIndex maxBufLen);
[DllImport(CoreFoundationLibrary)]
internal static extern __CFString* CFURLGetString(__CFURL* url);
[DllImport(CoreFoundationLibrary)]
internal static extern CFIndex CFDictionaryGetCount(__CFDictionary* theDict);
[DllImport(CoreFoundationLibrary)]
internal static extern void* CFDictionaryGetValue(__CFDictionary* theDict, void* key);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFDictionaryGetKeysAndValues(__CFDictionary* theDict, void** keys, void** values);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFNumberGetValue(__CFNumber* number, CLong theType, void* valuePtr);
}
internal struct __CFNumber;
internal struct __CFString;
internal struct __CFURL;
internal struct __CFArray;
internal struct __CFDictionary;
internal struct CFRange
{
public CFIndex location;
public CFIndex length;
}
#endif

View File

@@ -0,0 +1,54 @@
#if MACOS
using System.Runtime.InteropServices;
namespace Robust.Client.Interop.MacOS;
// ReSharper disable InconsistentNaming
/// <summary>
/// Binding to macOS Core Text.
/// </summary>
internal static unsafe class CoreText
{
private const string CoreTextLibrary = "/System/Library/Frameworks/CoreText.framework/CoreText";
public static readonly __CFString* kCTFontURLAttribute;
public static readonly __CFString* kCTFontNameAttribute;
public static readonly __CFString* kCTFontDisplayNameAttribute;
public static readonly __CFString* kCTFontFamilyNameAttribute;
public static readonly __CFString* kCTFontStyleNameAttribute;
public static readonly __CFString* kCTFontTraitsAttribute;
public static readonly __CFString* kCTFontWeightTrait;
public static readonly __CFString* kCTFontWidthTrait;
public static readonly __CFString* kCTFontSlantTrait;
static CoreText()
{
var lib = NativeLibrary.Load(CoreTextLibrary);
kCTFontURLAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontURLAttribute));
kCTFontNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontNameAttribute));
kCTFontDisplayNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontDisplayNameAttribute));
kCTFontFamilyNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontFamilyNameAttribute));
kCTFontStyleNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontStyleNameAttribute));
kCTFontTraitsAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontTraitsAttribute));
kCTFontWeightTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWeightTrait));
kCTFontWidthTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWidthTrait));
kCTFontSlantTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontSlantTrait));
}
[DllImport(CoreTextLibrary)]
public static extern __CTFontCollection* CTFontCollectionCreateFromAvailableFonts(__CFDictionary* options);
[DllImport(CoreTextLibrary)]
public static extern __CFArray* CTFontCollectionCreateMatchingFontDescriptors(__CTFontCollection* collection);
[DllImport(CoreTextLibrary)]
public static extern void* CTFontDescriptorCopyAttribute(__CTFontDescriptor* descriptor, __CFString* attribute);
[DllImport(CoreTextLibrary)]
public static extern __CFDictionary* CTFontDescriptorCopyAttributes(__CTFontDescriptor* descriptor);
}
internal struct __CTFontCollection;
internal struct __CTFontDescriptor;
#endif

View File

@@ -70,6 +70,11 @@
<Import Project="..\MSBuild\Robust.Properties.targets" />
<ItemGroup Condition="'$(IsFreedesktop)' == 'True'">
<PackageReference Include="SpaceWizards.Fontconfig.Interop" />
<RobustLinkAssemblies Include="SpaceWizards.Fontconfig.Interop" />
</ItemGroup>
<Import Project="..\MSBuild\XamlIL.targets" />
<Import Project="..\MSBuild\Robust.Trimming.targets" />

View File

@@ -1924,5 +1924,21 @@ namespace Robust.Shared
/// </summary>
public static readonly CVarDef<string> XamlHotReloadMarkerName =
CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY);
/*
* FONT
*/
/// <summary>
/// If false, disable system font support.
/// </summary>
public static readonly CVarDef<bool> FontSystem =
CVarDef.Create("font.system", true, CVar.CLIENTONLY);
/// <summary>
/// If true, allow Windows "downloadable" fonts to be exposed to the system fonts API.
/// </summary>
public static readonly CVarDef<bool> FontWindowsDownloadable =
CVarDef.Create("font.windows_downloadable", false, CVar.CLIENTONLY | CVar.ARCHIVE);
}
}