Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Windows 10 support for nearby font loading #12554

Merged
1 commit merged into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 3 additions & 57 deletions src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,8 @@
#include "EnumEntry.h"

#include <LibraryResources.h>
#include "..\WinRTUtils\inc\Utils.h"

// This function is a copy of DxFontInfo::_NearbyCollection() with
// * the call to DxFontInfo::s_GetNearbyFonts() inlined
// * checkForUpdates for GetSystemFontCollection() set to true
static wil::com_ptr<IDWriteFontCollection1> NearbyCollection(IDWriteFactory* dwriteFactory)
{
// The convenience interfaces for loading fonts from files
// are only available on Windows 10+.
wil::com_ptr<IDWriteFactory6> factory6;
// wil's query() facilities don't work inside WinRT land at the moment.
// They produce a compilation error due to IUnknown and winrt::Windows::Foundation::IUnknown being ambiguous.
if (!SUCCEEDED(dwriteFactory->QueryInterface(__uuidof(IDWriteFactory6), factory6.put_void())))
{
return nullptr;
}

wil::com_ptr<IDWriteFontCollection1> systemFontCollection;
THROW_IF_FAILED(factory6->GetSystemFontCollection(false, systemFontCollection.addressof(), true));

wil::com_ptr<IDWriteFontSet> systemFontSet;
THROW_IF_FAILED(systemFontCollection->GetFontSet(systemFontSet.addressof()));

wil::com_ptr<IDWriteFontSetBuilder2> fontSetBuilder2;
THROW_IF_FAILED(factory6->CreateFontSetBuilder(fontSetBuilder2.addressof()));

THROW_IF_FAILED(fontSetBuilder2->AddFontSet(systemFontSet.get()));

{
const std::filesystem::path module{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
const auto folder{ module.parent_path() };

for (const auto& p : std::filesystem::directory_iterator(folder))
{
if (til::ends_with(p.path().native(), L".ttf"))
{
fontSetBuilder2->AddFontFile(p.path().c_str());
}
}
}

wil::com_ptr<IDWriteFontSet> fontSet;
THROW_IF_FAILED(fontSetBuilder2->CreateFontSet(fontSet.addressof()));

wil::com_ptr<IDWriteFontCollection1> fontCollection;
THROW_IF_FAILED(factory6->CreateFontCollectionFromFontSet(fontSet.get(), &fontCollection));

return fontCollection;
}
#include "../WinRTUtils/inc/Utils.h"
#include "../../renderer/base/FontCache.h"

using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::UI::Xaml;
Expand Down Expand Up @@ -166,15 +119,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
std::vector<Editor::Font> fontList;
std::vector<Editor::Font> monospaceFontList;

// get a DWriteFactory
com_ptr<IDWriteFactory> factory;
THROW_IF_FAILED(DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory),
reinterpret_cast<::IUnknown**>(factory.put())));

// get the font collection; subscribe to updates
const auto fontCollection = NearbyCollection(factory.get());
const auto fontCollection = ::Microsoft::Console::Render::FontCache::GetFresh();

for (UINT32 i = 0; i < fontCollection->GetFontFamilyCount(); ++i)
{
Expand Down
9 changes: 9 additions & 0 deletions src/features.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
</alwaysDisabledBrandingTokens>
</feature>

<feature>
<name>Feature_NearbyFontLoading</name>
<description>Controls whether fonts in the same directory as the binary are used during rendering. Disabled for conhost so that it doesn't iterate the entire system32 directory.</description>
<stage>AlwaysEnabled</stage>
<alwaysDisabledBrandingTokens>
<brandingToken>WindowsInbox</brandingToken>
</alwaysDisabledBrandingTokens>
</feature>

<feature>
<name>Feature_AdjustIndistinguishableText</name>
<description>If enabled, the foreground color will, when necessary, be automatically adjusted to make it more visible.</description>
Expand Down
87 changes: 58 additions & 29 deletions src/renderer/atlas/AtlasEngine.api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include "pch.h"
#include "AtlasEngine.h"

#include "../base/FontCache.h"

// #### NOTE ####
// If you see any code in here that contains "_r." you might be seeing a race condition.
// The AtlasEngine::Present() method is called on a background thread without any locks,
Expand Down Expand Up @@ -227,7 +229,7 @@ try
}
#endif

_resolveFontMetrics(fontInfoDesired, fontInfo);
_resolveFontMetrics(nullptr, fontInfoDesired, fontInfo);
return S_OK;
}
CATCH_RETURN()
Expand Down Expand Up @@ -401,7 +403,50 @@ void AtlasEngine::ToggleShaderEffects() noexcept
}

[[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept
try
{
static constexpr std::array fallbackFaceNames{ static_cast<const wchar_t*>(nullptr), L"Consolas", L"Lucida Console", L"Courier New" };
auto it = fallbackFaceNames.begin();
const auto end = fallbackFaceNames.end();
Comment on lines +407 to +409
Copy link
Member Author

Choose a reason for hiding this comment

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

Windows Terminal ignores the HRESULT this function returns and will simply crash with a division by 0 error if it wasn't for this new code. This implements font fallback in a similar way as DxEngine does.

In the future Terminal shouldn't ignore the HRESULT and call UpdateFont() with fallback fonts itself until this method succeeds.

Copy link
Member

Choose a reason for hiding this comment

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

In the future

should we track this work?

Copy link
Member Author

Choose a reason for hiding this comment

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

I added a short note in #9999.


for (;;)
{
try
{
_updateFont(*it, fontInfoDesired, fontInfo, features, axes);
return S_OK;
}
catch (...)
{
++it;
if (it == end)
{
RETURN_CAUGHT_EXCEPTION();
}
else
{
LOG_CAUGHT_EXCEPTION();
}
}
}
}

void AtlasEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept
{
_api.hyperlinkHoveredId = hoveredId;
}

#pragma endregion

void AtlasEngine::_resolveAntialiasingMode() noexcept
{
// If the user asks for ClearType, but also for a transparent background
// (which our ClearType shader doesn't simultaneously support)
// then we need to sneakily force the renderer to grayscale AA.
const auto forceGrayscaleAA = _api.antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE && !_api.backgroundOpaqueMixin;
_api.realizedAntialiasingMode = forceGrayscaleAA ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : _api.antialiasingMode;
}
Comment on lines +433 to +447
Copy link
Member Author

@lhecker lhecker Feb 23, 2022

Choose a reason for hiding this comment

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

This code isn't new. It's just moved up from down below to group the code diff better.

Copy link
Member

Choose a reason for hiding this comment

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

this makes it difficult to review the diff -- can you point out the part that fixed the fallback issue you mentioned?

Copy link
Member Author

@lhecker lhecker Feb 23, 2022

Choose a reason for hiding this comment

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

Well, saying that I moved it might've been an overstatement.
I didn't actually move the code. The _updateFont function is new. It's just git diffing it poorly and no builtin algorithm improves this diff.


void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes)
{
std::vector<DWRITE_FONT_FEATURE> fontFeatures;
if (!features.empty())
Expand Down Expand Up @@ -478,7 +523,7 @@ try
}

const auto previousCellSize = _api.fontMetrics.cellSize;
_resolveFontMetrics(fontInfoDesired, fontInfo, &_api.fontMetrics);
_resolveFontMetrics(faceName, fontInfoDesired, fontInfo, &_api.fontMetrics);
_api.fontFeatures = std::move(fontFeatures);
_api.fontAxisValues = std::move(fontAxisValues);

Expand All @@ -489,37 +534,21 @@ try
_api.cellCount = _api.sizeInPixel / _api.fontMetrics.cellSize;
WI_SetFlag(_api.invalidations, ApiInvalidations::Size);
}

return S_OK;
}
CATCH_RETURN()

void AtlasEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept
{
_api.hyperlinkHoveredId = hoveredId;
}

#pragma endregion

void AtlasEngine::_resolveAntialiasingMode() noexcept
void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics) const
{
// If the user asks for ClearType, but also for a transparent background
// (which our ClearType shader doesn't simultaneously support)
// then we need to sneakily force the renderer to grayscale AA.
const auto forceGrayscaleAA = _api.antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE && !_api.backgroundOpaqueMixin;
_api.realizedAntialiasingMode = forceGrayscaleAA ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : _api.antialiasingMode;
}

void AtlasEngine::_resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics) const
{
auto requestedFaceName = fontInfoDesired.GetFaceName().c_str();
const auto requestedFamily = fontInfoDesired.GetFamily();
auto requestedWeight = fontInfoDesired.GetWeight();
auto requestedSize = fontInfoDesired.GetEngineSize();

if (!requestedFaceName)
{
requestedFaceName = L"Consolas";
requestedFaceName = fontInfoDesired.GetFaceName().c_str();
if (!requestedFaceName)
{
requestedFaceName = L"Consolas";
}
}
if (!requestedSize.Y)
{
Expand All @@ -530,16 +559,15 @@ void AtlasEngine::_resolveFontMetrics(const FontInfoDesired& fontInfoDesired, Fo
requestedWeight = DWRITE_FONT_WEIGHT_NORMAL;
}

wil::com_ptr<IDWriteFontCollection> systemFontCollection;
THROW_IF_FAILED(_sr.dwriteFactory->GetSystemFontCollection(systemFontCollection.addressof(), false));
auto fontCollection = FontCache::GetCached();

u32 index = 0;
BOOL exists = false;
THROW_IF_FAILED(systemFontCollection->FindFamilyName(requestedFaceName, &index, &exists));
THROW_IF_FAILED(fontCollection->FindFamilyName(requestedFaceName, &index, &exists));
THROW_HR_IF(DWRITE_E_NOFONT, !exists);

wil::com_ptr<IDWriteFontFamily> fontFamily;
THROW_IF_FAILED(systemFontCollection->GetFontFamily(index, fontFamily.addressof()));
THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof()));

wil::com_ptr<IDWriteFont> font;
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(static_cast<DWRITE_FONT_WEIGHT>(requestedWeight), DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof()));
Expand Down Expand Up @@ -601,6 +629,7 @@ void AtlasEngine::_resolveFontMetrics(const FontInfoDesired& fontInfoDesired, Fo
// NOTE: From this point onward no early returns or throwing code should exist,
// as we might cause _api to be in an inconsistent state otherwise.

fontMetrics->fontCollection = std::move(fontCollection);
fontMetrics->fontName = std::move(fontName);
fontMetrics->fontSizeInDIP = static_cast<float>(fontSizeInPx / static_cast<double>(_api.dpi) * 96.0);
fontMetrics->baselineInDIP = static_cast<float>(baseline / static_cast<double>(_api.dpi) * 96.0);
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/atlas/AtlasEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <shader_ps.h>
#include <shader_vs.h>

#include "../base/FontCache.h"
#include "../../interactivity/win32/CustomWindowMessages.h"

// #### NOTE ####
Expand Down Expand Up @@ -1023,7 +1024,7 @@ void AtlasEngine::_recreateFontDependentResources()
const auto fontStyle = italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
auto& textFormat = _r.textFormats[italic][bold];

THROW_IF_FAILED(_sr.dwriteFactory->CreateTextFormat(_api.fontMetrics.fontName.get(), nullptr, fontWeight, fontStyle, DWRITE_FONT_STRETCH_NORMAL, _api.fontMetrics.fontSizeInDIP, L"", textFormat.put()));
THROW_IF_FAILED(_sr.dwriteFactory->CreateTextFormat(_api.fontMetrics.fontName.get(), _api.fontMetrics.fontCollection.get(), fontWeight, fontStyle, DWRITE_FONT_STRETCH_NORMAL, _api.fontMetrics.fontSizeInDIP, L"", textFormat.put()));
textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);

Expand Down
4 changes: 3 additions & 1 deletion src/renderer/atlas/AtlasEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ namespace Microsoft::Console::Render

struct FontMetrics
{
wil::com_ptr<IDWriteFontCollection> fontCollection;
wil::unique_process_heap_string fontName;
float baselineInDIP = 0.0f;
float fontSizeInDIP = 0.0f;
Expand Down Expand Up @@ -615,7 +616,8 @@ namespace Microsoft::Console::Render

// AtlasEngine.api.cpp
void _resolveAntialiasingMode() noexcept;
void _resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics = nullptr) const;
void _updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes);
void _resolveFontMetrics(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics = nullptr) const;

// AtlasEngine.r.cpp
void _setShaderResources() const;
Expand Down
96 changes: 96 additions & 0 deletions src/renderer/base/FontCache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#pragma once

namespace Microsoft::Console::Render::FontCache
{
namespace details
{
inline const std::vector<wil::com_ptr<IDWriteFontFile>>& getNearbyFontFiles(IDWriteFactory5* factory5)
{
static const auto fontFiles = [=]() {
std::vector<wil::com_ptr<IDWriteFontFile>> files;

const std::filesystem::path module{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
const auto folder{ module.parent_path() };

for (const auto& p : std::filesystem::directory_iterator(folder))
{
if (til::ends_with(p.path().native(), L".ttf"))
{
wil::com_ptr<IDWriteFontFile> fontFile;
if (SUCCEEDED_LOG(factory5->CreateFontFileReference(p.path().c_str(), nullptr, fontFile.addressof())))
Copy link
Member

Choose a reason for hiding this comment

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

so it's safe to use these objects on different threads because it is a SHARED-type factory? neato

Copy link
Member Author

Choose a reason for hiding this comment

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

DWRITE_FACTORY_TYPE_SHARED only enables the use of the Windows Font Cache service for DirectWrite. It's unspecified whether DirectWrite is thread-safe, but it should be, since Direct2D can be used across threads. I'm gonna ask one of the dwrite devs about this...

{
files.emplace_back(std::move(fontFile));
}
}
}

files.shrink_to_fit();
return files;
}();
return fontFiles;
}

inline wil::com_ptr<IDWriteFontCollection> getFontCollection(bool forceUpdate)
Copy link
Member

Choose a reason for hiding this comment

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

I'm kinda just trusting that this bit of the code will work as expected. I don't totally know the mechanical differences between a IDWriteFactory6 and a IDWriteFactory, or a IDWriteFontSetBuilder1 and a IDWriteFontSetBuilder2, but I'm trusting it works

Copy link
Member Author

Choose a reason for hiding this comment

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

BTW the new code is what IDWriteFontSetBuilder2::AddFontFile does internally. It's implemented in terms of IDWriteFontSetBuilder1::AddFontFile.

{
wil::com_ptr<IDWriteFactory> factory;
THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof())));

wil::com_ptr<IDWriteFontCollection> systemFontCollection;
THROW_IF_FAILED(factory->GetSystemFontCollection(systemFontCollection.addressof(), forceUpdate));

if constexpr (Feature_NearbyFontLoading::IsEnabled())
{
// IDWriteFactory5 is supported since Windows 10, build 15021.
const auto factory5 = factory.try_query<IDWriteFactory5>();
if (!factory5)
{
return systemFontCollection;
}

const auto& nearbyFontFiles = getNearbyFontFiles(factory5.get());
if (nearbyFontFiles.empty())
{
return systemFontCollection;
}

wil::com_ptr<IDWriteFontSet> systemFontSet;
// IDWriteFontCollection1 is supported since Windows 7.
THROW_IF_FAILED(systemFontCollection.query<IDWriteFontCollection1>()->GetFontSet(systemFontSet.addressof()));

wil::com_ptr<IDWriteFontSetBuilder1> fontSetBuilder;
THROW_IF_FAILED(factory5->CreateFontSetBuilder(fontSetBuilder.addressof()));
THROW_IF_FAILED(fontSetBuilder->AddFontSet(systemFontSet.get()));

for (const auto& file : nearbyFontFiles)
{
LOG_IF_FAILED(fontSetBuilder->AddFontFile(file.get()));
}

wil::com_ptr<IDWriteFontSet> fontSet;
THROW_IF_FAILED(fontSetBuilder->CreateFontSet(fontSet.addressof()));

wil::com_ptr<IDWriteFontCollection1> fontCollection;
THROW_IF_FAILED(factory5->CreateFontCollectionFromFontSet(fontSet.get(), fontCollection.addressof()));

return std::move(fontCollection);
}
else
{
return systemFontCollection;
}
}
}

inline wil::com_ptr<IDWriteFontCollection> GetCached()
{
return details::getFontCollection(false);
}

inline wil::com_ptr<IDWriteFontCollection> GetFresh()
{
return details::getFontCollection(true);
}
}
1 change: 1 addition & 0 deletions src/renderer/base/lib/base.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<ClInclude Include="..\..\inc\IRenderTarget.hpp" />
<ClInclude Include="..\..\inc\RenderEngineBase.hpp" />
<ClInclude Include="..\..\inc\RenderSettings.hpp" />
<ClInclude Include="..\FontCache.h" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\renderer.hpp" />
<ClInclude Include="..\thread.hpp" />
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/base/lib/base.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@
<ClInclude Include="..\..\inc\RenderSettings.hpp">
<Filter>Header Files\inc</Filter>
</ClInclude>
<ClInclude Include="..\FontCache.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
Expand Down
Loading