#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;
///
/// Implementation of that uses DirectWrite on Windows.
///
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;
///
/// Implementation of that uses DirectWrite on Windows.
///
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(),
(IUnknown**)pFactory);
ThrowIfFailed(result);
}
}
private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory)
{
IDWriteFactory6* factory6;
IDWriteFontSet* fontSet;
var result = factory->QueryInterface(__uuidof(), (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(), (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(), (void**)&list));
try
{
return GetStrings(list);
}
finally
{
list->Release();
}
}
private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList)
{
var array = new DWriteLocalizedString[stringList->GetCount()];
var stringPool = ArrayPool.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.Shared.Return(stringPool);
return array;
}
private static void ExpandIfNecessary(ref char[] array, uint requiredLength)
{
if (requiredLength < array.Length)
return;
ArrayPool.Shared.Return(array);
array = ArrayPool.Shared.Rent(checked((int)requiredLength));
}
private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings)
{
var dict = new Dictionary();
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;
}
///
/// A simple implementation of a .NET Stream over a IDWriteFontFileStream.
///
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 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(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