diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt
index 35742a6111e..bcfa70b2568 100644
--- a/.github/actions/spelling/allow/allow.txt
+++ b/.github/actions/spelling/allow/allow.txt
@@ -1,12 +1,13 @@
apc
+Apc
+bsd
calt
ccmp
changelog
-cybersecurity
-Apc
clickable
clig
copyable
+cybersecurity
dalet
dcs
Dcs
@@ -34,16 +35,16 @@ It'd
kje
liga
lje
-locl
-lorem
Llast
Lmid
+locl
+lorem
Lorigin
maxed
mkmk
mru
-noreply
nje
+noreply
ogonek
ok'd
overlined
diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt
index 8d751a187ff..7d64932c95a 100644
--- a/.github/actions/spelling/excludes.txt
+++ b/.github/actions/spelling/excludes.txt
@@ -61,6 +61,7 @@ SUMS$
^src/host/runft\.bat$
^src/host/runut\.bat$
^src/interactivity/onecore/BgfxEngine\.
+^src/renderer/atlas/
^src/renderer/wddmcon/WddmConRenderer\.
^src/terminal/adapter/ut_adapter/run\.bat$
^src/terminal/parser/delfuzzpayload\.bat$
diff --git a/OpenConsole.sln b/OpenConsole.sln
index 493cba041da..d1f99131d2f 100644
--- a/OpenConsole.sln
+++ b/OpenConsole.sln
@@ -400,6 +400,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsTerminal.UIA.Tests",
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "api-ms-win-core-synch-l1-2-0", "src\api-ms-win-core-synch-l1-2-0\api-ms-win-core-synch-l1-2-0.vcxproj", "{9CF74355-F018-4C19-81AD-9DC6B7F2C6F5}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererAtlas", "src\renderer\atlas\atlas.vcxproj", "{8222900C-8B6C-452A-91AC-BE95DB04B95F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
AuditMode|Any CPU = AuditMode|Any CPU
@@ -3339,6 +3341,46 @@ Global
{9CF74355-F018-4C19-81AD-9DC6B7F2C6F5}.Release|x64.Build.0 = Release|x64
{9CF74355-F018-4C19-81AD-9DC6B7F2C6F5}.Release|x86.ActiveCfg = Release|Win32
{9CF74355-F018-4C19-81AD-9DC6B7F2C6F5}.Release|x86.Build.0 = Release|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|ARM.ActiveCfg = AuditMode|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|ARM64.Build.0 = AuditMode|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x64.ActiveCfg = AuditMode|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x64.Build.0 = AuditMode|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x86.ActiveCfg = AuditMode|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.AuditMode|x86.Build.0 = AuditMode|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|Any CPU.ActiveCfg = Debug|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM.ActiveCfg = Debug|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|ARM64.Build.0 = Debug|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x64.ActiveCfg = Debug|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x64.Build.0 = Debug|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x86.ActiveCfg = Debug|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Debug|x86.Build.0 = Debug|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x64.ActiveCfg = Fuzzing|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x64.Build.0 = Fuzzing|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Fuzzing|x86.Build.0 = Fuzzing|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|Any CPU.ActiveCfg = Release|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM.ActiveCfg = Release|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM64.ActiveCfg = Release|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|ARM64.Build.0 = Release|ARM64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x64Test.ActiveCfg = Release|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|DotNet_x86Test.ActiveCfg = Release|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x64.ActiveCfg = Release|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x64.Build.0 = Release|x64
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x86.ActiveCfg = Release|Win32
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3438,6 +3480,7 @@ Global
{C323DAEE-B307-4C7B-ACE5-7293CBEFCB5B} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E}
{F19DACD5-0C6E-40DC-B6E4-767A3200542C} = {BDB237B6-1D1D-400F-84CC-40A58FA59C8E}
{9CF74355-F018-4C19-81AD-9DC6B7F2C6F5} = {89CDCC5C-9F53-4054-97A4-639D99F169CD}
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F} = {05500DEF-2294-41E3-AF9A-24E580B82836}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}
diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
index 1665034e114..a7063bb9b28 100644
--- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
+++ b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
@@ -29,6 +29,9 @@
{18D09A24-8240-42D6-8CB6-236EEE820263}
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{af0a096a-8b3a-4949-81ef-7df8f0fee91f}
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index a402a2d5ea2..b08e4c4741c 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -3,16 +3,18 @@
#include "pch.h"
#include "ControlCore.h"
-#include
+
#include
#include
#include
-#include
#include
#include
+
+#include "EventArgs.h"
#include "../../types/inc/GlyphWidth.hpp"
-#include "../../types/inc/Utils.hpp"
#include "../../buffer/out/search.h"
+#include "../../renderer/atlas/AtlasEngine.h"
+#include "../../renderer/dx/DxRenderer.hpp"
#include "ControlCore.g.cpp"
@@ -202,6 +204,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const double actualHeight,
const double compositionScale)
{
+ assert(_settings);
+
_panelWidth = actualWidth;
_panelHeight = actualHeight;
_compositionScale = compositionScale;
@@ -222,10 +226,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return false;
}
- // Set up the DX Engine
- auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
- _renderer->AddRenderEngine(dxEngine.get());
- _renderEngine = std::move(dxEngine);
+ if (Feature_AtlasEngine::IsEnabled() && _settings.UseAtlasEngine())
+ {
+ _renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
+ }
+ else
+ {
+ _renderEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
+ }
+
+ _renderer->AddRenderEngine(_renderEngine.get());
// Initialize our font with the renderer
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
@@ -271,11 +281,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_renderEngine->SetSoftwareRendering(_settings.SoftwareRendering());
_renderEngine->SetIntenseIsBold(_settings.IntenseIsBold());
- _updateAntiAliasingMode(_renderEngine.get());
+ _updateAntiAliasingMode();
// GH#5098: Inform the engine of the opacity of the default text background.
// GH#11315: Always do this, even if they don't have acrylic on.
- _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.Opacity()));
+ const auto backgroundIsOpaque = _settings.Opacity() == 1.0 && _settings.BackgroundImage().empty();
+ _renderEngine->SetDefaultTextBackgroundOpacity(static_cast(backgroundIsOpaque));
THROW_IF_FAILED(_renderEngine->Enable());
@@ -616,7 +627,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering());
_renderEngine->SetSoftwareRendering(_settings.SoftwareRendering());
- _updateAntiAliasingMode(_renderEngine.get());
+ _updateAntiAliasingMode();
// Refresh our font with the renderer
const auto actualFontOldSize = _actualFont.GetSize();
@@ -650,22 +661,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}
- void ControlCore::_updateAntiAliasingMode(::Microsoft::Console::Render::DxEngine* const dxEngine)
+ void ControlCore::_updateAntiAliasingMode()
{
- // Update DxEngine's AntialiasingMode
+ D2D1_TEXT_ANTIALIAS_MODE mode;
+
switch (_settings.AntialiasingMode())
{
case TextAntialiasingMode::Cleartype:
- dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
+ mode = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE;
break;
case TextAntialiasingMode::Aliased:
- dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED);
+ mode = D2D1_TEXT_ANTIALIAS_MODE_ALIASED;
break;
- case TextAntialiasingMode::Grayscale:
default:
- dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
+ mode = D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE;
break;
}
+
+ _renderEngine->SetAntialiasingMode(mode);
}
// Method Description:
@@ -1296,7 +1309,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (_renderEngine)
{
auto lock = _terminal->LockForWriting();
- _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(opacity));
+ const auto backgroundIsOpaque = opacity == 1.0 && _settings.BackgroundImage().empty();
+ _renderEngine->SetDefaultTextBackgroundOpacity(static_cast(backgroundIsOpaque));
}
}
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 5beebf9e91b..e9fca464446 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -15,11 +15,8 @@
#pragma once
-#include "EventArgs.h"
#include "ControlCore.g.h"
#include "../../renderer/base/Renderer.hpp"
-#include "../../renderer/dx/DxRenderer.hpp"
-#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../buffer/out/search.h"
#include "cppwinrt_utils.h"
@@ -188,7 +185,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// As _renderer has a dependency on _renderEngine (through a raw pointer)
// we must ensure the _renderer is deallocated first.
// (C++ class members are destroyed in reverse order.)
- std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine{ nullptr };
+ std::unique_ptr<::Microsoft::Console::Render::IRenderEngine> _renderEngine{ nullptr };
std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer{ nullptr };
IControlSettings _settings{ nullptr };
@@ -248,7 +245,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
#pragma endregion
void _raiseReadOnlyWarning();
- void _updateAntiAliasingMode(::Microsoft::Console::Render::DxEngine* const dxEngine);
+ void _updateAntiAliasingMode();
void _connectionOutputHandler(const hstring& hstr);
void _updateHoveredCell(const std::optional terminalPosition);
diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h
index 71312afc38d..5dd818d9d62 100644
--- a/src/cascadia/TerminalControl/ControlInteractivity.h
+++ b/src/cascadia/TerminalControl/ControlInteractivity.h
@@ -22,6 +22,7 @@
#include "cppwinrt_utils.h"
#include "ControlCore.h"
+#include "../../renderer/uia/UiaRenderer.hpp"
namespace ControlUnitTests
{
diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl
index 1b67df91f0e..0466b515220 100644
--- a/src/cascadia/TerminalControl/IControlSettings.idl
+++ b/src/cascadia/TerminalControl/IControlSettings.idl
@@ -30,7 +30,7 @@ namespace Microsoft.Terminal.Control
Boolean UseAcrylic;
ScrollbarState ScrollState;
-
+ Boolean UseAtlasEngine;
String FontFace;
Int32 FontSize;
Windows.UI.Text.FontWeight FontWeight;
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index afae49d9dae..bd82cf1bab6 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -3,17 +3,16 @@
#include "pch.h"
#include "TermControl.h"
-#include
-#include
+
#include
#include
-#include
#include
+
+#include "TermControlAutomationPeer.h"
#include "../../types/inc/GlyphWidth.hpp"
-#include "../../types/inc/Utils.hpp"
+#include "../../renderer/atlas/AtlasEngine.h"
#include "TermControl.g.cpp"
-#include "TermControlAutomationPeer.h"
using namespace ::Microsoft::Console::Types;
using namespace ::Microsoft::Console::VirtualTerminal;
@@ -1828,13 +1827,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const winrt::Windows::Foundation::Size initialSize{ cols, rows };
- return GetProposedDimensions(initialSize,
- settings.FontSize(),
- settings.FontWeight(),
- settings.FontFace(),
- settings.ScrollState(),
- settings.Padding(),
- dpi);
+ return GetProposedDimensions(settings, dpi, initialSize);
}
// Function Description:
@@ -1855,16 +1848,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// caller knows what monitor the control is about to appear on.
// Return Value:
// - a size containing the requested dimensions in pixels.
- winrt::Windows::Foundation::Size TermControl::GetProposedDimensions(const winrt::Windows::Foundation::Size& initialSizeInChars,
- const int32_t& fontHeight,
- const winrt::Windows::UI::Text::FontWeight& fontWeight,
- const winrt::hstring& fontFace,
- const ScrollbarState& scrollState,
- const winrt::hstring& padding,
- const uint32_t dpi)
+ winrt::Windows::Foundation::Size TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi, const winrt::Windows::Foundation::Size& initialSizeInChars)
{
const auto cols = ::base::saturated_cast(initialSizeInChars.Width);
const auto rows = ::base::saturated_cast(initialSizeInChars.Height);
+ const auto fontSize = settings.FontSize();
+ const auto fontWeight = settings.FontWeight();
+ const auto fontFace = settings.FontFace();
+ const auto scrollState = settings.ScrollState();
+ const auto padding = settings.Padding();
// Initialize our font information.
// The font width doesn't terribly matter, we'll only be using the
@@ -1873,28 +1865,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// The family is only used to determine if the font is truetype or
// not, but DX doesn't use that info at all.
// The Codepage is additionally not actually used by the DX engine at all.
- FontInfo actualFont = { fontFace, 0, fontWeight.Weight, { 0, gsl::narrow_cast(fontHeight) }, CP_UTF8, false };
+ FontInfo actualFont = { fontFace, 0, fontWeight.Weight, { 0, gsl::narrow_cast(fontSize) }, CP_UTF8, false };
FontInfoDesired desiredFont = { actualFont };
// Create a DX engine and initialize it with our font and DPI. We'll
// then use it to measure how much space the requested rows and columns
// will take up.
// TODO: MSFT:21254947 - use a static function to do this instead of
- // instantiating a DxEngine
+ // instantiating a DxEngine/AtlasEngine.
// GH#10211 - UNDER NO CIRCUMSTANCE should this fail. If it does, the
// whole app will crash instantaneously on launch, which is no good.
- auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
- LOG_IF_FAILED(dxEngine->UpdateDpi(dpi));
- LOG_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont));
+ double scale;
+ if (Feature_AtlasEngine::IsEnabled() && settings.UseAtlasEngine())
+ {
+ auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
+ LOG_IF_FAILED(engine->UpdateDpi(dpi));
+ LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
+ scale = engine->GetScaling();
+ }
+ else
+ {
+ auto engine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
+ LOG_IF_FAILED(engine->UpdateDpi(dpi));
+ LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
+ scale = engine->GetScaling();
+ }
- const auto scale = dxEngine->GetScaling();
- const auto fontSize = actualFont.GetSize();
+ const auto actualFontSize = actualFont.GetSize();
// UWP XAML scrollbars aren't guaranteed to be the same size as the
// ComCtl scrollbars, but it's certainly close enough.
const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi);
- double width = cols * fontSize.X;
+ double width = cols * actualFontSize.X;
// Reserve additional space if scrollbar is intended to be visible
if (scrollState == ScrollbarState::Visible)
@@ -1902,7 +1905,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
width += scrollbarSize;
}
- double height = rows * fontSize.Y;
+ double height = rows * actualFontSize.Y;
const auto thickness = ParseThicknessFromPadding(padding);
// GH#2061 - make sure to account for the size the padding _will be_ scaled to
width += scale * (thickness.Left + thickness.Right);
@@ -1962,13 +1965,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const winrt::Windows::Foundation::Size minSize{ 1, 1 };
const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
const auto dpi = ::base::saturated_cast(USER_DEFAULT_SCREEN_DPI * scaleFactor);
- return GetProposedDimensions(minSize,
- _settings.FontSize(),
- _settings.FontWeight(),
- _settings.FontFace(),
- _settings.ScrollState(),
- _settings.Padding(),
- dpi);
+ return GetProposedDimensions(_settings, dpi, minSize);
}
}
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 8f758fb69dd..cde51ec1287 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -92,13 +92,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void Settings(IControlSettings newSettings);
static Windows::Foundation::Size GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi);
- static Windows::Foundation::Size GetProposedDimensions(const winrt::Windows::Foundation::Size& initialSizeInChars,
- const int32_t& fontSize,
- const winrt::Windows::UI::Text::FontWeight& fontWeight,
- const winrt::hstring& fontFace,
- const ScrollbarState& scrollState,
- const winrt::hstring& padding,
- const uint32_t dpi);
+ static Windows::Foundation::Size GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi, const winrt::Windows::Foundation::Size& initialSizeInChars);
void BellLightOn();
diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
index 3ce1d8a5292..a366650fc73 100644
--- a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
+++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
@@ -147,6 +147,7 @@
+
diff --git a/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj b/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj
index 9ced8ea9fcc..2f750978bba 100644
--- a/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj
+++ b/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj
@@ -37,6 +37,9 @@
{3ae13314-1939-4dfa-9c14-38ca0834050c}
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{48d21369-3d7b-4431-9967-24e81292cf62}
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.cpp b/src/cascadia/TerminalSettingsEditor/Profiles.cpp
index 6c507ecf456..9c476fc3b2a 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles.cpp
+++ b/src/cascadia/TerminalSettingsEditor/Profiles.cpp
@@ -246,13 +246,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return _profile.HasUnfocusedAppearance();
}
- bool ProfileViewModel::EditableUnfocusedAppearance()
+ bool ProfileViewModel::EditableUnfocusedAppearance() const noexcept
{
- if constexpr (Feature_EditableUnfocusedAppearance::IsEnabled())
- {
- return true;
- }
- return false;
+ return Feature_EditableUnfocusedAppearance::IsEnabled();
}
bool ProfileViewModel::ShowUnfocusedAppearance()
@@ -286,6 +282,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return _unfocusedAppearanceViewModel;
}
+ bool ProfileViewModel::AtlasEngineAvailable() const noexcept
+ {
+ return Feature_AtlasEngine::IsEnabled();
+ }
+
bool ProfileViewModel::UseParentProcessDirectory()
{
return StartingDirectory().empty();
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.h b/src/cascadia/TerminalSettingsEditor/Profiles.h
index 40ecd9c452d..7c12d975251 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles.h
+++ b/src/cascadia/TerminalSettingsEditor/Profiles.h
@@ -61,12 +61,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Editor::AppearanceViewModel DefaultAppearance();
Editor::AppearanceViewModel UnfocusedAppearance();
bool HasUnfocusedAppearance();
- bool EditableUnfocusedAppearance();
+ bool EditableUnfocusedAppearance() const noexcept;
bool ShowUnfocusedAppearance();
-
void CreateUnfocusedAppearance(const Windows::Foundation::Collections::IMapView& schemes,
const IHostedInWindow& windowRoot);
void DeleteUnfocusedAppearance();
+ bool AtlasEngineAvailable() const noexcept;
WINRT_PROPERTY(bool, IsBaseLayer, false);
@@ -95,6 +95,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
OBSERVABLE_PROJECTED_SETTING(_profile, SnapOnInput);
OBSERVABLE_PROJECTED_SETTING(_profile, AltGrAliasing);
OBSERVABLE_PROJECTED_SETTING(_profile, BellStyle);
+ OBSERVABLE_PROJECTED_SETTING(_profile, UseAtlasEngine);
private:
Model::Profile _profile;
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.idl b/src/cascadia/TerminalSettingsEditor/Profiles.idl
index 5d45a612cbb..a4746d560d3 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles.idl
+++ b/src/cascadia/TerminalSettingsEditor/Profiles.idl
@@ -32,6 +32,7 @@ namespace Microsoft.Terminal.Settings.Editor
Boolean EditableUnfocusedAppearance { get; };
Boolean ShowUnfocusedAppearance { get; };
AppearanceViewModel UnfocusedAppearance { get; };
+ Boolean AtlasEngineAvailable { get; };
void CreateUnfocusedAppearance(Windows.Foundation.Collections.IMapView Schemes, IHostedInWindow WindowRoot);
void DeleteUnfocusedAppearance();
@@ -61,6 +62,7 @@ namespace Microsoft.Terminal.Settings.Editor
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, SnapOnInput);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AltGrAliasing);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Settings.Model.BellStyle, BellStyle);
+ OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, UseAtlasEngine);
}
runtimeclass DeleteProfileEventArgs
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles.xaml b/src/cascadia/TerminalSettingsEditor/Profiles.xaml
index 230cfc2e874..c860578aa2b 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles.xaml
+++ b/src/cascadia/TerminalSettingsEditor/Profiles.xaml
@@ -319,7 +319,7 @@
+ Visibility="{x:Bind State.Profile.EditableUnfocusedAppearance}">
+
+
+
+
+
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index 0ebd1cef410..6304be967d8 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -927,6 +927,10 @@
Controls what happens when the application emits a BEL character.
A description for what the "bell style" setting does. Presented near "Profile_BellStyle".{Locked="BEL"}
+
+ Enable experimental text rendering engine
+ An option to enable an experimental text rendering engine
+
Audible
An option to choose from for the "bell style" setting. When selected, an audible cue is used to notify the user.
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index 6719818e745..a190e01669c 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -73,7 +73,8 @@ Author(s):
X(hstring, Icon, "icon", L"\uE756") \
X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Graceful) \
X(hstring, TabTitle, "tabTitle") \
- X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible)
+ X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
+ X(bool, UseAtlasEngine, "experimental.useAtlasEngine", false)
#define MTSM_FONT_SETTINGS(X) \
X(hstring, FontFace, "face", DEFAULT_FONT_FACE) \
diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl
index 746caad252b..35df4468350 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.idl
+++ b/src/cascadia/TerminalSettingsModel/Profile.idl
@@ -79,5 +79,6 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Boolean, SnapOnInput);
INHERITABLE_PROFILE_SETTING(Boolean, AltGrAliasing);
INHERITABLE_PROFILE_SETTING(BellStyle, BellStyle);
+ INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine);
}
}
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
index 5e4d1eeea8a..30d27174d9b 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp
@@ -297,6 +297,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_SuppressApplicationTitle = profile.SuppressApplicationTitle();
}
+ _UseAtlasEngine = profile.UseAtlasEngine();
_ScrollState = profile.ScrollState();
_AntialiasingMode = profile.AntialiasingMode();
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
index 90b7b60fef7..4d4686af368 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h
@@ -146,6 +146,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, hstring, EnvironmentVariables);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::ScrollbarState, ScrollState, Microsoft::Terminal::Control::ScrollbarState::Visible);
+ INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAtlasEngine, false);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale);
diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h
index 485f9b146b8..8128339b860 100644
--- a/src/cascadia/UnitTests_Control/MockControlSettings.h
+++ b/src/cascadia/UnitTests_Control/MockControlSettings.h
@@ -75,6 +75,7 @@ namespace ControlUnitTests
WINRT_PROPERTY(winrt::hstring, EnvironmentVariables);
WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible);
+ WINRT_PROPERTY(bool, UseAtlasEngine, false);
WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale);
diff --git a/src/features.xml b/src/features.xml
index 86fd1e2d3b2..8f489117b9c 100644
--- a/src/features.xml
+++ b/src/features.xml
@@ -79,4 +79,14 @@
+
+
+ Feature_AtlasEngine
+ If enabled, AtlasEngine and the experimental.useAtlasEngine setting are compiled into the project
+ AlwaysEnabled
+
+ Release
+ WindowsInbox
+
+
diff --git a/src/host/exe/Host.EXE.vcxproj b/src/host/exe/Host.EXE.vcxproj
index 3967d752f35..c21bb27c8a2 100644
--- a/src/host/exe/Host.EXE.vcxproj
+++ b/src/host/exe/Host.EXE.vcxproj
@@ -43,6 +43,9 @@
{af0a096a-8b3a-4949-81ef-7df8f0fee91f}
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{48d21369-3d7b-4431-9967-24e81292cf62}
diff --git a/src/host/ft_fuzzer/Host.FuzzWrapper.vcxproj b/src/host/ft_fuzzer/Host.FuzzWrapper.vcxproj
index e351317812f..8486a9e4894 100644
--- a/src/host/ft_fuzzer/Host.FuzzWrapper.vcxproj
+++ b/src/host/ft_fuzzer/Host.FuzzWrapper.vcxproj
@@ -38,6 +38,9 @@
{af0a096a-8b3a-4949-81ef-7df8f0fee91f}
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{48d21369-3d7b-4431-9967-24e81292cf62}
@@ -71,6 +74,7 @@
..;%(AdditionalIncludeDirectories)
+ winmm.lib;%(AdditionalDependencies)
Console
diff --git a/src/host/ft_host/CJK_DbcsTests.cpp b/src/host/ft_host/CJK_DbcsTests.cpp
index 499549944f1..aa98a3f6447 100644
--- a/src/host/ft_host/CJK_DbcsTests.cpp
+++ b/src/host/ft_host/CJK_DbcsTests.cpp
@@ -2320,9 +2320,6 @@ void ReadStringWithReadConsoleInputAHelper(HANDLE hIn, PCSTR pszExpectedText, si
while (cchRead < cchExpectedText)
{
- // expected read is either the size of the buffer or the number of characters remaining, whichever is smaller.
- DWORD const dwReadExpected = (DWORD)std::min(cbBuffer, cchExpectedText - cchRead);
-
DWORD dwRead;
if (!VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputA(hIn, irRead, (DWORD)cbBuffer, &dwRead), L"Attempt to read input into buffer."))
{
diff --git a/src/host/globals.h b/src/host/globals.h
index a955e991f87..3b116ea8ef5 100644
--- a/src/host/globals.h
+++ b/src/host/globals.h
@@ -22,10 +22,7 @@ Revision History:
#include "ConsoleArguments.hpp"
#include "ApiRoutines.h"
-#include "../renderer/inc/IRenderData.hpp"
-#include "../renderer/inc/IRenderEngine.hpp"
-#include "../renderer/inc/IRenderer.hpp"
-#include "../renderer/inc/IFontDefaultList.hpp"
+#include "../renderer/base/Renderer.hpp"
#include "../server/DeviceComm.h"
#include "../server/ConDrvDeviceComm.h"
@@ -62,7 +59,7 @@ class Globals
std::vector WordDelimiters;
- Microsoft::Console::Render::IRenderer* pRender;
+ Microsoft::Console::Render::Renderer* pRender;
Microsoft::Console::Render::IFontDefaultList* pFontDefaultList;
diff --git a/src/host/settings.cpp b/src/host/settings.cpp
index d1443cb3224..d5918166870 100644
--- a/src/host/settings.cpp
+++ b/src/host/settings.cpp
@@ -58,7 +58,7 @@ Settings::Settings() :
_fInterceptCopyPaste(0),
_DefaultForeground(INVALID_COLOR),
_DefaultBackground(INVALID_COLOR),
- _fUseDx(false),
+ _fUseDx(UseDx::Disabled),
_fCopyColor(false)
{
_dwScreenBufferSize.X = 80;
@@ -820,12 +820,9 @@ void Settings::SetTerminalScrolling(const bool terminalScrollingEnabled) noexcep
_TerminalScrolling = terminalScrollingEnabled;
}
-// Routine Description:
-// - Determines whether our primary renderer should be DirectX or GDI.
-// - This is based on user preference and velocity hold back state.
-// Return Value:
-// - True means use DirectX renderer. False means use GDI renderer.
-bool Settings::GetUseDx() const noexcept
+// Determines whether our primary renderer should be DirectX or GDI.
+// This is based on user preference and velocity hold back state.
+UseDx Settings::GetUseDx() const noexcept
{
return _fUseDx;
}
diff --git a/src/host/settings.hpp b/src/host/settings.hpp
index 896de0e20c8..3963be4bc39 100644
--- a/src/host/settings.hpp
+++ b/src/host/settings.hpp
@@ -26,6 +26,13 @@ constexpr unsigned short MIN_WINDOW_OPACITY = 0x4D; // 0x4D is approximately 30%
#include "ConsoleArguments.hpp"
#include "../inc/conattrs.hpp"
+enum class UseDx : DWORD
+{
+ Disabled = 0,
+ DxEngine,
+ AtlasEngine,
+};
+
class Settings
{
public:
@@ -188,7 +195,7 @@ class Settings
bool IsTerminalScrolling() const noexcept;
void SetTerminalScrolling(const bool terminalScrollingEnabled) noexcept;
- bool GetUseDx() const noexcept;
+ UseDx GetUseDx() const noexcept;
bool GetCopyColor() const noexcept;
private:
@@ -232,7 +239,7 @@ class Settings
bool _fAutoReturnOnNewline;
bool _fRenderGridWorldwide;
bool _fScreenReversed;
- bool _fUseDx;
+ UseDx _fUseDx;
bool _fCopyColor;
std::array _colorTable;
diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj
index 486965b2fb9..6b5bedf1b11 100644
--- a/src/host/ut_host/Host.UnitTests.vcxproj
+++ b/src/host/ut_host/Host.UnitTests.vcxproj
@@ -52,6 +52,9 @@
{ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00}
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{48d21369-3d7b-4431-9967-24e81292cf62}
diff --git a/src/interactivity/win32/lib/win32.LIB.vcxproj b/src/interactivity/win32/lib/win32.LIB.vcxproj
index da8965739b3..a29cec6458d 100644
--- a/src/interactivity/win32/lib/win32.LIB.vcxproj
+++ b/src/interactivity/win32/lib/win32.LIB.vcxproj
@@ -6,7 +6,7 @@
win32
InteractivityWin32
ConInteractivityWin32Lib
- StaticLibrary
+ StaticLibrary
@@ -61,6 +61,9 @@
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{48d21369-3d7b-4431-9967-24e81292cf62}
diff --git a/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj b/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj
index 2330bee3aa6..fa2fa30b086 100644
--- a/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj
+++ b/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj
@@ -22,6 +22,9 @@
{ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00}
+
+ {8222900C-8B6C-452A-91AC-BE95DB04B95F}
+
{48d21369-3d7b-4431-9967-24e81292cf62}
@@ -76,4 +79,4 @@
-
\ No newline at end of file
+
diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp
index 99ae12628ab..518dc87473f 100644
--- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp
+++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp
@@ -735,7 +735,6 @@ class UiaTextRangeTests
TEST_METHOD(CanMoveByCharacter)
{
const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().RightInclusive();
- const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1);
// GH#6986: This is used as the "end of the buffer" to help screen readers run faster
// instead of parsing through thousands of empty lines of text.
@@ -824,7 +823,6 @@ class UiaTextRangeTests
TEST_METHOD(CanMoveByLine)
{
const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1;
- const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1);
// GH#6986: This is used as the "end of the buffer" to help screen readers run faster
// instead of parsing through thousands of empty lines of text.
@@ -913,7 +911,6 @@ class UiaTextRangeTests
TEST_METHOD(CanMoveEndpointByUnitCharacter)
{
const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1;
- const SHORT bottomRow = static_cast(_pTextBuffer->TotalRowCount() - 1);
// GH#6986: This is used as the "end of the buffer" to help screen readers run faster
// instead of parsing through thousands of empty lines of text.
@@ -1197,7 +1194,6 @@ class UiaTextRangeTests
TEST_METHOD(CanMoveEndpointByUnitDocument)
{
- const SHORT lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1;
const SHORT bottomRow = gsl::narrow(_pTextBuffer->TotalRowCount() - 1);
// GH#6986: This is used as the "end of the buffer" to help screen readers run faster
diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp
index e5ea8173553..560a48c04c1 100644
--- a/src/interactivity/win32/window.cpp
+++ b/src/interactivity/win32/window.cpp
@@ -9,17 +9,14 @@
#include "window.hpp"
#include "windowio.hpp"
#include "windowdpiapi.hpp"
-#include "windowmetrics.hpp"
+#include "WindowMetrics.hpp"
#include "../../inc/conint.h"
#include "../../host/globals.h"
#include "../../host/dbcs.h"
-#include "../../host/getset.h"
#include "../../host/misc.h"
-#include "../../host/_output.h"
#include "../../host/output.h"
-#include "../../host/renderData.hpp"
#include "../../host/scrolling.hpp"
#include "../../host/srvinit.h"
#include "../../host/stream.h"
@@ -29,6 +26,9 @@
#include "../../renderer/base/renderer.hpp"
#include "../../renderer/gdi/gdirenderer.hpp"
+#if TIL_FEATURE_ATLASENGINE_ENABLED
+#include "../../renderer/atlas/AtlasEngine.h"
+#endif
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
#include "../../renderer/dx/DxRenderer.hpp"
#endif
@@ -209,16 +209,20 @@ void Window::_UpdateSystemMetrics() const
// Ensure we have appropriate system metrics before we start constructing the window.
_UpdateSystemMetrics();
- const bool useDx = pSettings->GetUseDx();
+ const auto useDx = pSettings->GetUseDx();
GdiEngine* pGdiEngine = nullptr;
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
- [[maybe_unused]] DxEngine* pDxEngine = nullptr;
+ DxEngine* pDxEngine = nullptr;
+#endif
+#if TIL_FEATURE_ATLASENGINE_ENABLED
+ AtlasEngine* pAtlasEngine = nullptr;
#endif
try
{
-#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
- if (useDx)
+ switch (useDx)
{
+#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
+ case UseDx::DxEngine:
pDxEngine = new DxEngine();
// TODO: MSFT:21255595 make this less gross
// Manually set the Dx Engine to Hwnd mode. When we're trying to
@@ -227,12 +231,18 @@ void Window::_UpdateSystemMetrics() const
// math in the hwnd mode, not the Composition mode.
THROW_IF_FAILED(pDxEngine->SetHwnd(nullptr));
g.pRender->AddRenderEngine(pDxEngine);
- }
- else
+ break;
#endif
- {
+#if TIL_FEATURE_ATLASENGINE_ENABLED
+ case UseDx::AtlasEngine:
+ pAtlasEngine = new AtlasEngine();
+ g.pRender->AddRenderEngine(pAtlasEngine);
+ break;
+#endif
+ default:
pGdiEngine = new GdiEngine();
g.pRender->AddRenderEngine(pGdiEngine);
+ break;
}
}
catch (...)
@@ -324,7 +334,7 @@ void Window::_UpdateSystemMetrics() const
_hWnd = hWnd;
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
- if (useDx)
+ if (pDxEngine)
{
status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pDxEngine->SetHwnd(hWnd))));
@@ -334,6 +344,13 @@ void Window::_UpdateSystemMetrics() const
}
}
else
+#endif
+#if TIL_FEATURE_ATLASENGINE_ENABLED
+ if (pAtlasEngine)
+ {
+ status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pAtlasEngine->SetHwnd(hWnd))));
+ }
+ else
#endif
{
status = NTSTATUS_FROM_WIN32(HRESULT_CODE((pGdiEngine->SetHwnd(hWnd))));
diff --git a/src/propslib/RegistrySerialization.cpp b/src/propslib/RegistrySerialization.cpp
index 09995d680a1..d9e9bcfbeb5 100644
--- a/src/propslib/RegistrySerialization.cpp
+++ b/src/propslib/RegistrySerialization.cpp
@@ -63,7 +63,7 @@ const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_PropertyMa
{ _RegPropertyType::Dword, CONSOLE_REGISTRY_DEFAULTFOREGROUND, SET_FIELD_AND_SIZE(_DefaultForeground) },
{ _RegPropertyType::Dword, CONSOLE_REGISTRY_DEFAULTBACKGROUND, SET_FIELD_AND_SIZE(_DefaultBackground) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_TERMINALSCROLLING, SET_FIELD_AND_SIZE(_TerminalScrolling) },
- { _RegPropertyType::Boolean, CONSOLE_REGISTRY_USEDX, SET_FIELD_AND_SIZE(_fUseDx) },
+ { _RegPropertyType::Dword, CONSOLE_REGISTRY_USEDX, SET_FIELD_AND_SIZE(_fUseDx) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_COPYCOLOR, SET_FIELD_AND_SIZE(_fCopyColor) }
};
@@ -251,7 +251,8 @@ NTSTATUS RegistrySerialization::s_OpenKey(_In_opt_ HKEY const hKey, _In_ PCWSTR
[[nodiscard]]
NTSTATUS RegistrySerialization::s_DeleteValue(const HKEY hKey, _In_ PCWSTR const pwszValueName)
{
- return NTSTATUS_FROM_WIN32(RegDeleteKeyValueW(hKey, nullptr, pwszValueName));
+ const auto result = RegDeleteKeyValueW(hKey, nullptr, pwszValueName);
+ return result == ERROR_FILE_NOT_FOUND ? S_OK : NTSTATUS_FROM_WIN32(result);
}
// Routine Description:
diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp
new file mode 100644
index 00000000000..f4b3ac9e199
--- /dev/null
+++ b/src/renderer/atlas/AtlasEngine.api.cpp
@@ -0,0 +1,605 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "AtlasEngine.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,
+// while any of the API methods (like AtlasEngine::Invalidate) might be called concurrently.
+// The usage of _r fields is unsafe as those are accessed and written to by the Present() method.
+
+#pragma warning(disable : 4100) // '...': unreferenced formal parameter
+// Disable a bunch of warnings which get in the way of writing performant code.
+#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23).
+#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
+#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
+
+using namespace Microsoft::Console::Render;
+
+// Like gsl::narrow but returns a HRESULT.
+#pragma warning(push)
+#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1).
+template
+constexpr HRESULT api_narrow(U val, T& out) noexcept
+{
+ out = static_cast(val);
+ return static_cast(out) != val || (std::is_signed_v != std::is_signed_v && out < T{} != val < U{}) ? HRESULT_FROM_WIN32(ERROR_ARITHMETIC_OVERFLOW) : S_OK;
+}
+#pragma warning(pop)
+
+template
+constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2& out) noexcept
+{
+ return api_narrow(x, out.x) | api_narrow(y, out.y);
+}
+
+#pragma region IRenderEngine
+
+[[nodiscard]] HRESULT AtlasEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept
+{
+ //assert(psrRegion->Top < psrRegion->Bottom && psrRegion->Top >= 0 && psrRegion->Bottom <= _api.cellCount.y);
+
+ // BeginPaint() protects against invalid out of bounds numbers.
+ _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, gsl::narrow_cast(psrRegion->Top));
+ _api.invalidatedRows.y = std::max(_api.invalidatedRows.y, gsl::narrow_cast(psrRegion->Bottom));
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept
+{
+ //assert(psrRegion->Left <= psrRegion->Right && psrRegion->Left >= 0 && psrRegion->Right <= _api.cellCount.x);
+ //assert(psrRegion->Top <= psrRegion->Bottom && psrRegion->Top >= 0 && psrRegion->Bottom <= _api.cellCount.y);
+
+ const auto left = gsl::narrow_cast(psrRegion->Left);
+ const auto top = gsl::narrow_cast(psrRegion->Top);
+ const auto right = gsl::narrow_cast(psrRegion->Right);
+ const auto bottom = gsl::narrow_cast(psrRegion->Bottom);
+
+ // BeginPaint() protects against invalid out of bounds numbers.
+ _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, left);
+ _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, top);
+ _api.invalidatedCursorArea.right = std::max(_api.invalidatedCursorArea.right, right);
+ _api.invalidatedCursorArea.bottom = std::max(_api.invalidatedCursorArea.bottom, bottom);
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept
+{
+ const auto top = prcDirtyClient->top / _api.fontMetrics.cellSize.y;
+ const auto bottom = prcDirtyClient->bottom / _api.fontMetrics.cellSize.y;
+
+ // BeginPaint() protects against invalid out of bounds numbers.
+ SMALL_RECT rect;
+ rect.Top = gsl::narrow_cast(top);
+ rect.Bottom = gsl::narrow_cast(bottom);
+ return Invalidate(&rect);
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateSelection(const std::vector& rectangles) noexcept
+{
+ for (const auto& rect : rectangles)
+ {
+ // BeginPaint() protects against invalid out of bounds numbers.
+ // TODO: rect can contain invalid out of bounds coordinates when the selection is being
+ // dragged outside of the viewport (and the window begins scrolling automatically).
+ _api.invalidatedRows.x = gsl::narrow_cast(std::min(_api.invalidatedRows.x, std::max(0, rect.Top)));
+ _api.invalidatedRows.y = gsl::narrow_cast(std::max(_api.invalidatedRows.y, std::max(0, rect.Bottom)));
+ }
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept
+{
+ const auto delta = pcoordDelta->Y;
+ if (delta == 0)
+ {
+ return S_OK;
+ }
+
+ _api.scrollOffset = gsl::narrow_cast(clamp(_api.scrollOffset + delta, i16min, i16max));
+
+ // InvalidateScroll() is a "synchronous" API. Any Invalidate()s after
+ // a InvalidateScroll() refer to the new viewport after the scroll.
+ // --> We need to shift the current invalidation rectangles as well.
+
+ _api.invalidatedCursorArea.top = gsl::narrow_cast(clamp(_api.invalidatedCursorArea.top + delta, u16min, u16max));
+ _api.invalidatedCursorArea.bottom = gsl::narrow_cast(clamp(_api.invalidatedCursorArea.bottom + delta, u16min, u16max));
+
+ if (delta < 0)
+ {
+ _api.invalidatedRows.x = gsl::narrow_cast(clamp(_api.invalidatedRows.x + delta, u16min, u16max));
+ _api.invalidatedRows.y = _api.cellCount.y;
+ }
+ else
+ {
+ _api.invalidatedRows.x = 0;
+ _api.invalidatedRows.y = gsl::narrow_cast(clamp(_api.invalidatedRows.y + delta, u16min, u16max));
+ }
+
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateAll() noexcept
+{
+ _api.invalidatedRows = invalidatedRowsAll;
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept
+{
+ RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
+ *pForcePaint = false;
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::InvalidateTitle(const std::wstring_view proposedTitle) noexcept
+{
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Title);
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, _Out_ FontInfo& fontInfo) noexcept
+{
+ return UpdateFont(fontInfoDesired, fontInfo, {}, {});
+}
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateSoftFont(const gsl::span bitPattern, const SIZE cellSize, const size_t centeringHint) noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateDpi(const int dpi) noexcept
+{
+ u16 newDPI;
+ RETURN_IF_FAILED(api_narrow(dpi, newDPI));
+
+ if (_api.dpi != newDPI)
+ {
+ _api.dpi = newDPI;
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Font);
+ }
+
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateViewport(const SMALL_RECT srNewViewport) noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::GetProposedFont(const FontInfoDesired& fontInfoDesired, _Out_ FontInfo& fontInfo, const int dpi) noexcept
+try
+{
+ // One day I'm going to implement GDI for AtlasEngine...
+ // Until then this code is work in progress.
+#if 0
+ wil::unique_hfont hfont;
+
+ // This block of code (for GDI fonts) is unfinished.
+ if (fontInfoDesired.IsDefaultRasterFont())
+ {
+ hfont.reset(static_cast(GetStockObject(OEM_FIXED_FONT)));
+ RETURN_HR_IF(E_FAIL, !hfont);
+ }
+ else if (requestedFaceName == DEFAULT_RASTER_FONT_FACENAME)
+ {
+ // GDI Windows Font Mapping reference:
+ // https://msdn.microsoft.com/en-us/library/ms969909.aspx
+
+ LOGFONTW lf;
+ lf.lfHeight = -MulDiv(requestedSize.Y, dpi, 72);
+ lf.lfWidth = 0;
+ lf.lfEscapement = 0;
+ lf.lfOrientation = 0;
+ lf.lfWeight = requestedWeight;
+ lf.lfItalic = FALSE;
+ lf.lfUnderline = FALSE;
+ lf.lfStrikeOut = FALSE;
+ lf.lfCharSet = OEM_CHARSET;
+ lf.lfOutPrecision = OUT_RASTER_PRECIS;
+ lf.lfClipPrecision = CLIP_DEFAULT_PRECIS;
+ lf.lfQuality = PROOF_QUALITY; // disables scaling for rasterized fonts
+ lf.lfPitchAndFamily = FIXED_PITCH | FF_MODERN;
+ // .size() only includes regular characters, but we also want to copy the trailing \0, so +1 it is.
+ memcpy(&lf.lfFaceName[0], &DEFAULT_RASTER_FONT_FACENAME[0], sizeof(DEFAULT_RASTER_FONT_FACENAME));
+
+ hfont.reset(CreateFontIndirectW(&lf));
+ RETURN_HR_IF(E_FAIL, !hfont);
+ }
+
+ if (hfont)
+ {
+// wil::unique_any_t's constructor says: "should not be WI_NOEXCEPT (may forward to a throwing constructor)".
+// The constructor we use by default doesn't throw.
+#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6).
+ wil::unique_hdc hdc{ CreateCompatibleDC(nullptr) };
+ RETURN_HR_IF(E_FAIL, !hdc);
+
+ DeleteObject(SelectObject(hdc.get(), hfont.get()));
+
+ SIZE sz;
+ RETURN_HR_IF(E_FAIL, !GetTextExtentPoint32W(hdc.get(), L"M", 1, &sz));
+ resultingCellSize.X = gsl::narrow(sz.cx);
+ resultingCellSize.Y = gsl::narrow(sz.cy);
+ }
+#endif
+
+ _resolveFontMetrics(fontInfoDesired, fontInfo);
+ return S_OK;
+}
+CATCH_RETURN()
+
+[[nodiscard]] HRESULT AtlasEngine::GetDirtyArea(gsl::span& area) noexcept
+{
+ area = gsl::span{ &_api.dirtyRect, 1 };
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept
+{
+ RETURN_HR_IF_NULL(E_INVALIDARG, pFontSize);
+ pFontSize->X = gsl::narrow_cast(_api.fontMetrics.cellSize.x);
+ pFontSize->Y = gsl::narrow_cast(_api.fontMetrics.cellSize.y);
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept
+{
+ RETURN_HR_IF_NULL(E_INVALIDARG, pResult);
+
+ wil::com_ptr textLayout;
+ RETURN_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast(glyph.size()), _getTextFormat(false, false), FLT_MAX, FLT_MAX, textLayout.addressof()));
+
+ DWRITE_TEXT_METRICS metrics;
+ RETURN_IF_FAILED(textLayout->GetMetrics(&metrics));
+
+ *pResult = static_cast(std::ceil(metrics.width)) > _api.fontMetrics.cellSize.x;
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateTitle(const std::wstring_view newTitle) noexcept
+{
+ return S_OK;
+}
+
+#pragma endregion
+
+#pragma region DxRenderer
+
+HRESULT AtlasEngine::Enable() noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] bool AtlasEngine::GetRetroTerminalEffect() const noexcept
+{
+ return false;
+}
+
+[[nodiscard]] float AtlasEngine::GetScaling() const noexcept
+{
+ return static_cast(_api.dpi) / static_cast(USER_DEFAULT_SCREEN_DPI);
+}
+
+[[nodiscard]] HANDLE AtlasEngine::GetSwapChainHandle()
+{
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Device))
+ {
+ _createResources();
+ WI_ClearFlag(_api.invalidations, ApiInvalidations::Device);
+ }
+
+ return _api.swapChainHandle.get();
+}
+
+[[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept
+{
+ assert(_api.fontMetrics.cellSize.x != 0);
+ assert(_api.fontMetrics.cellSize.y != 0);
+ return Types::Viewport::FromDimensions(viewInPixels.Origin(), COORD{ gsl::narrow_cast(viewInPixels.Width() / _api.fontMetrics.cellSize.x), gsl::narrow_cast(viewInPixels.Height() / _api.fontMetrics.cellSize.y) });
+}
+
+[[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept
+{
+ assert(_api.fontMetrics.cellSize.x != 0);
+ assert(_api.fontMetrics.cellSize.y != 0);
+ return Types::Viewport::FromDimensions(viewInCharacters.Origin(), COORD{ gsl::narrow_cast(viewInCharacters.Width() * _api.fontMetrics.cellSize.x), gsl::narrow_cast(viewInCharacters.Height() * _api.fontMetrics.cellSize.y) });
+}
+
+void AtlasEngine::SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept
+{
+ const auto mode = gsl::narrow_cast(antialiasingMode);
+ if (_api.antialiasingMode != mode)
+ {
+ _api.antialiasingMode = mode;
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Font);
+ }
+}
+
+void AtlasEngine::SetCallback(std::function pfn) noexcept
+{
+ _api.swapChainChangedCallback = std::move(pfn);
+}
+
+void AtlasEngine::SetDefaultTextBackgroundOpacity(const float opacity) noexcept
+{
+ const auto mixin = opacity == 1.0f ? 0xff000000 : 0x00000000;
+ if (_api.backgroundOpaqueMixin != mixin)
+ {
+ _api.backgroundOpaqueMixin = mixin;
+ WI_SetFlag(_api.invalidations, ApiInvalidations::SwapChain);
+ }
+}
+
+void AtlasEngine::SetForceFullRepaintRendering(bool enable) noexcept
+{
+}
+
+[[nodiscard]] HRESULT AtlasEngine::SetHwnd(const HWND hwnd) noexcept
+{
+ if (_api.hwnd != hwnd)
+ {
+ _api.hwnd = hwnd;
+ WI_SetFlag(_api.invalidations, ApiInvalidations::SwapChain);
+ }
+ return S_OK;
+}
+
+void AtlasEngine::SetPixelShaderPath(std::wstring_view value) noexcept
+{
+}
+
+void AtlasEngine::SetRetroTerminalEffect(bool enable) noexcept
+{
+}
+
+void AtlasEngine::SetSelectionBackground(const COLORREF color, const float alpha) noexcept
+{
+ const u32 selectionColor = (color & 0xffffff) | gsl::narrow_cast(std::lroundf(alpha * 255.0f)) << 24;
+ if (_api.selectionColor != selectionColor)
+ {
+ _api.selectionColor = selectionColor;
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Settings);
+ }
+}
+
+void AtlasEngine::SetSoftwareRendering(bool enable) noexcept
+{
+}
+
+void AtlasEngine::SetIntenseIsBold(bool enable) noexcept
+{
+}
+
+void AtlasEngine::SetWarningCallback(std::function pfn) noexcept
+{
+ _api.warningCallback = std::move(pfn);
+}
+
+[[nodiscard]] HRESULT AtlasEngine::SetWindowSize(const SIZE pixels) noexcept
+{
+ u16x2 newSize;
+ RETURN_IF_FAILED(vec2_narrow(pixels.cx, pixels.cy, newSize));
+
+ // At the time of writing:
+ // When Win+D is pressed, `TriggerRedrawCursor` is called and a render pass is initiated.
+ // As conhost is in the background, GetClientRect will return {0,0} and we'll get called with {0,0}.
+ // This isn't a valid value for _api.sizeInPixel and would crash _recreateSizeDependentResources().
+ if (_api.sizeInPixel != newSize && newSize != u16x2{})
+ {
+ _api.sizeInPixel = newSize;
+ _api.cellCount = _api.sizeInPixel / _api.fontMetrics.cellSize;
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Size);
+ }
+
+ return S_OK;
+}
+
+void AtlasEngine::ToggleShaderEffects() noexcept
+{
+}
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept
+try
+{
+ std::vector fontFeatures;
+ if (!features.empty())
+ {
+ fontFeatures.reserve(features.size() + 3);
+
+ // All of these features are enabled by default by DirectWrite.
+ // If you want to (and can) peek into the source of DirectWrite
+ // you can look for the "GenericDefaultGsubFeatures" and "GenericDefaultGposFeatures" arrays.
+ // Gsub is for GetGlyphs() and Gpos for GetGlyphPlacements().
+ //
+ // GH#10774: Apparently specifying all of the features is just redundant.
+ fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1 });
+ fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1 });
+ fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1 });
+
+ for (const auto& p : features)
+ {
+ if (p.first.size() == 4)
+ {
+ const auto s = p.first.data();
+ switch (const auto tag = DWRITE_MAKE_FONT_FEATURE_TAG(s[0], s[1], s[2], s[3]))
+ {
+ case DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES:
+ fontFeatures[0].parameter = p.second;
+ break;
+ case DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES:
+ fontFeatures[1].parameter = p.second;
+ break;
+ case DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES:
+ fontFeatures[2].parameter = p.second;
+ break;
+ default:
+ fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ tag, p.second });
+ break;
+ }
+ }
+ }
+ }
+
+ std::vector fontAxisValues;
+ if (!axes.empty())
+ {
+ fontAxisValues.reserve(axes.size() + 3);
+
+ // AtlasEngine::_recreateFontDependentResources() relies on these fields to
+ // exist in this particular order in order to create appropriate default axes.
+ fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, -1.0f });
+ fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f });
+ fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, -1.0f });
+
+ for (const auto& p : axes)
+ {
+ if (p.first.size() == 4)
+ {
+ const auto s = p.first.data();
+ switch (const auto tag = DWRITE_MAKE_FONT_AXIS_TAG(s[0], s[1], s[2], s[3]))
+ {
+ case DWRITE_FONT_AXIS_TAG_WEIGHT:
+ fontAxisValues[0].value = p.second;
+ break;
+ case DWRITE_FONT_AXIS_TAG_ITALIC:
+ fontAxisValues[1].value = p.second;
+ break;
+ case DWRITE_FONT_AXIS_TAG_SLANT:
+ fontAxisValues[2].value = p.second;
+ break;
+ default:
+ fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ tag, p.second });
+ break;
+ }
+ }
+ }
+ }
+
+ const auto previousCellSize = _api.fontMetrics.cellSize;
+ _resolveFontMetrics(fontInfoDesired, fontInfo, &_api.fontMetrics);
+ _api.fontFeatures = std::move(fontFeatures);
+ _api.fontAxisValues = std::move(fontAxisValues);
+
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Font);
+
+ if (previousCellSize != _api.fontMetrics.cellSize)
+ {
+ _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
+{
+}
+
+#pragma endregion
+
+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";
+ }
+ if (!requestedSize.Y)
+ {
+ requestedSize = { 0, 12 };
+ }
+ if (!requestedWeight)
+ {
+ requestedWeight = DWRITE_FONT_WEIGHT_NORMAL;
+ }
+
+ wil::com_ptr systemFontCollection;
+ THROW_IF_FAILED(_sr.dwriteFactory->GetSystemFontCollection(systemFontCollection.addressof(), false));
+
+ u32 index = 0;
+ BOOL exists = false;
+ THROW_IF_FAILED(systemFontCollection->FindFamilyName(requestedFaceName, &index, &exists));
+ THROW_HR_IF(DWRITE_E_NOFONT, !exists);
+
+ wil::com_ptr fontFamily;
+ THROW_IF_FAILED(systemFontCollection->GetFontFamily(index, fontFamily.addressof()));
+
+ wil::com_ptr font;
+ THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(static_cast(requestedWeight), DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof()));
+
+ wil::com_ptr fontFace;
+ THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof()));
+
+ DWRITE_FONT_METRICS metrics;
+ fontFace->GetMetrics(&metrics);
+
+ // According to Wikipedia:
+ // > One em was traditionally defined as the width of the capital 'M' in the current typeface and point size,
+ // > because the 'M' was commonly cast the full-width of the square blocks [...] which are used in printing presses.
+ // Even today M is often the widest character in a font that supports ASCII.
+ // In the future a more robust solution could be written, until then this simple solution works for most cases.
+ static constexpr u32 codePoint = L'M';
+ u16 glyphIndex;
+ THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex));
+
+ DWRITE_GLYPH_METRICS glyphMetrics;
+ THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics));
+
+ // Point sizes are commonly treated at a 72 DPI scale
+ // (including by OpenType), whereas DirectWrite uses 96 DPI.
+ // Since we want the height in px we multiply by the display's DPI.
+ const auto fontSizeInPx = std::ceil(requestedSize.Y / 72.0 * _api.dpi);
+
+ const auto designUnitsPerPx = fontSizeInPx / static_cast(metrics.designUnitsPerEm);
+ const auto ascentInPx = static_cast(metrics.ascent) * designUnitsPerPx;
+ const auto descentInPx = static_cast(metrics.descent) * designUnitsPerPx;
+ const auto lineGapInPx = static_cast(metrics.lineGap) * designUnitsPerPx;
+ const auto advanceWidthInPx = static_cast(glyphMetrics.advanceWidth) * designUnitsPerPx;
+
+ const auto halfGapInPx = lineGapInPx / 2.0;
+ const auto baseline = std::ceil(ascentInPx + halfGapInPx);
+ const auto cellWidth = gsl::narrow(std::ceil(advanceWidthInPx));
+ const auto cellHeight = gsl::narrow(std::ceil(baseline + descentInPx + halfGapInPx));
+
+ {
+ COORD resultingCellSize;
+ resultingCellSize.X = gsl::narrow(cellWidth);
+ resultingCellSize.Y = gsl::narrow(cellHeight);
+ fontInfo.SetFromEngine(requestedFaceName, requestedFamily, requestedWeight, false, resultingCellSize, requestedSize);
+ }
+
+ if (fontMetrics)
+ {
+ const auto underlineOffsetInPx = static_cast(-metrics.underlinePosition) * designUnitsPerPx;
+ const auto underlineThicknessInPx = static_cast(metrics.underlineThickness) * designUnitsPerPx;
+ const auto strikethroughOffsetInPx = static_cast(-metrics.strikethroughPosition) * designUnitsPerPx;
+ const auto strikethroughThicknessInPx = static_cast(metrics.strikethroughThickness) * designUnitsPerPx;
+ const auto lineThickness = gsl::narrow(std::round(std::min(underlineThicknessInPx, strikethroughThicknessInPx)));
+ const auto underlinePos = gsl::narrow(std::round(baseline + underlineOffsetInPx - lineThickness / 2.0));
+ const auto strikethroughPos = gsl::narrow(std::round(baseline + strikethroughOffsetInPx - lineThickness / 2.0));
+
+ auto fontName = wil::make_process_heap_string(requestedFaceName);
+ const auto fontWeight = gsl::narrow(requestedWeight);
+
+ // 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->fontName = std::move(fontName);
+ fontMetrics->fontSizeInDIP = static_cast(fontSizeInPx / static_cast(_api.dpi) * 96.0);
+ fontMetrics->baselineInDIP = static_cast(baseline / static_cast(_api.dpi) * 96.0);
+ fontMetrics->cellSize = { cellWidth, cellHeight };
+ fontMetrics->fontWeight = fontWeight;
+ fontMetrics->underlinePos = underlinePos;
+ fontMetrics->strikethroughPos = strikethroughPos;
+ fontMetrics->lineThickness = lineThickness;
+ }
+}
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
new file mode 100644
index 00000000000..ac187dadc70
--- /dev/null
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -0,0 +1,1458 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "AtlasEngine.h"
+
+#include
+#include
+
+#include "../../interactivity/win32/CustomWindowMessages.h"
+
+#define SHADER_SOURCE_DIRECTORY LR"(..\..\renderer\atlas\)"
+
+// #### NOTE ####
+// This file should only contain methods that are only accessed by the caller of Present() (the "Renderer" class).
+// Basically this file poses the "synchronization" point between the concurrently running
+// general IRenderEngine API (like the Invalidate*() methods) and the Present() method
+// and thus may access both _r and _api.
+
+#pragma warning(disable : 4100) // '...': unreferenced formal parameter
+// Disable a bunch of warnings which get in the way of writing performant code.
+#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23).
+#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
+#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
+
+using namespace Microsoft::Console::Render;
+
+#pragma warning(push)
+#pragma warning(disable : 26447) // The function is declared 'noexcept' but calls function 'operator()()' which may throw exceptions (f.6).
+__declspec(noinline) static void showOOMWarning() noexcept
+{
+ [[maybe_unused]] static const auto once = []() {
+ std::thread t{ []() noexcept {
+ MessageBoxW(nullptr, L"This application is using a highly experimental text rendering engine and has run out of memory. Text rendering will start to behave irrationally and you should restart this process.", L"Out Of Memory", MB_ICONERROR | MB_OK);
+ } };
+ t.detach();
+ return false;
+ }();
+}
+#pragma warning(pop)
+
+struct TextAnalyzer final : IDWriteTextAnalysisSource, IDWriteTextAnalysisSink
+{
+ constexpr TextAnalyzer(const std::vector& text, std::vector& results) noexcept :
+ _text{ text }, _results{ results }
+ {
+ Ensures(_text.size() <= UINT32_MAX);
+ }
+
+ // TextAnalyzer will be allocated on the stack and reference counting is pointless because of that.
+ // The debug version will assert that we don't leak any references though.
+#ifdef NDEBUG
+ ULONG __stdcall AddRef() noexcept override
+ {
+ return 1;
+ }
+
+ ULONG __stdcall Release() noexcept override
+ {
+ return 1;
+ }
+#else
+ ULONG _refCount = 1;
+
+ ~TextAnalyzer()
+ {
+ assert(_refCount == 1);
+ }
+
+ ULONG __stdcall AddRef() noexcept override
+ {
+ return ++_refCount;
+ }
+
+ ULONG __stdcall Release() noexcept override
+ {
+ return --_refCount;
+ }
+#endif
+
+ HRESULT __stdcall QueryInterface(const IID& riid, void** ppvObject) noexcept override
+ {
+ __assume(ppvObject != nullptr);
+
+ if (IsEqualGUID(riid, __uuidof(IDWriteTextAnalysisSource)) || IsEqualGUID(riid, __uuidof(IDWriteTextAnalysisSink)))
+ {
+ *ppvObject = this;
+ return S_OK;
+ }
+
+ *ppvObject = nullptr;
+ return E_NOINTERFACE;
+ }
+
+ HRESULT __stdcall GetTextAtPosition(UINT32 textPosition, const WCHAR** textString, UINT32* textLength) noexcept override
+ {
+ // Writing to address 0 is a crash in practice. Just what we want.
+ __assume(textString != nullptr);
+ __assume(textLength != nullptr);
+
+ const auto size = gsl::narrow_cast(_text.size());
+ textPosition = std::min(textPosition, size);
+ *textString = _text.data() + textPosition;
+ *textLength = size - textPosition;
+ return S_OK;
+ }
+
+ HRESULT __stdcall GetTextBeforePosition(UINT32 textPosition, const WCHAR** textString, UINT32* textLength) noexcept override
+ {
+ // Writing to address 0 is a crash in practice. Just what we want.
+ __assume(textString != nullptr);
+ __assume(textLength != nullptr);
+
+ const auto size = gsl::narrow_cast(_text.size());
+ textPosition = std::min(textPosition, size);
+ *textString = _text.data();
+ *textLength = textPosition;
+ return S_OK;
+ }
+
+ DWRITE_READING_DIRECTION __stdcall GetParagraphReadingDirection() noexcept override
+ {
+ return DWRITE_READING_DIRECTION_LEFT_TO_RIGHT;
+ }
+
+ HRESULT __stdcall GetLocaleName(UINT32 textPosition, UINT32* textLength, const WCHAR** localeName) noexcept override
+ {
+ // Writing to address 0 is a crash in practice. Just what we want.
+ __assume(textLength != nullptr);
+ __assume(localeName != nullptr);
+
+ *textLength = gsl::narrow_cast(_text.size()) - textPosition;
+ *localeName = nullptr;
+ return S_OK;
+ }
+
+ HRESULT __stdcall GetNumberSubstitution(UINT32 textPosition, UINT32* textLength, IDWriteNumberSubstitution** numberSubstitution) noexcept override
+ {
+ return E_NOTIMPL;
+ }
+
+ HRESULT __stdcall SetScriptAnalysis(UINT32 textPosition, UINT32 textLength, const DWRITE_SCRIPT_ANALYSIS* scriptAnalysis) noexcept override
+ try
+ {
+ _results.emplace_back(AtlasEngine::TextAnalyzerResult{ textPosition, textLength, scriptAnalysis->script, static_cast(scriptAnalysis->shapes), 0 });
+ return S_OK;
+ }
+ CATCH_RETURN()
+
+ HRESULT __stdcall SetLineBreakpoints(UINT32 textPosition, UINT32 textLength, const DWRITE_LINE_BREAKPOINT* lineBreakpoints) noexcept override
+ {
+ return E_NOTIMPL;
+ }
+
+ HRESULT __stdcall SetBidiLevel(UINT32 textPosition, UINT32 textLength, UINT8 explicitLevel, UINT8 resolvedLevel) noexcept override
+ {
+ return E_NOTIMPL;
+ }
+
+ HRESULT __stdcall SetNumberSubstitution(UINT32 textPosition, UINT32 textLength, IDWriteNumberSubstitution* numberSubstitution) noexcept override
+ {
+ return E_NOTIMPL;
+ }
+
+private:
+ const std::vector& _text;
+ std::vector& _results;
+};
+
+#pragma warning(suppress : 26455) // Default constructor may not throw. Declare it 'noexcept' (f.6).
+AtlasEngine::AtlasEngine()
+{
+#ifdef NDEBUG
+ THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, _sr.d2dFactory.addressof()));
+#else
+ static constexpr D2D1_FACTORY_OPTIONS options{ D2D1_DEBUG_LEVEL_INFORMATION };
+ THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, options, _sr.d2dFactory.addressof()));
+#endif
+
+ THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(_sr.dwriteFactory), reinterpret_cast<::IUnknown**>(_sr.dwriteFactory.addressof())));
+ if (const auto factory2 = _sr.dwriteFactory.try_query())
+ {
+ THROW_IF_FAILED(factory2->GetSystemFontFallback(_sr.systemFontFallback.addressof()));
+ }
+ {
+ wil::com_ptr textAnalyzer;
+ THROW_IF_FAILED(_sr.dwriteFactory->CreateTextAnalyzer(textAnalyzer.addressof()));
+ _sr.textAnalyzer = textAnalyzer.query();
+ }
+
+ _sr.isWindows10OrGreater = IsWindows10OrGreater();
+
+#ifndef NDEBUG
+ // If you run the Host.EXE project inside Visual Studio it'll have a working directory of
+ // $(SolutionDir)\src\host\exe. This relative path will thus end up in this source directory.
+ _sr.sourceCodeWatcher = wil::make_folder_change_reader_nothrow(SHADER_SOURCE_DIRECTORY, false, wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime, [this](wil::FolderChangeEvent, PCWSTR path) {
+ if (til::ends_with(path, L".hlsl"))
+ {
+ auto expected = INT64_MAX;
+ const auto invalidationTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
+ _sr.sourceCodeInvalidationTime.compare_exchange_strong(expected, invalidationTime.time_since_epoch().count(), std::memory_order_relaxed);
+ }
+ });
+#endif
+}
+
+#pragma region IRenderEngine
+
+// StartPaint() is called while the console buffer lock is being held.
+// --> Put as little in here as possible.
+[[nodiscard]] HRESULT AtlasEngine::StartPaint() noexcept
+try
+{
+ if (_api.hwnd)
+ {
+ RECT rect;
+ LOG_IF_WIN32_BOOL_FALSE(GetClientRect(_api.hwnd, &rect));
+ std::ignore = SetWindowSize({ rect.right - rect.left, rect.bottom - rect.top });
+
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Title))
+ {
+ LOG_IF_WIN32_BOOL_FALSE(PostMessageW(_api.hwnd, CM_UPDATE_TITLE, 0, 0));
+ WI_ClearFlag(_api.invalidations, ApiInvalidations::Title);
+ }
+ }
+
+ // It's important that we invalidate here instead of in Present() with the rest.
+ // Other functions, those called before Present(), might depend on _r fields.
+ // But most of the time _invalidations will be ::none, making this very cheap.
+ if (_api.invalidations != ApiInvalidations::None)
+ {
+ RETURN_HR_IF(E_UNEXPECTED, _api.cellCount == u16x2{});
+
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Device))
+ {
+ _createResources();
+ }
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::SwapChain))
+ {
+ _createSwapChain();
+ }
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Size))
+ {
+ _recreateSizeDependentResources();
+ }
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Font))
+ {
+ _recreateFontDependentResources();
+ }
+ if (WI_IsFlagSet(_api.invalidations, ApiInvalidations::Settings))
+ {
+ _r.selectionColor = _api.selectionColor;
+ WI_SetFlag(_r.invalidations, RenderInvalidations::ConstBuffer);
+ }
+
+ // Equivalent to InvalidateAll().
+ _api.invalidatedRows = invalidatedRowsAll;
+ }
+
+#ifndef NDEBUG
+ if (const auto invalidationTime = _sr.sourceCodeInvalidationTime.load(std::memory_order_relaxed); invalidationTime != INT64_MAX && invalidationTime <= std::chrono::steady_clock::now().time_since_epoch().count())
+ {
+ _sr.sourceCodeInvalidationTime.store(INT64_MAX, std::memory_order_relaxed);
+
+ try
+ {
+ static const auto compile = [](const wchar_t* path, const char* target) {
+ const wil::unique_hfile fileHandle{ CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
+ THROW_LAST_ERROR_IF(!fileHandle);
+
+ const auto fileSize = GetFileSize(fileHandle.get(), nullptr);
+ const wil::unique_handle mappingHandle{ CreateFileMappingW(fileHandle.get(), nullptr, PAGE_READONLY, 0, fileSize, nullptr) };
+ THROW_LAST_ERROR_IF(!mappingHandle);
+
+ const wil::unique_mapview_ptr dataBeg{ MapViewOfFile(mappingHandle.get(), FILE_MAP_READ, 0, 0, 0) };
+ THROW_LAST_ERROR_IF(!dataBeg);
+
+ wil::com_ptr error;
+ wil::com_ptr blob;
+ const auto hr = D3DCompile(
+ /* pSrcData */ dataBeg.get(),
+ /* SrcDataSize */ fileSize,
+ /* pFileName */ nullptr,
+ /* pDefines */ nullptr,
+ /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE,
+ /* pEntrypoint */ "main",
+ /* pTarget */ target,
+ /* Flags1 */ D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION | D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS,
+ /* Flags2 */ 0,
+ /* ppCode */ blob.addressof(),
+ /* ppErrorMsgs */ error.addressof());
+
+ if (error)
+ {
+ std::thread t{ [error = std::move(error)]() noexcept {
+ MessageBoxA(nullptr, static_cast(error->GetBufferPointer()), "Compilation error", MB_ICONERROR | MB_OK);
+ } };
+ t.detach();
+ }
+
+ THROW_IF_FAILED(hr);
+ return blob;
+ };
+
+ const auto vs = compile(SHADER_SOURCE_DIRECTORY "shader_vs.hlsl", "vs_4_1");
+ const auto ps = compile(SHADER_SOURCE_DIRECTORY "shader_ps.hlsl", "ps_4_1");
+
+ THROW_IF_FAILED(_r.device->CreateVertexShader(vs->GetBufferPointer(), vs->GetBufferSize(), nullptr, _r.vertexShader.put()));
+ THROW_IF_FAILED(_r.device->CreatePixelShader(ps->GetBufferPointer(), ps->GetBufferSize(), nullptr, _r.pixelShader.put()));
+ _setShaderResources();
+ }
+ CATCH_LOG()
+ }
+#endif
+
+ if (_api.invalidatedRows == invalidatedRowsAll)
+ {
+ // Skip all the partial updates, since we redraw everything anyways.
+ _api.invalidatedCursorArea = invalidatedAreaNone;
+ _api.invalidatedRows = { 0, _api.cellCount.y };
+ _api.scrollOffset = 0;
+ }
+ else
+ {
+ // Clamp invalidation rects into valid value ranges.
+ {
+ _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _api.cellCount.x);
+ _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _api.cellCount.y);
+ _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _api.cellCount.x);
+ _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _api.cellCount.y);
+ }
+ {
+ _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, _api.cellCount.y);
+ _api.invalidatedRows.y = clamp(_api.invalidatedRows.y, _api.invalidatedRows.x, _api.cellCount.y);
+ }
+ {
+ const auto limit = gsl::narrow_cast(_api.cellCount.y & 0x7fff);
+ _api.scrollOffset = gsl::narrow_cast(clamp(_api.scrollOffset, -limit, limit));
+ }
+
+ // Scroll the buffer by the given offset and mark the newly uncovered rows as "invalid".
+ if (_api.scrollOffset != 0)
+ {
+ const auto nothingInvalid = _api.invalidatedRows.x == _api.invalidatedRows.y;
+ const auto offset = static_cast(_api.scrollOffset) * _api.cellCount.x;
+ const auto data = _r.cells.data();
+ auto count = _r.cells.size();
+#pragma warning(suppress : 26494) // Variable 'dst' is uninitialized. Always initialize an object (type.5).
+ Cell* dst;
+#pragma warning(suppress : 26494) // Variable 'src' is uninitialized. Always initialize an object (type.5).
+ Cell* src;
+
+ if (_api.scrollOffset < 0)
+ {
+ // Scroll up (for instance when new text is being written at the end of the buffer).
+ dst = data;
+ src = data - offset;
+ count += offset;
+
+ const u16 endRow = _api.cellCount.y + _api.scrollOffset;
+ _api.invalidatedRows.x = nothingInvalid ? endRow : std::min(_api.invalidatedRows.x, endRow);
+ _api.invalidatedRows.y = _api.cellCount.y;
+ }
+ else
+ {
+ // Scroll down.
+ dst = data + offset;
+ src = data;
+ count -= offset;
+
+ _api.invalidatedRows.x = 0;
+ _api.invalidatedRows.y = nothingInvalid ? _api.scrollOffset : std::max(_api.invalidatedRows.y, _api.scrollOffset);
+ }
+
+ memmove(dst, src, count * sizeof(Cell));
+ }
+ }
+
+ _api.dirtyRect = til::rectangle{
+ static_cast(0),
+ static_cast(_api.invalidatedRows.x),
+ static_cast(_api.cellCount.x),
+ static_cast(_api.invalidatedRows.y),
+ };
+
+ return S_OK;
+}
+catch (const wil::ResultException& exception)
+{
+ return _handleException(exception);
+}
+CATCH_RETURN()
+
+[[nodiscard]] HRESULT AtlasEngine::EndPaint() noexcept
+try
+{
+ _flushBufferLine();
+
+ _api.invalidatedCursorArea = invalidatedAreaNone;
+ _api.invalidatedRows = invalidatedRowsNone;
+ _api.scrollOffset = 0;
+ return S_OK;
+}
+CATCH_RETURN()
+
+[[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept
+{
+ return continuousRedraw;
+}
+
+void AtlasEngine::WaitUntilCanRender() noexcept
+{
+ if constexpr (!debugGeneralPerformance)
+ {
+ if (_r.frameLatencyWaitableObject)
+ {
+ WaitForSingleObjectEx(_r.frameLatencyWaitableObject.get(), 100, true);
+#ifndef NDEBUG
+ _r.frameLatencyWaitableObjectUsed = true;
+#endif
+ }
+ else
+ {
+ Sleep(8);
+ }
+ }
+}
+
+[[nodiscard]] HRESULT AtlasEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept
+{
+ RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
+ *pForcePaint = false;
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::ScrollFrame() noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::PrepareRenderInfo(const RenderFrameInfo& info) noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::ResetLineTransform() noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::PrepareLineTransform(const LineRendition lineRendition, const size_t targetRow, const size_t viewportLeft) noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::PaintBackground() noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::PaintBufferLine(const gsl::span clusters, const COORD coord, const bool fTrimLeft, const bool lineWrapped) noexcept
+try
+{
+ const auto x = gsl::narrow_cast(clamp(coord.X, 0, _api.cellCount.x));
+ const auto y = gsl::narrow_cast(clamp(coord.Y, 0, _api.cellCount.y));
+
+ if (_api.currentRow != y)
+ {
+ _flushBufferLine();
+ }
+
+ _api.currentRow = y;
+
+ // Due to the current IRenderEngine interface (that wasn't refactored yet) we need to assemble
+ // the current buffer line first as the remaining function operates on whole lines of text.
+ {
+ if (!_api.bufferLineColumn.empty())
+ {
+ _api.bufferLineColumn.pop_back();
+ }
+
+ u16 column = x;
+ for (const auto& cluster : clusters)
+ {
+ for (const auto& ch : cluster.GetText())
+ {
+ _api.bufferLine.emplace_back(ch);
+ _api.bufferLineColumn.emplace_back(column);
+ }
+
+ column += gsl::narrow_cast(cluster.GetColumns());
+ }
+
+ _api.bufferLineColumn.emplace_back(column);
+
+ const BufferLineMetadata metadata{ _api.currentColor, _api.flags };
+ std::fill_n(_api.bufferLineMetadata.data() + x, column - x, metadata);
+ }
+
+ return S_OK;
+}
+CATCH_RETURN()
+
+[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept
+{
+ return S_OK;
+}
+
+[[nodiscard]] HRESULT AtlasEngine::PaintSelection(SMALL_RECT rect) noexcept
+try
+{
+ // Unfortunately there's no step after Renderer::_PaintBufferOutput that
+ // would inform us that it's done with the last AtlasEngine::PaintBufferLine.
+ // As such we got to call _flushBufferLine() here just to be sure.
+ _flushBufferLine();
+ _setCellFlags(rect, CellFlags::Selected, CellFlags::Selected);
+ return S_OK;
+}
+CATCH_RETURN()
+
+[[nodiscard]] HRESULT AtlasEngine::PaintCursor(const CursorOptions& options) noexcept
+try
+{
+ // Unfortunately there's no step after Renderer::_PaintBufferOutput that
+ // would inform us that it's done with the last AtlasEngine::PaintBufferLine.
+ // As such we got to call _flushBufferLine() here just to be sure.
+ _flushBufferLine();
+
+ {
+ const CachedCursorOptions cachedOptions{
+ gsl::narrow_cast(options.fUseColor ? options.cursorColor | 0xff000000 : INVALID_COLOR),
+ gsl::narrow_cast(options.cursorType),
+ gsl::narrow_cast(options.ulCursorHeightPercent),
+ };
+ if (_r.cursorOptions != cachedOptions)
+ {
+ _r.cursorOptions = cachedOptions;
+ WI_SetFlag(_r.invalidations, RenderInvalidations::Cursor);
+ }
+ }
+
+ // Clear the previous cursor
+ if (_api.invalidatedCursorArea.non_empty())
+ {
+ _setCellFlags(til::bit_cast(_api.invalidatedCursorArea), CellFlags::Cursor, CellFlags::None);
+ }
+
+ if (options.isOn)
+ {
+ const auto point = options.coordCursor;
+ // TODO: options.coordCursor can contain invalid out of bounds coordinates when
+ // the window is being resized and the cursor is on the last line of the viewport.
+ const auto x = gsl::narrow_cast(std::min(point.X, _r.cellCount.x - 1));
+ const auto y = gsl::narrow_cast(std::min(point.Y, _r.cellCount.y - 1));
+ const SHORT right = x + 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar));
+ const SHORT bottom = y + 1;
+ _setCellFlags({ x, y, right, bottom }, CellFlags::Cursor, CellFlags::Cursor);
+ }
+
+ return S_OK;
+}
+CATCH_RETURN()
+
+[[nodiscard]] HRESULT AtlasEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept
+try
+{
+ const auto [fg, bg] = pData->GetAttributeColors(textAttributes);
+
+ if (!isSettingDefaultBrushes)
+ {
+ auto flags = CellFlags::None;
+ WI_SetFlagIf(flags, CellFlags::BorderLeft, textAttributes.IsLeftVerticalDisplayed());
+ WI_SetFlagIf(flags, CellFlags::BorderTop, textAttributes.IsTopHorizontalDisplayed());
+ WI_SetFlagIf(flags, CellFlags::BorderRight, textAttributes.IsRightVerticalDisplayed());
+ WI_SetFlagIf(flags, CellFlags::BorderBottom, textAttributes.IsBottomHorizontalDisplayed());
+ WI_SetFlagIf(flags, CellFlags::Underline, textAttributes.IsUnderlined());
+ WI_SetFlagIf(flags, CellFlags::UnderlineDotted, textAttributes.IsHyperlink());
+ WI_SetFlagIf(flags, CellFlags::UnderlineDouble, textAttributes.IsDoublyUnderlined());
+ WI_SetFlagIf(flags, CellFlags::Strikethrough, textAttributes.IsCrossedOut());
+
+ const u32x2 newColors{ gsl::narrow_cast(fg | 0xff000000), gsl::narrow_cast(bg | _api.backgroundOpaqueMixin) };
+ const AtlasKeyAttributes attributes{ 0, textAttributes.IsBold(), textAttributes.IsItalic(), 0 };
+
+ if (_api.attributes != attributes)
+ {
+ _flushBufferLine();
+ }
+
+ _api.currentColor = newColors;
+ _api.attributes = attributes;
+ _api.flags = flags;
+ }
+ else if (textAttributes.BackgroundIsDefault() && bg != _r.backgroundColor)
+ {
+ _r.backgroundColor = bg | _api.backgroundOpaqueMixin;
+ WI_SetFlag(_r.invalidations, RenderInvalidations::ConstBuffer);
+ }
+
+ return S_OK;
+}
+CATCH_RETURN()
+
+#pragma endregion
+
+[[nodiscard]] HRESULT AtlasEngine::_handleException(const wil::ResultException& exception) noexcept
+{
+ const auto hr = exception.GetErrorCode();
+ if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET || hr == D2DERR_RECREATE_TARGET)
+ {
+ WI_SetFlag(_api.invalidations, ApiInvalidations::Device);
+ return E_PENDING; // Indicate a retry to the renderer
+ }
+
+ // NOTE: This isn't thread safe, as _handleException is called by AtlasEngine.r.cpp.
+ // However it's not too much of a concern at the moment as SetWarningCallback()
+ // is only called once during construction in practice.
+ if (_api.warningCallback)
+ {
+ try
+ {
+ _api.warningCallback(hr);
+ }
+ CATCH_LOG()
+ }
+
+ return hr;
+}
+
+void AtlasEngine::_createResources()
+{
+ _releaseSwapChain();
+ _r = {};
+
+#ifdef NDEBUG
+ static constexpr
+#endif
+ auto deviceFlags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT;
+
+#ifndef NDEBUG
+ // DXGI debug messages + enabling D3D11_CREATE_DEVICE_DEBUG if the Windows SDK was installed.
+ if (const wil::unique_hmodule module{ LoadLibraryExW(L"dxgi.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) })
+ {
+ deviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
+
+ if (const auto DXGIGetDebugInterface1 = GetProcAddressByFunctionDeclaration(module.get(), DXGIGetDebugInterface1))
+ {
+ if (wil::com_ptr infoQueue; SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(infoQueue.addressof()))))
+ {
+ // I didn't want to link with dxguid.lib just for getting DXGI_DEBUG_ALL. This GUID is publicly documented.
+ static constexpr GUID dxgiDebugAll = { 0xe48ae283, 0xda80, 0x490b, { 0x87, 0xe6, 0x43, 0xe9, 0xa9, 0xcf, 0xda, 0x8 } };
+ for (const auto severity : std::array{ DXGI_INFO_QUEUE_MESSAGE_SEVERITY_CORRUPTION, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_WARNING })
+ {
+ infoQueue->SetBreakOnSeverity(dxgiDebugAll, severity, true);
+ }
+ }
+
+ if (wil::com_ptr debug; SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(debug.addressof()))))
+ {
+ debug->EnableLeakTrackingForThread();
+ }
+ }
+ }
+#endif // NDEBUG
+
+ // D3D device setup (basically a D3D class factory)
+ {
+ wil::com_ptr deviceContext;
+
+ // Why D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS:
+ // This flag prevents the driver from creating a large thread pool for things like shader computations
+ // that would be advantageous for games. For us this has only a minimal performance benefit,
+ // but comes with a large memory usage overhead. At the time of writing the Nvidia
+ // driver launches $cpu_thread_count more worker threads without this flag.
+ static constexpr std::array driverTypes{
+ std::pair{ D3D_DRIVER_TYPE_HARDWARE, D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS },
+ std::pair{ D3D_DRIVER_TYPE_WARP, static_cast(0) },
+ };
+ static constexpr std::array featureLevels{
+ D3D_FEATURE_LEVEL_11_1,
+ D3D_FEATURE_LEVEL_11_0,
+ D3D_FEATURE_LEVEL_10_1,
+ };
+
+ HRESULT hr = S_OK;
+ for (const auto& [driverType, additionalFlags] : driverTypes)
+ {
+ hr = D3D11CreateDevice(
+ /* pAdapter */ nullptr,
+ /* DriverType */ driverType,
+ /* Software */ nullptr,
+ /* Flags */ deviceFlags | additionalFlags,
+ /* pFeatureLevels */ featureLevels.data(),
+ /* FeatureLevels */ gsl::narrow_cast(featureLevels.size()),
+ /* SDKVersion */ D3D11_SDK_VERSION,
+ /* ppDevice */ _r.device.put(),
+ /* pFeatureLevel */ nullptr,
+ /* ppImmediateContext */ deviceContext.put());
+ if (SUCCEEDED(hr))
+ {
+ break;
+ }
+ }
+ THROW_IF_FAILED(hr);
+
+ _r.deviceContext = deviceContext.query();
+ }
+
+#ifndef NDEBUG
+ // D3D debug messages
+ if (deviceFlags & D3D11_CREATE_DEVICE_DEBUG)
+ {
+ const auto infoQueue = _r.device.query();
+ for (const auto severity : std::array{ D3D11_MESSAGE_SEVERITY_CORRUPTION, D3D11_MESSAGE_SEVERITY_ERROR, D3D11_MESSAGE_SEVERITY_WARNING })
+ {
+ infoQueue->SetBreakOnSeverity(severity, true);
+ }
+ }
+#endif // NDEBUG
+
+ // Our constant buffer will never get resized
+ {
+ D3D11_BUFFER_DESC desc{};
+ desc.ByteWidth = sizeof(ConstBuffer);
+ desc.Usage = D3D11_USAGE_DEFAULT;
+ desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
+ THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.constantBuffer.put()));
+ }
+
+ THROW_IF_FAILED(_r.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _r.vertexShader.put()));
+ THROW_IF_FAILED(_r.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _r.pixelShader.put()));
+
+ WI_ClearFlag(_api.invalidations, ApiInvalidations::Device);
+ WI_SetAllFlags(_api.invalidations, ApiInvalidations::SwapChain);
+}
+
+void AtlasEngine::_releaseSwapChain()
+{
+ // Flush() docs:
+ // However, if an application must actually destroy an old swap chain and create a new swap chain,
+ // the application must force the destruction of all objects that the application freed.
+ // To force the destruction, call ID3D11DeviceContext::ClearState (or otherwise ensure
+ // no views are bound to pipeline state), and then call Flush on the immediate context.
+ if (_r.swapChain && _r.deviceContext)
+ {
+ _r.frameLatencyWaitableObject.reset();
+ _r.swapChain.reset();
+ _r.renderTargetView.reset();
+ _r.deviceContext->ClearState();
+ _r.deviceContext->Flush();
+ }
+}
+
+void AtlasEngine::_createSwapChain()
+{
+ _releaseSwapChain();
+
+ // D3D swap chain setup (the thing that allows us to present frames on the screen)
+ {
+ const auto supportsFrameLatencyWaitableObject = IsWindows8Point1OrGreater();
+
+ // With C++20 we'll finally have designated initializers.
+ DXGI_SWAP_CHAIN_DESC1 desc{};
+ desc.Width = _api.sizeInPixel.x;
+ desc.Height = _api.sizeInPixel.y;
+ desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ desc.SampleDesc.Count = 1;
+ desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+ desc.BufferCount = 2; // TODO: 3?
+ desc.Scaling = DXGI_SCALING_NONE;
+ desc.SwapEffect = _sr.isWindows10OrGreater ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
+ // * HWND swap chains can't do alpha.
+ // * If our background is opaque we can enable "independent" flips by setting DXGI_SWAP_EFFECT_FLIP_DISCARD and DXGI_ALPHA_MODE_IGNORE.
+ // As our swap chain won't have to compose with DWM anymore it reduces the display latency dramatically.
+ desc.AlphaMode = _api.hwnd || _api.backgroundOpaqueMixin ? DXGI_ALPHA_MODE_IGNORE : DXGI_ALPHA_MODE_PREMULTIPLIED;
+ desc.Flags = supportsFrameLatencyWaitableObject ? DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT : 0;
+
+ wil::com_ptr dxgiFactory;
+ THROW_IF_FAILED(CreateDXGIFactory1(IID_PPV_ARGS(dxgiFactory.addressof())));
+
+ if (_api.hwnd)
+ {
+ desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
+
+ if (FAILED(dxgiFactory->CreateSwapChainForHwnd(_r.device.get(), _api.hwnd, &desc, nullptr, nullptr, _r.swapChain.put())))
+ {
+ // Platform Update for Windows 7:
+ // DXGI_SCALING_NONE is not supported on Windows 7 or Windows Server 2008 R2 with the Platform Update for
+ // Windows 7 installed and causes CreateSwapChainForHwnd to return DXGI_ERROR_INVALID_CALL when called.
+ desc.Scaling = DXGI_SCALING_STRETCH;
+ THROW_IF_FAILED(dxgiFactory->CreateSwapChainForHwnd(_r.device.get(), _api.hwnd, &desc, nullptr, nullptr, _r.swapChain.put()));
+ }
+ }
+ else
+ {
+ // We can't link with dcomp.lib as dcomp.dll doesn't exist on Windows 7.
+ const wil::unique_hmodule module{ LoadLibraryExW(L"dcomp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) };
+ THROW_LAST_ERROR_IF(!module);
+ const auto DCompositionCreateSurfaceHandle = GetProcAddressByFunctionDeclaration(module.get(), DCompositionCreateSurfaceHandle);
+ THROW_LAST_ERROR_IF(!DCompositionCreateSurfaceHandle);
+
+ // As per: https://docs.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatesurfacehandle
+ static constexpr DWORD COMPOSITIONSURFACE_ALL_ACCESS = 0x0003L;
+ THROW_IF_FAILED(DCompositionCreateSurfaceHandle(COMPOSITIONSURFACE_ALL_ACCESS, nullptr, _api.swapChainHandle.put()));
+ THROW_IF_FAILED(dxgiFactory.query()->CreateSwapChainForCompositionSurfaceHandle(_r.device.get(), _api.swapChainHandle.get(), &desc, nullptr, _r.swapChain.put()));
+ }
+
+ if (supportsFrameLatencyWaitableObject)
+ {
+ const auto swapChain2 = _r.swapChain.query();
+ THROW_IF_FAILED(swapChain2->SetMaximumFrameLatency(1)); // TODO: 2?
+ _r.frameLatencyWaitableObject.reset(swapChain2->GetFrameLatencyWaitableObject());
+ THROW_LAST_ERROR_IF(!_r.frameLatencyWaitableObject);
+ }
+ }
+
+ // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method:
+ // > For every frame it renders, the app should wait on this handle before starting any rendering operations.
+ // > Note that this requirement includes the first frame the app renders with the swap chain.
+ WaitUntilCanRender();
+
+ if (_api.swapChainChangedCallback)
+ {
+ try
+ {
+ _api.swapChainChangedCallback();
+ }
+ CATCH_LOG();
+ }
+
+ WI_ClearFlag(_api.invalidations, ApiInvalidations::SwapChain);
+ WI_SetAllFlags(_api.invalidations, ApiInvalidations::Size | ApiInvalidations::Font);
+}
+
+void AtlasEngine::_recreateSizeDependentResources()
+{
+ // ResizeBuffer() docs:
+ // Before you call ResizeBuffers, ensure that the application releases all references [...].
+ // You can use ID3D11DeviceContext::ClearState to ensure that all [internal] references are released.
+ if (_r.renderTargetView)
+ {
+ _r.renderTargetView.reset();
+ _r.deviceContext->ClearState();
+ _r.deviceContext->Flush();
+ THROW_IF_FAILED(_r.swapChain->ResizeBuffers(0, _api.sizeInPixel.x, _api.sizeInPixel.y, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT));
+ }
+
+ // The RenderTargetView is later used with OMSetRenderTargets
+ // to tell D3D where stuff is supposed to be rendered at.
+ {
+ wil::com_ptr buffer;
+ THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void()));
+ THROW_IF_FAILED(_r.device->CreateRenderTargetView(buffer.get(), nullptr, _r.renderTargetView.put()));
+ }
+
+ // Tell D3D which parts of the render target will be visible.
+ // Everything outside of the viewport will be black.
+ //
+ // In the future this should cover the entire _api.sizeInPixel.x/_api.sizeInPixel.y.
+ // The pixel shader should draw the remaining content in the configured background color.
+ {
+ D3D11_VIEWPORT viewport{};
+ viewport.Width = static_cast(_api.sizeInPixel.x);
+ viewport.Height = static_cast(_api.sizeInPixel.y);
+ _r.deviceContext->RSSetViewports(1, &viewport);
+ }
+
+ if (_api.cellCount != _r.cellCount)
+ {
+ const auto totalCellCount = static_cast(_api.cellCount.x) * static_cast(_api.cellCount.y);
+ // Let's guess that every cell consists of a surrogate pair.
+ const auto projectedTextSize = static_cast(_api.cellCount.x) * 2;
+ // IDWriteTextAnalyzer::GetGlyphs says:
+ // The recommended estimate for the per-glyph output buffers is (3 * textLength / 2 + 16).
+ // We already set the textLength to twice the cell count.
+ const auto projectedGlyphSize = 3 * projectedTextSize + 16;
+
+ // This buffer is a bit larger than the others (multiple MB).
+ // Prevent a memory usage spike, by first deallocating and then allocating.
+ _r.cells = {};
+ // Our render loop heavily relies on memcpy() which is between 1.5x
+ // and 40x faster for allocations with an alignment of 32 or greater.
+ // (40x on AMD Zen1-3, which have a rep movsb performance issue. MSFT:33358259.)
+ _r.cells = Buffer{ totalCellCount };
+ _r.cellCount = _api.cellCount;
+
+ // .clear() doesn't free the memory of these buffers.
+ // This code allows them to shrink again.
+ _api.bufferLine = {};
+ _api.bufferLine.reserve(projectedTextSize);
+ _api.bufferLineColumn.reserve(projectedTextSize + 1);
+ _api.bufferLineMetadata = Buffer{ _api.cellCount.x };
+ _api.analysisResults = {};
+
+ _api.clusterMap = Buffer{ projectedTextSize };
+ _api.textProps = Buffer{ projectedTextSize };
+ _api.glyphIndices = Buffer{ projectedGlyphSize };
+ _api.glyphProps = Buffer{ projectedGlyphSize };
+
+ D3D11_BUFFER_DESC desc;
+ desc.ByteWidth = gsl::narrow(totalCellCount * sizeof(Cell)); // totalCellCount can theoretically be UINT32_MAX!
+ desc.Usage = D3D11_USAGE_DYNAMIC;
+ desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
+ desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
+ desc.StructureByteStride = sizeof(Cell);
+ THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.cellBuffer.put()));
+ THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.cellBuffer.get(), nullptr, _r.cellView.put()));
+ }
+
+ // We have called _r.deviceContext->ClearState() in the beginning and lost all D3D state.
+ // This forces us to set up everything up from scratch again.
+ _setShaderResources();
+
+ WI_ClearFlag(_api.invalidations, ApiInvalidations::Size);
+ WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer);
+}
+
+void AtlasEngine::_recreateFontDependentResources()
+{
+ {
+ // We're likely resizing the atlas anyways and can
+ // thus also release any of these buffers prematurely.
+ _r.d2dRenderTarget.reset(); // depends on _r.atlasScratchpad
+ _r.atlasScratchpad.reset();
+ _r.atlasView.reset();
+ _r.atlasBuffer.reset();
+ }
+
+ // D3D
+ {
+ // TODO: Consider using IDXGIAdapter3::QueryVideoMemoryInfo() and IDXGIAdapter3::RegisterVideoMemoryBudgetChangeNotificationEvent()
+ // That way we can make better to use of a user's available video memory.
+
+ static constexpr size_t sizePerPixel = 4;
+ static constexpr size_t sizeLimit = D3D10_REQ_RESOURCE_SIZE_IN_MEGABYTES * 1024 * 1024;
+ const size_t dimensionLimit = _r.device->GetFeatureLevel() >= D3D_FEATURE_LEVEL_11_0 ? D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION : D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ const size_t csx = _api.fontMetrics.cellSize.x;
+ const size_t csy = _api.fontMetrics.cellSize.y;
+ const auto xLimit = (dimensionLimit / csx) * csx;
+ const auto pixelsPerCellRow = xLimit * csy;
+ const auto yLimitDueToDimension = (dimensionLimit / csy) * csy;
+ const auto yLimitDueToSize = ((sizeLimit / sizePerPixel) / pixelsPerCellRow) * csy;
+ const auto yLimit = std::min(yLimitDueToDimension, yLimitDueToSize);
+ const auto scaling = GetScaling();
+
+ _r.cellSizeDIP.x = static_cast(_api.fontMetrics.cellSize.x) / scaling;
+ _r.cellSizeDIP.y = static_cast(_api.fontMetrics.cellSize.y) / scaling;
+ _r.cellSize = _api.fontMetrics.cellSize;
+ _r.cellCount = _api.cellCount;
+ // x/yLimit are strictly smaller than dimensionLimit, which is smaller than a u16.
+ _r.atlasSizeInPixelLimit = u16x2{ gsl::narrow_cast(xLimit), gsl::narrow_cast(yLimit) };
+ _r.atlasSizeInPixel = { 0, 0 };
+ // The first Cell at {0, 0} is always our cursor texture.
+ // --> The first glyph starts at {1, 0}.
+ _r.atlasPosition.x = _api.fontMetrics.cellSize.x;
+ _r.atlasPosition.y = 0;
+
+ _r.glyphs = {};
+ _r.glyphQueue = {};
+ _r.glyphQueue.reserve(64);
+ }
+ // D3D specifically for UpdateDpi()
+ // This compensates for the built in scaling factor in a XAML SwapChainPanel (CompositionScaleX/Y).
+ if (!_api.hwnd)
+ {
+ if (const auto swapChain2 = _r.swapChain.try_query())
+ {
+ const auto inverseScale = static_cast(USER_DEFAULT_SCREEN_DPI) / static_cast(_api.dpi);
+ DXGI_MATRIX_3X2_F matrix{};
+ matrix._11 = inverseScale;
+ matrix._22 = inverseScale;
+ THROW_IF_FAILED(swapChain2->SetMatrixTransform(&matrix));
+ }
+ }
+
+ // D2D
+ {
+ _r.underlinePos = _api.fontMetrics.underlinePos;
+ _r.strikethroughPos = _api.fontMetrics.strikethroughPos;
+ _r.lineThickness = _api.fontMetrics.lineThickness;
+ _r.dpi = _api.dpi;
+ _r.maxEncounteredCellCount = 0;
+ _r.scratchpadCellWidth = 0;
+ }
+ {
+ // See AtlasEngine::UpdateFont.
+ // It hardcodes indices 0/1/2 in fontAxisValues to the weight/italic/slant axes.
+ // If they're -1.0f they haven't been set by the user and must be filled by us.
+ // When we call SetFontAxisValues() we basically override (disable) DirectWrite's internal font axes,
+ // and if either of the 3 aren't set we'd make it impossible for the user to see bold/italic text.
+#pragma warning(suppress : 26494) // Variable 'standardAxes' is uninitialized. Always initialize an object (type.5).
+ std::array standardAxes;
+
+ if (!_api.fontAxisValues.empty())
+ {
+ Expects(_api.fontAxisValues.size() >= standardAxes.size());
+ memcpy(standardAxes.data(), _api.fontAxisValues.data(), sizeof(standardAxes));
+ }
+
+ const auto restoreFontAxisValues = wil::scope_exit([&]() noexcept {
+ if (!_api.fontAxisValues.empty())
+ {
+ memcpy(_api.fontAxisValues.data(), standardAxes.data(), sizeof(standardAxes));
+ }
+ });
+
+ for (auto italic = 0; italic < 2; ++italic)
+ {
+ for (auto bold = 0; bold < 2; ++bold)
+ {
+ const auto fontWeight = bold ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_api.fontMetrics.fontWeight);
+ 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()));
+ textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
+ textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);
+
+ // DWRITE_LINE_SPACING_METHOD_UNIFORM:
+ // > Lines are explicitly set to uniform spacing, regardless of contained font sizes.
+ // > This can be useful to avoid the uneven appearance that can occur from font fallback.
+ // We want that. Otherwise fallback fonts might be rendered with an incorrect baseline and get cut off vertically.
+ THROW_IF_FAILED(textFormat->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, _r.cellSizeDIP.y, _api.fontMetrics.baselineInDIP));
+
+ if (!_api.fontAxisValues.empty())
+ {
+ if (const auto textFormat3 = textFormat.try_query())
+ {
+ // The wght axis defaults to the font weight.
+ _api.fontAxisValues[0].value = bold || standardAxes[0].value == -1.0f ? static_cast(fontWeight) : standardAxes[0].value;
+ // The ital axis defaults to 1 if this is italic and 0 otherwise.
+ _api.fontAxisValues[1].value = italic ? 1.0f : (standardAxes[1].value == -1.0f ? 0.0f : standardAxes[1].value);
+ // The slnt axis defaults to -12 if this is italic and 0 otherwise.
+ _api.fontAxisValues[2].value = italic ? -12.0f : (standardAxes[2].value == -1.0f ? 0.0f : standardAxes[2].value);
+
+ THROW_IF_FAILED(textFormat3->SetFontAxisValues(_api.fontAxisValues.data(), gsl::narrow_cast(_api.fontAxisValues.size())));
+ _r.textFormatAxes[italic][bold] = { _api.fontAxisValues.data(), _api.fontAxisValues.size() };
+ }
+ }
+ }
+ }
+ }
+ {
+ _r.typography.reset();
+
+ if (!_api.fontFeatures.empty())
+ {
+ _sr.dwriteFactory->CreateTypography(_r.typography.addressof());
+ for (const auto& v : _api.fontFeatures)
+ {
+ THROW_IF_FAILED(_r.typography->AddFontFeature(v));
+ }
+ }
+ }
+
+ WI_ClearFlag(_api.invalidations, ApiInvalidations::Font);
+ WI_SetAllFlags(_r.invalidations, RenderInvalidations::Cursor | RenderInvalidations::ConstBuffer);
+}
+
+IDWriteTextFormat* AtlasEngine::_getTextFormat(bool bold, bool italic) const noexcept
+{
+ return _r.textFormats[italic][bold].get();
+}
+
+const AtlasEngine::Buffer& AtlasEngine::_getTextFormatAxis(bool bold, bool italic) const noexcept
+{
+ return _r.textFormatAxes[italic][bold];
+}
+
+AtlasEngine::Cell* AtlasEngine::_getCell(u16 x, u16 y) noexcept
+{
+ assert(x < _r.cellCount.x);
+ assert(y < _r.cellCount.y);
+ return _r.cells.data() + static_cast(_r.cellCount.x) * y + x;
+}
+
+void AtlasEngine::_setCellFlags(SMALL_RECT coords, CellFlags mask, CellFlags bits) noexcept
+{
+ assert(coords.Left <= coords.Right);
+ assert(coords.Top <= coords.Bottom);
+ assert(coords.Right <= _r.cellCount.x);
+ assert(coords.Bottom <= _r.cellCount.y);
+
+ const auto filter = ~mask;
+ const auto width = static_cast(coords.Right) - coords.Left;
+ const auto height = static_cast(coords.Bottom) - coords.Top;
+ const auto stride = static_cast(_r.cellCount.x);
+ auto row = _r.cells.data() + static_cast(_r.cellCount.x) * coords.Top + coords.Left;
+ const auto end = row + height * stride;
+
+ for (; row != end; row += stride)
+ {
+ const auto dataEnd = row + width;
+ for (auto data = row; data != dataEnd; ++data)
+ {
+ const auto current = data->flags;
+ data->flags = (current & filter) | bits;
+ }
+ }
+}
+
+AtlasEngine::u16x2 AtlasEngine::_allocateAtlasTile() noexcept
+{
+ const auto ret = _r.atlasPosition;
+
+ _r.atlasPosition.x += _r.cellSize.x;
+ if (_r.atlasPosition.x >= _r.atlasSizeInPixelLimit.x)
+ {
+ _r.atlasPosition.x = 0;
+ _r.atlasPosition.y += _r.cellSize.y;
+ if (_r.atlasPosition.y >= _r.atlasSizeInPixelLimit.y)
+ {
+ _r.atlasPosition.x = _r.cellSize.x;
+ _r.atlasPosition.y = 0;
+ showOOMWarning();
+ }
+ }
+
+ return ret;
+}
+
+void AtlasEngine::_flushBufferLine()
+{
+ if (_api.bufferLine.empty())
+ {
+ return;
+ }
+
+ const auto cleanup = wil::scope_exit([this]() noexcept {
+ _api.bufferLine.clear();
+ _api.bufferLineColumn.clear();
+ });
+
+ // This would seriously blow us up otherwise.
+ Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1);
+
+ // NOTE:
+ // This entire function is one huge hack to see if it works.
+
+ // UH OH UNICODE MADNESS AHEAD
+ //
+ // # What do we want?
+ //
+ // Segment a line of text (_api.bufferLine) into unicode "clusters".
+ // Each cluster is one "whole" glyph with diacritics, ligatures, zero width joiners
+ // and whatever else, that should be cached as a whole in our texture atlas.
+ //
+ // # How do we get that?
+ //
+ // ## The unfortunate preface
+ //
+ // DirectWrite can be "reluctant" to segment text into clusters and I found no API which offers simply that.
+ // What it offers are a large number of low level APIs that can sort of be used in combination to do this.
+ // The resulting text parsing is very slow unfortunately, consuming up to 95% of rendering time in extreme cases.
+ //
+ // ## The actual approach
+ //
+ // DirectWrite has 2 APIs which can segment text properly (including ligatures and zero width joiners):
+ // * IDWriteTextAnalyzer1::GetTextComplexity
+ // * IDWriteTextAnalyzer::GetGlyphs
+ //
+ // Both APIs require us to attain an IDWriteFontFace as the functions themselves don't handle font fallback.
+ // This forces us to call IDWriteFontFallback::MapCharacters first.
+ //
+ // Additionally IDWriteTextAnalyzer::GetGlyphs requires an instance of DWRITE_SCRIPT_ANALYSIS,
+ // which can only be attained by running IDWriteTextAnalyzer::AnalyzeScript first.
+ //
+ // Font fallback with IDWriteFontFallback::MapCharacters is very slow.
+
+ const auto textFormat = _getTextFormat(_api.attributes.bold, _api.attributes.italic);
+ const auto& textFormatAxis = _getTextFormatAxis(_api.attributes.bold, _api.attributes.italic);
+
+ TextAnalyzer atlasAnalyzer{ _api.bufferLine, _api.analysisResults };
+
+ wil::com_ptr fontCollection;
+ THROW_IF_FAILED(textFormat->GetFontCollection(fontCollection.addressof()));
+
+ wil::com_ptr mappedFontFace;
+
+#pragma warning(suppress : 26494) // Variable 'mappedEnd' is uninitialized. Always initialize an object (type.5).
+ for (u32 idx = 0, mappedEnd; idx < _api.bufferLine.size(); idx = mappedEnd)
+ {
+ float scale = 1.0f;
+
+ if (_sr.systemFontFallback)
+ {
+ u32 mappedLength = 0;
+
+ if (textFormatAxis)
+ {
+ wil::com_ptr fontFace5;
+ THROW_IF_FAILED(_sr.systemFontFallback.query()->MapCharacters(
+ /* analysisSource */ &atlasAnalyzer,
+ /* textPosition */ idx,
+ /* textLength */ gsl::narrow_cast(_api.bufferLine.size()) - idx,
+ /* baseFontCollection */ fontCollection.get(),
+ /* baseFamilyName */ _api.fontMetrics.fontName.get(),
+ /* fontAxisValues */ textFormatAxis.data(),
+ /* fontAxisValueCount */ gsl::narrow_cast(textFormatAxis.size()),
+ /* mappedLength */ &mappedLength,
+ /* scale */ &scale,
+ /* mappedFontFace */ fontFace5.put()));
+ mappedFontFace = std::move(fontFace5);
+ }
+ else
+ {
+ const auto baseWeight = _api.attributes.bold ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_api.fontMetrics.fontWeight);
+ const auto baseStyle = _api.attributes.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
+ wil::com_ptr font;
+
+ THROW_IF_FAILED(_sr.systemFontFallback->MapCharacters(
+ /* analysisSource */ &atlasAnalyzer,
+ /* textPosition */ idx,
+ /* textLength */ gsl::narrow_cast(_api.bufferLine.size()) - idx,
+ /* baseFontCollection */ fontCollection.get(),
+ /* baseFamilyName */ _api.fontMetrics.fontName.get(),
+ /* baseWeight */ baseWeight,
+ /* baseStyle */ baseStyle,
+ /* baseStretch */ DWRITE_FONT_STRETCH_NORMAL,
+ /* mappedLength */ &mappedLength,
+ /* mappedFont */ font.addressof(),
+ /* scale */ &scale));
+
+ mappedFontFace.reset();
+ if (font)
+ {
+ THROW_IF_FAILED(font->CreateFontFace(mappedFontFace.addressof()));
+ }
+ }
+
+ mappedEnd = idx + mappedLength;
+
+ if (!mappedFontFace)
+ {
+ // Task: Replace all characters in this range with unicode replacement characters.
+ // Input (where "n" is a narrow and "ww" is a wide character):
+ // _api.bufferLine = "nwwnnw"
+ // _api.bufferLineColumn = {0, 1, 1, 2, 3, 4, 4, 5}
+ // n w w n n w w
+ // Solution:
+ // Iterate through bufferLineColumn until the value changes, because this indicates we passed over a
+ // complete (narrow or wide) cell. To do so we'll use col1 (previous column) and col2 (next column).
+ // Then we emit a replacement character by telling _emplaceGlyph that this range has no font face.
+ auto pos1 = idx;
+ auto col1 = _api.bufferLineColumn[pos1];
+ for (auto pos2 = idx + 1; pos2 <= mappedEnd; ++pos2)
+ {
+ if (const auto col2 = _api.bufferLineColumn[pos2]; col1 != col2)
+ {
+ _emplaceGlyph(nullptr, scale, pos1, pos2);
+ pos1 = pos2;
+ col1 = col2;
+ }
+ }
+
+ continue;
+ }
+ }
+ else
+ {
+ if (!mappedFontFace)
+ {
+ const auto baseWeight = _api.attributes.bold ? DWRITE_FONT_WEIGHT_BOLD : static_cast(_api.fontMetrics.fontWeight);
+ const auto baseStyle = _api.attributes.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
+
+ wil::com_ptr fontFamily;
+ THROW_IF_FAILED(fontCollection->GetFontFamily(0, fontFamily.addressof()));
+
+ wil::com_ptr font;
+ THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(baseWeight, DWRITE_FONT_STRETCH_NORMAL, baseStyle, font.addressof()));
+
+ THROW_IF_FAILED(font->CreateFontFace(mappedFontFace.put()));
+ }
+
+ mappedEnd = gsl::narrow_cast(_api.bufferLine.size());
+ }
+
+ // We can reuse idx here, as it'll be reset to "idx = mappedEnd" in the outer loop anyways.
+ for (u32 complexityLength = 0; idx < mappedEnd; idx += complexityLength)
+ {
+ BOOL isTextSimple;
+ THROW_IF_FAILED(_sr.textAnalyzer->GetTextComplexity(_api.bufferLine.data() + idx, mappedEnd - idx, mappedFontFace.get(), &isTextSimple, &complexityLength, _api.glyphIndices.data()));
+
+ if (isTextSimple)
+ {
+ for (size_t i = 0; i < complexityLength; ++i)
+ {
+ _emplaceGlyph(mappedFontFace.get(), scale, idx + i, idx + i + 1u);
+ }
+ }
+ else
+ {
+ _api.analysisResults.clear();
+ THROW_IF_FAILED(_sr.textAnalyzer->AnalyzeScript(&atlasAnalyzer, idx, complexityLength, &atlasAnalyzer));
+ //_sr.textAnalyzer->AnalyzeBidi(&atlasAnalyzer, idx, complexityLength, &atlasAnalyzer);
+
+ for (const auto& a : _api.analysisResults)
+ {
+ DWRITE_SCRIPT_ANALYSIS scriptAnalysis{ a.script, static_cast(a.shapes) };
+ u32 actualGlyphCount = 0;
+
+#pragma warning(push)
+#pragma warning(disable : 26494) // Variable '...' is uninitialized. Always initialize an object (type.5).
+ // None of these variables need to be initialized.
+ // features/featureRangeLengths are marked _In_reads_opt_(featureRanges).
+ // featureRanges is only > 0 when we also initialize all these variables.
+ DWRITE_TYPOGRAPHIC_FEATURES feature;
+ const DWRITE_TYPOGRAPHIC_FEATURES* features;
+ u32 featureRangeLengths;
+#pragma warning(pop)
+ u32 featureRanges = 0;
+
+ if (!_api.fontFeatures.empty())
+ {
+ feature.features = _api.fontFeatures.data();
+ feature.featureCount = gsl::narrow_cast(_api.fontFeatures.size());
+ features = &feature;
+ featureRangeLengths = a.textLength;
+ featureRanges = 1;
+ }
+
+ if (_api.clusterMap.size() < a.textLength)
+ {
+ _api.clusterMap = Buffer{ a.textLength };
+ _api.textProps = Buffer{ a.textLength };
+ }
+
+ for (auto retry = 0;;)
+ {
+ const auto hr = _sr.textAnalyzer->GetGlyphs(
+ /* textString */ _api.bufferLine.data() + a.textPosition,
+ /* textLength */ a.textLength,
+ /* fontFace */ mappedFontFace.get(),
+ /* isSideways */ false,
+ /* isRightToLeft */ a.bidiLevel & 1,
+ /* scriptAnalysis */ &scriptAnalysis,
+ /* localeName */ nullptr,
+ /* numberSubstitution */ nullptr,
+ /* features */ &features,
+ /* featureRangeLengths */ &featureRangeLengths,
+ /* featureRanges */ featureRanges,
+ /* maxGlyphCount */ gsl::narrow_cast(_api.glyphProps.size()),
+ /* clusterMap */ _api.clusterMap.data(),
+ /* textProps */ _api.textProps.data(),
+ /* glyphIndices */ _api.glyphIndices.data(),
+ /* glyphProps */ _api.glyphProps.data(),
+ /* actualGlyphCount */ &actualGlyphCount);
+
+ if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) && ++retry < 8)
+ {
+ // Grow factor 1.5x.
+ auto size = _api.glyphProps.size();
+ size = size + (size >> 1);
+ // Overflow check.
+ Expects(size > _api.glyphProps.size());
+ _api.glyphIndices = Buffer{ size };
+ _api.glyphProps = Buffer(size);
+ continue;
+ }
+
+ THROW_IF_FAILED(hr);
+ break;
+ }
+
+ _api.textProps[a.textLength - 1].canBreakShapingAfter = 1;
+
+ size_t beg = 0;
+ for (size_t i = 0; i < a.textLength; ++i)
+ {
+ if (_api.textProps[i].canBreakShapingAfter)
+ {
+ _emplaceGlyph(mappedFontFace.get(), scale, a.textPosition + beg, a.textPosition + i + 1);
+ beg = i + 1;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, float scale, size_t bufferPos1, size_t bufferPos2)
+{
+ static constexpr auto replacement = L'\uFFFD';
+
+ // This would seriously blow us up otherwise.
+ Expects(bufferPos1 < bufferPos2 && bufferPos2 <= _api.bufferLine.size());
+
+ const auto chars = fontFace ? &_api.bufferLine[bufferPos1] : &replacement;
+ const auto charCount = fontFace ? bufferPos2 - bufferPos1 : 1;
+
+ // _flushBufferLine() ensures that bufferLineColumn.size() > bufferLine.size().
+ const auto x1 = _api.bufferLineColumn[bufferPos1];
+ const auto x2 = _api.bufferLineColumn[bufferPos2];
+
+ Expects(x1 < x2 && x2 <= _api.cellCount.x);
+
+ const u16 cellCount = x2 - x1;
+
+ auto attributes = _api.attributes;
+ attributes.cellCount = cellCount;
+
+ const auto [it, inserted] = _r.glyphs.emplace(std::piecewise_construct, std::forward_as_tuple(attributes, gsl::narrow(charCount), chars), std::forward_as_tuple());
+ const auto& key = it->first;
+ auto& value = it->second;
+
+ if (inserted)
+ {
+ // Do fonts exist *in practice* which contain both colored and uncolored glyphs? I'm pretty sure...
+ // However doing it properly means using either of:
+ // * IDWriteFactory2::TranslateColorGlyphRun
+ // * IDWriteFactory4::TranslateColorGlyphRun
+ // * IDWriteFontFace4::GetGlyphImageData
+ //
+ // For the first two I wonder how one is supposed to restitch the 27 required parameters from a
+ // partial (!) text range returned by GetGlyphs(). Our caller breaks the GetGlyphs() result up
+ // into runs of characters up until the first canBreakShapingAfter == true after all.
+ // There's no documentation for it and I'm sure I'd mess it up.
+ // For very obvious reasons I didn't want to write this code.
+ //
+ // IDWriteFontFace4::GetGlyphImageData seems like the best approach and DirectWrite uses the
+ // same code that GetGlyphImageData uses to implement TranslateColorGlyphRun (well partially).
+ // However, it's a heavy operation and parses the font file on disk on every call (TranslateColorGlyphRun doesn't).
+ // For less obvious reasons I didn't want to write this code either.
+ //
+ // So this is a job for future me/someone.
+ // Bonus points for doing it without impacting performance.
+ auto flags = CellFlags::None;
+ if (fontFace)
+ {
+ const auto fontFace2 = wil::try_com_query(fontFace);
+ WI_SetFlagIf(flags, CellFlags::ColoredGlyph, fontFace2 && fontFace2->IsColorFont());
+ }
+
+ const auto coords = value.initialize(flags, cellCount);
+ for (u16 i = 0; i < cellCount; ++i)
+ {
+ coords[i] = _allocateAtlasTile();
+ }
+
+ _r.glyphQueue.push_back(AtlasQueueItem{ &key, &value, scale });
+ _r.maxEncounteredCellCount = std::max(_r.maxEncounteredCellCount, cellCount);
+ }
+
+ const auto valueData = value.data();
+ const auto coords = &valueData->coords[0];
+ const auto flags = valueData->flags | _api.bufferLineMetadata[x1].flags;
+ const auto color = _api.bufferLineMetadata[x1].colors;
+ const auto data = _getCell(x1, _api.currentRow);
+
+ for (u32 i = 0; i < cellCount; ++i)
+ {
+ data[i].tileIndex = coords[i];
+ data[i].flags = flags;
+ data[i].color = color;
+ }
+}
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
new file mode 100644
index 00000000000..8b9b6d0caa0
--- /dev/null
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -0,0 +1,761 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "../../renderer/inc/IRenderEngine.hpp"
+
+namespace Microsoft::Console::Render
+{
+ class AtlasEngine final : public IRenderEngine
+ {
+ public:
+ explicit AtlasEngine();
+
+ AtlasEngine(const AtlasEngine&) = delete;
+ AtlasEngine& operator=(const AtlasEngine&) = delete;
+
+ // IRenderEngine
+ [[nodiscard]] HRESULT StartPaint() noexcept override;
+ [[nodiscard]] HRESULT EndPaint() noexcept override;
+ [[nodiscard]] bool RequiresContinuousRedraw() noexcept override;
+ void WaitUntilCanRender() noexcept override;
+ [[nodiscard]] HRESULT Present() noexcept override;
+ [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* pForcePaint) noexcept override;
+ [[nodiscard]] HRESULT ScrollFrame() noexcept override;
+ [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* psrRegion) noexcept override;
+ [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* psrRegion) noexcept override;
+ [[nodiscard]] HRESULT InvalidateSystem(const RECT* prcDirtyClient) noexcept override;
+ [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override;
+ [[nodiscard]] HRESULT InvalidateScroll(const COORD* pcoordDelta) noexcept override;
+ [[nodiscard]] HRESULT InvalidateAll() noexcept override;
+ [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* pForcePaint) noexcept override;
+ [[nodiscard]] HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept override;
+ [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
+ [[nodiscard]] HRESULT ResetLineTransform() noexcept override;
+ [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, size_t targetRow, size_t viewportLeft) noexcept override;
+ [[nodiscard]] HRESULT PaintBackground() noexcept override;
+ [[nodiscard]] HRESULT PaintBufferLine(gsl::span clusters, COORD coord, bool fTrimLeft, bool lineWrapped) noexcept override;
+ [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, COORD coordTarget) noexcept override;
+ [[nodiscard]] HRESULT PaintSelection(SMALL_RECT rect) noexcept override;
+ [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
+ [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override;
+ [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override;
+ [[nodiscard]] HRESULT UpdateSoftFont(gsl::span bitPattern, SIZE cellSize, size_t centeringHint) noexcept override;
+ [[nodiscard]] HRESULT UpdateDpi(int iDpi) noexcept override;
+ [[nodiscard]] HRESULT UpdateViewport(SMALL_RECT srNewViewport) noexcept override;
+ [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, int iDpi) noexcept override;
+ [[nodiscard]] HRESULT GetDirtyArea(gsl::span& area) noexcept override;
+ [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* pFontSize) noexcept override;
+ [[nodiscard]] HRESULT IsGlyphWideByFont(std::wstring_view glyph, _Out_ bool* pResult) noexcept override;
+ [[nodiscard]] HRESULT UpdateTitle(std::wstring_view newTitle) noexcept override;
+
+ // DxRenderer - getter
+ HRESULT Enable() noexcept override;
+ [[nodiscard]] bool GetRetroTerminalEffect() const noexcept override;
+ [[nodiscard]] float GetScaling() const noexcept override;
+ [[nodiscard]] HANDLE GetSwapChainHandle() override;
+ [[nodiscard]] Types::Viewport GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept override;
+ [[nodiscard]] Types::Viewport GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept override;
+ // DxRenderer - setter
+ void SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept override;
+ void SetCallback(std::function pfn) noexcept override;
+ void SetDefaultTextBackgroundOpacity(float opacity) noexcept override;
+ void SetForceFullRepaintRendering(bool enable) noexcept override;
+ [[nodiscard]] HRESULT SetHwnd(HWND hwnd) noexcept override;
+ void SetPixelShaderPath(std::wstring_view value) noexcept override;
+ void SetRetroTerminalEffect(bool enable) noexcept override;
+ void SetSelectionBackground(COLORREF color, float alpha = 0.5f) noexcept override;
+ void SetSoftwareRendering(bool enable) noexcept override;
+ void SetIntenseIsBold(bool enable) noexcept override;
+ void SetWarningCallback(std::function pfn) noexcept override;
+ [[nodiscard]] HRESULT SetWindowSize(SIZE pixels) noexcept override;
+ void ToggleShaderEffects() noexcept override;
+ [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept override;
+ void UpdateHyperlinkHoveredId(uint16_t hoveredId) noexcept override;
+
+ // Some helper classes for the implementation.
+ // public because I don't want to sprinkle the code with friends.
+ public:
+#define ATLAS_POD_OPS(type) \
+ constexpr bool operator==(const type& rhs) const noexcept \
+ { \
+ return __builtin_memcmp(this, &rhs, sizeof(rhs)) == 0; \
+ } \
+ \
+ constexpr bool operator!=(const type& rhs) const noexcept \
+ { \
+ return __builtin_memcmp(this, &rhs, sizeof(rhs)) != 0; \
+ }
+
+#define ATLAS_FLAG_OPS(type, underlying) \
+ friend constexpr type operator~(type v) noexcept { return static_cast(~static_cast(v)); } \
+ friend constexpr type operator|(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) | static_cast(rhs)); } \
+ friend constexpr type operator&(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) & static_cast(rhs)); } \
+ friend constexpr void operator|=(type& lhs, type rhs) noexcept { lhs = lhs | rhs; } \
+ friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; }
+
+ template
+ struct vec2
+ {
+ T x{};
+ T y{};
+
+ ATLAS_POD_OPS(vec2)
+
+ constexpr vec2 operator/(const vec2& rhs) noexcept
+ {
+ assert(rhs.x != 0 && rhs.y != 0);
+ return { gsl::narrow_cast(x / rhs.x), gsl::narrow_cast(y / rhs.y) };
+ }
+ };
+
+ template
+ struct vec4
+ {
+ T x{};
+ T y{};
+ T z{};
+ T w{};
+
+ ATLAS_POD_OPS(vec4)
+ };
+
+ template
+ struct rect
+ {
+ T left{};
+ T top{};
+ T right{};
+ T bottom{};
+
+ ATLAS_POD_OPS(rect)
+
+ constexpr bool non_empty() noexcept
+ {
+ return (left < right) & (top < bottom);
+ }
+ };
+
+ using u8 = uint8_t;
+
+ using u16 = uint16_t;
+ using u16x2 = vec2;
+ using u16r = rect;
+
+ using i16 = int16_t;
+
+ using u32 = uint32_t;
+ using u32x2 = vec2;
+
+ using i32 = int32_t;
+
+ using f32 = float;
+ using f32x2 = vec2;
+ using f32x4 = vec4;
+
+ struct TextAnalyzerResult
+ {
+ u32 textPosition = 0;
+ u32 textLength = 0;
+
+ // These 2 fields represent DWRITE_SCRIPT_ANALYSIS.
+ // Not using DWRITE_SCRIPT_ANALYSIS drops the struct size from 20 down to 12 bytes.
+ u16 script = 0;
+ u8 shapes = 0;
+
+ u8 bidiLevel = 0;
+ };
+
+ private:
+ template
+ struct Buffer
+ {
+ constexpr Buffer() noexcept = default;
+
+ explicit Buffer(size_t size) :
+ _data{ allocate(size) },
+ _size{ size }
+ {
+ }
+
+ Buffer(const T* data, size_t size) :
+ _data{ allocate(size) },
+ _size{ size }
+ {
+ static_assert(std::is_trivially_copyable_v |