Compare commits

...

2 Commits

Author SHA1 Message Date
Leonard Hecker
aeb7a9fe27 wip 2023-11-15 16:52:20 +01:00
Leonard Hecker
c73a82afec Remove DxEngine 2023-11-07 13:22:18 +01:00
102 changed files with 610 additions and 13637 deletions

View File

@@ -161,8 +161,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererVt.unittest", "src\
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BufferOut", "src\buffer\out\lib\bufferout.vcxproj", "{0CF235BD-2DA0-407E-90EE-C467E8BBC714}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererDx", "src\renderer\dx\lib\dx.vcxproj", "{48D21369-3D7B-4431-9967-24E81292CF62}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalConnection", "src\cascadia\TerminalConnection\TerminalConnection.vcxproj", "{CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}"
ProjectSection(ProjectDependencies) = postProject
{71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} = {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}
@@ -319,8 +317,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{D3EF
build\scripts\Test-WindowsTerminalPackage.ps1 = build\scripts\Test-WindowsTerminalPackage.ps1
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dx.Unit.Tests", "src\renderer\dx\ut_dx\Dx.Unit.Tests.vcxproj", "{95B136F9-B238-490C-A7C5-5843C1FECAC4}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winconpty.Tests.Feature", "src\winconpty\ft_pty\winconpty.FeatureTests.vcxproj", "{024052DE-83FB-4653-AEA4-90790D29D5BD}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAzBridge", "src\cascadia\TerminalAzBridge\TerminalAzBridge.vcxproj", "{067F0A06-FCB7-472C-96E9-B03B54E8E18D}"

View File

@@ -2786,11 +2786,6 @@
"description": "Use to set a path to a pixel shader to use with the Terminal. Overrides `experimental.retroTerminalEffect`. This is an experimental feature, and its continued existence is not guaranteed.",
"type": "string"
},
"useAtlasEngine": {
"description": "Windows Terminal 1.16 and later ship with a new, performant text renderer. Set this to false to revert back to the old text renderer.",
"type": "boolean",
"default": true
},
"fontFace": {
"default": "Cascadia Mono",
"description": "[deprecated] Define 'face' within the 'font' object instead.",

View File

@@ -104,6 +104,7 @@ struct ScrollMark
class TextBuffer final
{
public:
TextBuffer() = default;
TextBuffer(const til::size screenBufferSize,
const TextAttribute defaultAttributes,
const UINT cursorSize,

View File

@@ -17,7 +17,6 @@
#include "../../types/inc/GlyphWidth.hpp"
#include "../../buffer/out/search.h"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/dx/DxRenderer.hpp"
#include "ControlCore.g.cpp"
#include "SelectionColor.g.cpp"
@@ -335,15 +334,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return false;
}
if (_settings->UseAtlasEngine())
{
_renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
}
else
{
_renderEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
}
_renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
_renderer->AddRenderEngine(_renderEngine.get());
// Initialize our font with the renderer
@@ -359,7 +350,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
// Update DxEngine's SelectionBackground
// Update AtlasEngine's SelectionBackground
_renderEngine->SetSelectionBackground(til::color{ _settings->SelectionBackground() });
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
@@ -878,10 +869,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Update the terminal core with its new Core settings
_terminal->UpdateAppearance(*newAppearance);
// Update DxEngine settings under the lock
// Update AtlasEngine settings under the lock
if (_renderEngine)
{
// Update DxEngine settings under the lock
// Update AtlasEngine settings under the lock
_renderEngine->SetSelectionBackground(til::color{ newAppearance->SelectionBackground() });
_renderEngine->SetRetroTerminalEffect(newAppearance->RetroTerminalEffect());
_renderEngine->SetPixelShaderPath(newAppearance->PixelShaderPath());
@@ -917,7 +908,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void ControlCore::_updateAntiAliasingMode()
{
D2D1_TEXT_ANTIALIAS_MODE mode;
// Update DxEngine's AntialiasingMode
// Update AtlasEngine's AntialiasingMode
switch (_settings->AntialiasingMode())
{
case TextAntialiasingMode::Cleartype:

View File

@@ -5,7 +5,7 @@
// - ControlCore.h
//
// Abstract:
// - This encapsulates a `Terminal` instance, a `DxEngine` and `Renderer`, and
// - This encapsulates a `Terminal` instance, a `AtlasEngine` and `Renderer`, and
// an `ITerminalConnection`. This is intended to be everything that someone
// might need to stand up a terminal instance in a control, but without any
// regard for how the UX works.

View File

@@ -3,10 +3,16 @@
#include "pch.h"
#include "HwndTerminal.hpp"
#include <windowsx.h>
#include <DefaultSettings.h>
#include <windowsx.h>
#include "HwndTerminalAutomationPeer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/base/renderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../types/viewport.cpp"
#include "../../types/inc/GlyphWidth.hpp"
using namespace ::Microsoft::Terminal::Core;
@@ -207,10 +213,10 @@ HRESULT HwndTerminal::Initialize()
RETURN_HR_IF_NULL(E_POINTER, localPointerToThread);
RETURN_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
RETURN_IF_FAILED(dxEngine->SetHwnd(_hwnd.get()));
RETURN_IF_FAILED(dxEngine->Enable());
_renderer->AddRenderEngine(dxEngine.get());
auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
RETURN_IF_FAILED(engine->SetHwnd(_hwnd.get()));
RETURN_IF_FAILED(engine->Enable());
_renderer->AddRenderEngine(engine.get());
_UpdateFont(USER_DEFAULT_SCREEN_DPI);
RECT windowRect;
@@ -221,9 +227,9 @@ HRESULT HwndTerminal::Initialize()
// Fist set up the dx engine with the window size in pixels.
// Then, using the font, get the number of characters that can fit.
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
RETURN_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
RETURN_IF_FAILED(engine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
_renderEngine = std::move(dxEngine);
_renderEngine = std::move(engine);
_terminal->Create({ 80, 25 }, 9001, *_renderer);
_terminal->SetWriteInputCallback([=](std::wstring_view input) noexcept { _WriteTextToConnection(input); });
@@ -752,7 +758,7 @@ try
ScreenToClient(_hwnd.get(), cursorPosition.as_win32_point());
}
const TerminalInput::MouseButtonState state{
const Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state{
WI_IsFlagSet(GetKeyState(VK_LBUTTON), KeyPressed),
WI_IsFlagSet(GetKeyState(VK_MBUTTON), KeyPressed),
WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed)
@@ -872,7 +878,7 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font
[[gsl::suppress(bounds .3)]] renderSettings.SetColorTableEntry(tableIndex, gsl::at(theme.ColorTable, tableIndex));
}
publicTerminal->_terminal->SetCursorStyle(static_cast<DispatchTypes::CursorStyle>(theme.CursorStyle));
publicTerminal->_terminal->SetCursorStyle(static_cast<Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle>(theme.CursorStyle));
publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, static_cast<float>(fontSize), CP_UTF8 };
publicTerminal->_UpdateFont(newDpi);

View File

@@ -3,14 +3,31 @@
#pragma once
#include "../../renderer/base/Renderer.hpp"
#include "../../renderer/dx/DxRenderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../../buffer/out/textBuffer.hpp"
#include "../../renderer/inc/FontInfoDesired.hpp"
#include "../../types/IControlAccessibilityInfo.h"
#include "HwndTerminalAutomationPeer.hpp"
using namespace Microsoft::Console::VirtualTerminal;
namespace Microsoft::Console::Render::Atlas
{
class AtlasEngine;
}
namespace Microsoft::Console::Render
{
using AtlasEngine = Atlas::AtlasEngine;
class IRenderData;
class Renderer;
class UiaEngine;
}
namespace Microsoft::Terminal::Core
{
class Terminal;
}
class FontInfo;
class FontInfoDesired;
class HwndTerminalAutomationPeer;
// Keep in sync with TerminalTheme.cs
typedef struct _TerminalTheme
@@ -79,7 +96,7 @@ private:
std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal;
std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer;
std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine;
std::unique_ptr<::Microsoft::Console::Render::AtlasEngine> _renderEngine;
std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine;
bool _focused{ false };

View File

@@ -34,8 +34,6 @@ namespace Microsoft.Terminal.Control
Boolean EnableUnfocusedAcrylic;
ScrollbarState ScrollState { get; };
Boolean UseAtlasEngine { get; };
String FontFace { get; };
Single FontSize { get; };
Windows.UI.Text.FontWeight FontWeight { get; };

View File

@@ -2404,25 +2404,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// 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/AtlasEngine.
// instantiating a AtlasEngine.
// GH#10211 - UNDER NO CIRCUMSTANCE should this fail. If it does, the
// whole app will crash instantaneously on launch, which is no good.
float scale;
if (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 engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
LOG_IF_FAILED(engine->UpdateDpi(dpi));
LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
const auto scale = engine->GetScaling();
const auto actualFontSize = actualFont.GetSize();
// UWP XAML scrollbars aren't guaranteed to be the same size as the

View File

@@ -7,7 +7,6 @@
#include "XamlLights.h"
#include "EventArgs.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"

View File

@@ -31,7 +31,7 @@
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\common.nugetversions.props" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<!-- ========================= Headers ======================== -->
<ItemGroup>
<ClInclude Include="pch.h" />
@@ -166,7 +166,6 @@
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\base\lib\base.vcxproj" />
<ProjectReference Include="..\..\renderer\atlas\atlas.vcxproj" />
<ProjectReference Include="..\..\renderer\dx\lib\dx.vcxproj" />
<ProjectReference Include="..\..\renderer\uia\lib\uia.vcxproj" />
<ProjectReference Include="..\..\terminal\parser\lib\parser.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\terminal\input\lib\terminalinput.vcxproj" />

View File

@@ -43,9 +43,6 @@
<ProjectReference Include="$(OpenConsoleDir)src\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\audio\midi\lib\midi.vcxproj">
<Project>{3c67784e-1453-49c2-9660-483e2cc7f7ad}</Project>
</ProjectReference>

View File

@@ -102,7 +102,6 @@ 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);
OBSERVABLE_PROJECTED_SETTING(_profile, Elevate);
OBSERVABLE_PROJECTED_SETTING(_profile, VtPassthrough);
OBSERVABLE_PROJECTED_SETTING(_profile, ReloadEnvironmentVariables);

View File

@@ -103,7 +103,6 @@ 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);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, Elevate);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, VtPassthrough);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables);

View File

@@ -118,15 +118,6 @@
</StackPanel>
</local:SettingContainer>
<!-- AtlasEngine -->
<local:SettingContainer x:Uid="Profile_UseAtlasEngine"
ClearSettingValue="{x:Bind Profile.ClearUseAtlasEngine}"
HasSettingValue="{x:Bind Profile.HasUseAtlasEngine, Mode=OneWay}"
SettingOverrideSource="{x:Bind Profile.UseAtlasEngineOverrideSource, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind Profile.UseAtlasEngine, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- VtPassthrough -->
<local:SettingContainer x:Uid="Profile_VtPassthrough"
ClearSettingValue="{x:Bind Profile.ClearVtPassthrough}"

View File

@@ -23,12 +23,6 @@
<TextBlock x:Uid="Globals_RenderingDisclaimer"
Style="{StaticResource DisclaimerStyle}" />
<!-- AtlasEngine -->
<local:SettingContainer x:Uid="Profile_UseAtlasEngine">
<ToggleSwitch IsOn="{x:Bind ViewModel.UseAtlasEngine, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Force Full Repaint -->
<local:SettingContainer x:Uid="Globals_ForceFullRepaint">
<ToggleSwitch IsOn="{x:Bind ViewModel.ForceFullRepaintRendering, Mode=TwoWay}"

View File

@@ -12,7 +12,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
explicit RenderingViewModel(Model::CascadiaSettings settings) noexcept;
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.ProfileDefaults(), UseAtlasEngine);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), ForceFullRepaintRendering);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), SoftwareRendering);

View File

@@ -11,7 +11,6 @@ namespace Microsoft.Terminal.Settings.Editor
{
RenderingViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, UseAtlasEngine);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ForceFullRepaintRendering);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, SoftwareRendering);
}

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -1138,10 +1138,6 @@
<value>Controls what happens when the application emits a BEL character.</value>
<comment>A description for what the "bell style" setting does. Presented near "Profile_BellStyle".{Locked="BEL"}</comment>
</data>
<data name="Profile_UseAtlasEngine.Header" xml:space="preserve">
<value>Use the new text renderer ("AtlasEngine")</value>
<comment>{Locked="AtlasEngine"}</comment>
</data>
<data name="Profile_ReloadEnvVars.Header" xml:space="preserve">
<value>Launch this application with a new environment block</value>
<comment>"environment variables" are user-definable values that can affect the way running processes will behave on a computer</comment>

View File

@@ -949,25 +949,6 @@ void CascadiaSettings::_researchOnLoad()
// Only do this if we're actually being sampled
if (TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
{
// GH#13936: We're interested in how many users opt out of useAtlasEngine,
// indicating major issues that would require us to disable it by default again.
{
size_t enabled[2]{};
for (const auto& profile : _activeProfiles)
{
enabled[profile.UseAtlasEngine()]++;
}
TraceLoggingWrite(
g_hSettingsModelProvider,
"AtlasEngine_Usage",
TraceLoggingDescription("Event emitted upon settings load, containing the number of profiles opted-in/out of useAtlasEngine"),
TraceLoggingUIntPtr(enabled[0], "UseAtlasEngineDisabled", "Number of profiles for which AtlasEngine is disabled"),
TraceLoggingUIntPtr(enabled[1], "UseAtlasEngineEnabled", "Number of profiles for which AtlasEngine is enabled"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
// ----------------------------- RE: Themes ----------------------------
const auto numThemes = GlobalSettings().Themes().Size();
const auto themeInUse = GlobalSettings().CurrentTheme().Name();

View File

@@ -90,7 +90,6 @@ Author(s):
X(hstring, TabTitle, "tabTitle") \
X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \
X(bool, UseAtlasEngine, "useAtlasEngine", Feature_AtlasEngine::IsEnabled()) \
X(bool, RightClickContextMenu, "experimental.rightClickContextMenu", false) \
X(Windows::Foundation::Collections::IVector<winrt::hstring>, BellSound, "bellSound", nullptr) \
X(bool, Elevate, "elevate", false) \

View File

@@ -85,7 +85,6 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap<String COMMA String>, EnvironmentVariables);
INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine);
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector<String>, BellSound);
INHERITABLE_PROFILE_SETTING(Boolean, Elevate);

View File

@@ -307,7 +307,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_SuppressApplicationTitle = profile.SuppressApplicationTitle();
}
_UseAtlasEngine = profile.UseAtlasEngine();
_ScrollState = profile.ScrollState();
_AntialiasingMode = profile.AntialiasingMode();

View File

@@ -148,7 +148,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, IEnvironmentVariableMap, 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);

View File

@@ -40,7 +40,6 @@
<ProjectReference Include="$(OpenConsoleDir)src\buffer\out\lib\bufferout.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\base\lib\base.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\dx\lib\dx.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\uia\lib\uia.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\terminal\parser\lib\parser.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\terminal\input\lib\terminalinput.vcxproj" />

View File

@@ -8,7 +8,6 @@
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"

View File

@@ -8,7 +8,6 @@
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"

View File

@@ -72,7 +72,6 @@
X(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \
X(bool, ForceFullRepaintRendering, false) \
X(bool, SoftwareRendering, false) \
X(bool, UseAtlasEngine, false) \
X(bool, UseBackgroundImageForWindow, false) \
X(bool, ShowMarks, false) \
X(bool, RightClickContextMenu, false)

View File

@@ -26,15 +26,6 @@
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_ConhostDxEngine</name>
<description>Controls whether conhost supports the DX engine and the UseDx registry key</description>
<stage>AlwaysEnabled</stage>
<alwaysDisabledBrandingTokens>
<brandingToken>WindowsInbox</brandingToken>
</alwaysDisabledBrandingTokens>
</feature>
<feature>
<name>Feature_ConhostAtlasEngine</name>
<description>Controls whether conhost supports the Atlas engine</description>
@@ -44,15 +35,6 @@
</alwaysDisabledBrandingTokens>
</feature>
<feature>
<name>Feature_DxEngineShaderSupport</name>
<description>Controls whether the DX engine is built with shader support.</description>
<stage>AlwaysEnabled</stage>
<alwaysDisabledBrandingTokens>
<brandingToken>WindowsInbox</brandingToken>
</alwaysDisabledBrandingTokens>
</feature>
<feature>
<name>Feature_UseNumpadEventsForClipboardInput</name>
<description>Controls whether the clipboard converter (and ConPTY InputStateMachine) uses Numpad events instead of UChar</description>

View File

@@ -47,9 +47,6 @@
<ProjectReference Include="..\..\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\gdi\lib\gdi.vcxproj">
<Project>{1c959542-bac2-4e55-9a6d-13251914cbb9}</Project>
</ProjectReference>

View File

@@ -42,9 +42,6 @@
<ProjectReference Include="..\..\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\gdi\lib\gdi.vcxproj">
<Project>{1c959542-bac2-4e55-9a6d-13251914cbb9}</Project>
</ProjectReference>

View File

@@ -87,14 +87,16 @@ std::vector<Viewport> RenderData::GetSelectionRects() noexcept
// they're done with any querying they need to do.
void RenderData::LockConsole() noexcept
{
::LockConsole();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.LockConsole();
}
// Method Description:
// - Unlocks the console after a call to RenderData::LockConsole.
void RenderData::UnlockConsole() noexcept
{
::UnlockConsole();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.UnlockConsole();
}
// Method Description:

View File

@@ -53,7 +53,7 @@ Settings::Settings() :
_fUseWindowSizePixels(false),
// window size pixels initialized below
_fInterceptCopyPaste(0),
_fUseDx(UseDx::Disabled),
_fUseDx(false),
_fCopyColor(false)
{
_dwScreenBufferSize.X = 80;
@@ -767,7 +767,7 @@ void Settings::SetTerminalScrolling(const bool terminalScrollingEnabled) noexcep
// 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
bool Settings::GetUseDx() const noexcept
{
return _fUseDx;
}

View File

@@ -24,13 +24,6 @@ constexpr unsigned short MIN_WINDOW_OPACITY = 0x4D; // 0x4D is approximately 30%
#include "ConsoleArguments.hpp"
#include "../renderer/inc/RenderSettings.hpp"
enum class UseDx : DWORD
{
Disabled = 0,
DxEngine,
AtlasEngine,
};
class Settings
{
using RenderSettings = Microsoft::Console::Render::RenderSettings;
@@ -176,7 +169,7 @@ public:
bool IsTerminalScrolling() const noexcept;
void SetTerminalScrolling(const bool terminalScrollingEnabled) noexcept;
UseDx GetUseDx() const noexcept;
bool GetUseDx() const noexcept;
bool GetCopyColor() const noexcept;
private:
@@ -219,7 +212,7 @@ private:
std::wstring _LaunchFaceName;
bool _fAllowAltF4Close;
DWORD _dwVirtTermLevel;
UseDx _fUseDx;
bool _fUseDx;
bool _fCopyColor;
// this is used for the special STARTF_USESIZE mode.

View File

@@ -48,9 +48,6 @@
<ProjectReference Include="..\..\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
<ProjectReference Include="..\..\renderer\vt\ut_lib\vt.unittest.vcxproj">
<Project>{990F2657-8580-4828-943F-5DD657D11843}</Project>
</ProjectReference>

View File

@@ -12,10 +12,6 @@
#include "../../renderer/vt/Xterm256Engine.hpp"
#include "../../renderer/vt/XtermEngine.hpp"
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
#include "../../renderer/dx/DxRenderer.hpp"
#endif
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
@@ -38,10 +34,6 @@ class Microsoft::Console::VirtualTerminal::VtIoTests
TEST_METHOD(RendererDtorAndThread);
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
TEST_METHOD(RendererDtorAndThreadAndDx);
#endif
TEST_METHOD(BasicAnonymousPipeOpeningWithSignalChannelTest);
};
@@ -419,37 +411,6 @@ void VtIoTests::RendererDtorAndThread()
}
}
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
void VtIoTests::RendererDtorAndThreadAndDx()
{
Log::Comment(NoThrowString().Format(
L"Test deleting a Renderer a bunch of times"));
for (auto i = 0; i < 16; ++i)
{
auto data = std::make_unique<MockRenderData>();
auto thread = std::make_unique<Microsoft::Console::Render::RenderThread>();
auto* pThread = thread.get();
auto pRenderer = std::make_unique<Microsoft::Console::Render::Renderer>(RenderSettings{}, data.get(), nullptr, 0, std::move(thread));
VERIFY_SUCCEEDED(pThread->Initialize(pRenderer.get()));
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
pRenderer->AddRenderEngine(dxEngine.get());
// Sleep for a hot sec to make sure the thread starts before we enable painting
// If you don't, the thread might wait on the paint enabled event AFTER
// EnablePainting gets called, and if that happens, then the thread will
// never get destructed. This will only ever happen in the vstest test runner,
// which is what CI uses.
/*Sleep(500);*/
(void)dxEngine->Enable();
pThread->EnablePainting();
pRenderer->TriggerTeardown();
pRenderer.reset();
}
}
#endif
void VtIoTests::BasicAnonymousPipeOpeningWithSignalChannelTest()
{
Log::Comment(L"Test using anonymous pipes for the input and adding a signal channel.");

View File

@@ -37,14 +37,14 @@ DEFINE_ENUM_FLAG_OPERATORS(CharacterAttributes);
constexpr uint8_t UNDERLINE_STYLE_SHIFT = 6;
enum class CursorType : unsigned int
enum class CursorType
{
Legacy = 0x0, // uses the cursor's height value to range from underscore-like to full box
VerticalBar = 0x1, // A single vertical line, '|'
Underscore = 0x2, // a single horizontal underscore, smaller that the min height legacy cursor.
EmptyBox = 0x3, // Just the outline of a full box
FullBox = 0x4, // a full box, similar to legacy with height=100%
DoubleUnderscore = 0x5 // a double horizontal underscore
DoubleUnderscore = 0x5, // a double horizontal underscore
};
// Valid COLORREFs are of the pattern 0x00bbggrr. -1 works as an invalid color,

View File

@@ -36,7 +36,7 @@ namespace til
// * small and cheap T
// * >= 50% successful lookups
// * <= 50% load factor (LoadFactor >= 2, which is the minimum anyways)
template<typename T, size_t LoadFactor = 2, size_t GrowthExponent = 1>
template<typename T, typename Traits, size_t LoadFactor = 2, size_t GrowthExponent = 1>
struct linear_flat_set
{
static_assert(LoadFactor >= 2);
@@ -98,27 +98,28 @@ namespace til
return nullptr;
}
const auto hash = ::std::hash<T>{}(key) >> _shift;
const auto hash = Traits::hash(key) >> _shift;
for (auto i = hash;; ++i)
{
auto& slot = _map[i & _mask];
if (!slot)
if (!Traits::occupied(slot))
{
return nullptr;
}
if (slot == key) [[likely]]
if (Traits::equals(slot, key)) [[likely]]
{
return &slot;
}
}
}
// NOTE: insert() does not initialize the returned slot. You must do that yourself
// in way that ensures that Traits::occupied(slot) now returns true.
// This method returns a pointer only to be symmetric with lookup().
template<typename U>
std::pair<T&, bool> insert(U&& key)
T* insert(U&& key)
{
// Putting this into the lookup path is a little pessimistic, but it
// allows us to default-construct this hashmap with a size of 0.
if (_load >= _capacity) [[unlikely]]
{
_bumpSize();
@@ -129,20 +130,16 @@ namespace til
// many times in literature that such a scheme performs the best on average.
// As such, we perform the divide here to get the topmost bits down.
// See flat_set_hash_integer.
const auto hash = ::std::hash<T>{}(key) >> _shift;
const auto hash = Traits::hash(key) >> _shift;
for (auto i = hash;; ++i)
{
auto& slot = _map[i & _mask];
if (!slot)
if (!Traits::occupied(slot))
{
slot = std::forward<U>(key);
_load += LoadFactor;
return { slot, true };
}
if (slot == key) [[likely]]
{
return { slot, false };
Traits::assign(slot, key);
return &slot;
}
}
}
@@ -166,17 +163,17 @@ namespace til
// This mirrors the insert() function, but without the lookup part.
for (auto& oldSlot : container())
{
if (!oldSlot)
if (!Traits::occupied(oldSlot))
{
continue;
}
const auto hash = ::std::hash<T>{}(oldSlot) >> newShift;
const auto hash = Traits::hash(oldSlot) >> newShift;
for (auto i = hash;; ++i)
{
auto& slot = newMap[i & newMask];
if (!slot)
if (!Traits::occupied(slot))
{
slot = std::move_if_noexcept(oldSlot);
break;

View File

@@ -63,9 +63,6 @@
<ProjectReference Include="..\..\..\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>

View File

@@ -26,9 +26,6 @@
<ProjectReference Include="..\..\..\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\renderer\vt\lib\vt.vcxproj">
<Project>{990f2657-8580-4828-943f-5dd657d11842}</Project>
</ProjectReference>

View File

@@ -29,9 +29,6 @@
#if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED
#include "../../renderer/atlas/AtlasEngine.h"
#endif
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
#include "../../renderer/dx/DxRenderer.hpp"
#endif
#include "../inc/ServiceLocator.hpp"
#include "../../types/inc/Viewport.hpp"
@@ -71,9 +68,6 @@ Window::~Window()
// reducing the change for existing race conditions to turn into deadlocks.
#ifndef NDEBUG
delete pGdiEngine;
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
delete pDxEngine;
#endif
#if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED
delete pAtlasEngine;
#endif
@@ -218,31 +212,17 @@ void Window::_UpdateSystemMetrics() const
const auto useDx = pSettings->GetUseDx();
try
{
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
// determine the initial window size, which happens BEFORE the
// window is created, we'll want to make sure the DX engine does
// math in the hwnd mode, not the Composition mode.
THROW_IF_FAILED(pDxEngine->SetHwnd(nullptr));
g.pRender->AddRenderEngine(pDxEngine);
break;
#endif
#if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED
case UseDx::AtlasEngine:
if (useDx)
{
pAtlasEngine = new AtlasEngine();
g.pRender->AddRenderEngine(pAtlasEngine);
break;
}
else
#endif
default:
{
pGdiEngine = new GdiEngine();
g.pRender->AddRenderEngine(pGdiEngine);
break;
#pragma warning(suppress : 4065)
}
}
catch (...)
@@ -333,20 +313,8 @@ void Window::_UpdateSystemMetrics() const
{
_hWnd = hWnd;
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
if (pDxEngine)
{
HRESULT hr = S_OK;
if (SUCCEEDED(hr = pDxEngine->SetHwnd(hWnd)))
{
hr = pDxEngine->Enable();
}
status = NTSTATUS_FROM_HRESULT(hr);
}
else
#endif
#if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED
if (pAtlasEngine)
if (pAtlasEngine)
{
const auto hr = pAtlasEngine->SetHwnd(hWnd);
status = NTSTATUS_FROM_HRESULT(hr);

View File

@@ -24,7 +24,6 @@ namespace Microsoft::Console::Render::Atlas
namespace Microsoft::Console::Render
{
using AtlasEngine = Atlas::AtlasEngine;
class DxEngine;
class GdiEngine;
}
@@ -118,9 +117,6 @@ namespace Microsoft::Console::Interactivity::Win32
HWND _hWnd;
Render::GdiEngine* pGdiEngine = nullptr;
#if TIL_FEATURE_CONHOSTDXENGINE_ENABLED
Render::DxEngine* pDxEngine = nullptr;
#endif
#if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED
Render::AtlasEngine* pAtlasEngine = nullptr;
#endif

View File

@@ -60,7 +60,7 @@ const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_PropertyMa
{ _RegPropertyType::Dword, CONSOLE_REGISTRY_CURSORTYPE, SET_FIELD_AND_SIZE(_CursorType) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_INTERCEPTCOPYPASTE, SET_FIELD_AND_SIZE(_fInterceptCopyPaste) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_TERMINALSCROLLING, SET_FIELD_AND_SIZE(_TerminalScrolling) },
{ _RegPropertyType::Dword, CONSOLE_REGISTRY_USEDX, SET_FIELD_AND_SIZE(_fUseDx) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_USEDX, SET_FIELD_AND_SIZE(_fUseDx) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_COPYCOLOR, SET_FIELD_AND_SIZE(_fCopyColor) }
// Special cases that are handled manually in Registry::LoadFromRegistry:

View File

@@ -40,483 +40,12 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2<T>& out) noexcept
return api_narrow(x, out.x) | api_narrow(y, out.y);
}
#pragma region IRenderEngine
[[nodiscard]] HRESULT AtlasEngine::Invalidate(const til::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.start = std::min(_api.invalidatedRows.start, gsl::narrow_cast<u16>(psrRegion->top));
_api.invalidatedRows.end = std::max(_api.invalidatedRows.end, gsl::narrow_cast<u16>(psrRegion->bottom));
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateCursor(const til::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<u16>(psrRegion->left);
const auto top = gsl::narrow_cast<u16>(psrRegion->top);
const auto right = gsl::narrow_cast<u16>(psrRegion->right);
const auto bottom = gsl::narrow_cast<u16>(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 til::rect* const prcDirtyClient) noexcept
{
const auto top = prcDirtyClient->top / _api.s->font->cellSize.y;
const auto bottom = prcDirtyClient->bottom / _api.s->font->cellSize.y;
// BeginPaint() protects against invalid out of bounds numbers.
til::rect rect;
rect.top = top;
rect.bottom = bottom;
return Invalidate(&rect);
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateSelection(const std::vector<til::rect>& 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.start = gsl::narrow_cast<u16>(std::min<int>(_api.invalidatedRows.start, std::max<int>(0, rect.top)));
_api.invalidatedRows.end = gsl::narrow_cast<u16>(std::max<int>(_api.invalidatedRows.end, std::max<int>(0, rect.bottom)));
}
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateScroll(const til::point* const pcoordDelta) noexcept
{
// 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.
if (const auto delta = pcoordDelta->x)
{
_api.invalidatedCursorArea.left = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedCursorArea.left + delta, u16min, u16max));
_api.invalidatedCursorArea.right = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedCursorArea.right + delta, u16min, u16max));
_api.invalidatedRows = invalidatedRowsAll;
}
if (const auto delta = pcoordDelta->y)
{
_api.scrollOffset = gsl::narrow_cast<i16>(clamp<int>(_api.scrollOffset + delta, i16min, i16max));
_api.invalidatedCursorArea.top = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedCursorArea.top + delta, u16min, u16max));
_api.invalidatedCursorArea.bottom = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedCursorArea.bottom + delta, u16min, u16max));
if (delta < 0)
{
_api.invalidatedRows.start = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedRows.start + delta, u16min, u16max));
_api.invalidatedRows.end = _api.s->viewportCellCount.y;
}
else
{
_api.invalidatedRows.start = 0;
_api.invalidatedRows.end = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedRows.end + delta, u16min, u16max));
}
}
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateAll() noexcept
{
_api.invalidatedRows = invalidatedRowsAll;
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateFlush(_In_ const bool /*circled*/, _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
{
_api.invalidatedTitle = true;
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::NotifyNewText(const std::wstring_view newText) noexcept
{
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, _Out_ FontInfo& fontInfo) noexcept
{
return UpdateFont(fontInfoDesired, fontInfo, {}, {});
}
[[nodiscard]] HRESULT AtlasEngine::UpdateSoftFont(const std::span<const uint16_t> bitPattern, const til::size cellSize, const size_t centeringHint) noexcept
{
const auto softFont = _api.s.write()->font.write();
softFont->softFontPattern.assign(bitPattern.begin(), bitPattern.end());
softFont->softFontCellSize.width = std::max(0, cellSize.width);
softFont->softFontCellSize.height = std::max(0, cellSize.height);
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::UpdateDpi(const int dpi) noexcept
{
u16 newDPI;
RETURN_IF_FAILED(api_narrow(dpi, newDPI));
if (_api.s->font->dpi != newDPI)
{
_api.s.write()->font.write()->dpi = newDPI;
}
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept
try
{
const u16x2 viewportCellCount{
gsl::narrow<u16>(std::max(1, srNewViewport.right - srNewViewport.left + 1)),
gsl::narrow<u16>(std::max(1, srNewViewport.bottom - srNewViewport.top + 1)),
};
const u16x2 viewportOffset{
gsl::narrow<u16>(srNewViewport.left),
gsl::narrow<u16>(srNewViewport.top),
};
if (_api.s->viewportCellCount != viewportCellCount)
{
_api.s.write()->viewportCellCount = viewportCellCount;
}
if (_api.s->viewportOffset != viewportOffset)
{
_api.s.write()->viewportOffset = viewportOffset;
}
return S_OK;
}
CATCH_RETURN()
[[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<HFONT>(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.height, 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()));
til::size sz;
RETURN_HR_IF(E_FAIL, !GetTextExtentPoint32W(hdc.get(), L"M", 1, &sz));
resultingCellSize.width = sz.width;
resultingCellSize.height = sz.height;
}
#endif
_resolveFontMetrics(nullptr, fontInfoDesired, fontInfo);
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::GetDirtyArea(std::span<const til::rect>& area) noexcept
{
area = std::span{ &_api.dirtyRect, 1 };
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::GetFontSize(_Out_ til::size* pFontSize) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, pFontSize);
pFontSize->width = _api.s->font->cellSize.x;
pFontSize->height = _api.s->font->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<IDWriteTextFormat> textFormat;
RETURN_IF_FAILED(_p.dwriteFactory->CreateTextFormat(
/* fontFamilyName */ _api.s->font->fontName.c_str(),
/* fontCollection */ _api.s->font->fontCollection.get(),
/* fontWeight */ static_cast<DWRITE_FONT_WEIGHT>(_api.s->font->fontWeight),
/* fontStyle */ DWRITE_FONT_STYLE_NORMAL,
/* fontStretch */ DWRITE_FONT_STRETCH_NORMAL,
/* fontSize */ _api.s->font->fontSize,
/* localeName */ L"",
/* textFormat */ textFormat.put()));
wil::com_ptr<IDWriteTextLayout> textLayout;
RETURN_IF_FAILED(_p.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast<uint32_t>(glyph.size()), textFormat.get(), FLT_MAX, FLT_MAX, textLayout.addressof()));
DWRITE_TEXT_METRICS metrics{};
RETURN_IF_FAILED(textLayout->GetMetrics(&metrics));
const auto minWidth = (_api.s->font->cellSize.x * 1.2f);
*pResult = metrics.width > minWidth;
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]] std::wstring_view AtlasEngine::GetPixelShaderPath() noexcept
{
return _api.s->misc->customPixelShaderPath;
}
[[nodiscard]] bool AtlasEngine::GetRetroTerminalEffect() const noexcept
{
return _api.s->misc->useRetroTerminalEffect;
}
[[nodiscard]] float AtlasEngine::GetScaling() const noexcept
{
return static_cast<f32>(_api.s->font->dpi) / static_cast<f32>(USER_DEFAULT_SCREEN_DPI);
}
[[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept
{
assert(_api.s->font->cellSize.x != 0);
assert(_api.s->font->cellSize.y != 0);
return Types::Viewport::FromDimensions(viewInPixels.Origin(), { viewInPixels.Width() / _api.s->font->cellSize.x, viewInPixels.Height() / _api.s->font->cellSize.y });
}
[[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept
{
assert(_api.s->font->cellSize.x != 0);
assert(_api.s->font->cellSize.y != 0);
return Types::Viewport::FromDimensions(viewInCharacters.Origin(), { viewInCharacters.Width() * _api.s->font->cellSize.x, viewInCharacters.Height() * _api.s->font->cellSize.y });
}
void AtlasEngine::SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept
{
const auto mode = static_cast<AntialiasingMode>(antialiasingMode);
if (_api.antialiasingMode != mode)
{
_api.antialiasingMode = mode;
_resolveTransparencySettings();
}
}
void AtlasEngine::SetCallback(std::function<void(HANDLE)> pfn) noexcept
{
_p.swapChainChangedCallback = std::move(pfn);
}
void AtlasEngine::EnableTransparentBackground(const bool isTransparent) noexcept
{
if (_api.enableTransparentBackground != isTransparent)
{
_api.enableTransparentBackground = isTransparent;
_resolveTransparencySettings();
}
}
void AtlasEngine::SetForceFullRepaintRendering(bool enable) noexcept
{
}
[[nodiscard]] HRESULT AtlasEngine::SetHwnd(const HWND hwnd) noexcept
{
if (_api.s->target->hwnd != hwnd)
{
_api.s.write()->target.write()->hwnd = hwnd;
}
return S_OK;
}
void AtlasEngine::SetPixelShaderPath(std::wstring_view value) noexcept
try
{
if (_api.s->misc->customPixelShaderPath != value)
{
_api.s.write()->misc.write()->customPixelShaderPath = value;
_resolveTransparencySettings();
}
}
CATCH_LOG()
void AtlasEngine::SetRetroTerminalEffect(bool enable) noexcept
{
if (_api.s->misc->useRetroTerminalEffect != enable)
{
_api.s.write()->misc.write()->useRetroTerminalEffect = enable;
_resolveTransparencySettings();
}
}
void AtlasEngine::SetSelectionBackground(const COLORREF color, const float alpha) noexcept
{
const u32 selectionColor = (color & 0xffffff) | gsl::narrow_cast<u32>(lrintf(alpha * 255.0f)) << 24;
if (_api.s->misc->selectionColor != selectionColor)
{
_api.s.write()->misc.write()->selectionColor = selectionColor;
}
}
void AtlasEngine::SetSoftwareRendering(bool enable) noexcept
{
if (_api.s->target->useSoftwareRendering != enable)
{
_api.s.write()->target.write()->useSoftwareRendering = enable;
}
}
void AtlasEngine::SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept
{
_p.warningCallback = std::move(pfn);
}
[[nodiscard]] HRESULT AtlasEngine::SetWindowSize(const til::size pixels) noexcept
{
// When Win+D is pressed, `GetClientRect` returns {0,0}.
// There's probably more situations in which our callers may pass us invalid data.
if (!pixels)
{
return S_OK;
}
const u16x2 newSize{
gsl::narrow_cast<u16>(clamp<til::CoordType>(pixels.width, 1, u16max)),
gsl::narrow_cast<u16>(clamp<til::CoordType>(pixels.height, 1, u16max)),
};
if (_api.s->targetSize != newSize)
{
_api.s.write()->targetSize = newSize;
}
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept
{
try
{
_updateFont(fontInfoDesired.GetFaceName().c_str(), fontInfoDesired, fontInfo, features, axes);
return S_OK;
}
CATCH_LOG();
if constexpr (Feature_NearbyFontLoading::IsEnabled())
{
try
{
// _resolveFontMetrics() checks `_api.s->font->fontCollection` for a pre-existing font collection,
// before falling back to using the system font collection. This way we can inject our custom one. See GH#9375.
// Doing it this way is a bit hacky, but it does have the benefit that we can cache a font collection
// instance across font changes, like when zooming the font size rapidly using the scroll wheel.
_api.s.write()->font.write()->fontCollection = FontCache::GetCached();
_updateFont(fontInfoDesired.GetFaceName().c_str(), fontInfoDesired, fontInfo, features, axes);
return S_OK;
}
CATCH_LOG();
}
try
{
_updateFont(nullptr, fontInfoDesired, fontInfo, features, axes);
return S_OK;
}
CATCH_RETURN();
}
void AtlasEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept
{
_api.hyperlinkHoveredId = hoveredId;
}
#pragma endregion
void AtlasEngine::_resolveTransparencySettings() noexcept
{
// An opaque background allows us to use true "independent" flips. See AtlasEngine::_createSwapChain().
// We can't enable them if custom shaders are specified, because it's unknown, whether they support opaque inputs.
const bool useAlpha = _api.enableTransparentBackground || !_api.s->misc->customPixelShaderPath.empty();
// If the user asks for ClearType, but also for a transparent background
// (which our ClearType shader doesn't simultaneously support)
// then we need to sneakily force the renderer to grayscale AA.
const auto antialiasingMode = useAlpha && _api.antialiasingMode == AntialiasingMode::ClearType ? AntialiasingMode::Grayscale : _api.antialiasingMode;
if (antialiasingMode != _api.s->font->antialiasingMode || useAlpha != _api.s->target->useAlpha)
{
const auto s = _api.s.write();
s->font.write()->antialiasingMode = antialiasingMode;
s->target.write()->useAlpha = useAlpha;
_api.backgroundOpaqueMixin = useAlpha ? 0x00000000 : 0xff000000;
}
}
void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes)
void AtlasEngine::_updateFont(const wchar_t* faceName, const FontSettings& fontSettings)
{
std::vector<DWRITE_FONT_FEATURE> fontFeatures;
if (!features.empty())
if (!fontSettings.features.empty())
{
fontFeatures.reserve(features.size() + 3);
fontFeatures.reserve(fontSettings.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
@@ -528,7 +57,7 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1);
fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1);
for (const auto& p : features)
for (const auto& p : fontSettings.features)
{
if (p.first.size() == 4)
{
@@ -553,9 +82,9 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
}
std::vector<DWRITE_FONT_AXIS_VALUE> fontAxisValues;
if (!axes.empty())
if (!fontSettings.axes.empty())
{
fontAxisValues.reserve(axes.size() + 3);
fontAxisValues.reserve(fontSettings.axes.size() + 3);
// AtlasEngine::_recreateFontDependentResources() relies on these fields to
// exist in this particular order in order to create appropriate default axes.
@@ -563,7 +92,7 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f);
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_SLANT, -1.0f);
for (const auto& p : axes)
for (const auto& p : fontSettings.axes)
{
if (p.first.size() == 4)
{
@@ -588,26 +117,23 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
}
const auto font = _api.s.write()->font.write();
_resolveFontMetrics(faceName, fontInfoDesired, fontInfo, font);
_resolveFontMetrics(faceName, fontSettings, font);
font->fontFeatures = std::move(fontFeatures);
font->fontAxisValues = std::move(fontAxisValues);
}
void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics) const
void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontSettings& fontSettings, ResolvedFontSettings* fontMetrics) const
{
const auto requestedFamily = fontInfoDesired.GetFamily();
auto requestedWeight = fontInfoDesired.GetWeight();
auto fontSize = fontInfoDesired.GetFontSize();
auto requestedSize = fontInfoDesired.GetEngineSize();
auto requestedWeight = fontSettings.weight;
auto fontSize = fontSettings.fontSize;
if (!requestedFaceName)
{
requestedFaceName = L"Consolas";
}
if (!requestedSize.height)
if (!fontSize)
{
fontSize = 12.0f;
requestedSize = { 0, 12 };
}
if (!requestedWeight)
{
@@ -675,8 +201,8 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
}
}
auto adjustedWidth = std::roundf(fontInfoDesired.GetCellWidth().Resolve(advanceWidth, dpi, fontSizeInPx, advanceWidth));
auto adjustedHeight = std::roundf(fontInfoDesired.GetCellHeight().Resolve(advanceHeight, dpi, fontSizeInPx, advanceWidth));
auto adjustedWidth = std::roundf(fontSettings.cellWidth.Resolve(advanceWidth, dpi, fontSizeInPx, advanceWidth));
auto adjustedHeight = std::roundf(fontSettings.cellHeight.Resolve(advanceHeight, dpi, fontSizeInPx, advanceWidth));
// Protection against bad user values in GetCellWidth/Y.
// AtlasEngine fails hard with 0 cell sizes.
@@ -715,22 +241,6 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
const auto cellWidth = gsl::narrow<u16>(lrintf(adjustedWidth));
const auto cellHeight = gsl::narrow<u16>(lrintf(adjustedHeight));
{
til::size coordSize;
coordSize.width = cellWidth;
coordSize.height = cellHeight;
if (requestedSize.width == 0)
{
// The coordSizeUnscaled parameter to SetFromEngine is used for API functions like GetConsoleFontSize.
// Since clients expect that settings the font height to Y yields back a font height of Y,
// we're scaling the X relative/proportional to the actual cellWidth/cellHeight ratio.
requestedSize.width = gsl::narrow_cast<til::CoordType>(lrintf(fontSize / cellHeight * cellWidth));
}
fontInfo.SetFromEngine(requestedFaceName, requestedFamily, requestedWeight, false, coordSize, requestedSize);
}
if (fontMetrics)
{
std::wstring fontName{ requestedFaceName };

View File

@@ -45,461 +45,19 @@ AtlasEngine::AtlasEngine()
_p.textAnalyzer = textAnalyzer.query<IDWriteTextAnalyzer1>();
}
#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
void AtlasEngine::WaitUntilCanRender() noexcept
{
if (const auto hwnd = _api.s->target->hwnd)
if constexpr (ATLAS_DEBUG_RENDER_DELAY)
{
RECT rect;
LOG_IF_WIN32_BOOL_FALSE(GetClientRect(hwnd, &rect));
std::ignore = SetWindowSize({ rect.right - rect.left, rect.bottom - rect.top });
if (_api.invalidatedTitle)
{
LOG_IF_WIN32_BOOL_FALSE(PostMessageW(hwnd, CM_UPDATE_TITLE, 0, 0));
_api.invalidatedTitle = false;
}
Sleep(ATLAS_DEBUG_RENDER_DELAY);
}
if (_p.s != _api.s)
{
_handleSettingsUpdate();
}
if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION)
{
_api.invalidatedRows = invalidatedRowsAll;
_api.scrollOffset = 0;
}
// Clamp invalidation rects into valid value ranges.
{
_api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _p.s->viewportCellCount.x);
_api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _p.s->viewportCellCount.y);
_api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _p.s->viewportCellCount.x);
_api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _p.s->viewportCellCount.y);
}
{
_api.invalidatedRows.start = std::min(_api.invalidatedRows.start, _p.s->viewportCellCount.y);
_api.invalidatedRows.end = clamp(_api.invalidatedRows.end, _api.invalidatedRows.start, _p.s->viewportCellCount.y);
}
if (_api.scrollOffset)
{
const auto limit = gsl::narrow_cast<i16>(_p.s->viewportCellCount.y & 0x7fff);
const auto offset = gsl::narrow_cast<i16>(clamp<int>(_api.scrollOffset, -limit, limit));
const auto nothingInvalid = _api.invalidatedRows.start == _api.invalidatedRows.end;
_api.scrollOffset = offset;
// Mark the newly scrolled in rows as invalidated
if (offset < 0)
{
const u16 begRow = _p.s->viewportCellCount.y + offset;
_api.invalidatedRows.start = nothingInvalid ? begRow : std::min(_api.invalidatedRows.start, begRow);
_api.invalidatedRows.end = _p.s->viewportCellCount.y;
}
else
{
const u16 endRow = offset;
_api.invalidatedRows.start = 0;
_api.invalidatedRows.end = nothingInvalid ? endRow : std::max(_api.invalidatedRows.end, endRow);
}
}
_api.dirtyRect = {
0,
_api.invalidatedRows.start,
_p.s->viewportCellCount.x,
_api.invalidatedRows.end,
};
_p.dirtyRectInPx = {
til::CoordTypeMax,
til::CoordTypeMax,
til::CoordTypeMin,
til::CoordTypeMin,
};
_p.invalidatedRows = _api.invalidatedRows;
_p.cursorRect = {};
_p.scrollOffset = _api.scrollOffset;
// This if condition serves 2 purposes:
// * By setting top/bottom to the full height we ensure that we call Present() without
// any dirty rects and not Present1() on the first frame after the settings change.
// * If the scrollOffset is so large that it scrolls the entire viewport, invalidatedRows will span
// the entire viewport as well. We need to set scrollOffset to 0 then, not just because scrolling
// the contents of the entire swap chain is redundant, but more importantly because the scroll rect
// is the subset of the contents that are being scrolled into. If you scroll the entire viewport
// then the scroll rect is empty, which Present1() will loudly complain about.
if (_p.invalidatedRows == range<u16>{ 0, _p.s->viewportCellCount.y })
{
_p.MarkAllAsDirty();
}
if (const auto offset = _p.scrollOffset)
{
if (offset < 0)
{
// scrollOffset/offset = -1
// +----------+ +----------+
// | | | xxxxxxxxx|
// | xxxxxxxxx| -> |xxxxxxx |
// |xxxxxxx | | |
// +----------+ +----------+
const auto dst = std::copy_n(_p.rows.begin() - offset, _p.rows.size() + offset, _p.rowsScratch.begin());
std::copy_n(_p.rows.begin(), -offset, dst);
}
else
{
// scrollOffset/offset = 1
// +----------+ +----------+
// | xxxxxxxxx| | |
// |xxxxxxx | -> | xxxxxxxxx|
// | | |xxxxxxx |
// +----------+ +----------+
const auto dst = std::copy_n(_p.rows.end() - offset, offset, _p.rowsScratch.begin());
std::copy_n(_p.rows.begin(), _p.rows.size() - offset, dst);
}
std::swap(_p.rows, _p.rowsScratch);
// Now that the rows have scrolled, their cached dirty rects, naturally also need to do the same.
// It doesn't really matter that some of these will end up being out of bounds,
// because we'll call ShapedRow::Clear() later on which resets them.
{
const auto deltaPx = offset * _p.s->font->cellSize.y;
for (const auto r : _p.rows)
{
r->dirtyTop += deltaPx;
r->dirtyBottom += deltaPx;
}
}
// Scrolling the background bitmap is a lot easier because we can rely on memmove which works
// with both forwards and backwards copying. It's a mystery why the STL doesn't have this.
{
const auto srcOffset = std::max<ptrdiff_t>(0, -offset) * gsl::narrow_cast<ptrdiff_t>(_p.colorBitmapRowStride);
const auto dstOffset = std::max<ptrdiff_t>(0, offset) * gsl::narrow_cast<ptrdiff_t>(_p.colorBitmapRowStride);
const auto count = _p.colorBitmapDepthStride - std::max(srcOffset, dstOffset);
assert(dstOffset >= 0 && dstOffset + count <= _p.colorBitmapDepthStride);
assert(srcOffset >= 0 && srcOffset + count <= _p.colorBitmapDepthStride);
auto src = _p.colorBitmap.data() + srcOffset;
auto dst = _p.colorBitmap.data() + dstOffset;
const auto bytes = count * sizeof(u32);
for (size_t i = 0; i < 2; ++i)
{
// Avoid bumping the colorBitmapGeneration unless necessary. This approx. further halves
// the (already small) GPU load. This could easily be replaced with some custom SIMD
// to avoid going over the memory twice, but... that's a story for another day.
if (memcmp(dst, src, bytes) != 0)
{
memmove(dst, src, bytes);
_p.colorBitmapGenerations[i].bump();
}
src += _p.colorBitmapDepthStride;
dst += _p.colorBitmapDepthStride;
}
}
}
// This serves two purposes. For each invalidated row, this will:
// * Get the old dirty rect and mark that region as needing invalidation during the upcoming Present1(),
// because it'll now be replaced with something else (for instance nothing/whitespace).
// * Clear() them to prepare them for the new incoming content from the TextBuffer.
if (_p.invalidatedRows.non_empty())
{
const til::CoordType targetSizeX = _p.s->targetSize.x;
const til::CoordType targetSizeY = _p.s->targetSize.y;
_p.dirtyRectInPx.left = 0;
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, _p.invalidatedRows.start * _p.s->font->cellSize.y);
_p.dirtyRectInPx.right = targetSizeX;
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.invalidatedRows.end * _p.s->font->cellSize.y);
for (auto y = _p.invalidatedRows.start; y < _p.invalidatedRows.end; ++y)
{
const auto r = _p.rows[y];
const auto clampedTop = clamp(r->dirtyTop, 0, targetSizeY);
const auto clampedBottom = clamp(r->dirtyBottom, 0, targetSizeY);
if (clampedTop != clampedBottom)
{
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, clampedTop);
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, clampedBottom);
}
r->Clear(y, _p.s->font->cellSize.y);
}
}
#if ATLAS_DEBUG_CONTINUOUS_REDRAW
_p.MarkAllAsDirty();
#endif
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::EndPaint() noexcept
try
{
_flushBufferLine();
// PaintCursor() is only called when the cursor is visible, but we need to invalidate the cursor area
// even if it isn't. Otherwise a transition from a visible to an invisible cursor wouldn't be rendered.
if (const auto r = _api.invalidatedCursorArea; r.non_empty())
{
_p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, r.left * _p.s->font->cellSize.x);
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, r.top * _p.s->font->cellSize.y);
_p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, r.right * _p.s->font->cellSize.x);
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, r.bottom * _p.s->font->cellSize.y);
}
_api.invalidatedCursorArea = invalidatedAreaNone;
_api.invalidatedRows = invalidatedRowsNone;
_api.scrollOffset = 0;
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
*pForcePaint = false;
return S_OK;
_waitUntilCanRender();
}
[[nodiscard]] HRESULT AtlasEngine::ScrollFrame() noexcept
void AtlasEngine::Render(const Render::RenderingPayload& payload)
{
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 til::CoordType targetRow, const til::CoordType viewportLeft) noexcept
{
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(targetRow, 0, _p.s->viewportCellCount.y));
_p.rows[y]->lineRendition = lineRendition;
_api.lineRendition = lineRendition;
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::PaintBackground() noexcept
{
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::PaintBufferLine(std::span<const Cluster> clusters, til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept
try
{
const auto y = gsl::narrow_cast<u16>(clamp<int>(coord.y, 0, _p.s->viewportCellCount.y));
if (_api.lastPaintBufferLineCoord.y != y)
{
_flushBufferLine();
}
const auto shift = gsl::narrow_cast<u8>(_api.lineRendition != LineRendition::SingleWidth);
const auto x = gsl::narrow_cast<u16>(clamp<int>(coord.x - (_p.s->viewportOffset.x >> shift), 0, _p.s->viewportCellCount.x));
auto columnEnd = x;
// _api.bufferLineColumn contains 1 more item than _api.bufferLine, as it represents the
// past-the-end index. It'll get appended again later once we built our new _api.bufferLine.
if (!_api.bufferLineColumn.empty())
{
_api.bufferLineColumn.pop_back();
}
// 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.
{
for (const auto& cluster : clusters)
{
for (const auto& ch : cluster.GetText())
{
_api.bufferLine.emplace_back(ch);
_api.bufferLineColumn.emplace_back(columnEnd);
}
columnEnd += gsl::narrow_cast<u16>(cluster.GetColumns());
}
_api.bufferLineColumn.emplace_back(columnEnd);
}
{
const auto row = _p.colorBitmap.begin() + _p.colorBitmapRowStride * y;
auto beg = row + (static_cast<size_t>(x) << shift);
auto end = row + (static_cast<size_t>(columnEnd) << shift);
const u32 colors[] = {
u32ColorPremultiply(_api.currentBackground),
_api.currentForeground,
};
for (size_t i = 0; i < 2; ++i)
{
const auto color = colors[i];
for (auto it = beg; it != end; ++it)
{
if (*it != color)
{
_p.colorBitmapGenerations[i].bump();
std::fill(it, end, color);
break;
}
}
beg += _p.colorBitmapDepthStride;
end += _p.colorBitmapDepthStride;
}
}
_api.lastPaintBufferLineCoord = { x, y };
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept
try
{
const auto shift = gsl::narrow_cast<u8>(_api.lineRendition != LineRendition::SingleWidth);
const auto x = std::max(0, coordTarget.x - (_p.s->viewportOffset.x >> shift));
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(coordTarget.y, 0, _p.s->viewportCellCount.y));
const auto from = gsl::narrow_cast<u16>(clamp<til::CoordType>(x << shift, 0, _p.s->viewportCellCount.x - 1));
const auto to = gsl::narrow_cast<u16>(clamp<size_t>((x + cchLine) << shift, from, _p.s->viewportCellCount.x));
const auto fg = gsl::narrow_cast<u32>(color) | 0xff000000;
_p.rows[y]->gridLineRanges.emplace_back(lines, fg, from, to);
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::PaintSelection(const til::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();
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.top, 0, _p.s->viewportCellCount.y));
const auto from = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.left, 0, _p.s->viewportCellCount.x - 1));
const auto to = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.right, from, _p.s->viewportCellCount.x));
auto& row = *_p.rows[y];
row.selectionFrom = from;
row.selectionTo = to;
_p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, from * _p.s->font->cellSize.x);
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, y * _p.s->font->cellSize.y);
_p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, to * _p.s->font->cellSize.x);
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.dirtyRectInPx.top + _p.s->font->cellSize.y);
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 CursorSettings cachedOptions{
.cursorColor = gsl::narrow_cast<u32>(options.fUseColor ? options.cursorColor | 0xff000000 : INVALID_COLOR),
.cursorType = gsl::narrow_cast<u16>(options.cursorType),
.heightPercentage = gsl::narrow_cast<u16>(options.ulCursorHeightPercent),
};
if (*_api.s->cursor != cachedOptions)
{
*_api.s.write()->cursor.write() = cachedOptions;
*_p.s.write()->cursor.write() = cachedOptions;
}
}
if (options.isOn)
{
const auto cursorWidth = 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar));
const auto top = options.coordCursor.y;
const auto bottom = top + 1;
const auto shift = gsl::narrow_cast<u8>(_p.rows[top]->lineRendition != LineRendition::SingleWidth);
auto left = options.coordCursor.x - (_p.s->viewportOffset.x >> shift);
auto right = left + cursorWidth;
left <<= shift;
right <<= shift;
_p.cursorRect = {
std::max<til::CoordType>(left, 0),
std::max<til::CoordType>(top, 0),
std::min<til::CoordType>(right, _p.s->viewportCellCount.x),
std::min<til::CoordType>(bottom, _p.s->viewportCellCount.y),
};
if (_p.cursorRect)
{
_p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, left * _p.s->font->cellSize.x);
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, top * _p.s->font->cellSize.y);
_p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, right * _p.s->font->cellSize.x);
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, bottom * _p.s->font->cellSize.y);
}
}
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, const gsl::not_null<IRenderData*> /*pData*/, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept
try
{
auto [fg, bg] = renderSettings.GetAttributeColorsWithAlpha(textAttributes);
fg |= 0xff000000;
bg |= _api.backgroundOpaqueMixin;
if (!isSettingDefaultBrushes)
{
auto attributes = FontRelevantAttributes::None;
WI_SetFlagIf(attributes, FontRelevantAttributes::Bold, textAttributes.IsIntense() && renderSettings.GetRenderMode(RenderSettings::Mode::IntenseIsBold));
WI_SetFlagIf(attributes, FontRelevantAttributes::Italic, textAttributes.IsItalic());
if (_api.attributes != attributes)
{
_flushBufferLine();
}
_api.currentBackground = gsl::narrow_cast<u32>(bg);
_api.currentForeground = gsl::narrow_cast<u32>(fg);
_api.attributes = attributes;
}
else if (textAttributes.BackgroundIsDefault() && bg != _api.s->misc->backgroundColor)
{
_api.s.write()->misc.write()->backgroundColor = bg;
_p.s.write()->misc.write()->backgroundColor = bg;
}
return S_OK;
}
CATCH_RETURN()
#pragma endregion
void AtlasEngine::_handleSettingsUpdate()
{
const auto targetChanged = _p.s->target != _api.s->target;
@@ -522,8 +80,6 @@ void AtlasEngine::_handleSettingsUpdate()
{
_recreateCellCountDependentResources();
}
_api.invalidatedRows = invalidatedRowsAll;
}
void AtlasEngine::_recreateFontDependentResources()

View File

@@ -4,8 +4,6 @@
#pragma once
#include <dwrite_3.h>
#include <d3d11_2.h>
#include <dxgi1_3.h>
#include "common.h"
@@ -21,63 +19,8 @@ namespace Microsoft::Console::Render::Atlas
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 til::rect* psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
[[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
[[nodiscard]] HRESULT InvalidateTitle(std::wstring_view proposedTitle) noexcept override;
[[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override;
[[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
[[nodiscard]] HRESULT ResetLineTransform() noexcept override;
[[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override;
[[nodiscard]] HRESULT PaintBackground() noexcept override;
[[nodiscard]] HRESULT PaintBufferLine(std::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override;
[[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override;
[[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
[[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
[[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null<IRenderData*> pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override;
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override;
[[nodiscard]] HRESULT UpdateSoftFont(std::span<const uint16_t> bitPattern, til::size cellSize, size_t centeringHint) noexcept override;
[[nodiscard]] HRESULT UpdateDpi(int iDpi) noexcept override;
[[nodiscard]] HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept override;
[[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, int iDpi) noexcept override;
[[nodiscard]] HRESULT GetDirtyArea(std::span<const til::rect>& area) noexcept override;
[[nodiscard]] HRESULT GetFontSize(_Out_ til::size* 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]] std::wstring_view GetPixelShaderPath() noexcept override;
[[nodiscard]] bool GetRetroTerminalEffect() const noexcept override;
[[nodiscard]] float GetScaling() const noexcept 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<void(HANDLE)> pfn) noexcept override;
void EnableTransparentBackground(const bool isTransparent) 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 SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept override;
[[nodiscard]] HRESULT SetWindowSize(til::size pixels) noexcept override;
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept override;
void UpdateHyperlinkHoveredId(uint16_t hoveredId) noexcept override;
void Render(const Render::RenderingPayload& payload) override;
private:
// AtlasEngine.cpp
@@ -91,8 +34,8 @@ namespace Microsoft::Console::Render::Atlas
// AtlasEngine.api.cpp
void _resolveTransparencySettings() noexcept;
void _updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes);
void _resolveFontMetrics(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics = nullptr) const;
void _updateFont(const wchar_t* faceName, const FontSettings& fontSettings);
void _resolveFontMetrics(const wchar_t* faceName, const FontSettings& fontSettings, ResolvedFontSettings* fontMetrics = nullptr) const;
// AtlasEngine.r.cpp
ATLAS_ATTR_COLD void _recreateAdapter();
@@ -127,7 +70,7 @@ namespace Microsoft::Console::Render::Atlas
// These two are redundant with TargetSettings/MiscellaneousSettings, but that's because _resolveTransparencySettings()
// turns the given settings into potentially different actual settings (which are then written into the Settings).
bool enableTransparentBackground = false;
AntialiasingMode antialiasingMode = DefaultAntialiasingMode;
TextAntialiasMode antialiasingMode = DefaultAntialiasingMode;
std::vector<wchar_t> bufferLine;
std::vector<u16> bufferLineColumn;
@@ -147,8 +90,6 @@ namespace Microsoft::Console::Render::Atlas
u16 replacementCharacterGlyphIndex = 0;
bool replacementCharacterLookedUp = false;
// PrepareLineTransform()
LineRendition lineRendition = LineRendition::SingleWidth;
// UpdateDrawingBrushes()
u32 backgroundOpaqueMixin = 0xff000000;
u32 currentBackground = 0;
@@ -157,13 +98,6 @@ namespace Microsoft::Console::Render::Atlas
u16x2 lastPaintBufferLineCoord{};
// UpdateHyperlinkHoveredId()
u16 hyperlinkHoveredId = 0;
// dirtyRect is a computed value based on invalidatedRows.
til::rect dirtyRect;
// These "invalidation" fields are reset in EndPaint()
u16r invalidatedCursorArea = invalidatedAreaNone;
range<u16> invalidatedRows = invalidatedRowsNone; // x is treated as "top" and y as "bottom"
i16 scrollOffset = 0;
} _api;
};
}

View File

@@ -25,72 +25,6 @@
using namespace Microsoft::Console::Render::Atlas;
#pragma region IRenderEngine
// Present() is called without the console buffer lock being held.
// --> Put as much in here as possible.
[[nodiscard]] HRESULT AtlasEngine::Present() noexcept
try
{
if (!_p.dxgi.adapter || !_p.dxgi.factory->IsCurrent())
{
_recreateAdapter();
}
if (!_b)
{
_recreateBackend();
}
if (_p.swapChain.generation != _p.s.generation())
{
_handleSwapChainUpdate();
}
_b->Render(_p);
_present();
return S_OK;
}
catch (const wil::ResultException& exception)
{
const auto hr = exception.GetErrorCode();
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
{
_p.dxgi = {};
return E_PENDING;
}
if (_p.warningCallback)
{
try
{
_p.warningCallback(hr);
}
CATCH_LOG()
}
_b.reset();
return hr;
}
CATCH_RETURN()
[[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept
{
return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw());
}
void AtlasEngine::WaitUntilCanRender() noexcept
{
if constexpr (ATLAS_DEBUG_RENDER_DELAY)
{
Sleep(ATLAS_DEBUG_RENDER_DELAY);
}
_waitUntilCanRender();
}
#pragma endregion
void AtlasEngine::_recreateAdapter()
{
#ifndef NDEBUG

View File

@@ -15,6 +15,7 @@
TIL_FAST_MATH_BEGIN
// Disable a bunch of warnings which get in the way of writing performant code.
#pragma warning(disable : 4100)
#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 : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
@@ -258,7 +259,7 @@ void BackendD2D::_drawText(RenderingPayload& p)
}
}
if (!row->gridLineRanges.empty())
//if (!row->gridLineRanges.empty())
{
_drawGridlineRow(p, row, y);
}
@@ -394,7 +395,7 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32
void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y)
{
const auto widthShift = gsl::narrow_cast<u8>(row->lineRendition != LineRendition::SingleWidth);
/*const auto widthShift = gsl::narrow_cast<u8>(row->lineRendition != LineRendition::SingleWidth);
const auto cellSize = p.s->font->cellSize;
const auto rowTop = gsl::narrow_cast<i16>(cellSize.y * y);
const auto rowBottom = gsl::narrow_cast<i16>(rowTop + cellSize.y);
@@ -474,7 +475,7 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro
{
appendHorizontalLine(r, p.s->font->strikethrough, nullptr);
}
}
}*/
}
void BackendD2D::_drawCursorPart1(const RenderingPayload& p)

View File

@@ -24,6 +24,8 @@ TIL_FAST_MATH_BEGIN
// This code packs various data into smaller-than-int types to save both CPU and GPU memory. This warning would force
// us to add dozens upon dozens of gsl::narrow_cast<>s throughout the file which is more annoying than helpful.
#pragma warning(disable : 4100)
#pragma warning(disable : 4189)
#pragma warning(disable : 4242) // '=': conversion from '...' to '...', possible loss of data
#pragma warning(disable : 4244) // 'initializing': conversion from '...' to '...', possible loss of data
#pragma warning(disable : 4267) // 'argument': conversion from '...' to '...', possible loss of data
@@ -42,46 +44,6 @@ TIL_FAST_MATH_BEGIN
using namespace Microsoft::Console::Render::Atlas;
template<>
struct std::hash<u16>
{
constexpr size_t operator()(u16 key) const noexcept
{
return til::flat_set_hash_integer(key);
}
};
template<>
struct std::hash<BackendD3D::AtlasGlyphEntry>
{
constexpr size_t operator()(u16 key) const noexcept
{
return til::flat_set_hash_integer(key);
}
constexpr size_t operator()(const BackendD3D::AtlasGlyphEntry& slot) const noexcept
{
return til::flat_set_hash_integer(slot.glyphIndex);
}
};
template<>
struct std::hash<BackendD3D::AtlasFontFaceEntry>
{
using T = BackendD3D::AtlasFontFaceEntry;
size_t operator()(const BackendD3D::AtlasFontFaceKey& key) const noexcept
{
return til::flat_set_hash_integer(std::bit_cast<uintptr_t>(key.fontFace) | static_cast<u8>(key.lineRendition));
}
size_t operator()(const BackendD3D::AtlasFontFaceEntry& slot) const noexcept
{
const auto& inner = *slot.inner;
return til::flat_set_hash_integer(std::bit_cast<uintptr_t>(inner.fontFace.get()) | static_cast<u8>(inner.lineRendition));
}
};
BackendD3D::BackendD3D(const RenderingPayload& p)
{
THROW_IF_FAILED(p.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _vertexShader.addressof()));
@@ -238,9 +200,7 @@ void BackendD3D::Render(RenderingPayload& p)
_drawCursorBackground(p);
_drawText(p);
_drawSelection(p);
#if ATLAS_DEBUG_SHOW_DIRTY
_debugShowDirty(p);
#endif
_flushQuads(p);
if (_customPixelShader)
@@ -248,9 +208,7 @@ void BackendD3D::Render(RenderingPayload& p)
_executeCustomShader(p);
}
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
_debugDumpRenderTarget(p);
#endif
}
bool BackendD3D::RequiresContinuousRedraw() noexcept
@@ -308,7 +266,7 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put());
// Clearing the atlas requires BeginDraw(), which is expensive. Defer this until we need Direct2D anyways.
_fontChangedResetGlyphAtlas = true;
_textShadingType = font.antialiasingMode == AntialiasingMode::ClearType ? ShadingType::TextClearType : ShadingType::TextGrayscale;
_textShadingType = font.antialiasingMode == TextAntialiasMode::ClearType ? ShadingType::TextClearType : ShadingType::TextGrayscale;
{
auto ligaturesDisabled = false;
@@ -403,24 +361,23 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p)
/* ppCode */ blob.addressof(),
/* ppErrorMsgs */ error.addressof());
// Unless we can determine otherwise, assume this shader requires evaluation every frame
_requiresContinuousRedraw = true;
if (SUCCEEDED(hr))
{
THROW_IF_FAILED(p.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _customPixelShader.addressof()));
THROW_IF_FAILED(p.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _customPixelShader.put()));
// Try to determine whether the shader uses the Time variable
wil::com_ptr<ID3D11ShaderReflection> reflector;
if (SUCCEEDED_LOG(D3DReflect(blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(reflector.addressof()))))
if (SUCCEEDED_LOG(D3DReflect(blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(reflector.put()))))
{
// Depending on the version of the d3dcompiler_*.dll, the next two functions either return nullptr
// on failure or an instance of CInvalidSRConstantBuffer or CInvalidSRVariable respectively,
// which cause GetDesc() to return E_FAIL. In other words, we have to assume that any failure in the
// next few lines indicates that the cbuffer is entirely unused (--> _requiresContinuousRedraw=false).
if (ID3D11ShaderReflectionConstantBuffer* constantBufferReflector = reflector->GetConstantBufferByIndex(0)) // shader buffer
{
if (ID3D11ShaderReflectionVariable* variableReflector = constantBufferReflector->GetVariableByIndex(0)) // time
{
D3D11_SHADER_VARIABLE_DESC variableDescriptor;
if (SUCCEEDED(variableReflector->GetDesc(&variableDescriptor)))
if (SUCCEEDED_LOG(variableReflector->GetDesc(&variableDescriptor)))
{
// only if time is used
_requiresContinuousRedraw = WI_IsFlagSet(variableDescriptor.uFlags, D3D_SVF_USED);
@@ -428,11 +385,6 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p)
}
}
}
else
{
// Unless we can determine otherwise, assume this shader requires evaluation every frame
_requiresContinuousRedraw = true;
}
}
else
{
@@ -453,6 +405,8 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p)
else if (p.s->misc->useRetroTerminalEffect)
{
THROW_IF_FAILED(p.device->CreatePixelShader(&custom_shader_ps[0], sizeof(custom_shader_ps), nullptr, _customPixelShader.put()));
// We know the built-in retro shader doesn't require continuous redraw.
_requiresContinuousRedraw = false;
}
if (_customPixelShader)
@@ -541,7 +495,7 @@ void BackendD3D::_recreateConstBuffer(const RenderingPayload& p) const
data.backgroundCellSize = { static_cast<f32>(p.s->font->cellSize.x), static_cast<f32>(p.s->font->cellSize.y) };
data.backgroundCellCount = { static_cast<f32>(p.s->viewportCellCount.x), static_cast<f32>(p.s->viewportCellCount.y) };
DWrite_GetGammaRatios(_gamma, data.gammaRatios);
data.enhancedContrast = p.s->font->antialiasingMode == AntialiasingMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast;
data.enhancedContrast = p.s->font->antialiasingMode == TextAntialiasMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast;
data.underlineWidth = p.s->font->underline.height;
p.deviceContext->UpdateSubresource(_psConstantBuffer.get(), 0, nullptr, &data, 0, 0);
}
@@ -731,9 +685,9 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
// It's not great, but it's not terrible.
for (auto& slot : _glyphAtlasMap.container())
{
if (slot.inner)
for (auto& glyphs : slot.glyphs)
{
slot.inner->glyphs.clear();
glyphs.clear();
}
}
@@ -992,58 +946,54 @@ void BackendD3D::_drawText(RenderingPayload& p)
for (const auto& m : row->mappings)
{
auto x = m.glyphsFrom;
const AtlasFontFaceKey fontFaceKey{
.fontFace = m.fontFace.get(),
.lineRendition = row->lineRendition,
};
// This goto label exists to allow us to retry rendering a glyph if the glyph atlas was full.
// We need to goto here, because a retry will cause the atlas texture as well as the
// _glyphCache hashmap to be cleared, and so we'll have to call insert() again.
drawGlyphRetry:
const auto [fontFaceEntryOuter, fontFaceInserted] = _glyphAtlasMap.insert(fontFaceKey);
auto& fontFaceEntry = *fontFaceEntryOuter.inner;
if (fontFaceInserted)
// The lack of a fontFace indicates a soft font.
AtlasFontFaceEntry* fontFaceEntry = nullptr;
if (m.fontFace) [[likely]]
{
_initializeFontFaceEntry(fontFaceEntry);
fontFaceEntry = _glyphAtlasMap.insert(m.fontFace.get());
if (!fontFaceEntry)
{
fontFaceEntry = _insertFontFaceEntry(m.fontFace.get());
}
}
else
{
fontFaceEntry = _createSoftFontFaceEntry();
}
auto& glyphs = fontFaceEntry->glyphs[WI_EnumValue(row->lineRendition)];
while (x < m.glyphsTo)
{
const auto [glyphEntry, inserted] = fontFaceEntry.glyphs.insert(row->glyphIndices[x]);
if (inserted && !_drawGlyph(p, fontFaceEntry, glyphEntry))
const auto glyphIndex = row->glyphIndices[x];
auto glyphEntry = glyphs.lookup(glyphIndex);
if (!glyphEntry)
{
// A deadlock in this retry loop is detected in _drawGlyphPrepareRetry.
//
// Yes, I agree, avoid goto. Sometimes. It's not my fault that C++ still doesn't
// have a `continue outerloop;` like other languages had it for decades. :(
#pragma warning(suppress : 26438) // Avoid 'goto' (es.76).
#pragma warning(suppress : 26448) // Consider using gsl::finally if final action is intended (gsl.util).
goto drawGlyphRetry;
glyphEntry = _drawGlyph(p, *row, *fontFaceEntry, glyphIndex);
}
if (glyphEntry.data.GetShadingType() != ShadingType::Default)
// A shadingType of 0 (ShadingType::Default) indicates a glyph that is whitespace.
if (glyphEntry->shadingType)
{
auto l = static_cast<til::CoordType>(lrintf((baselineX + row->glyphOffsets[x].advanceOffset) * scaleX));
auto t = static_cast<til::CoordType>(lrintf((baselineY - row->glyphOffsets[x].ascenderOffset) * scaleY));
l += glyphEntry.data.offset.x;
t += glyphEntry.data.offset.y;
l += glyphEntry->offset.x;
t += glyphEntry->offset.y;
row->dirtyTop = std::min(row->dirtyTop, t);
row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry.data.size.y);
row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry->size.y);
_appendQuad() = {
.shadingType = glyphEntry.data.GetShadingType(),
.shadingType = ShadingType{ glyphEntry->shadingType },
.position = { static_cast<i16>(l), static_cast<i16>(t) },
.size = glyphEntry.data.size,
.texcoord = glyphEntry.data.texcoord,
.size = glyphEntry->size,
.texcoord = glyphEntry->texcoord,
.color = row->colors[x],
};
if (glyphEntry.data.overlapSplit)
if (glyphEntry->overlapSplit)
{
_drawTextOverlapSplit(p, y);
}
@@ -1054,7 +1004,7 @@ void BackendD3D::_drawText(RenderingPayload& p)
}
}
if (!row->gridLineRanges.empty())
//if (!row->gridLineRanges.empty())
{
_drawGridlines(p, y);
}
@@ -1168,12 +1118,9 @@ void BackendD3D::_drawTextOverlapSplit(const RenderingPayload& p, u16 y)
}
}
void BackendD3D::_initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry)
BackendD3D::AtlasFontFaceEntry* BackendD3D::_insertFontFaceEntry(IDWriteFontFace2* fontFace)
{
if (!fontFaceEntry.fontFace)
{
return;
}
const auto fontFaceEntry = _glyphAtlasMap.insert(fontFace);
ALLOW_UNINITIALIZED_BEGIN
std::array<u32, 0x100> codepoints;
@@ -1185,54 +1132,43 @@ void BackendD3D::_initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry
codepoints[i] = 0x2500 + i;
}
THROW_IF_FAILED(fontFaceEntry.fontFace->GetGlyphIndicesW(codepoints.data(), codepoints.size(), indices.data()));
THROW_IF_FAILED(fontFace->GetGlyphIndicesW(codepoints.data(), codepoints.size(), indices.data()));
for (u32 i = 0; i < indices.size(); ++i)
{
if (const auto idx = indices[i])
{
fontFaceEntry.boxGlyphs.insert(idx);
fontFaceEntry->boxGlyphs.insert(idx);
}
}
return fontFaceEntry;
}
bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
BackendD3D::AtlasFontFaceEntry* BackendD3D::_createSoftFontFaceEntry()
{
if (!_softFontFaceEntry)
{
_softFontFaceEntry = std::make_unique<AtlasFontFaceEntry>();
}
return _softFontFaceEntry.get();
}
BackendD3D::AtlasGlyphEntry* BackendD3D::_drawGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u16 glyphIndex)
{
// The lack of a fontFace indicates a soft font.
if (!fontFaceEntry.fontFace)
{
return _drawSoftFontGlyph(p, fontFaceEntry, glyphEntry);
return _drawSoftFontGlyph(p, row, fontFaceEntry, glyphIndex);
}
const DWRITE_GLYPH_RUN glyphRun{
.fontFace = fontFaceEntry.fontFace.get(),
.fontEmSize = p.s->font->fontSize,
.glyphCount = 1,
.glyphIndices = &glyphEntry.glyphIndex,
.glyphIndices = &glyphIndex,
};
// To debug issues with this function it may be helpful to know which file
// a given font face corresponds to. This code works for most cases.
#if 0
wchar_t filePath[MAX_PATH]{};
{
UINT32 fileCount = 1;
wil::com_ptr<IDWriteFontFile> file;
if (SUCCEEDED(fontFaceEntry.fontFace->GetFiles(&fileCount, file.addressof())))
{
wil::com_ptr<IDWriteFontFileLoader> loader;
THROW_IF_FAILED(file->GetLoader(loader.addressof()));
if (const auto localLoader = loader.try_query<IDWriteLocalFontFileLoader>())
{
void const* fontFileReferenceKey;
UINT32 fontFileReferenceKeySize;
THROW_IF_FAILED(file->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize));
THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &filePath[0], MAX_PATH));
}
}
}
#endif
// It took me a while to figure out how to rasterize glyphs manually with DirectWrite without depending on Direct2D.
// The benefits are a reduction in memory usage, an increase in performance under certain circumstances and most
// importantly, the ability to debug the renderer more easily, because many graphics debuggers don't support Direct2D.
@@ -1301,16 +1237,15 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
// The buffer now contains a grayscale alpha mask.
#endif
const auto lineRendition = static_cast<LineRendition>(fontFaceEntry.lineRendition);
const auto needsTransform = lineRendition != LineRendition::SingleWidth;
const int scale = row.lineRendition != LineRendition::SingleWidth;
static constexpr D2D1_MATRIX_3X2_F identityTransform{ .m11 = 1, .m22 = 1 };
D2D1_MATRIX_3X2_F transform = identityTransform;
if (needsTransform)
if (scale)
{
transform.m11 = 2.0f;
transform.m22 = lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f;
transform.m22 = row.lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f;
_d2dRenderTarget->SetTransform(&transform);
}
@@ -1372,7 +1307,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
// ---+---+---
// This avoids the issue in most cases by simply clipping the glyph to the size of a single cell.
// The downside is that it fails to work well for custom line heights, etc.
const auto isBoxGlyph = fontFaceEntry.boxGlyphs.lookup(glyphEntry.glyphIndex) != nullptr;
const auto isBoxGlyph = fontFaceEntry.boxGlyphs.lookup(glyphIndex) != nullptr;
if (isBoxGlyph)
{
// NOTE: As mentioned above, the "origin" of a glyph's coordinate system is its baseline.
@@ -1385,7 +1320,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
// The bounds may be empty if the glyph is whitespace.
if (bounds.left >= bounds.right || bounds.top >= bounds.bottom)
{
return true;
return nullptr;
}
const auto bl = lrintf(bounds.left);
@@ -1399,10 +1334,11 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
};
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
{
// TODO:
_drawGlyphPrepareRetry(p);
return false;
}
const auto glyphEntry = fontFaceEntry.glyphs[WI_EnumValue(row.lineRendition)].insert(glyphIndex);
const D2D1_POINT_2F baselineOrigin{
static_cast<f32>(rect.x - bl),
static_cast<f32>(rect.y - bt),
@@ -1427,7 +1363,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
}
});
if (needsTransform)
if (scale)
{
transform.dx = (1.0f - transform.m11) * baselineOrigin.x;
transform.dy = (1.0f - transform.m22) * baselineOrigin.y;
@@ -1453,41 +1389,39 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
//
// The former condition makes sure to exclude diacritics and such from being considered a ligature,
// while the latter condition-pair makes sure to exclude regular BMP wide glyphs that overlap a little.
const auto horizontalScale = lineRendition != LineRendition::SingleWidth ? 2 : 1;
const auto triggerLeft = _ligatureOverhangTriggerLeft * horizontalScale;
const auto triggerRight = _ligatureOverhangTriggerRight * horizontalScale;
const auto triggerLeft = _ligatureOverhangTriggerLeft << scale;
const auto triggerRight = _ligatureOverhangTriggerRight << scale;
const auto overlapSplit = rect.w >= p.s->font->cellSize.x && (bl <= triggerLeft || br >= triggerRight);
glyphEntry.data.shadingType = static_cast<u16>(isColorGlyph ? ShadingType::TextPassthrough : _textShadingType);
glyphEntry.data.overlapSplit = overlapSplit;
glyphEntry.data.offset.x = bl;
glyphEntry.data.offset.y = bt;
glyphEntry.data.size.x = rect.w;
glyphEntry.data.size.y = rect.h;
glyphEntry.data.texcoord.x = rect.x;
glyphEntry.data.texcoord.y = rect.y;
glyphEntry->shadingType = static_cast<u16>(isColorGlyph ? ShadingType::TextPassthrough : _textShadingType);
glyphEntry->overlapSplit = overlapSplit;
glyphEntry->offset.x = bl;
glyphEntry->offset.y = bt;
glyphEntry->size.x = rect.w;
glyphEntry->size.y = rect.h;
glyphEntry->texcoord.x = rect.x;
glyphEntry->texcoord.y = rect.y;
if (lineRendition >= LineRendition::DoubleHeightTop)
if (row.lineRendition >= LineRendition::DoubleHeightTop)
{
_splitDoubleHeightGlyph(p, fontFaceEntry, glyphEntry);
_splitDoubleHeightGlyph(p, row, fontFaceEntry, glyphEntry);
}
return true;
return glyphEntry;
}
bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
BackendD3D::AtlasGlyphEntry* BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u16 glyphIndex)
{
stbrp_rect rect{
.w = p.s->font->cellSize.x,
.h = p.s->font->cellSize.y,
};
const auto lineRendition = static_cast<LineRendition>(fontFaceEntry.lineRendition);
auto baseline = p.s->font->baseline;
if (lineRendition != LineRendition::SingleWidth)
if (row.lineRendition != LineRendition::SingleWidth)
{
const auto heightShift = static_cast<u8>(lineRendition >= LineRendition::DoubleHeightTop);
const auto heightShift = static_cast<u8>(row.lineRendition >= LineRendition::DoubleHeightTop);
rect.w <<= 1;
rect.h <<= heightShift;
baseline <<= heightShift;
@@ -1495,14 +1429,16 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
{
// TODO:
_drawGlyphPrepareRetry(p);
return false;
}
const auto glyphEntry = fontFaceEntry.glyphs[WI_EnumValue(row.lineRendition)].insert(glyphIndex);
if (!_softFontBitmap)
{
// Allocating such a tiny texture is very wasteful (min. texture size on GPUs
// right now is 64kB), but this is a seldom used feature so it's fine...
// right now is 64kB), but this is a seldomly used feature so it's fine...
const D2D1_SIZE_U size{
static_cast<UINT32>(p.s->font->softFontCellSize.width),
static_cast<UINT32>(p.s->font->softFontCellSize.height),
@@ -1515,7 +1451,31 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa
THROW_IF_FAILED(_d2dRenderTarget->CreateBitmap(size, nullptr, 0, &bitmapProperties, _softFontBitmap.addressof()));
}
const auto interpolation = p.s->font->antialiasingMode == AntialiasingMode::Aliased ? D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC;
{
const auto width = static_cast<size_t>(p.s->font->softFontCellSize.width);
const auto height = static_cast<size_t>(p.s->font->softFontCellSize.height);
auto bitmapData = Buffer<u32>{ width * height };
const auto softFontIndex = glyphIndex - 0xEF20u;
auto src = p.s->font->softFontPattern.begin() + height * softFontIndex;
auto dst = bitmapData.begin();
for (size_t y = 0; y < height; y++)
{
auto srcBits = *src++;
for (size_t x = 0; x < width; x++)
{
const auto srcBitIsSet = (srcBits & 0x8000) != 0;
*dst++ = srcBitIsSet ? 0xffffffff : 0x00000000;
srcBits <<= 1;
}
}
const auto pitch = static_cast<UINT32>(width * sizeof(u32));
THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch));
}
const auto interpolation = p.s->font->antialiasingMode == TextAntialiasMode::Aliased ? D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC;
const D2D1_RECT_F dest{
static_cast<f32>(rect.x),
static_cast<f32>(rect.y),
@@ -1524,74 +1484,27 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa
};
_d2dBeginDrawing();
_drawSoftFontGlyphInBitmap(p, glyphEntry);
_d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &dest, 1, interpolation, nullptr, nullptr);
glyphEntry.data.shadingType = static_cast<u16>(ShadingType::TextGrayscale);
glyphEntry.data.overlapSplit = 0;
glyphEntry.data.offset.x = 0;
glyphEntry.data.offset.y = -baseline;
glyphEntry.data.size.x = rect.w;
glyphEntry.data.size.y = rect.h;
glyphEntry.data.texcoord.x = rect.x;
glyphEntry.data.texcoord.y = rect.y;
glyphEntry->shadingType = static_cast<u16>(ShadingType::TextGrayscale);
glyphEntry->overlapSplit = 0;
glyphEntry->offset.x = 0;
glyphEntry->offset.y = -baseline;
glyphEntry->size.x = rect.w;
glyphEntry->size.y = rect.h;
glyphEntry->texcoord.x = rect.x;
glyphEntry->texcoord.y = rect.y;
if (lineRendition >= LineRendition::DoubleHeightTop)
if (row.lineRendition >= LineRendition::DoubleHeightTop)
{
_splitDoubleHeightGlyph(p, fontFaceEntry, glyphEntry);
_splitDoubleHeightGlyph(p, row, fontFaceEntry, glyphEntry);
}
return true;
}
void BackendD3D::_drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const
{
if (!isSoftFontChar(glyphEntry.glyphIndex))
{
// AtlasEngine::_mapReplacementCharacter should have filtered inputs with isSoftFontChar already.
assert(false);
return;
}
const auto width = static_cast<size_t>(p.s->font->softFontCellSize.width);
const auto height = static_cast<size_t>(p.s->font->softFontCellSize.height);
const auto& softFontPattern = p.s->font->softFontPattern;
// The isSoftFontChar() range is [0xEF20,0xEF80).
const auto offset = glyphEntry.glyphIndex - 0xEF20u;
const auto offsetBeg = offset * height;
const auto offsetEnd = offsetBeg + height;
if (offsetEnd > softFontPattern.size())
{
// Out of range values should not occur, but they do and it's unknown why: GH#15553
assert(false);
return;
}
Buffer<u32> bitmapData{ width * height };
auto dst = bitmapData.begin();
auto it = softFontPattern.begin() + offsetBeg;
const auto end = softFontPattern.begin() + offsetEnd;
while (it != end)
{
auto srcBits = *it++;
for (size_t x = 0; x < width; x++)
{
const auto srcBitIsSet = (srcBits & 0x8000) != 0;
*dst++ = srcBitIsSet ? 0xffffffff : 0x00000000;
srcBits <<= 1;
}
}
const auto pitch = static_cast<UINT32>(width * sizeof(u32));
THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch));
return glyphEntry;
}
void BackendD3D::_drawGlyphPrepareRetry(const RenderingPayload& p)
{
THROW_HR_IF_MSG(E_UNEXPECTED, _glyphAtlasMap.empty(), "BackendD3D::_drawGlyph deadlock");
_d2dEndDrawing();
_flushQuads(p);
_resetGlyphAtlas(p);
@@ -1601,41 +1514,36 @@ void BackendD3D::_drawGlyphPrepareRetry(const RenderingPayload& p)
// One for the top/bottom half each, because that's how DECDHL works. This will clip the
// `glyphEntry` to only contain the one specified by `fontFaceEntry.lineRendition`
// and create a second entry in our glyph cache hashmap that contains the other half.
void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, AtlasGlyphEntry* glyphEntry)
{
// Twice the line height, twice the descender gap. For both.
glyphEntry.data.offset.y -= p.s->font->descender;
glyphEntry->offset.y -= p.s->font->descender;
const auto isTop = fontFaceEntry.lineRendition == LineRendition::DoubleHeightTop;
const auto isTop = row.lineRendition == LineRendition::DoubleHeightTop;
const auto otherLineRendition = isTop ? LineRendition::DoubleHeightBottom : LineRendition::DoubleHeightTop;
const auto entry2 = fontFaceEntry.glyphs[WI_EnumValue(otherLineRendition)].insert(glyphEntry->glyphIndex);
const AtlasFontFaceKey key2{
.fontFace = fontFaceEntry.fontFace.get(),
.lineRendition = isTop ? LineRendition::DoubleHeightBottom : LineRendition::DoubleHeightTop,
};
*entry2 = *glyphEntry;
auto& glyphCache = _glyphAtlasMap.insert(key2).first.inner->glyphs;
auto& entry2 = glyphCache.insert(glyphEntry.glyphIndex).first;
entry2.data = glyphEntry.data;
const auto top = isTop ? glyphEntry : entry2;
const auto bottom = isTop ? entry2 : glyphEntry;
const auto topSize = clamp(-glyphEntry->offset.y - p.s->font->baseline, 0, static_cast<int>(glyphEntry->size.y));
auto& top = isTop ? glyphEntry : entry2;
auto& bottom = isTop ? entry2 : glyphEntry;
const auto topSize = clamp(-glyphEntry.data.offset.y - p.s->font->baseline, 0, static_cast<int>(glyphEntry.data.size.y));
top.data.offset.y += p.s->font->cellSize.y;
top.data.size.y = topSize;
bottom.data.offset.y += topSize;
bottom.data.size.y = std::max(0, bottom.data.size.y - topSize);
bottom.data.texcoord.y += topSize;
top->offset.y += p.s->font->cellSize.y;
top->size.y = topSize;
bottom->offset.y += topSize;
bottom->size.y = std::max(0, bottom->size.y - topSize);
bottom->texcoord.y += topSize;
// Things like diacritics might be so small that they only exist on either half of the
// double-height row. This effectively turns the other (unneeded) side into whitespace.
if (!top.data.size.y)
if (!top->size.y)
{
top.data.shadingType = static_cast<u16>(ShadingType::Default);
top->shadingType = static_cast<u16>(ShadingType::Default);
}
if (!bottom.data.size.y)
if (!bottom->size.y)
{
bottom.data.shadingType = static_cast<u16>(ShadingType::Default);
bottom->shadingType = static_cast<u16>(ShadingType::Default);
}
}
@@ -1661,7 +1569,7 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
const i32 clipTop = row->lineRendition == LineRendition::DoubleHeightBottom ? rowTop : 0;
const i32 clipBottom = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : p.s->targetSize.y;
const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) {
/*const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) {
const auto textCellWidth = cellSize.x << horizontalShift;
const auto offset = pos.position << horizontalShift;
const auto width = static_cast<u16>(pos.height << horizontalShift);
@@ -1743,7 +1651,7 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
{
appendHorizontalLine(r, p.s->font->strikethrough, ShadingType::SolidLine);
}
}
}*/
}
void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
@@ -2109,9 +2017,9 @@ void BackendD3D::_drawSelection(const RenderingPayload& p)
}
}
#if ATLAS_DEBUG_SHOW_DIRTY
void BackendD3D::_debugShowDirty(const RenderingPayload& p)
{
#if ATLAS_DEBUG_SHOW_DIRTY
_presentRects[_presentRectsPos] = p.dirtyRectInPx;
_presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects);
@@ -2134,12 +2042,14 @@ void BackendD3D::_debugShowDirty(const RenderingPayload& p)
};
}
}
}
#else
UNREFERENCED_PARAMETER(p);
#endif
}
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p)
{
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
if (_dumpRenderTargetCounter == 0)
{
ExpandEnvironmentStringsW(ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH, &_dumpRenderTargetBasePath[0], gsl::narrow_cast<DWORD>(std::size(_dumpRenderTargetBasePath)));
@@ -2150,8 +2060,10 @@ void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p)
swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter);
SaveTextureToPNG(p.deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]);
_dumpRenderTargetCounter++;
}
#else
UNREFERENCED_PARAMETER(p);
#endif
}
void BackendD3D::_executeCustomShader(RenderingPayload& p)
{

View File

@@ -93,94 +93,121 @@ namespace Microsoft::Console::Render::Atlas
alignas(u32) u32 color;
};
struct alignas(u32) AtlasGlyphEntryData
{
u16 shadingType;
u16 overlapSplit;
i16x2 offset;
u16x2 size;
u16x2 texcoord;
constexpr ShadingType GetShadingType() const noexcept
{
return static_cast<ShadingType>(shadingType);
}
};
// NOTE: Don't initialize any members in this struct. This ensures that no
// zero-initialization needs to occur when we allocate large buffers of this object.
struct AtlasGlyphEntry
{
u16 glyphIndex;
// All data in QuadInstance is u32-aligned anyways, so this simultaneously serves as padding.
u16 _occupied;
AtlasGlyphEntryData data;
constexpr bool operator==(u16 key) const noexcept
{
return glyphIndex == key;
}
constexpr operator bool() const noexcept
{
return _occupied != 0;
}
constexpr AtlasGlyphEntry& operator=(u16 key) noexcept
{
glyphIndex = key;
_occupied = 1;
return *this;
}
u16 shadingType;
u16 overlapSplit;
i16x2 offset;
u16x2 size;
u16x2 texcoord;
};
// This exists so that we can look up a AtlasFontFaceEntry without AddRef()/Release()ing fontFace first.
struct AtlasFontFaceKey
struct AtlasGlyphEntryHashTraits
{
IDWriteFontFace2* fontFace;
LineRendition lineRendition;
static constexpr size_t hash(const u16 glyphIndex) noexcept
{
return til::flat_set_hash_integer(glyphIndex);
}
static constexpr size_t hash(const AtlasGlyphEntry& entry) noexcept
{
return til::flat_set_hash_integer(entry.glyphIndex);
}
static constexpr bool occupied(const AtlasGlyphEntry& entry) noexcept
{
return entry._occupied != 0;
}
static constexpr bool equals(const AtlasGlyphEntry& entry, u16 glyphIndex) noexcept
{
return entry.glyphIndex == glyphIndex;
}
static constexpr void assign(AtlasGlyphEntry& entry, u16 glyphIndex) noexcept
{
entry.glyphIndex = glyphIndex;
entry._occupied = 1;
}
};
struct AtlasFontFaceEntryInner
struct BoxGlyphEntry
{
u16 glyphIndex;
};
struct BoxGlyphEntryHashTraits
{
static constexpr size_t hash(const u16 glyphIndex) noexcept
{
return til::flat_set_hash_integer(glyphIndex);
}
static constexpr size_t hash(const BoxGlyphEntry& entry) noexcept
{
return til::flat_set_hash_integer(entry.glyphIndex);
}
static constexpr bool occupied(const BoxGlyphEntry& entry) noexcept
{
return entry.glyphIndex != 0;
}
static constexpr bool equals(const BoxGlyphEntry& entry, u16 glyphIndex) noexcept
{
return entry.glyphIndex == glyphIndex;
}
static constexpr void assign(BoxGlyphEntry& entry, u16 glyphIndex) noexcept
{
entry.glyphIndex = glyphIndex;
}
};
struct AtlasFontFaceEntry
{
// BODGY: At the time of writing IDWriteFontFallback::MapCharacters returns the same IDWriteFontFace instance
// for the same font face variant as long as someone is holding a reference to the instance (see ActiveFaceCache).
// This allows us to hash the value of the pointer as if it was uniquely identifying the font face variant.
wil::com_ptr<IDWriteFontFace2> fontFace;
LineRendition lineRendition = LineRendition::SingleWidth;
til::linear_flat_set<AtlasGlyphEntry> glyphs;
// The 4 entries map to the 4 corresponding LineRendition enum values.
til::linear_flat_set<AtlasGlyphEntry, AtlasGlyphEntryHashTraits> glyphs[4];
// boxGlyphs gets an increased growth rate of 2^2 = 4x, because presumably fonts either contain very
// few or almost all of the box glyphs. This reduces the cost of _initializeFontFaceEntry quite a bit.
til::linear_flat_set<u16, 2, 2> boxGlyphs;
til::linear_flat_set<BoxGlyphEntry, BoxGlyphEntryHashTraits, 2, 2> boxGlyphs;
};
struct AtlasFontFaceEntry
struct AtlasFontFaceEntryHashTraits
{
// This being a heap allocated allows us to insert into `glyphs` in `_splitDoubleHeightGlyph`
// (which might resize the hashmap!), while the caller `_drawText` is holding onto `glyphs`.
// If it wasn't heap allocated, all pointers into `linear_flat_set` would be invalidated.
std::unique_ptr<AtlasFontFaceEntryInner> inner;
bool operator==(const AtlasFontFaceKey& key) const noexcept
static constexpr size_t hash(const IDWriteFontFace2* fontFace) noexcept
{
const auto& i = *inner;
return i.fontFace.get() == key.fontFace && i.lineRendition == key.lineRendition;
return til::flat_set_hash_integer(std::bit_cast<uintptr_t>(fontFace));
}
operator bool() const noexcept
static size_t hash(const AtlasFontFaceEntry& entry) noexcept
{
return static_cast<bool>(inner);
return hash(entry.fontFace.get());
}
AtlasFontFaceEntry& operator=(const AtlasFontFaceKey& key)
static bool occupied(const AtlasFontFaceEntry& entry) noexcept
{
inner = std::make_unique<AtlasFontFaceEntryInner>();
auto& i = *inner;
i.fontFace = key.fontFace;
i.lineRendition = key.lineRendition;
return *this;
return static_cast<bool>(entry.fontFace);
}
static bool equals(const AtlasFontFaceEntry& entry, const IDWriteFontFace2* fontFace) noexcept
{
return entry.fontFace.get() == fontFace;
}
static void assign(AtlasFontFaceEntry& entry, IDWriteFontFace2* fontFace) noexcept
{
entry.fontFace = fontFace;
}
};
@@ -217,12 +244,12 @@ namespace Microsoft::Console::Render::Atlas
void _uploadBackgroundBitmap(const RenderingPayload& p);
void _drawText(RenderingPayload& p);
ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y);
ATLAS_ATTR_COLD static void _initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry);
[[nodiscard]] ATLAS_ATTR_COLD bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
bool _drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
void _drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const;
ATLAS_ATTR_COLD AtlasFontFaceEntry* _insertFontFaceEntry(IDWriteFontFace2* fontFace);
ATLAS_ATTR_COLD AtlasFontFaceEntry* _createSoftFontFaceEntry();
ATLAS_ATTR_COLD [[nodiscard]] AtlasGlyphEntry* _drawGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u16 glyphIndex);
AtlasGlyphEntry* _drawSoftFontGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u16 glyphIndex);
void _drawGlyphPrepareRetry(const RenderingPayload& p);
void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
static void _splitDoubleHeightGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, AtlasGlyphEntry* glyphEntry);
void _drawGridlines(const RenderingPayload& p, u16 y);
void _drawCursorBackground(const RenderingPayload& p);
ATLAS_ATTR_COLD void _drawCursorForeground();
@@ -259,7 +286,8 @@ namespace Microsoft::Console::Render::Atlas
wil::com_ptr<ID3D11Texture2D> _glyphAtlas;
wil::com_ptr<ID3D11ShaderResourceView> _glyphAtlasView;
til::linear_flat_set<AtlasFontFaceEntry> _glyphAtlasMap;
til::linear_flat_set<AtlasFontFaceEntry, AtlasFontFaceEntryHashTraits> _glyphAtlasMap;
std::unique_ptr<AtlasFontFaceEntry> _softFontFaceEntry;
Buffer<stbrp_node> _rectPackerData;
stbrp_context _rectPacker{};
til::CoordType _ligatureOverhangTriggerLeft = 0;

View File

@@ -8,7 +8,6 @@ graph TD
Renderer["Renderer (base/renderer.cpp)\n<small>breaks the text buffer down into GDI-oriented graphics\nprimitives (#quot;change brush to color X#quot;, #quot;draw string Y#quot;, ...)</small>"]
RenderEngineBase[/"RenderEngineBase\n(base/RenderEngineBase.cpp)\n<small>abstracts 24 LOC 👻</small>"\]
GdiEngine["GdiEngine (gdi/...)"]
DxEngine["DxEngine (dx/...)"]
subgraph AtlasEngine["AtlasEngine (atlas/...)"]
AtlasEngine.cpp["AtlasEngine.cpp\n<small>Implements IRenderEngine text rendering API\nbreaks GDI graphics primitives down into DWRITE_GLYPH_RUNs</small>"]
@@ -24,7 +23,6 @@ graph TD
Renderer -.-> RenderEngineBase
%% Mermaid.js has no support for backwards arrow at the moment
RenderEngineBase <-.->|extends| GdiEngine
RenderEngineBase <-.->|extends| DxEngine
Renderer ----> AtlasEngine
AtlasEngine.cpp <--> AtlasEngine.api.cpp
AtlasEngine.cpp <--> AtlasEngine.r.cpp

View File

@@ -317,14 +317,7 @@ namespace Microsoft::Console::Render::Atlas
bool useSoftwareRendering = false;
};
enum class AntialiasingMode : u8
{
ClearType = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE,
Grayscale = D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE,
Aliased = D2D1_TEXT_ANTIALIAS_MODE_ALIASED,
};
inline constexpr auto DefaultAntialiasingMode = AntialiasingMode::ClearType;
inline constexpr auto DefaultAntialiasingMode = TextAntialiasMode::ClearType;
struct FontDecorationPosition
{
@@ -332,7 +325,7 @@ namespace Microsoft::Console::Render::Atlas
u16 height = 0;
};
struct FontSettings
struct ResolvedFontSettings
{
wil::com_ptr<IDWriteFontCollection> fontCollection;
wil::com_ptr<IDWriteFontFamily> fontFamily;
@@ -358,7 +351,7 @@ namespace Microsoft::Console::Render::Atlas
FontDecorationPosition overline;
u16 dpi = 96;
AntialiasingMode antialiasingMode = DefaultAntialiasingMode;
TextAntialiasMode antialiasingMode = DefaultAntialiasingMode;
std::vector<uint16_t> softFontPattern;
til::size softFontCellSize;
@@ -384,7 +377,7 @@ namespace Microsoft::Console::Render::Atlas
struct Settings
{
til::generational<TargetSettings> target;
til::generational<FontSettings> font;
til::generational<ResolvedFontSettings> font;
til::generational<CursorSettings> cursor;
til::generational<MiscellaneousSettings> misc;
// Size of the viewport / swap chain in pixel.
@@ -402,7 +395,7 @@ namespace Microsoft::Console::Render::Atlas
return GenerationalSettings{
til::generation_t{ 1 },
til::generational<TargetSettings>{ til::generation_t{ 1 } },
til::generational<FontSettings>{ til::generation_t{ 1 } },
til::generational<ResolvedFontSettings>{ til::generation_t{ 1 } },
til::generational<CursorSettings>{ til::generation_t{ 1 } },
til::generational<MiscellaneousSettings>{ til::generation_t{ 1 } },
};
@@ -423,14 +416,6 @@ namespace Microsoft::Console::Render::Atlas
u32 glyphsTo = 0;
};
struct GridLineRange
{
GridLineSet lines;
u32 color = 0;
u16 from = 0;
u16 to = 0;
};
struct ShapedRow
{
void Clear(u16 y, u16 cellHeight) noexcept
@@ -440,7 +425,6 @@ namespace Microsoft::Console::Render::Atlas
glyphAdvances.clear();
glyphOffsets.clear();
colors.clear();
gridLineRanges.clear();
lineRendition = LineRendition::SingleWidth;
selectionFrom = 0;
selectionTo = 0;
@@ -453,7 +437,6 @@ namespace Microsoft::Console::Render::Atlas
std::vector<f32> glyphAdvances; // same size as glyphIndices
std::vector<DWRITE_GLYPH_OFFSET> glyphOffsets; // same size as glyphIndices
std::vector<u32> colors; // same size as glyphIndices
std::vector<GridLineRange> gridLineRanges;
LineRendition lineRendition = LineRendition::SingleWidth;
u16 selectionFrom = 0;
u16 selectionTo = 0;

View File

@@ -1,94 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "../inc/RenderEngineBase.hpp"
#pragma hdrstop
using namespace Microsoft::Console;
using namespace Microsoft::Console::Render;
HRESULT RenderEngineBase::InvalidateTitle(const std::wstring_view proposedTitle) noexcept
{
if (proposedTitle != _lastFrameTitle)
{
_titleChanged = true;
}
return S_OK;
}
HRESULT RenderEngineBase::UpdateTitle(const std::wstring_view newTitle) noexcept
{
auto hr = S_FALSE;
if (newTitle != _lastFrameTitle)
{
RETURN_IF_FAILED(_DoUpdateTitle(newTitle));
_lastFrameTitle = newTitle;
_titleChanged = false;
hr = S_OK;
}
return hr;
}
HRESULT RenderEngineBase::NotifyNewText(const std::wstring_view /*newText*/) noexcept
{
return S_FALSE;
}
HRESULT RenderEngineBase::UpdateSoftFont(const std::span<const uint16_t> /*bitPattern*/,
const til::size /*cellSize*/,
const size_t /*centeringHint*/) noexcept
{
return S_FALSE;
}
HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noexcept
{
return S_FALSE;
}
HRESULT RenderEngineBase::ResetLineTransform() noexcept
{
return S_FALSE;
}
HRESULT RenderEngineBase::PrepareLineTransform(const LineRendition /*lineRendition*/,
const til::CoordType /*targetRow*/,
const til::CoordType /*viewportLeft*/) noexcept
{
return S_FALSE;
}
// Method Description:
// - By default, no one should need continuous redraw. It ruins performance
// in terms of CPU, memory, and battery life to just paint forever.
// That's why we sleep when there's nothing to draw.
// But if you REALLY WANT to do special effects... you need to keep painting.
[[nodiscard]] bool RenderEngineBase::RequiresContinuousRedraw() noexcept
{
return false;
}
// Method Description:
// - Blocks until the engine is able to render without blocking.
void RenderEngineBase::WaitUntilCanRender() noexcept
{
// Throttle the render loop a bit by default (~60 FPS), improving throughput.
Sleep(8);
}
// Routine Description:
// - Notifies us that we're about to circle the buffer, giving us a chance to
// force a repaint before the buffer contents are lost.
// - The default implementation of flush, is to do nothing for most renderers.
// Arguments:
// - circled - ignored
// - pForcePaint - Always filled with false
// Return Value:
// - S_FALSE because we don't use this.
[[nodiscard]] HRESULT RenderEngineBase::InvalidateFlush(_In_ const bool /*circled*/, _Out_ bool* const pForcePaint) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
*pForcePaint = false;
return S_FALSE;
}

View File

@@ -16,7 +16,6 @@
<ClCompile Include="..\FontInfoBase.cpp" />
<ClCompile Include="..\FontInfoDesired.cpp" />
<ClCompile Include="..\FontResource.cpp" />
<ClCompile Include="..\RenderEngineBase.cpp" />
<ClCompile Include="..\RenderSettings.cpp" />
<ClCompile Include="..\renderer.cpp" />
<ClCompile Include="..\thread.cpp" />
@@ -34,7 +33,6 @@
<ClInclude Include="..\..\inc\IFontDefaultList.hpp" />
<ClInclude Include="..\..\inc\IRenderData.hpp" />
<ClInclude Include="..\..\inc\IRenderEngine.hpp" />
<ClInclude Include="..\..\inc\RenderEngineBase.hpp" />
<ClInclude Include="..\..\inc\RenderSettings.hpp" />
<ClInclude Include="..\FontCache.h" />
<ClInclude Include="..\precomp.h" />
@@ -44,4 +42,4 @@
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
</Project>
</Project>

View File

@@ -39,9 +39,6 @@
<ClCompile Include="..\precomp.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\RenderEngineBase.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\RenderSettings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -80,9 +77,6 @@
<ClInclude Include="..\..\inc\IRenderEngine.hpp">
<Filter>Header Files\inc</Filter>
</ClInclude>
<ClInclude Include="..\..\inc\RenderEngineBase.hpp">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\..\inc\Cluster.hpp">
<Filter>Header Files\inc</Filter>
</ClInclude>
@@ -98,5 +92,6 @@
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -53,80 +53,25 @@ namespace Microsoft::Console::Render
void TriggerRedrawCursor(const til::point* const pcoord);
void TriggerRedrawAll(const bool backgroundChanged = false, const bool frameChanged = false);
void TriggerTeardown() noexcept;
void TriggerSelection();
void TriggerScroll();
void TriggerScroll(const til::point* const pcoordDelta);
void TriggerFlush(const bool circling);
void TriggerTitleChange();
void TriggerNewTextNotification(const std::wstring_view newText);
void TriggerFontChange(const int iDpi,
const FontInfoDesired& FontInfoDesired,
_Out_ FontInfo& FontInfo);
void UpdateSoftFont(const std::span<const uint16_t> bitPattern,
const til::size cellSize,
const size_t centeringHint);
[[nodiscard]] HRESULT GetProposedFont(const int iDpi,
const FontInfoDesired& FontInfoDesired,
_Out_ FontInfo& FontInfo);
bool IsGlyphWideByFont(const std::wstring_view glyph);
void UpdateSoftFont(const std::span<const uint16_t> bitPattern, const til::size cellSize, const size_t centeringHint);
bool IsGlyphWideByFont(const std::wstring_view& glyph);
void EnablePainting();
void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs);
void WaitUntilCanRender();
void AddRenderEngine(_In_ IRenderEngine* const pEngine);
void RemoveRenderEngine(_In_ IRenderEngine* const pEngine);
void SetBackgroundColorChangedCallback(std::function<void()> pfn);
void SetFrameColorChangedCallback(std::function<void()> pfn);
void SetRendererEnteredErrorStateCallback(std::function<void()> pfn);
void ResetErrorStateAndResume();
void UpdateHyperlinkHoveredId(uint16_t id) noexcept;
void UpdateLastHoveredInterval(const std::optional<interval_tree::IntervalTree<til::point, size_t>::interval>& newInterval);
private:
static GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept;
static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar);
[[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept;
bool _CheckViewportAndScroll();
[[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine);
void _PaintBufferOutput(_In_ IRenderEngine* const pEngine);
void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target, const bool lineWrapped);
void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget);
bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept;
void _PaintSelection(_In_ IRenderEngine* const pEngine);
void _PaintCursor(_In_ IRenderEngine* const pEngine);
void _PaintOverlays(_In_ IRenderEngine* const pEngine);
void _PaintOverlay(IRenderEngine& engine, const RenderOverlay& overlay);
[[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool usingSoftFont, const bool isSettingDefaultBrushes);
[[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine);
std::vector<til::rect> _GetSelectionRects() const;
void _ScrollPreviousSelection(const til::point delta);
[[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine);
bool _isInHoveredInterval(til::point coordTarget) const noexcept;
[[nodiscard]] std::optional<CursorOptions> _GetCursorInfo();
[[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine);
const RenderSettings& _renderSettings;
std::array<IRenderEngine*, 2> _engines{};
IRenderData* _pData = nullptr; // Non-ownership pointer
std::unique_ptr<RenderThread> _pThread;
static constexpr size_t _firstSoftFontChar = 0xEF20;
size_t _lastSoftFontChar = 0;
uint16_t _hyperlinkHoveredId = 0;
std::optional<interval_tree::IntervalTree<til::point, size_t>::interval> _hoveredInterval;
Microsoft::Console::Types::Viewport _viewport;
std::vector<Cluster> _clusterBuffer;
std::vector<til::rect> _previousSelection;
std::function<void()> _pfnBackgroundColorChanged;
std::function<void()> _pfnFrameColorChanged;
std::function<void()> _pfnRendererEnteredErrorState;

View File

@@ -1,30 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "BoxDrawingEffect.h"
using namespace Microsoft::Console::Render;
BoxDrawingEffect::BoxDrawingEffect() noexcept :
_scale{ 1.0f, 0.0f, 1.0f, 0.0f }
{
}
#pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize
HRESULT BoxDrawingEffect::RuntimeClassInitialize(float verticalScale, float verticalTranslate, float horizontalScale, float horizontalTranslate) noexcept
{
_scale.VerticalScale = verticalScale;
_scale.VerticalTranslation = verticalTranslate;
_scale.HorizontalScale = horizontalScale;
_scale.HorizontalTranslation = horizontalTranslate;
return S_OK;
}
[[nodiscard]] HRESULT STDMETHODCALLTYPE BoxDrawingEffect::GetScale(BoxScale* scale) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, scale);
*scale = _scale;
return S_OK;
}

View File

@@ -1,30 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <wrl.h>
#include <wrl/client.h>
#include <wrl/implements.h>
#include "IBoxDrawingEffect_h.h"
namespace Microsoft::Console::Render
{
class BoxDrawingEffect : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | ::Microsoft::WRL::InhibitFtmBase>, IBoxDrawingEffect>
{
public:
BoxDrawingEffect() noexcept;
HRESULT RuntimeClassInitialize(float verticalScale, float verticalTranslate, float horizontalScale, float horizontalTranslate) noexcept;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetScale(BoxScale* scale) noexcept override;
protected:
private:
BoxScale _scale;
#ifdef UNIT_TESTING
public:
friend class BoxDrawingEffectTests;
#endif
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,216 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <dwrite.h>
#include <d2d1.h>
#include <wrl.h>
#include <wrl/client.h>
#include <wrl/implements.h>
#include "BoxDrawingEffect.h"
#include "DxFontRenderData.h"
#include "../inc/Cluster.hpp"
namespace Microsoft::Console::Render
{
class CustomTextLayout : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | ::Microsoft::WRL::InhibitFtmBase>, IDWriteTextAnalysisSource, IDWriteTextAnalysisSink>
{
public:
// Based on the Windows 7 SDK sample at https://github.com/pauldotknopf/WindowsSDK7-Samples/tree/master/multimedia/DirectWrite/CustomLayout
CustomTextLayout(const gsl::not_null<DxFontRenderData*> fontRenderData);
[[nodiscard]] HRESULT STDMETHODCALLTYPE AppendClusters(const std::span<const ::Microsoft::Console::Render::Cluster> clusters);
[[nodiscard]] HRESULT STDMETHODCALLTYPE Reset() noexcept;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetColumns(_Out_ UINT32* columns);
// IDWriteTextLayout methods (but we don't actually want to implement them all, so just this one matching the existing interface)
[[nodiscard]] HRESULT STDMETHODCALLTYPE Draw(_In_opt_ void* clientDrawingContext,
_In_ IDWriteTextRenderer* renderer,
FLOAT originX,
FLOAT originY) noexcept;
// IDWriteTextAnalysisSource methods
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetTextAtPosition(UINT32 textPosition,
_Outptr_result_buffer_(*textLength) WCHAR const** textString,
_Out_ UINT32* textLength) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetTextBeforePosition(UINT32 textPosition,
_Outptr_result_buffer_(*textLength) WCHAR const** textString,
_Out_ UINT32* textLength) noexcept override;
[[nodiscard]] DWRITE_READING_DIRECTION STDMETHODCALLTYPE GetParagraphReadingDirection() noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetLocaleName(UINT32 textPosition,
_Out_ UINT32* textLength,
_Outptr_result_z_ const WCHAR** localeName) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetNumberSubstitution(UINT32 textPosition,
_Out_ UINT32* textLength,
_COM_Outptr_ IDWriteNumberSubstitution** numberSubstitution) noexcept override;
// IDWriteTextAnalysisSink methods
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetScriptAnalysis(UINT32 textPosition,
UINT32 textLength,
_In_ const DWRITE_SCRIPT_ANALYSIS* scriptAnalysis) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetLineBreakpoints(UINT32 textPosition,
UINT32 textLength,
_In_reads_(textLength) DWRITE_LINE_BREAKPOINT const* lineBreakpoints) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetBidiLevel(UINT32 textPosition,
UINT32 textLength,
UINT8 explicitLevel,
UINT8 resolvedLevel) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetNumberSubstitution(UINT32 textPosition,
UINT32 textLength,
_In_ IDWriteNumberSubstitution* numberSubstitution) override;
protected:
// A single contiguous run of characters containing the same analysis results.
struct Run
{
Run() noexcept :
textStart(),
textLength(),
glyphStart(),
glyphCount(),
bidiLevel(),
script(),
isNumberSubstituted(),
isSideways(),
fontFace{ nullptr },
fontScale{ 1.0 },
drawingEffect{ nullptr }
{
}
UINT32 textStart; // starting text position of this run
UINT32 textLength; // number of contiguous code units covered
UINT32 glyphStart; // starting glyph in the glyphs array
UINT32 glyphCount; // number of glyphs associated with this run of text
DWRITE_SCRIPT_ANALYSIS script;
UINT8 bidiLevel;
bool isNumberSubstituted;
bool isSideways;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
FLOAT fontScale;
::Microsoft::WRL::ComPtr<IUnknown> drawingEffect;
inline bool ContainsTextPosition(UINT32 desiredTextPosition) const noexcept
{
return desiredTextPosition >= textStart && desiredTextPosition < textStart + textLength;
}
inline bool operator==(UINT32 desiredTextPosition) const noexcept
{
// Search by text position using std::find
return ContainsTextPosition(desiredTextPosition);
}
};
// Single text analysis run, which points to the next run.
struct LinkedRun : Run
{
LinkedRun() noexcept :
nextRunIndex(0)
{
}
UINT32 nextRunIndex; // index of next run
};
[[nodiscard]] LinkedRun& _FetchNextRun(UINT32& textLength);
[[nodiscard]] LinkedRun& _GetCurrentRun();
void _SetCurrentRun(const UINT32 textPosition);
void _SplitCurrentRun(const UINT32 splitPosition);
void _OrderRuns();
[[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFontFace(UINT32 textPosition, UINT32 textLength, const ::Microsoft::WRL::ComPtr<IDWriteFontFace>& fontFace, FLOAT const scale);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeBoxDrawing(const gsl::not_null<IDWriteTextAnalysisSource*> source, UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _SetBoxEffect(UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT _AnalyzeTextComplexity() noexcept;
[[nodiscard]] HRESULT _AnalyzeRuns() noexcept;
[[nodiscard]] HRESULT _ShapeGlyphRuns() noexcept;
[[nodiscard]] HRESULT _ShapeGlyphRun(const UINT32 runIndex, UINT32& glyphStart) noexcept;
[[nodiscard]] HRESULT _CorrectGlyphRuns() noexcept;
[[nodiscard]] HRESULT _CorrectGlyphRun(const UINT32 runIndex) noexcept;
[[nodiscard]] HRESULT STDMETHODCALLTYPE _CorrectBoxDrawing() noexcept;
[[nodiscard]] HRESULT _DrawGlyphRuns(_In_opt_ void* clientDrawingContext,
IDWriteTextRenderer* renderer,
const D2D_POINT_2F origin) noexcept;
[[nodiscard]] HRESULT _DrawGlyphRun(_In_opt_ void* clientDrawingContext,
gsl::not_null<IDWriteTextRenderer*> renderer,
D2D_POINT_2F& mutableOrigin,
const Run& run) noexcept;
[[nodiscard]] static constexpr UINT32 _EstimateGlyphCount(const UINT32 textLength) noexcept;
private:
// DirectWrite font render data
DxFontRenderData* _fontRenderData;
// DirectWrite text formats
IDWriteTextFormat* _formatInUse;
// DirectWrite font faces
IDWriteFontFace1* _fontInUse;
// The text we're analyzing and processing into a layout
std::wstring _text;
std::vector<UINT16> _textClusterColumns;
size_t _width;
// Properties of the text that might be relevant.
std::wstring _localeName;
::Microsoft::WRL::ComPtr<IDWriteNumberSubstitution> _numberSubstitution;
DWRITE_READING_DIRECTION _readingDirection;
// Text analysis results
std::vector<LinkedRun> _runs;
std::vector<DWRITE_LINE_BREAKPOINT> _breakpoints;
// Text analysis interim status variable (to assist the Analyzer Sink in operations involving _runs)
UINT32 _runIndex;
// Glyph shaping results
// Whether the entire text is determined to be simple and does not require full script shaping.
bool _isEntireTextSimple;
std::vector<DWRITE_GLYPH_OFFSET> _glyphOffsets;
// Clusters are complicated. They're in respect to each individual run.
// The offsets listed here are in respect to the _text string, but from the beginning index of
// each run.
// That means if we have two runs, we will see 0 1 2 3 4 0 1 2 3 4 5 6 7... in this clusters count.
std::vector<UINT16> _glyphClusters;
// This appears to be the index of the glyph inside each font.
std::vector<UINT16> _glyphIndices;
// This is for calculating glyph advances when the entire text is simple.
std::vector<INT32> _glyphDesignUnitAdvances;
std::vector<float> _glyphAdvances;
struct ScaleCorrection
{
UINT32 textIndex;
UINT32 textLength;
float scale;
};
// These are used to further break the runs apart and adjust the font size so glyphs fit inside the cells.
std::vector<ScaleCorrection> _glyphScaleCorrections;
#ifdef UNIT_TESTING
public:
CustomTextLayout() = default;
friend class CustomTextLayoutTests;
#endif
};
}

View File

@@ -1,979 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "CustomTextRenderer.h"
#include "../../inc/DefaultSettings.h"
#include <wrl.h>
#include <wrl/client.h>
#include <VersionHelpers.h>
using namespace Microsoft::Console::Render;
#pragma region IDWritePixelSnapping methods
// Routine Description:
// - Implementation of IDWritePixelSnapping::IsPixelSnappingDisabled
// - Determines if we're allowed to snap text to pixels for this particular drawing context
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - isDisabled - TRUE if we do not snap to nearest pixels. FALSE otherwise.
// Return Value:
// - S_OK
[[nodiscard]] HRESULT CustomTextRenderer::IsPixelSnappingDisabled(void* /*clientDrawingContext*/,
_Out_ BOOL* isDisabled) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, isDisabled);
*isDisabled = false;
return S_OK;
}
// Routine Description:
// - Implementation of IDWritePixelSnapping::GetPixelsPerDip
// - Retrieves the number of real monitor pixels to use per device-independent-pixel (DIP)
// - DIPs are used by DirectX all the way until the final drawing surface so things are only
// scaled at the very end and the complexity can be abstracted.
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - pixelsPerDip - The number of pixels per DIP. 96 is standard DPI.
// Return Value:
// - S_OK
[[nodiscard]] HRESULT CustomTextRenderer::GetPixelsPerDip(void* clientDrawingContext,
_Out_ FLOAT* pixelsPerDip) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, pixelsPerDip);
const DrawingContext* drawingContext = static_cast<DrawingContext*>(clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, drawingContext);
float dpiX, dpiY;
drawingContext->renderTarget->GetDpi(&dpiX, &dpiY);
*pixelsPerDip = dpiX / USER_DEFAULT_SCREEN_DPI;
return S_OK;
}
// Routine Description:
// - Implementation of IDWritePixelSnapping::GetCurrentTransform
// - Retrieves the matrix transform to be used while laying pixels onto the
// drawing context
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - transform - The matrix transform to use to adapt DIP representations into real monitor coordinates.
// Return Value:
// - S_OK
[[nodiscard]] HRESULT CustomTextRenderer::GetCurrentTransform(void* clientDrawingContext,
DWRITE_MATRIX* transform) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, transform);
const DrawingContext* drawingContext = static_cast<DrawingContext*>(clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, drawingContext);
// Retrieve as D2D1 matrix then copy into DWRITE matrix.
D2D1_MATRIX_3X2_F d2d1Matrix{ 0 };
drawingContext->renderTarget->GetTransform(&d2d1Matrix);
transform->dx = d2d1Matrix.dx;
transform->dy = d2d1Matrix.dy;
transform->m11 = d2d1Matrix.m11;
transform->m12 = d2d1Matrix.m12;
transform->m21 = d2d1Matrix.m21;
transform->m22 = d2d1Matrix.m22;
return S_OK;
}
#pragma endregion
#pragma region IDWriteTextRenderer methods
// Routine Description:
// - Implementation of IDWriteTextRenderer::DrawUnderline
// - Directs us to draw an underline on the given context at the given position.
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - baselineOriginX - The text baseline position's X coordinate
// - baselineOriginY - The text baseline position's Y coordinate
// - The baseline is generally not the top nor the bottom of the "cell" that
// text is drawn into. It's usually somewhere "in the middle" and depends on the
// font and the glyphs. It can be calculated during layout and analysis in respect
// to the given font and glyphs.
// - underline - The properties of the underline that we should use for drawing
// - clientDrawingEffect - any special effect to pass along for rendering
// Return Value:
// - S_OK
[[nodiscard]] HRESULT CustomTextRenderer::DrawUnderline(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
_In_ const DWRITE_UNDERLINE* underline,
IUnknown* clientDrawingEffect) noexcept
{
return _FillRectangle(clientDrawingContext,
clientDrawingEffect,
baselineOriginX,
baselineOriginY + underline->offset,
underline->width,
underline->thickness,
underline->readingDirection,
underline->flowDirection);
}
// Routine Description:
// - Implementation of IDWriteTextRenderer::DrawStrikethrough
// - Directs us to draw a strikethrough on the given context at the given position.
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - baselineOriginX - The text baseline position's X coordinate
// - baselineOriginY - The text baseline position's Y coordinate
// - The baseline is generally not the top nor the bottom of the "cell" that
// text is drawn into. It's usually somewhere "in the middle" and depends on the
// font and the glyphs. It can be calculated during layout and analysis in respect
// to the given font and glyphs.
// - strikethrough - The properties of the strikethrough that we should use for drawing
// - clientDrawingEffect - any special effect to pass along for rendering
// Return Value:
// - S_OK
[[nodiscard]] HRESULT CustomTextRenderer::DrawStrikethrough(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
_In_ const DWRITE_STRIKETHROUGH* strikethrough,
IUnknown* clientDrawingEffect) noexcept
{
return _FillRectangle(clientDrawingContext,
clientDrawingEffect,
baselineOriginX,
baselineOriginY + strikethrough->offset,
strikethrough->width,
strikethrough->thickness,
strikethrough->readingDirection,
strikethrough->flowDirection);
}
// Routine Description:
// - Helper method to draw a line through our text.
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - clientDrawingEffect - any special effect passed along for rendering
// - x - The left coordinate of the rectangle
// - y - The top coordinate of the rectangle
// - width - The width of the rectangle (from X to the right)
// - height - The height of the rectangle (from Y down)
// - readingDirection - textual reading information that could affect the rectangle
// - flowDirection - textual flow information that could affect the rectangle
// Return Value:
// - S_OK
[[nodiscard]] HRESULT CustomTextRenderer::_FillRectangle(void* clientDrawingContext,
IUnknown* clientDrawingEffect,
float x,
float y,
float width,
float thickness,
DWRITE_READING_DIRECTION /*readingDirection*/,
DWRITE_FLOW_DIRECTION /*flowDirection*/) noexcept
{
auto drawingContext = static_cast<DrawingContext*>(clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, drawingContext);
// Get brush
ID2D1Brush* brush = drawingContext->foregroundBrush;
if (clientDrawingEffect != nullptr)
{
brush = static_cast<ID2D1Brush*>(clientDrawingEffect);
}
const auto rect = D2D1::RectF(x, y, x + width, y + thickness);
drawingContext->renderTarget->FillRectangle(&rect, brush);
return S_OK;
}
// Routine Description:
// - Implementation of IDWriteTextRenderer::DrawInlineObject
// - Passes drawing control from the outer layout down into the context of an embedded object
// which can have its own drawing layout and renderer properties at a given position
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - originX - The left coordinate of the draw position
// - originY - The top coordinate of the draw position
// - inlineObject - The object to draw at the position
// - isSideways - Should be drawn vertically instead of horizontally
// - isRightToLeft - Should be drawn RTL (or bottom to top) instead of the default way
// - clientDrawingEffect - any special effect passed along for rendering
// Return Value:
// - S_OK or appropriate error from the delegated inline object's draw call
[[nodiscard]] HRESULT CustomTextRenderer::DrawInlineObject(void* clientDrawingContext,
FLOAT originX,
FLOAT originY,
IDWriteInlineObject* inlineObject,
BOOL isSideways,
BOOL isRightToLeft,
IUnknown* clientDrawingEffect) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, inlineObject);
return inlineObject->Draw(clientDrawingContext,
this,
originX,
originY,
isSideways,
isRightToLeft,
clientDrawingEffect);
}
// Function Description:
// - Attempt to draw the cursor. If the cursor isn't visible or on, this
// function will do nothing. If the cursor isn't within the bounds of the
// current run of text, then this function will do nothing.
// - This function will get called twice during a run, once before the text is
// drawn (underneath the text), and again after the text is drawn (above the
// text). Depending on if the cursor wants to be drawn above or below the
// text, this function will do nothing for the first/second pass
// (respectively).
// Arguments:
// - d2dContext - Pointer to the current D2D drawing context
// - textRunBounds - The bounds of the current run of text.
// - drawingContext - Pointer to structure of information required to draw
// - firstPass - true if we're being called before the text is drawn, false afterwards.
// Return Value:
// - S_FALSE if we did nothing, S_OK if we successfully painted, otherwise an appropriate HRESULT
[[nodiscard]] HRESULT CustomTextRenderer::DrawCursor(gsl::not_null<ID2D1DeviceContext*> d2dContext,
D2D1_RECT_F textRunBounds,
const DrawingContext& drawingContext,
const bool firstPass)
try
{
if (!drawingContext.cursorInfo.has_value())
{
return S_FALSE;
}
const auto& options = drawingContext.cursorInfo.value();
// if the cursor is off, do nothing - it should not be visible.
if (!options.isOn)
{
return S_FALSE;
}
const auto fInvert = !options.fUseColor;
// The normal, colored FullBox and legacy cursors are drawn in the first pass
// so they go behind the text.
// Inverted cursors are drawn in two passes.
// All other cursors are drawn in the second pass only.
if (!fInvert)
{
if (firstPass != (options.cursorType == CursorType::FullBox))
{
return S_FALSE;
}
}
// TODO GH#6338: Add support for `"cursorTextColor": null` for letting the
// cursor draw on top again.
// Create rectangular block representing where the cursor can fill.
D2D1_RECT_F rect;
rect.left = options.coordCursor.x * drawingContext.cellSize.width;
rect.top = options.coordCursor.y * drawingContext.cellSize.height;
rect.right = rect.left + drawingContext.cellSize.width;
rect.bottom = rect.top + drawingContext.cellSize.height;
// If we're double-width, make it one extra glyph wider
if (options.fIsDoubleWidth)
{
rect.right += drawingContext.cellSize.width;
}
// If the cursor isn't within the bounds of this current run of text, do nothing.
if (rect.top > textRunBounds.bottom ||
rect.bottom <= textRunBounds.top ||
rect.left > textRunBounds.right ||
rect.right <= textRunBounds.left)
{
return S_FALSE;
}
auto paintType = CursorPaintType::Fill;
switch (options.cursorType)
{
case CursorType::Legacy:
{
// Enforce min/max cursor height
auto ulHeight = std::clamp(options.ulCursorHeightPercent, MinCursorHeightPercent, MaxCursorHeightPercent);
ulHeight = gsl::narrow_cast<ULONG>(drawingContext.cellSize.height * ulHeight) / 100;
ulHeight = std::max(ulHeight, MinCursorHeightPixels); // No smaller than 1px
rect.top = rect.bottom - ulHeight;
break;
}
case CursorType::VerticalBar:
{
// It can't be wider than one cell or we'll have problems in invalidation, so restrict here.
// It's either the left + the proposed width from the ease of access setting, or
// it's the right edge of the block cursor as a maximum.
rect.right = std::min(rect.right, rect.left + options.cursorPixelWidth);
break;
}
case CursorType::Underscore:
{
rect.top = rect.bottom - 1;
break;
}
case CursorType::DoubleUnderscore:
{
// Use rect for lower line.
rect.top = rect.bottom - 1;
break;
}
case CursorType::EmptyBox:
{
paintType = CursorPaintType::Outline;
break;
}
case CursorType::FullBox:
{
break;
}
default:
return E_NOTIMPL;
}
// **DRAW** PHASE
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
Microsoft::WRL::ComPtr<ID2D1Image> originalTarget;
Microsoft::WRL::ComPtr<ID2D1CommandList> commandList;
D2D1::Matrix3x2F originalTransform;
if (!fInvert)
{
// Make sure to make the cursor opaque
RETURN_IF_FAILED(d2dContext->CreateSolidColorBrush(til::color{ options.cursorColor }, &brush));
}
else
{
// CURSOR INVERSION
// We're trying to invert the cursor and the character underneath it without redrawing the text (as
// doing so would break up the run if it were part of a ligature). To do that, we're going to try
// to invert the content of the screen where the cursor would have been.
//
// This renderer, however, supports transparency. In fact, in its default configuration it will not
// have a background at all (it delegates background handling to somebody else.) You can't invert what
// isn't there.
//
// To properly invert the cursor in such a configuration, then, we have to play some tricks. Examples
// are given below for two cursor types, but this applies to all of them.
//
// First, we'll draw a "backplate" in the user's requested background color (with the alpha channel
// set to 0xFF). (firstPass == true)
//
// EMPTY BOX FILLED BOX
// ===== =====
// = = =====
// = = =====
// = = =====
// ===== =====
//
// Then, outside of DrawCursor, the glyph is drawn:
//
// EMPTY BOX FILLED BOX
// ==A== ==A==
// =A A= =A=A=
// AAAAA AAAAA
// A A A===A
// A===A A===A
//
// Last, we'll draw the cursor again in all white and use that as the *mask* for inverting the already-
// drawn pixels. (firstPass == false) (# = mask, a = inverted A)
//
// EMPTY BOX FILLED BOX
// ##a## ##a##
// #A A# #a#a#
// aAAAa aaaaa
// a a a###a
// a###a a###a
if (firstPass)
{
// Draw a backplate behind the cursor in the *background* color so that we can invert it later.
// Make sure the cursor is always readable (see gh-3647)
const til::color color{ til::color{ drawingContext.backgroundBrush->GetColor() } ^ RGB(63, 63, 63) };
RETURN_IF_FAILED(d2dContext->CreateSolidColorBrush(color.with_alpha(255),
&brush));
}
else
{
// When we're drawing an inverted cursor on the second pass (foreground), we want to draw it into a
// command list, which we will then draw down with MASK_INVERT. We'll draw it in white,
// which will ensure that every component is masked.
RETURN_IF_FAILED(d2dContext->CreateCommandList(&commandList));
d2dContext->GetTarget(&originalTarget);
d2dContext->SetTarget(commandList.Get());
// We use an identity transform here to avoid the active transform being applied twice.
d2dContext->GetTransform(&originalTransform);
d2dContext->SetTransform(D2D1::Matrix3x2F::Identity());
RETURN_IF_FAILED(d2dContext->CreateSolidColorBrush(COLOR_WHITE, &brush));
}
}
switch (paintType)
{
case CursorPaintType::Fill:
{
d2dContext->FillRectangle(rect, brush.Get());
break;
}
case CursorPaintType::Outline:
{
// DrawRectangle in straddles physical pixels in an attempt to draw a line
// between them. To avoid this, bump the rectangle around by half the stroke width.
rect.top += 0.5f;
rect.left += 0.5f;
rect.bottom -= 0.5f;
rect.right -= 0.5f;
d2dContext->DrawRectangle(rect, brush.Get());
break;
}
default:
return E_NOTIMPL;
}
if (options.cursorType == CursorType::DoubleUnderscore)
{
// Draw upper line directly.
auto upperLine = rect;
upperLine.top -= 2;
upperLine.bottom -= 2;
d2dContext->FillRectangle(upperLine, brush.Get());
}
if (commandList)
{
// We drew the entire cursor in a command list
// so now we draw that command list using MASK_INVERT over the existing image
RETURN_IF_FAILED(commandList->Close());
d2dContext->SetTarget(originalTarget.Get());
d2dContext->SetTransform(originalTransform);
d2dContext->DrawImage(commandList.Get(), D2D1_INTERPOLATION_MODE_LINEAR, D2D1_COMPOSITE_MODE_MASK_INVERT);
}
return S_OK;
}
CATCH_RETURN()
// Routine Description:
// - Implementation of IDWriteTextRenderer::DrawInlineObject
// - Passes drawing control from the outer layout down into the context of an embedded object
// which can have its own drawing layout and renderer properties at a given position
// Arguments:
// - clientDrawingContext - Pointer to structure of information required to draw
// - baselineOriginX - The text baseline position's X coordinate
// - baselineOriginY - The text baseline position's Y coordinate
// - The baseline is generally not the top nor the bottom of the "cell" that
// text is drawn into. It's usually somewhere "in the middle" and depends on the
// font and the glyphs. It can be calculated during layout and analysis in respect
// to the given font and glyphs.
// - measuringMode - The mode to measure glyphs in the DirectWrite context
// - glyphRun - Information on the glyphs
// - glyphRunDescription - Further metadata about the glyphs used while drawing
// - clientDrawingEffect - any special effect passed along for rendering
// Return Value:
// - S_OK, GSL/WIL/STL error, or appropriate DirectX/Direct2D/DirectWrite based error while drawing.
[[nodiscard]] HRESULT CustomTextRenderer::DrawGlyphRun(
void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
DWRITE_MEASURING_MODE measuringMode,
const DWRITE_GLYPH_RUN* glyphRun,
const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
IUnknown* clientDrawingEffect)
{
// Color glyph rendering sourced from https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/DWriteColorGlyph
#pragma warning(suppress : 26429) // Symbol 'drawingContext' is never tested for nullness, it can be marked as not_null (f.23).
auto drawingContext = static_cast<DrawingContext*>(clientDrawingContext);
// Since we've delegated the drawing of the background of the text into this function, the origin passed in isn't actually the baseline.
// It's the top left corner. Save that off first.
const auto origin = D2D1::Point2F(baselineOriginX, baselineOriginY);
// Then make a copy for the baseline origin (which is part way down the left side of the text, not the top or bottom).
// We'll use this baseline Origin for drawing the actual text.
const D2D1_POINT_2F baselineOrigin{ origin.x, origin.y + drawingContext->spacing.baseline };
::Microsoft::WRL::ComPtr<ID2D1DeviceContext> d2dContext;
RETURN_IF_FAILED(drawingContext->renderTarget->QueryInterface(d2dContext.GetAddressOf()));
// Determine clip rectangle
D2D1_RECT_F clipRect;
clipRect.top = origin.y + drawingContext->topClipOffset;
clipRect.bottom = origin.y + drawingContext->cellSize.height - drawingContext->bottomClipOffset;
clipRect.left = 0;
clipRect.right = FLT_MAX;
// If we already have a clip rectangle, check if it different than the previous one.
if (_clipRect.has_value())
{
const auto storedVal = _clipRect.value();
// If it is different, pop off the old one and push the new one on.
if (storedVal.top != clipRect.top || storedVal.bottom != clipRect.bottom ||
storedVal.left != clipRect.left || storedVal.right != clipRect.right)
{
d2dContext->PopAxisAlignedClip();
// Clip all drawing in this glyph run to where we expect.
// We need the AntialiasMode here to be Aliased to ensure
// that background boxes line up with each other and don't leave behind
// stray colors.
// See GH#3626 for more details.
d2dContext->PushAxisAlignedClip(clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
_clipRect = clipRect;
}
}
// If we have no clip rectangle, it's easy. Push it on and go.
else
{
// See above for aliased flag explanation.
d2dContext->PushAxisAlignedClip(clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
_clipRect = clipRect;
}
// Draw the background
// The rectangle needs to be deduced based on the origin and the BidiDirection
const auto advancesSpan = std::span{ glyphRun->glyphAdvances, glyphRun->glyphCount };
const auto totalSpan = std::accumulate(advancesSpan.begin(), advancesSpan.end(), 0.0f);
D2D1_RECT_F rect;
rect.top = origin.y;
rect.bottom = rect.top + drawingContext->cellSize.height;
rect.left = origin.x;
// Check for RTL, if it is, move rect.left to the left from the baseline
if (WI_IsFlagSet(glyphRun->bidiLevel, 1))
{
rect.left -= totalSpan;
}
rect.right = rect.left + totalSpan;
d2dContext->FillRectangle(rect, drawingContext->backgroundBrush);
RETURN_IF_FAILED(DrawCursor(d2dContext.Get(), rect, *drawingContext, true));
// GH#5098: If we're rendering with cleartype text, we need to always render
// onto an opaque background. If our background _isn't_ opaque, then we need
// to use grayscale AA for this run of text.
//
// We can force grayscale AA for just this run of text by pushing a new
// layer onto the d2d context. We'll only need to do this for cleartype
// text, when our eventual background isn't actually opaque. See
// DxEngine::PaintBufferLine and DxEngine::UpdateDrawingBrushes for more
// details.
//
// DANGER: Layers slow us down. Only do this in the specific case where
// someone has chosen the slower ClearType antialiasing (versus the faster
// grayscale antialiasing).
// First, create the scope_exit to pop the layer. If we don't need the
// layer, we'll just gracefully release it.
auto popLayer = wil::scope_exit([&d2dContext]() noexcept {
d2dContext->PopLayer();
});
if (drawingContext->forceGrayscaleAA)
{
// Mysteriously, D2D1_LAYER_OPTIONS_INITIALIZE_FOR_CLEARTYPE actually
// gets us the behavior we want, which is grayscale.
d2dContext->PushLayer(D2D1::LayerParameters(rect,
nullptr,
D2D1_ANTIALIAS_MODE_ALIASED,
D2D1::IdentityMatrix(),
1.0,
nullptr,
D2D1_LAYER_OPTIONS_INITIALIZE_FOR_CLEARTYPE),
nullptr);
}
else
{
popLayer.release();
}
// Now go onto drawing the text.
// First check if we want a color font and try to extract color emoji first.
// Color emoji are only available on Windows 10+
static const auto s_isWindows10OrGreater = IsWindows10OrGreater();
if (WI_IsFlagSet(drawingContext->options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT) && s_isWindows10OrGreater)
{
::Microsoft::WRL::ComPtr<ID2D1DeviceContext4> d2dContext4;
RETURN_IF_FAILED(d2dContext.As(&d2dContext4));
::Microsoft::WRL::ComPtr<IDWriteFactory4> dwriteFactory4;
RETURN_IF_FAILED(drawingContext->dwriteFactory->QueryInterface(dwriteFactory4.GetAddressOf()));
// The list of glyph image formats this renderer is prepared to support.
const auto supportedFormats =
DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE |
DWRITE_GLYPH_IMAGE_FORMATS_CFF |
DWRITE_GLYPH_IMAGE_FORMATS_COLR |
DWRITE_GLYPH_IMAGE_FORMATS_SVG |
DWRITE_GLYPH_IMAGE_FORMATS_PNG |
DWRITE_GLYPH_IMAGE_FORMATS_JPEG |
DWRITE_GLYPH_IMAGE_FORMATS_TIFF |
DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8;
// Determine whether there are any color glyph runs within glyphRun. If
// there are, glyphRunEnumerator can be used to iterate through them.
::Microsoft::WRL::ComPtr<IDWriteColorGlyphRunEnumerator1> glyphRunEnumerator;
const auto hr = dwriteFactory4->TranslateColorGlyphRun(baselineOrigin,
glyphRun,
glyphRunDescription,
supportedFormats,
measuringMode,
nullptr,
0,
&glyphRunEnumerator);
// If the analysis found no color glyphs in the run, just draw normally.
if (hr == DWRITE_E_NOCOLOR)
{
RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext,
baselineOrigin,
measuringMode,
glyphRun,
glyphRunDescription,
drawingContext->foregroundBrush,
clientDrawingEffect));
}
else
{
RETURN_IF_FAILED(hr);
::Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> tempBrush;
// Complex case: the run has one or more color runs within it. Iterate
// over the sub-runs and draw them, depending on their format.
for (;;)
{
BOOL haveRun;
RETURN_IF_FAILED(glyphRunEnumerator->MoveNext(&haveRun));
if (!haveRun)
break;
DWRITE_COLOR_GLYPH_RUN1 const* colorRun;
RETURN_IF_FAILED(glyphRunEnumerator->GetCurrentRun(&colorRun));
const auto currentBaselineOrigin = D2D1::Point2F(colorRun->baselineOriginX, colorRun->baselineOriginY);
switch (colorRun->glyphImageFormat)
{
case DWRITE_GLYPH_IMAGE_FORMATS_PNG:
case DWRITE_GLYPH_IMAGE_FORMATS_JPEG:
case DWRITE_GLYPH_IMAGE_FORMATS_TIFF:
case DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8:
{
// This run is bitmap glyphs. Use Direct2D to draw them.
d2dContext4->DrawColorBitmapGlyphRun(colorRun->glyphImageFormat,
currentBaselineOrigin,
&colorRun->glyphRun,
measuringMode);
}
break;
case DWRITE_GLYPH_IMAGE_FORMATS_SVG:
{
// This run is SVG glyphs. Use Direct2D to draw them.
d2dContext4->DrawSvgGlyphRun(currentBaselineOrigin,
&colorRun->glyphRun,
drawingContext->foregroundBrush,
nullptr, // svgGlyphStyle
0, // colorPaletteIndex
measuringMode);
}
break;
case DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE:
case DWRITE_GLYPH_IMAGE_FORMATS_CFF:
case DWRITE_GLYPH_IMAGE_FORMATS_COLR:
default:
{
// This run is solid-color outlines, either from non-color
// glyphs or from COLR glyph layers. Use Direct2D to draw them.
ID2D1Brush* layerBrush{ nullptr };
// The rule is "if 0xffff, use current brush." See:
// https://docs.microsoft.com/en-us/windows/desktop/api/dwrite_2/ns-dwrite_2-dwrite_color_glyph_run
if (colorRun->paletteIndex == 0xFFFF)
{
// This run uses the current text color.
layerBrush = drawingContext->foregroundBrush;
}
else
{
if (!tempBrush)
{
RETURN_IF_FAILED(d2dContext4->CreateSolidColorBrush(colorRun->runColor, &tempBrush));
}
else
{
// This run specifies its own color.
tempBrush->SetColor(colorRun->runColor);
}
layerBrush = tempBrush.Get();
}
// Draw the run with the selected color.
RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext,
currentBaselineOrigin,
measuringMode,
&colorRun->glyphRun,
colorRun->glyphRunDescription,
layerBrush,
clientDrawingEffect));
}
break;
}
}
}
}
else
{
// Simple case: the run has no color glyphs. Draw the main glyph run
// using the current text color.
RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext,
baselineOrigin,
measuringMode,
glyphRun,
glyphRunDescription,
drawingContext->foregroundBrush,
clientDrawingEffect));
}
RETURN_IF_FAILED(DrawCursor(d2dContext.Get(), rect, *drawingContext, false));
return S_OK;
}
#pragma endregion
[[nodiscard]] HRESULT CustomTextRenderer::EndClip(void* clientDrawingContext) noexcept
try
{
auto drawingContext = static_cast<DrawingContext*>(clientDrawingContext);
RETURN_HR_IF(E_INVALIDARG, !drawingContext);
if (_clipRect.has_value())
{
drawingContext->renderTarget->PopAxisAlignedClip();
_clipRect = std::nullopt;
}
return S_OK;
}
CATCH_RETURN()
[[nodiscard]] HRESULT CustomTextRenderer::_DrawBasicGlyphRun(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
ID2D1Brush* brush,
_In_opt_ IUnknown* clientDrawingEffect)
{
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun);
RETURN_HR_IF_NULL(E_INVALIDARG, brush);
::Microsoft::WRL::ComPtr<ID2D1DeviceContext> d2dContext;
RETURN_IF_FAILED(clientDrawingContext->renderTarget->QueryInterface(d2dContext.GetAddressOf()));
// If a special drawing effect was specified, see if we know how to deal with it.
if (clientDrawingEffect)
{
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> boxEffect;
if (SUCCEEDED(clientDrawingEffect->QueryInterface<IBoxDrawingEffect>(&boxEffect)))
{
return _DrawBoxRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription, boxEffect.Get());
}
//_DrawBasicGlyphRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription);
//_DrawGlowGlyphRun(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription);
}
// If we get down here, there either was no special effect or we don't know what to do with it. Use the standard GlyphRun drawing.
// Using the context is the easiest/default way of drawing.
d2dContext->DrawGlyphRun(baselineOrigin, glyphRun, glyphRunDescription, brush, measuringMode);
return S_OK;
}
[[nodiscard]] HRESULT CustomTextRenderer::_DrawBoxRunManually(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE /*measuringMode*/,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/,
_In_ IBoxDrawingEffect* clientDrawingEffect) noexcept
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun);
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingEffect);
::Microsoft::WRL::ComPtr<ID2D1Factory> d2dFactory;
clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf());
::Microsoft::WRL::ComPtr<ID2D1PathGeometry> pathGeometry;
d2dFactory->CreatePathGeometry(pathGeometry.GetAddressOf());
::Microsoft::WRL::ComPtr<ID2D1GeometrySink> geometrySink;
pathGeometry->Open(geometrySink.GetAddressOf());
glyphRun->fontFace->GetGlyphRunOutline(
glyphRun->fontEmSize,
glyphRun->glyphIndices,
glyphRun->glyphAdvances,
glyphRun->glyphOffsets,
glyphRun->glyphCount,
glyphRun->isSideways,
glyphRun->bidiLevel % 2,
geometrySink.Get());
geometrySink->Close();
// Can be used to see the dimensions of what is written.
/*D2D1_RECT_F bounds;
pathGeometry->GetBounds(D2D1::IdentityMatrix(), &bounds);*/
// The bounds here are going to be centered around the baseline of the font.
// That is, the DWRITE_GLYPH_METRICS property for this glyph's baseline is going
// to be at the 0 point in the Y direction when we receive the geometry.
// The ascent will go up negative from Y=0 and the descent will go down positive from Y=0.
// As for the horizontal direction, I didn't study this in depth, but it appears to always be
// positive X with both the left and right edges being positive and away from X=0.
// For one particular instance, we might ask for the geometry for a U+2588 box and see the bounds as:
//
// Top=
// -20.315
// -----------
// | |
// | |
// Left= | | Right=
// 13.859 | | 26.135
// | |
// Origin --> X | |
// (0,0) | |
// -----------
// Bottom=
// 5.955
// Dig out the box drawing effect parameters.
BoxScale scale;
RETURN_IF_FAILED(clientDrawingEffect->GetScale(&scale));
// The scale transform will inflate the entire geometry first.
// We want to do this before it moves out of its original location as generally our
// algorithms for fitting cells will blow up the glyph to the size it needs to be first and then
// nudge it into place with the translations.
const auto scaleTransform = D2D1::Matrix3x2F::Scale(scale.HorizontalScale, scale.VerticalScale);
// Now shift it all the way to where the baseline says it should be.
const auto baselineTransform = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y);
// Finally apply the little "nudge" that we may have been directed to align it better with the cell.
const auto offsetTransform = D2D1::Matrix3x2F::Translation(scale.HorizontalTranslation, scale.VerticalTranslation);
// The order is important here. Scale it first, then slide it into place.
const auto matrixTransformation = scaleTransform * baselineTransform * offsetTransform;
::Microsoft::WRL::ComPtr<ID2D1TransformedGeometry> transformedGeometry;
d2dFactory->CreateTransformedGeometry(pathGeometry.Get(),
&matrixTransformation,
transformedGeometry.GetAddressOf());
// Can be used to see the dimensions after translation.
/*D2D1_RECT_F boundsAfter;
transformedGeometry->GetBounds(D2D1::IdentityMatrix(), &boundsAfter);*/
// Compare this to the original bounds above to see what the matrix did.
// To make it useful, first visualize for yourself the pixel dimensions of the cell
// based on the baselineOrigin and the exact integer cell width and heights that we're storing.
// You'll also probably need the full-pixel ascent and descent because the point we're given
// is the baseline, not the top left corner of the cell as we're used to.
// Most of these metrics can be found in the initial font creation routines or in
// the line spacing applied to the text format (member variables on the renderer).
// baselineOrigin = (0, 567)
// fullPixelAscent = 39
// fullPixelDescent = 9
// cell dimensions = 26 x 48 (notice 48 height is 39 + 9 or ascent + descent)
// This means that our cell should be the rectangle
//
// T=528
// |-------|
// L=0 | |
// | |
// Baseline->x |
// Origin | | R=26
// |-------|
// B=576
//
// And we'll want to check that the bounds after transform will fit the glyph nicely inside
// this box.
// If not? We didn't do the scaling or translation correctly. Oops.
// Fill in the geometry. Don't outline, it can leave stuff outside the area we expect.
clientDrawingContext->renderTarget->FillGeometry(transformedGeometry.Get(), clientDrawingContext->foregroundBrush);
return S_OK;
}
CATCH_RETURN();
[[nodiscard]] HRESULT CustomTextRenderer::_DrawGlowGlyphRun(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE /*measuringMode*/,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun);
// This is glow text manually
::Microsoft::WRL::ComPtr<ID2D1Factory> d2dFactory;
clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf());
::Microsoft::WRL::ComPtr<ID2D1PathGeometry> pathGeometry;
d2dFactory->CreatePathGeometry(pathGeometry.GetAddressOf());
::Microsoft::WRL::ComPtr<ID2D1GeometrySink> geometrySink;
pathGeometry->Open(geometrySink.GetAddressOf());
glyphRun->fontFace->GetGlyphRunOutline(
glyphRun->fontEmSize,
glyphRun->glyphIndices,
glyphRun->glyphAdvances,
glyphRun->glyphOffsets,
glyphRun->glyphCount,
glyphRun->isSideways,
glyphRun->bidiLevel % 2,
geometrySink.Get());
geometrySink->Close();
const auto matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y);
::Microsoft::WRL::ComPtr<ID2D1TransformedGeometry> transformedGeometry;
d2dFactory->CreateTransformedGeometry(pathGeometry.Get(),
&matrixAlign,
transformedGeometry.GetAddressOf());
::Microsoft::WRL::ComPtr<ID2D1TransformedGeometry> alignedGeometry;
d2dFactory->CreateTransformedGeometry(pathGeometry.Get(),
&matrixAlign,
alignedGeometry.GetAddressOf());
::Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
::Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> outlineBrush;
clientDrawingContext->renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 1.0f), brush.GetAddressOf());
clientDrawingContext->renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red, 1.0f), outlineBrush.GetAddressOf());
clientDrawingContext->renderTarget->DrawGeometry(transformedGeometry.Get(), outlineBrush.Get(), 2.0f);
clientDrawingContext->renderTarget->FillGeometry(alignedGeometry.Get(), brush.Get());
return S_OK;
}

View File

@@ -1,153 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <wrl/implements.h>
#include "BoxDrawingEffect.h"
#include "../../renderer/inc/CursorOptions.h"
namespace Microsoft::Console::Render
{
struct DrawingContext
{
DrawingContext(ID2D1RenderTarget* renderTarget,
ID2D1SolidColorBrush* foregroundBrush,
ID2D1SolidColorBrush* backgroundBrush,
bool forceGrayscaleAA,
IDWriteFactory* dwriteFactory,
const DWRITE_LINE_SPACING spacing,
const D2D_SIZE_F cellSize,
const D2D_SIZE_F targetSize,
const std::optional<CursorOptions>& cursorInfo,
const D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE) noexcept :
renderTarget(renderTarget),
foregroundBrush(foregroundBrush),
backgroundBrush(backgroundBrush),
useBoldFont(false),
useItalicFont(false),
forceGrayscaleAA(forceGrayscaleAA),
dwriteFactory(dwriteFactory),
spacing(spacing),
cellSize(cellSize),
targetSize(targetSize),
cursorInfo(cursorInfo),
options(options),
topClipOffset(0),
bottomClipOffset(0)
{
}
ID2D1RenderTarget* renderTarget;
ID2D1SolidColorBrush* foregroundBrush;
ID2D1SolidColorBrush* backgroundBrush;
bool useBoldFont;
bool useItalicFont;
bool forceGrayscaleAA;
IDWriteFactory* dwriteFactory;
DWRITE_LINE_SPACING spacing;
D2D_SIZE_F cellSize;
D2D_SIZE_F targetSize;
std::optional<CursorOptions> cursorInfo;
D2D1_DRAW_TEXT_OPTIONS options;
FLOAT topClipOffset;
FLOAT bottomClipOffset;
};
// Helper to choose which Direct2D method to use when drawing the cursor rectangle
enum class CursorPaintType
{
Fill,
Outline
};
constexpr const ULONG MinCursorHeightPixels = 1;
constexpr const ULONG MinCursorHeightPercent = 1;
constexpr const ULONG MaxCursorHeightPercent = 100;
class CustomTextRenderer : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | ::Microsoft::WRL::InhibitFtmBase>, IDWriteTextRenderer>
{
public:
// http://www.charlespetzold.com/blog/2014/01/Character-Formatting-Extensions-with-DirectWrite.html
// https://docs.microsoft.com/en-us/windows/desktop/DirectWrite/how-to-implement-a-custom-text-renderer
// IDWritePixelSnapping methods
[[nodiscard]] HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled(void* clientDrawingContext,
_Out_ BOOL* isDisabled) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetPixelsPerDip(void* clientDrawingContext,
_Out_ FLOAT* pixelsPerDip) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetCurrentTransform(void* clientDrawingContext,
_Out_ DWRITE_MATRIX* transform) noexcept override;
// IDWriteTextRenderer methods
[[nodiscard]] HRESULT STDMETHODCALLTYPE DrawGlyphRun(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
IUnknown* clientDrawingEffect) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE DrawUnderline(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
_In_ const DWRITE_UNDERLINE* underline,
IUnknown* clientDrawingEffect) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE DrawStrikethrough(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
_In_ const DWRITE_STRIKETHROUGH* strikethrough,
IUnknown* clientDrawingEffect) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE DrawInlineObject(void* clientDrawingContext,
FLOAT originX,
FLOAT originY,
IDWriteInlineObject* inlineObject,
BOOL isSideways,
BOOL isRightToLeft,
IUnknown* clientDrawingEffect) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE EndClip(void* clientDrawingContext) noexcept;
[[nodiscard]] static HRESULT DrawCursor(gsl::not_null<ID2D1DeviceContext*> d2dContext,
D2D1_RECT_F textRunBounds,
const DrawingContext& drawingContext,
const bool firstPass);
private:
[[nodiscard]] HRESULT _FillRectangle(void* clientDrawingContext,
IUnknown* clientDrawingEffect,
float x,
float y,
float width,
float thickness,
DWRITE_READING_DIRECTION readingDirection,
DWRITE_FLOW_DIRECTION flowDirection) noexcept;
[[nodiscard]] HRESULT _DrawBasicGlyphRun(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
ID2D1Brush* brush,
_In_opt_ IUnknown* clientDrawingEffect);
[[nodiscard]] HRESULT _DrawBoxRunManually(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
_In_ IBoxDrawingEffect* clientDrawingEffect) noexcept;
[[nodiscard]] HRESULT _DrawGlowGlyphRun(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription) noexcept;
std::optional<D2D1_RECT_F> _clipRect;
};
}

View File

@@ -1,300 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "DxFontInfo.h"
#include <unicode.hpp>
#include <VersionHelpers.h>
#include "../base/FontCache.h"
static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" };
using namespace Microsoft::Console::Render;
DxFontInfo::DxFontInfo(IDWriteFactory1* dwriteFactory) :
DxFontInfo{ dwriteFactory, {}, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL }
{
}
DxFontInfo::DxFontInfo(
IDWriteFactory1* dwriteFactory,
std::wstring_view familyName,
DWRITE_FONT_WEIGHT weight,
DWRITE_FONT_STYLE style,
DWRITE_FONT_STRETCH stretch) :
_familyName(familyName),
_weight(weight),
_style(style),
_stretch(stretch),
_didFallback(false)
{
__assume(dwriteFactory != nullptr);
THROW_IF_FAILED(dwriteFactory->GetSystemFontCollection(_fontCollection.addressof(), FALSE));
}
bool DxFontInfo::operator==(const DxFontInfo& other) const noexcept
{
return (_familyName == other._familyName &&
_weight == other._weight &&
_style == other._style &&
_stretch == other._stretch &&
_didFallback == other._didFallback);
}
std::wstring_view DxFontInfo::GetFamilyName() const noexcept
{
return _familyName;
}
void DxFontInfo::SetFamilyName(const std::wstring_view familyName)
{
_familyName = familyName;
}
DWRITE_FONT_WEIGHT DxFontInfo::GetWeight() const noexcept
{
return _weight;
}
void DxFontInfo::SetWeight(const DWRITE_FONT_WEIGHT weight) noexcept
{
_weight = weight;
}
DWRITE_FONT_STYLE DxFontInfo::GetStyle() const noexcept
{
return _style;
}
void DxFontInfo::SetStyle(const DWRITE_FONT_STYLE style) noexcept
{
_style = style;
}
DWRITE_FONT_STRETCH DxFontInfo::GetStretch() const noexcept
{
return _stretch;
}
void DxFontInfo::SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept
{
_stretch = stretch;
}
bool DxFontInfo::GetFallback() const noexcept
{
return _didFallback;
}
IDWriteFontCollection* DxFontInfo::GetFontCollection() const noexcept
{
return _fontCollection.get();
}
void DxFontInfo::SetFromEngine(const std::wstring_view familyName,
const DWRITE_FONT_WEIGHT weight,
const DWRITE_FONT_STYLE style,
const DWRITE_FONT_STRETCH stretch)
{
_familyName = familyName;
_weight = weight;
_style = style;
_stretch = stretch;
}
// Routine Description:
// - Attempts to locate the font given, but then begins falling back if we cannot find it.
// - We'll try to fall back to Consolas with the given weight/stretch/style first,
// then try Consolas again with normal weight/stretch/style,
// and if nothing works, then we'll throw an error.
// Arguments:
// - dwriteFactory - The DWrite factory to use
// - localeName - Locale to search for appropriate fonts
// Return Value:
// - Smart pointer holding interface reference for queryable font data.
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontInfo::ResolveFontFaceWithFallback(std::wstring& localeName)
{
// First attempt to find exactly what the user asked for.
_didFallback = false;
Microsoft::WRL::ComPtr<IDWriteFontFace1> face{ nullptr };
// GH#10211 - wrap this all up in a try/catch. If the nearby fonts are
// corrupted, then we don't want to throw out of this top half of this
// method. We still want to fall back to a font that's reasonable, below.
try
{
face = _FindFontFace(localeName);
if (!face)
{
// If we missed, try looking a little more by trimming the last word off the requested family name a few times.
// Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and
// an unexpected error dialog. We theoretically could detect the weight words and convert them, but this
// is the quick fix for the majority scenario.
// The long/full fix is backlogged to GH#9744
// Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over
// this resolution.
while (!face && !_familyName.empty())
{
const auto lastSpace = _familyName.find_last_of(UNICODE_SPACE);
// value is unsigned and npos will be greater than size.
// if we didn't find anything to trim, leave.
if (lastSpace >= _familyName.size())
{
break;
}
// trim string down to just before the found space
// (space found at 6... trim from 0 for 6 length will give us 0-5 as the new string)
_familyName = _familyName.substr(0, lastSpace);
// Try to find it with the shortened family name
face = _FindFontFace(localeName);
}
}
}
CATCH_LOG();
if constexpr (Feature_NearbyFontLoading::IsEnabled())
{
if (!face)
{
_fontCollection = FontCache::GetCached();
face = _FindFontFace(localeName);
}
}
// Alright, if our quick shot at trimming didn't work either...
// move onto looking up a font from our hard-coded list of fonts
// that should really always be available.
if (!face)
{
for (const auto fallbackFace : FALLBACK_FONT_FACES)
{
_familyName = fallbackFace;
face = _FindFontFace(localeName);
if (face)
{
_didFallback = true;
break;
}
}
}
THROW_HR_IF_NULL(E_FAIL, face);
return face;
}
// Routine Description:
// - Locates a suitable font face from the given information
// Arguments:
// - dwriteFactory - The DWrite factory to use
// - localeName - Locale to search for appropriate fonts
// Return Value:
// - Smart pointer holding interface reference for queryable font data.
#pragma warning(suppress : 26429) // C26429: Symbol 'fontCollection' is never tested for nullness, it can be marked as not_null (f.23).
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontInfo::_FindFontFace(std::wstring& localeName)
{
Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
UINT32 familyIndex;
BOOL familyExists;
THROW_IF_FAILED(_fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists));
if (familyExists)
{
Microsoft::WRL::ComPtr<IDWriteFontFamily> fontFamily;
THROW_IF_FAILED(_fontCollection->GetFontFamily(familyIndex, &fontFamily));
Microsoft::WRL::ComPtr<IDWriteFont> font;
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(GetWeight(), GetStretch(), GetStyle(), &font));
Microsoft::WRL::ComPtr<IDWriteFontFace> fontFace0;
THROW_IF_FAILED(font->CreateFontFace(&fontFace0));
THROW_IF_FAILED(fontFace0.As(&fontFace));
// Retrieve metrics in case the font we created was different than what was requested.
_weight = font->GetWeight();
_stretch = font->GetStretch();
_style = font->GetStyle();
// Dig the family name out at the end to return it.
_familyName = _GetFontFamilyName(fontFamily.Get(), localeName);
}
return fontFace;
}
// Routine Description:
// - Retrieves the font family name out of the given object in the given locale.
// - If we can't find a valid name for the given locale, we'll fallback and report it back.
// Arguments:
// - fontFamily - DirectWrite font family object
// - localeName - The locale in which the name should be retrieved.
// - If fallback occurred, this is updated to what we retrieved instead.
// Return Value:
// - Localized string name of the font family
[[nodiscard]] std::wstring DxFontInfo::_GetFontFamilyName(const gsl::not_null<IDWriteFontFamily*> fontFamily,
std::wstring& localeName)
{
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> familyNames;
THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames));
// First we have to find the right family name for the locale. We're going to bias toward what the caller
// requested, but fallback if we need to and reply with the locale we ended up choosing.
UINT32 index = 0;
BOOL exists = false;
// This returns S_OK whether or not it finds a locale name. Check exists field instead.
// If it returns an error, it's a real problem, not an absence of this locale name.
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
// If we tried and it still doesn't exist, try with the fallback locale.
if (!exists)
{
localeName = L"en-us";
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
}
// If it still doesn't exist, we're going to try index 0.
if (!exists)
{
index = 0;
// Get the locale name out so at least the caller knows what locale this name goes with.
UINT32 length = 0;
THROW_IF_FAILED(familyNames->GetLocaleNameLength(index, &length));
localeName.resize(length);
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalenamelength
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalename
// GetLocaleNameLength does not include space for null terminator, but GetLocaleName needs it so add one.
THROW_IF_FAILED(familyNames->GetLocaleName(index, localeName.data(), length + 1));
}
// OK, now that we've decided which family name and the locale that it's in... let's go get it.
UINT32 length = 0;
THROW_IF_FAILED(familyNames->GetStringLength(index, &length));
// Make our output buffer and resize it so it is allocated.
std::wstring retVal;
retVal.resize(length);
// FINALLY, go fetch the string name.
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstringlength
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstring
// Once again, GetStringLength is without the null, but GetString needs the null. So add one.
THROW_IF_FAILED(familyNames->GetString(index, retVal.data(), length + 1));
// and return it.
return retVal;
}

View File

@@ -1,72 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <dwrite.h>
#include <dwrite_1.h>
#include <dwrite_2.h>
#include <dwrite_3.h>
namespace Microsoft::Console::Render
{
class DxFontInfo
{
public:
DxFontInfo(IDWriteFactory1* dwriteFactory);
DxFontInfo(
IDWriteFactory1* dwriteFactory,
std::wstring_view familyName,
DWRITE_FONT_WEIGHT weight,
DWRITE_FONT_STYLE style,
DWRITE_FONT_STRETCH stretch);
bool operator==(const DxFontInfo& other) const noexcept;
std::wstring_view GetFamilyName() const noexcept;
void SetFamilyName(const std::wstring_view familyName);
DWRITE_FONT_WEIGHT GetWeight() const noexcept;
void SetWeight(const DWRITE_FONT_WEIGHT weight) noexcept;
DWRITE_FONT_STYLE GetStyle() const noexcept;
void SetStyle(const DWRITE_FONT_STYLE style) noexcept;
DWRITE_FONT_STRETCH GetStretch() const noexcept;
void SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept;
bool GetFallback() const noexcept;
IDWriteFontCollection* GetFontCollection() const noexcept;
void SetFromEngine(const std::wstring_view familyName,
const DWRITE_FONT_WEIGHT weight,
const DWRITE_FONT_STYLE style,
const DWRITE_FONT_STRETCH stretch);
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> ResolveFontFaceWithFallback(std::wstring& localeName);
private:
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _FindFontFace(std::wstring& localeName);
[[nodiscard]] std::wstring _GetFontFamilyName(const gsl::not_null<IDWriteFontFamily*> fontFamily,
std::wstring& localeName);
// The font name we should be looking for
std::wstring _familyName;
// The weight (bold, light, etc.)
DWRITE_FONT_WEIGHT _weight;
// Normal, italic, etc.
DWRITE_FONT_STYLE _style;
// The stretch of the font is the spacing between each letter
DWRITE_FONT_STRETCH _stretch;
wil::com_ptr<IDWriteFontCollection> _fontCollection;
// Indicates whether we couldn't match the user request and had to choose from a hard-coded default list.
bool _didFallback;
};
}

View File

@@ -1,923 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "DxFontRenderData.h"
#include <VersionHelpers.h>
static constexpr float POINTS_PER_INCH = 72.0f;
static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us";
static constexpr size_t TAG_LENGTH = 4;
using namespace Microsoft::Console::Render;
DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) :
_dwriteFactory(std::move(dwriteFactory)),
_defaultFontInfo{ _dwriteFactory.Get() },
_lineSpacing{},
_lineMetrics{},
_fontSize{}
{
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> DxFontRenderData::Analyzer()
{
if (!_dwriteTextAnalyzer)
{
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer> analyzer;
THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer));
}
return _dwriteTextAnalyzer;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> DxFontRenderData::SystemFontFallback()
{
if (!_systemFontFallback)
{
::Microsoft::WRL::ComPtr<IDWriteFactory2> factory2;
THROW_IF_FAILED(_dwriteFactory.As(&factory2));
factory2->GetSystemFontFallback(&_systemFontFallback);
}
return _systemFontFallback;
}
[[nodiscard]] std::wstring DxFontRenderData::UserLocaleName()
{
if (_userLocaleName.empty())
{
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> localeName;
const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow<int>(localeName.size()));
if (returnCode)
{
_userLocaleName = { localeName.data() };
}
else
{
_userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
}
}
return _userLocaleName;
}
[[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept
{
return _glyphCell;
}
[[nodiscard]] DxFontRenderData::LineMetrics DxFontRenderData::GetLineMetrics() noexcept
{
return _lineMetrics;
}
[[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() noexcept
{
return _defaultFontInfo.GetWeight();
}
[[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept
{
return _defaultFontInfo.GetStyle();
}
[[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept
{
return _defaultFontInfo.GetStretch();
}
[[nodiscard]] const std::vector<DWRITE_FONT_FEATURE>& DxFontRenderData::DefaultFontFeatures() const noexcept
{
return _featureVector;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::DefaultTextFormat()
{
return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch());
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::DefaultFontFace()
{
return FontFaceWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch());
}
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DxFontRenderData::DefaultBoxDrawingEffect()
{
if (!_boxDrawingEffect)
{
// Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already.
THROW_IF_FAILED(s_CalculateBoxEffect(DefaultTextFormat().Get(), _glyphCell.width, DefaultFontFace().Get(), 1.0f, &_boxDrawingEffect));
}
return _boxDrawingEffect;
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight,
DWRITE_FONT_STYLE style,
DWRITE_FONT_STRETCH stretch)
{
const auto textFormatIt = _textFormatMap.find(_ToMapKey(weight, style, stretch));
if (textFormatIt == _textFormatMap.end())
{
auto fontInfo = _defaultFontInfo;
fontInfo.SetWeight(weight);
fontInfo.SetStyle(style);
fontInfo.SetStretch(stretch);
// Create the font with the fractional pixel height size.
// It should have an integer pixel width by our math.
// Then below, apply the line spacing to the format to position the floating point pixel height characters
// into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out.
auto localeName = UserLocaleName();
Microsoft::WRL::ComPtr<IDWriteTextFormat> textFormat;
THROW_IF_FAILED(_BuildTextFormat(fontInfo, localeName).As(&textFormat));
THROW_IF_FAILED(textFormat->SetLineSpacing(_lineSpacing.method, _lineSpacing.height, _lineSpacing.baseline));
THROW_IF_FAILED(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR));
THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
_textFormatMap.emplace(_ToMapKey(weight, style, stretch), textFormat);
return textFormat;
}
else
{
return textFormatIt->second;
}
}
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight,
DWRITE_FONT_STYLE style,
DWRITE_FONT_STRETCH stretch)
{
const auto fontFaceIt = _fontFaceMap.find(_ToMapKey(weight, style, stretch));
if (fontFaceIt == _fontFaceMap.end())
{
auto fontInfo = _defaultFontInfo;
fontInfo.SetWeight(weight);
fontInfo.SetStyle(style);
fontInfo.SetStretch(stretch);
auto fontLocaleName = UserLocaleName();
auto fontFace = fontInfo.ResolveFontFaceWithFallback(fontLocaleName);
_fontFaceMap.emplace(_ToMapKey(weight, style, stretch), fontFace);
return fontFace;
}
else
{
return fontFaceIt->second;
}
}
// Routine Description:
// - Updates the font used for drawing
// Arguments:
// - desired - Information specifying the font that is requested
// - actual - Filled with the nearest font actually chosen for drawing
// - dpi - The DPI of the screen
// Return Value:
// - S_OK or relevant DirectX error
[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept
{
try
{
_userLocaleName.clear();
_textFormatMap.clear();
_fontFaceMap.clear();
_boxDrawingEffect.Reset();
// Initialize the default font info and build everything from here.
_defaultFontInfo = DxFontInfo(
_dwriteFactory.Get(),
desired.GetFaceName(),
static_cast<DWRITE_FONT_WEIGHT>(desired.GetWeight()),
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL);
_SetFeatures(features);
_SetAxes(axes);
_BuildFontRenderData(desired, actual, dpi);
}
CATCH_RETURN();
return S_OK;
}
// Routine Description:
// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible.
// Arguments:
// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font.
// - widthPixels - The pixel width of the available cell.
// - face - The font face that is currently being used, may differ from the base font from the layout.
// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling.
// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required.
// Return Value:
// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors.
[[nodiscard]] HRESULT STDMETHODCALLTYPE DxFontRenderData::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept
try
{
// Check for bad in parameters.
RETURN_HR_IF(E_INVALIDARG, !format);
RETURN_HR_IF(E_INVALIDARG, !face);
// Check the out parameter and fill it up with null.
RETURN_HR_IF(E_INVALIDARG, !effect);
*effect = nullptr;
// The format is based around the main font that was specified by the user.
// We need to know its size as well as the final spacing that was calculated around
// it when it was first selected to get an idea of how large the bounding box is.
const auto fontSize = format->GetFontSize();
DWRITE_LINE_SPACING_METHOD spacingMethod;
float lineSpacing; // total height of the cells
float baseline; // vertical position counted down from the top where the characters "sit"
RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline));
const auto ascentPixels = baseline;
const auto descentPixels = lineSpacing - baseline;
// We need this for the designUnitsPerEm which will be required to move back and forth between
// Design Units and Pixels. I'll elaborate below.
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
// If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different
// than the font size used for the original format (IDWriteTextFormat).
const auto scaledFontSize = fontScale * fontSize;
// This is Unicode FULL BLOCK U+2588.
// We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis
// in knowing exactly where to touch every single edge.
// We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe
// inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent.
const UINT32 blockCodepoint = L'\x2588';
// Get the index of the block out of the font.
UINT16 glyphIndex;
RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex));
// If it was 0, it wasn't found in the font. We're going to try again with
// Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching
// all the edges of the possible rectangle, much like a full block should.
if (glyphIndex == 0)
{
const UINT32 alternateCp = L'\x253C';
RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex));
}
// If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions.
// So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't
// stop the rendering pipeline.
RETURN_HR_IF(S_FALSE, glyphIndex == 0);
// Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing
// glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet.
DWRITE_GLYPH_METRICS boxMetrics = { 0 };
RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics));
// NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic
// way of describing proportions.
// Converting back and forth between real pixels and design units is possible using
// any font's specific fontSize and the designUnitsPerEm FONT_METRIC value.
//
// Here's what to know about the boxMetrics:
//
//
//
// topLeft --> +--------------------------------+ ---
// | ^ | |
// | | topSide | |
// | | Bearing | |
// | v | |
// | +-----------------+ | |
// | | | | |
// | | | | | a
// | | | | | d
// | | | | | v
// +<---->+ | | | a
// | | | | | n
// | left | | | | c
// | Side | | | | e
// | Bea- | | | | H
// | ring | | right | | e
// vertical | | | Side | | i
// OriginY --> x | | Bea- | | g
// | | | ring | | h
// | | | | | t
// | | +<----->+ |
// | +-----------------+ | |
// | ^ | |
// | bottomSide | | |
// | Bearing | | |
// | v | |
// +--------------------------------+ ---
//
//
// | |
// +--------------------------------+
// | advanceWidth |
//
//
// NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box
// as defined by the advanceHeight/width.
// See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics
// The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens.
const auto defaultBoxVerticalScaleFactor = 1.0f;
auto boxVerticalScaleFactor = defaultBoxVerticalScaleFactor;
const auto defaultBoxVerticalTranslation = 0.0f;
auto boxVerticalTranslation = defaultBoxVerticalTranslation;
{
// First, find the dimensions of the glyph representing our fully filled box.
// Ascent is how far up from the baseline we'll draw.
// verticalOriginY is the measure from the topLeft corner of the bounding box down to where
// the glyph's version of the baseline is.
// topSideBearing is how much "gap space" is left between that topLeft and where the glyph
// starts drawing. Subtract the gap space to find how far is drawn upward from baseline.
const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing;
// Descent is how far down from the baseline we'll draw.
// advanceHeight is the total height of the drawn bounding box.
// verticalOriginY is how much was given to the ascent, so subtract that out.
// What remains is then the descent value. Remove the
// bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline.
const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing;
// The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below.
const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits;
// Second, find the dimensions of the cell we're going to attempt to fit within.
// We know about the exact ascent/descent units in pixels as calculated when we chose a font and
// adjusted the ascent/descent for a nice perfect baseline and integer total height.
// All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above.
// Use the formula: Pixels * Design Units Per Em / Font Size = Design Units
const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits;
// OK, now do a few checks. If the drawn box touches the top and bottom of the cell
// and the box is overall tall enough, then we'll not bother adjusting.
// We will presume the font author has set things as they wish them to be.
const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits;
const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits;
const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits;
// If not...
if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell))
{
// Find a scaling factor that will make the total height drawn of this box
// perfectly fit the same number of design units as the cell.
// Since scale factor is a multiplier, it doesn't matter that this is design units.
// The fraction between the two heights in pixels should be exactly the same
// (which is what will matter when we go to actually render it... the pixels that is.)
// Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale.
boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f);
// The box as scaled might be hanging over the top or bottom of the cell (or both).
// We find out the amount of overhang/underhang on both the top and the bottom.
const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits;
const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits;
// This took a bit of time and effort and it's difficult to put into words, but here goes.
// We want the average of the two magnitudes to find out how much to "take" from one and "give"
// to the other such that both are equal. We presume the glyphs are designed to be drawn
// centered in their box vertically to look good.
// The ordering around subtraction is required to ensure that the direction is correct with a negative
// translation moving up (taking excess descent and adding to ascent) and positive is the opposite.
const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2;
// The translation is just a raw movement of pixels up or down. Since we were working in Design Units,
// we need to run the opposite algorithm shown above to go from Design Units to Pixels.
boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// The horizontal adjustments follow the exact same logic as the vertical ones.
const auto defaultBoxHorizontalScaleFactor = 1.0f;
auto boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor;
const auto defaultBoxHorizontalTranslation = 0.0f;
auto boxHorizontalTranslation = defaultBoxHorizontalTranslation;
{
// This is the only difference. We don't have a horizontalOriginX from the metrics.
// However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says
// the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin.
// So we'll use that as the "center" and apply it the role that verticalOriginY had above.
const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2;
const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing;
const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits;
const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits;
const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellLeftDesignUnits = cellWidthDesignUnits / 2;
const auto cellRightDesignUnits = cellLeftDesignUnits;
const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits;
const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits;
const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits;
if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell))
{
boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f);
const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits;
const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits;
const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2;
boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// If we set anything, make a drawing effect. Otherwise, there isn't one.
if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor ||
defaultBoxVerticalTranslation != boxVerticalTranslation ||
defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor ||
defaultBoxHorizontalTranslation != boxHorizontalTranslation)
{
// OK, make the object that will represent our effect, stuff the metrics into it, and return it.
RETURN_IF_FAILED(WRL::MakeAndInitialize<BoxDrawingEffect>(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation));
}
return S_OK;
}
CATCH_RETURN()
// Routine Description:
// - Returns whether the user set or updated any of the font features to be applied
bool DxFontRenderData::DidUserSetFeatures() const noexcept
{
return _didUserSetFeatures;
}
// Routine Description:
// - Returns whether the user set or updated any of the font axes to be applied
bool DxFontRenderData::DidUserSetAxes() const noexcept
{
return _didUserSetAxes;
}
// Routine Description:
// - Function called to inform us whether to use the user set weight
// in the font axes
// - Called by CustomTextLayout, when the text attribute is intense we should
// ignore the user set weight, otherwise setting the bold font axis
// breaks the bold font attribute
// Arguments:
// - inhibitUserWeight: boolean that tells us if we should use the user set weight
// in the font axes
void DxFontRenderData::InhibitUserWeight(bool inhibitUserWeight) noexcept
{
_inhibitUserWeight = inhibitUserWeight;
}
// Routine Description:
// - Returns whether the set italic in the font axes
// Return Value:
// - True if the user set the italic axis to 1,
// false if the italic axis is not present or the italic axis is set to 0
bool DxFontRenderData::DidUserSetItalic() const noexcept
{
return _didUserSetItalic;
}
// Routine Description:
// - Updates our internal map of font features with the given features
// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the feature changes
// to take place
// Arguments:
// - features - the features to update our map with
void DxFontRenderData::_SetFeatures(const std::unordered_map<std::wstring_view, uint32_t>& features)
{
// Populate the feature map with the standard list first
std::unordered_map<DWRITE_FONT_FEATURE_TAG, uint32_t> featureMap{
{ DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'), 1 }, // Contextual Alternates
{ DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'), 1 }, // Standard Ligatures
{ DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'), 1 }, // Contextual Ligatures
{ DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), 1 } // Kerning
};
// Update our feature map with the provided features
if (!features.empty())
{
for (const auto& [tag, param] : features)
{
if (tag.length() == TAG_LENGTH)
{
featureMap.insert_or_assign(DWRITE_MAKE_FONT_FEATURE_TAG(til::at(tag, 0), til::at(tag, 1), til::at(tag, 2), til::at(tag, 3)), param);
}
}
_didUserSetFeatures = true;
}
else
{
_didUserSetFeatures = false;
}
// Convert the data to DWRITE_FONT_FEATURE and store it in a vector for CustomTextLayout
_featureVector.clear();
for (const auto [tag, param] : featureMap)
{
_featureVector.push_back(DWRITE_FONT_FEATURE{ tag, param });
}
}
// Routine Description:
// - Updates our internal map of font axes with the given axes
// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the axes changes
// to take place
// Arguments:
// - axes - the axes to update our map with
void DxFontRenderData::_SetAxes(const std::unordered_map<std::wstring_view, float>& axes)
{
// Clear out the old vector and booleans in case this is a hot reload
_axesVector = std::vector<DWRITE_FONT_AXIS_VALUE>{};
_didUserSetAxes = false;
_didUserSetItalic = false;
// Update our axis map with the provided axes
if (!axes.empty())
{
// Store the weight aside: we will be creating a span of all the axes in the vector except the weight,
// and then we will add the weight to the vector
// We are doing this so that when the text attribute is intense, we can apply all the axes except the weight
std::optional<DWRITE_FONT_AXIS_VALUE> weightAxis;
// Since we are calling an 'emplace_back' after creating the span,
// there is a chance a reallocation happens (if the vector needs to grow), which would make the span point to
// deallocated memory. To avoid this, make sure to reserve enough memory in the vector.
_axesVector.reserve(axes.size());
#pragma warning(suppress : 26445) // the analyzer doesn't like reference to string_view
for (const auto& [axis, value] : axes)
{
if (axis.length() == TAG_LENGTH)
{
const auto dwriteFontAxis = DWRITE_FONT_AXIS_VALUE{ DWRITE_MAKE_FONT_AXIS_TAG(til::at(axis, 0), til::at(axis, 1), til::at(axis, 2), til::at(axis, 3)), value };
if (dwriteFontAxis.axisTag != DWRITE_FONT_AXIS_TAG_WEIGHT)
{
_axesVector.emplace_back(dwriteFontAxis);
}
else
{
weightAxis = dwriteFontAxis;
}
_didUserSetItalic |= dwriteFontAxis.axisTag == DWRITE_FONT_AXIS_TAG_ITALIC && value == 1;
}
}
// Make the span, which has all the axes except the weight
_axesVectorWithoutWeight = std::span{ _axesVector };
// Add the weight axis to the vector if needed
if (weightAxis)
{
_axesVector.emplace_back(weightAxis.value());
}
_didUserSetAxes = true;
}
}
// Method Description:
// - Converts a DWRITE_FONT_STRETCH enum into the corresponding float value to
// create a DWRITE_FONT_AXIS_VALUE with
// Arguments:
// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value
// Return value:
// - The float value corresponding to the passed in fontStretch
float DxFontRenderData::_FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept
{
// 10 elements from DWRITE_FONT_STRETCH_UNDEFINED (0) to DWRITE_FONT_STRETCH_ULTRA_EXPANDED (9)
static constexpr auto fontStretchEnumToVal = std::array{ 100.0f, 50.0f, 62.5f, 75.0f, 87.5f, 100.0f, 112.5f, 125.0f, 150.0f, 200.0f };
if (gsl::narrow_cast<size_t>(fontStretch) > fontStretchEnumToVal.size())
{
fontStretch = DWRITE_FONT_STRETCH_NORMAL;
}
return til::at(fontStretchEnumToVal, fontStretch);
}
// Method Description:
// - Converts a DWRITE_FONT_STYLE enum into the corresponding float value to
// create a DWRITE_FONT_AXIS_VALUE with
// Arguments:
// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value
// Return value:
// - The float value corresponding to the passed in fontStyle
float DxFontRenderData::_FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept
{
// DWRITE_FONT_STYLE_NORMAL (0), DWRITE_FONT_STYLE_OBLIQUE (1), DWRITE_FONT_STYLE_ITALIC (2)
static constexpr auto fontStyleEnumToVal = std::array{ 0.0f, -20.0f, -12.0f };
// Both DWRITE_FONT_STYLE_OBLIQUE and DWRITE_FONT_STYLE_ITALIC default to having slant.
// Though an italic font technically need not have slant (there exist upright ones), the
// vast majority of italic fonts are also slanted. Ideally the slant comes from the
// 'slnt' value in the STAT or fvar table, or the post table italic angle.
if (gsl::narrow_cast<size_t>(fontStyle) > fontStyleEnumToVal.size())
{
fontStyle = DWRITE_FONT_STYLE_NORMAL;
}
return til::at(fontStyleEnumToVal, fontStyle);
}
// Method Description:
// - Fill any missing axis values that might be known but were unspecified, such as omitting
// the 'wght' axis tag but specifying the old DWRITE_FONT_WEIGHT enum
// - This function will only be called with a valid IDWriteTextFormat3
// (on platforms where IDWriteTextFormat3 is supported)
// Arguments:
// - fontWeight: the old DWRITE_FONT_WEIGHT enum to be converted into an axis value
// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value
// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value
// - fontSize: the number to convert into an axis value
// - format: the IDWriteTextFormat3 to get the defined axes from
// Return value:
// - The fully formed axes vector
#pragma warning(suppress : 26429) // the analyzer doesn't detect that our FAIL_FAST_IF_NULL macro \
// checks format for nullness
std::vector<DWRITE_FONT_AXIS_VALUE> DxFontRenderData::GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight,
const DWRITE_FONT_STRETCH fontStretch,
const DWRITE_FONT_STYLE fontStyle,
IDWriteTextFormat3* format)
{
FAIL_FAST_IF_NULL(format);
const auto axesCount = format->GetFontAxisValueCount();
std::vector<DWRITE_FONT_AXIS_VALUE> axesVector;
axesVector.resize(axesCount);
format->GetFontAxisValues(axesVector.data(), axesCount);
auto axisTagPresence = AxisTagPresence::None;
for (const auto& fontAxisValue : axesVector)
{
switch (fontAxisValue.axisTag)
{
case DWRITE_FONT_AXIS_TAG_WEIGHT:
WI_SetFlag(axisTagPresence, AxisTagPresence::Weight);
break;
case DWRITE_FONT_AXIS_TAG_WIDTH:
WI_SetFlag(axisTagPresence, AxisTagPresence::Width);
break;
case DWRITE_FONT_AXIS_TAG_ITALIC:
WI_SetFlag(axisTagPresence, AxisTagPresence::Italic);
break;
case DWRITE_FONT_AXIS_TAG_SLANT:
WI_SetFlag(axisTagPresence, AxisTagPresence::Slant);
break;
}
}
if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Weight))
{
axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, gsl::narrow<float>(fontWeight) });
}
if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Width))
{
axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WIDTH, _FontStretchToWidthAxisValue(fontStretch) });
}
if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Italic))
{
axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, (fontStyle == DWRITE_FONT_STYLE_ITALIC ? 1.0f : 0.0f) });
}
if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Slant))
{
axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, _FontStyleToSlantFixedAxisValue(fontStyle) });
}
return axesVector;
}
// Routine Description:
// - Build the needed data for rendering according to the font used
// Arguments:
// - desired - Information specifying the font that is requested
// - actual - Filled with the nearest font actually chosen for drawing
// - dpi - The DPI of the screen
// Return Value:
// - None
void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi)
{
const auto dpiF = static_cast<float>(dpi);
auto fontLocaleName = UserLocaleName();
// This is the first attempt to resolve font face after `UpdateFont`.
// Note that the following line may cause property changes _inside_ `_defaultFontInfo` because the desired font may not exist.
// See the implementation of `ResolveFontFaceWithFallback` for details.
const auto face = _defaultFontInfo.ResolveFontFaceWithFallback(fontLocaleName);
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
const UINT32 spaceCodePoint = L'M';
UINT16 spaceGlyphIndex;
THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex));
INT32 advanceInDesignUnits;
THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits));
DWRITE_GLYPH_METRICS spaceMetrics = { 0 };
THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics));
// The math here is actually:
// Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor.
// - DPI = dots per inch
// - PPI = points per inch or "points" as usually seen when choosing a font size
// - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI.
// - The Points to Pixels factor is based on the typography definition of 72 points per inch.
// As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch
// to get a factor of 1 and 1/3.
// This turns into something like:
// - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%)
// - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%)
// - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%)
const auto heightDesired = desired.GetEngineSize().height / POINTS_PER_INCH * dpiF;
// The advance is the number of pixels left-to-right (X dimension) for the given font.
// We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
const auto widthAdvance = static_cast<float>(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;
// Use the real pixel height desired by the "em" factor for the width to get the number of pixels
// we will need per character in width. This will almost certainly result in fractional X-dimension pixels.
const auto widthAdvanceInPx = heightDesired * widthAdvance;
// Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional
// height in pixels of each character. It's easier for us to pad out height and align vertically
// than it is horizontally.
const auto fontSize = roundf(widthAdvanceInPx) / widthAdvance;
_fontSize = fontSize;
// Now figure out the basic properties of the character height which include ascent and descent
// for this specific font size.
const auto ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm;
const auto descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm;
// Get the gap.
const auto gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm;
const auto halfGap = gap / 2;
// We're going to build a line spacing object here to track all of this data in our format.
DWRITE_LINE_SPACING lineSpacing = {};
lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM;
// We need to make sure the baseline falls on a round pixel (not a fractional pixel).
// If the baseline is fractional, the text appears blurry, especially at small scales.
// Since we also need to make sure the bounding box as a whole is round pixels
// (because the entire console system maths in full cell units),
// we're just going to ceiling up the ascent and descent to make a full pixel amount
// and set the baseline to the full round pixel ascent value.
//
// For reference, for the letters "ag":
// ...
// gggggg bottom of previous line
//
// ----------------- <===========================================|
// | topSideBearing | 1/2 lineGap |
// aaaaaa ggggggg <-------------------------|-------------| |
// a g g | | |
// aaaaa ggggg |<-ascent | |
// a a g | | |---- lineHeight
// aaaaa a gggggg <----baseline, verticalOriginY----------|---|
// g g |<-descent | |
// gggggg <-------------------------|-------------| |
// | bottomSideBearing | 1/2 lineGap |
// ----------------- <===========================================|
//
// aaaaaa ggggggg top of next line
// ...
//
// Also note...
// We're going to add half the line gap to the ascent and half the line gap to the descent
// to ensure that the spacing is balanced vertically.
// Generally speaking, the line gap is added to the ascent by DirectWrite itself for
// horizontally drawn text which can place the baseline and glyphs "lower" in the drawing
// box than would be desired for proper alignment of things like line and box characters
// which will try to sit centered in the area and touch perfectly with their neighbors.
const auto fullPixelAscent = ceil(ascent + halfGap);
const auto fullPixelDescent = ceil(descent + halfGap);
const auto defaultHeight = fullPixelAscent + fullPixelDescent;
const auto lineHeight = desired.GetCellHeight().Resolve(defaultHeight, dpiF, heightDesired, widthAdvanceInPx);
const auto baseline = fullPixelAscent + (lineHeight - defaultHeight) / 2.0f;
lineSpacing.height = roundf(lineHeight);
lineSpacing.baseline = roundf(baseline);
// According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
// Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED;
_lineSpacing = lineSpacing;
const auto widthApprox = desired.GetCellWidth().Resolve(widthAdvanceInPx, dpiF, heightDesired, widthAdvanceInPx);
const auto widthExact = roundf(widthApprox);
// The scaled size needs to represent the pixel box that each character will fit within for the purposes
// of hit testing math and other such multiplication/division.
til::size coordSize;
coordSize.width = static_cast<til::CoordType>(widthExact);
coordSize.height = static_cast<til::CoordType>(lineSpacing.height);
// Unscaled is for the purposes of re-communicating this font back to the renderer again later.
// As such, we need to give the same original size parameter back here without padding
// or rounding or scaling manipulation.
const auto unscaled = desired.GetEngineSize();
const auto scaled = coordSize;
actual.SetFromEngine(_defaultFontInfo.GetFamilyName(),
desired.GetFamily(),
DefaultTextFormat()->GetFontWeight(),
false,
scaled,
unscaled);
actual.SetFallback(_defaultFontInfo.GetFallback());
LineMetrics lineMetrics;
// There is no font metric for the grid line width, so we use a small
// multiple of the font size, which typically rounds to a pixel.
lineMetrics.gridlineWidth = std::round(fontSize * 0.025f);
// All other line metrics are in design units, so to get a pixel value,
// we scale by the font size divided by the design-units-per-em.
const auto scale = fontSize / fontMetrics.designUnitsPerEm;
lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale);
lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale);
lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale);
lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale);
// We always want the lines to be visible, so if a stroke width ends up
// at zero after rounding, we need to make it at least 1 pixel.
lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f);
lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f);
lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.0f);
// Offsets are relative to the base line of the font, so we subtract
// from the ascent to get an offset relative to the top of the cell.
lineMetrics.underlineOffset = lineSpacing.baseline - lineMetrics.underlineOffset;
lineMetrics.strikethroughOffset = lineSpacing.baseline - lineMetrics.strikethroughOffset;
// For double underlines we need a second offset, just below the first,
// but with a bit of a gap (about double the grid line width).
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset +
lineMetrics.underlineWidth +
std::round(fontSize * 0.05f);
// However, we don't want the underline to extend past the bottom of the
// cell, so we clamp the offset to fit just inside.
const auto maxUnderlineOffset = lineSpacing.height - lineMetrics.underlineWidth;
lineMetrics.underlineOffset2 = std::min(lineMetrics.underlineOffset2, maxUnderlineOffset);
// But if the resulting gap isn't big enough even to register as a thicker
// line, it's better to place the second line slightly above the first.
if (lineMetrics.underlineOffset2 < lineMetrics.underlineOffset + lineMetrics.gridlineWidth)
{
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth;
}
// We also add half the stroke width to the offsets, since the line
// coordinates designate the center of the line.
lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f;
lineMetrics.underlineOffset2 += lineMetrics.underlineWidth / 2.0f;
lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f;
_lineMetrics = lineMetrics;
_glyphCell = actual.GetSize();
}
Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::_BuildTextFormat(const DxFontInfo& fontInfo, const std::wstring_view localeName)
{
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(),
fontInfo.GetFontCollection(),
fontInfo.GetWeight(),
fontInfo.GetStyle(),
fontInfo.GetStretch(),
_fontSize,
localeName.data(),
&format));
// If the OS supports IDWriteTextFormat3, set the font axes
::Microsoft::WRL::ComPtr<IDWriteTextFormat3> format3;
if (!FAILED(format->QueryInterface(IID_PPV_ARGS(&format3))))
{
if (_inhibitUserWeight && !_axesVectorWithoutWeight.empty())
{
format3->SetFontAxisValues(_axesVectorWithoutWeight.data(), gsl::narrow<uint32_t>(_axesVectorWithoutWeight.size()));
}
else if (!_inhibitUserWeight && !_axesVector.empty())
{
format3->SetFontAxisValues(_axesVector.data(), gsl::narrow<uint32_t>(_axesVector.size()));
}
}
return format;
}

View File

@@ -1,142 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../../renderer/inc/FontInfoDesired.hpp"
#include "DxFontInfo.h"
#include "BoxDrawingEffect.h"
#include <dwrite.h>
#include <dwrite_1.h>
#include <dwrite_2.h>
#include <dwrite_3.h>
#include <wrl.h>
namespace Microsoft::Console::Render
{
enum class AxisTagPresence : BYTE
{
None = 0x00,
Weight = 0x01,
Width = 0x02,
Italic = 0x04,
Slant = 0x08,
};
DEFINE_ENUM_FLAG_OPERATORS(AxisTagPresence);
class DxFontRenderData
{
public:
struct LineMetrics
{
float gridlineWidth;
float underlineOffset;
float underlineOffset2;
float underlineWidth;
float strikethroughOffset;
float strikethroughWidth;
};
DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory);
// DirectWrite text analyzer from the factory
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> Analyzer();
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> SystemFontFallback();
// A locale that can be used on construction of assorted DX objects that want to know one.
[[nodiscard]] std::wstring UserLocaleName();
[[nodiscard]] til::size GlyphCell() noexcept;
[[nodiscard]] LineMetrics GetLineMetrics() noexcept;
// The weight of default font
[[nodiscard]] DWRITE_FONT_WEIGHT DefaultFontWeight() noexcept;
// The style of default font
[[nodiscard]] DWRITE_FONT_STYLE DefaultFontStyle() noexcept;
// The stretch of default font
[[nodiscard]] DWRITE_FONT_STRETCH DefaultFontStretch() noexcept;
// The font features of the default font
[[nodiscard]] const std::vector<DWRITE_FONT_FEATURE>& DefaultFontFeatures() const noexcept;
// The DirectWrite format object representing the size and other text properties to be applied (by default)
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DefaultTextFormat();
// The DirectWrite font face to use while calculating layout (by default)
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DefaultFontFace();
// Box drawing scaling effects that are cached for the base font across layouts
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DefaultBoxDrawingEffect();
// The attributed variants of the format object representing the size and other text properties
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight,
DWRITE_FONT_STYLE style,
DWRITE_FONT_STRETCH stretch);
// The attributed variants of the font face to use while calculating layout
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight,
DWRITE_FONT_STYLE style,
DWRITE_FONT_STRETCH stretch);
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi, const std::unordered_map<std::wstring_view, uint32_t>& features = {}, const std::unordered_map<std::wstring_view, float>& axes = {}) noexcept;
[[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept;
bool DidUserSetFeatures() const noexcept;
bool DidUserSetAxes() const noexcept;
void InhibitUserWeight(bool inhibitUserWeight) noexcept;
bool DidUserSetItalic() const noexcept;
std::vector<DWRITE_FONT_AXIS_VALUE> GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight,
const DWRITE_FONT_STRETCH fontStretch,
const DWRITE_FONT_STYLE fontStyle,
IDWriteTextFormat3* format);
private:
using FontAttributeMapKey = uint32_t;
bool _inhibitUserWeight{ false };
bool _didUserSetItalic{ false };
bool _didUserSetFeatures{ false };
bool _didUserSetAxes{ false };
// The font features to apply to the text
std::vector<DWRITE_FONT_FEATURE> _featureVector;
// The font axes to apply to the text
std::vector<DWRITE_FONT_AXIS_VALUE> _axesVector;
std::span<DWRITE_FONT_AXIS_VALUE> _axesVectorWithoutWeight;
// We use this to identify font variants with different attributes.
static FontAttributeMapKey _ToMapKey(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) noexcept
{
return (weight << 16) | (style << 8) | stretch;
};
void _SetFeatures(const std::unordered_map<std::wstring_view, uint32_t>& features);
void _SetAxes(const std::unordered_map<std::wstring_view, float>& axes);
float _FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept;
float _FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept;
void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi);
Microsoft::WRL::ComPtr<IDWriteTextFormat> _BuildTextFormat(const DxFontInfo& fontInfo, const std::wstring_view localeName);
std::unordered_map<FontAttributeMapKey, ::Microsoft::WRL::ComPtr<IDWriteTextFormat>> _textFormatMap;
std::unordered_map<FontAttributeMapKey, ::Microsoft::WRL::ComPtr<IDWriteFontFace1>> _fontFaceMap;
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
::Microsoft::WRL::ComPtr<IDWriteFontFallback> _systemFontFallback;
::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _dwriteTextAnalyzer;
std::wstring _userLocaleName;
DxFontInfo _defaultFontInfo;
til::size _glyphCell;
DWRITE_LINE_SPACING _lineSpacing;
LineMetrics _lineMetrics;
float _fontSize;
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,326 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../../renderer/inc/RenderEngineBase.hpp"
#include <functional>
#include <dxgi.h>
#include <dxgi1_2.h>
#include <dxgi1_3.h>
#include <d3d11.h>
#include <d2d1.h>
#include <d2d1_1.h>
#include <d2d1helper.h>
#include <DirectXMath.h>
#include <dwrite.h>
#include <dwrite_1.h>
#include <dwrite_2.h>
#include <dwrite_3.h>
#include <wrl.h>
#include <wrl/client.h>
#include "CustomTextLayout.h"
#include "CustomTextRenderer.h"
#include "DxFontRenderData.h"
#include "DxSoftFont.h"
#include "../../types/inc/Viewport.hpp"
#include <TraceLoggingProvider.h>
TRACELOGGING_DECLARE_PROVIDER(g_hDxRenderProvider);
namespace Microsoft::Console::Render
{
class DxEngine final : public RenderEngineBase
{
public:
DxEngine();
~DxEngine();
DxEngine(const DxEngine&) = default;
DxEngine(DxEngine&&) = default;
DxEngine& operator=(const DxEngine&) = default;
DxEngine& operator=(DxEngine&&) = default;
// Used to release device resources so that another instance of
// conhost can render to the screen (i.e. only one DirectX
// application may control the screen at a time.)
[[nodiscard]] HRESULT Enable() noexcept override;
[[nodiscard]] HRESULT Disable() noexcept;
[[nodiscard]] HRESULT SetHwnd(const HWND hwnd) noexcept override;
[[nodiscard]] HRESULT SetWindowSize(const til::size pixels) noexcept override;
void SetCallback(std::function<void(const HANDLE)> pfn) noexcept override;
void SetWarningCallback(std::function<void(const HRESULT)> pfn) noexcept override;
bool GetRetroTerminalEffect() const noexcept override;
void SetRetroTerminalEffect(bool enable) noexcept override;
std::wstring_view GetPixelShaderPath() noexcept override;
void SetPixelShaderPath(std::wstring_view value) noexcept override;
void SetForceFullRepaintRendering(bool enable) noexcept override;
void SetSoftwareRendering(bool enable) noexcept override;
// IRenderEngine Members
[[nodiscard]] HRESULT Invalidate(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
[[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
[[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 ScrollFrame() noexcept override;
[[nodiscard]] HRESULT UpdateSoftFont(const std::span<const uint16_t> bitPattern,
const til::size cellSize,
const size_t centeringHint) noexcept override;
[[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
[[nodiscard]] HRESULT ResetLineTransform() noexcept override;
[[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
const til::CoordType targetRow,
const til::CoordType viewportLeft) noexcept override;
[[nodiscard]] HRESULT PaintBackground() noexcept override;
[[nodiscard]] HRESULT PaintBufferLine(const std::span<const Cluster> clusters,
const til::point coord,
const bool fTrimLeft,
const bool lineWrapped) noexcept override;
[[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override;
[[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
[[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
[[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes,
const RenderSettings& renderSettings,
const gsl::not_null<IRenderData*> pData,
const bool usingSoftFont,
const bool isSettingDefaultBrushes) noexcept override;
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override;
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept override;
[[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override;
[[nodiscard]] HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept override;
[[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const int iDpi) noexcept override;
[[nodiscard]] HRESULT GetDirtyArea(std::span<const til::rect>& area) noexcept override;
[[nodiscard]] HRESULT GetFontSize(_Out_ til::size* pFontSize) noexcept override;
[[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override;
[[nodiscard]] ::Microsoft::Console::Types::Viewport GetViewportInCharacters(const ::Microsoft::Console::Types::Viewport& viewInPixels) const noexcept override;
[[nodiscard]] ::Microsoft::Console::Types::Viewport GetViewportInPixels(const ::Microsoft::Console::Types::Viewport& viewInCharacters) const noexcept override;
float GetScaling() const noexcept override;
void SetSelectionBackground(const COLORREF color, const float alpha = 0.5f) noexcept override;
void SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept override;
void EnableTransparentBackground(const bool isTransparent) noexcept override;
void UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept override;
protected:
[[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring_view newTitle) noexcept override;
[[nodiscard]] HRESULT _PaintTerminalEffects() noexcept;
[[nodiscard]] bool _FullRepaintNeeded() const noexcept;
private:
enum class SwapChainMode
{
ForHwnd,
ForComposition
};
SwapChainMode _chainMode;
HWND _hwndTarget;
til::size _sizeTarget;
int _dpi;
float _scale;
float _prevScale;
std::function<void(const HANDLE)> _pfn;
std::function<void(const HRESULT)> _pfnWarningCallback;
bool _isEnabled;
bool _isPainting;
til::size _displaySizePixels;
D2D1_COLOR_F _defaultForegroundColor;
D2D1_COLOR_F _defaultBackgroundColor;
D2D1_COLOR_F _foregroundColor;
D2D1_COLOR_F _backgroundColor;
D2D1_COLOR_F _selectionBackground;
LineRendition _currentLineRendition;
D2D1::Matrix3x2F _currentLineTransform;
uint16_t _hyperlinkHoveredId;
bool _firstFrame;
std::pmr::unsynchronized_pool_resource _pool;
til::pmr::bitmap _invalidMap;
til::point _invalidScroll;
bool _allInvalid;
bool _presentReady;
std::vector<til::rect> _presentDirty;
RECT _presentScroll;
POINT _presentOffset;
DXGI_PRESENT_PARAMETERS _presentParams;
static std::atomic<size_t> _tracelogCount;
wil::unique_handle _swapChainHandle;
// Device-Independent Resources
::Microsoft::WRL::ComPtr<ID2D1Factory1> _d2dFactory;
::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;
::Microsoft::WRL::ComPtr<CustomTextLayout> _customLayout;
::Microsoft::WRL::ComPtr<CustomTextRenderer> _customRenderer;
::Microsoft::WRL::ComPtr<ID2D1StrokeStyle> _strokeStyle;
::Microsoft::WRL::ComPtr<ID2D1StrokeStyle> _dashStrokeStyle;
std::unique_ptr<DxFontRenderData> _fontRenderData;
DxSoftFont _softFont;
bool _usingSoftFont;
D2D1_STROKE_STYLE_PROPERTIES _strokeStyleProperties;
D2D1_STROKE_STYLE_PROPERTIES _dashStrokeStyleProperties;
// Device-Dependent Resources
bool _recreateDeviceRequested;
bool _haveDeviceResources;
::Microsoft::WRL::ComPtr<ID3D11Device> _d3dDevice;
::Microsoft::WRL::ComPtr<ID3D11DeviceContext> _d3dDeviceContext;
::Microsoft::WRL::ComPtr<ID2D1Device> _d2dDevice;
::Microsoft::WRL::ComPtr<ID2D1DeviceContext> _d2dDeviceContext;
::Microsoft::WRL::ComPtr<ID2D1Bitmap1> _d2dBitmap;
::Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> _d2dBrushForeground;
::Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> _d2dBrushBackground;
::Microsoft::WRL::ComPtr<IDXGIFactory2> _dxgiFactory2;
::Microsoft::WRL::ComPtr<IDXGIFactoryMedia> _dxgiFactoryMedia;
::Microsoft::WRL::ComPtr<IDXGIDevice> _dxgiDevice;
::Microsoft::WRL::ComPtr<IDXGISurface> _dxgiSurface;
DXGI_SWAP_CHAIN_DESC1 _swapChainDesc;
::Microsoft::WRL::ComPtr<IDXGISwapChain1> _dxgiSwapChain;
wil::unique_handle _swapChainFrameLatencyWaitableObject;
std::unique_ptr<DrawingContext> _drawingContext;
// Terminal effects resources.
// Controls if configured terminal effects are enabled
bool _terminalEffectsEnabled;
// Experimental and deprecated retro terminal effect
// Preserved for backwards compatibility
// Implemented in terms of the more generic pixel shader effect
// Has precedence over pixel shader effect
bool _retroTerminalEffect;
// Experimental and pixel shader effect
// Allows user to load a pixel shader from a few presets or from a file path
std::wstring _pixelShaderPath;
bool _pixelShaderLoaded{ false };
std::chrono::steady_clock::time_point _shaderStartTime;
// DX resources needed for terminal effects
::Microsoft::WRL::ComPtr<ID3D11RenderTargetView> _renderTargetView;
::Microsoft::WRL::ComPtr<ID3D11VertexShader> _vertexShader;
::Microsoft::WRL::ComPtr<ID3D11PixelShader> _pixelShader;
::Microsoft::WRL::ComPtr<ID3D11InputLayout> _vertexLayout;
::Microsoft::WRL::ComPtr<ID3D11Buffer> _screenQuadVertexBuffer;
::Microsoft::WRL::ComPtr<ID3D11Buffer> _pixelShaderSettingsBuffer;
::Microsoft::WRL::ComPtr<ID3D11SamplerState> _samplerState;
::Microsoft::WRL::ComPtr<ID3D11Texture2D> _framebufferCapture;
// Preferences and overrides
bool _softwareRendering;
bool _forceFullRepaintRendering;
D2D1_TEXT_ANTIALIAS_MODE _antialiasingMode;
bool _defaultBackgroundIsTransparent;
// DirectX constant buffers need to be a multiple of 16; align to pad the size.
__declspec(align(16)) struct
{
// Note: This can be seen as API endpoint towards user provided pixel shaders.
// Changes here can break existing pixel shaders so be careful with changing datatypes
// and order of parameters
float Time;
float Scale;
DirectX::XMFLOAT2 Resolution;
DirectX::XMFLOAT4 Background;
#pragma warning(suppress : 4324) // structure was padded due to __declspec(align())
} _pixelShaderSettings;
[[nodiscard]] HRESULT _CreateDeviceResources(const bool createSwapChain) noexcept;
[[nodiscard]] HRESULT _CreateSurfaceHandle() noexcept;
bool _HasTerminalEffects() const noexcept;
std::string _LoadPixelShaderFile() const;
HRESULT _SetupTerminalEffects();
void _ComputePixelShaderSettings() noexcept;
[[nodiscard]] HRESULT _PrepareRenderTarget() noexcept;
void _ReleaseDeviceResources() noexcept;
bool _ShouldForceGrayscaleAA() noexcept;
[[nodiscard]] HRESULT _CreateTextLayout(
_In_reads_(StringLength) PCWCHAR String,
_In_ size_t StringLength,
_Out_ IDWriteTextLayout** ppTextLayout) noexcept;
[[nodiscard]] HRESULT _CopyFrontToBack() noexcept;
[[nodiscard]] HRESULT _EnableDisplayAccess(const bool outputEnabled) noexcept;
[[nodiscard]] til::size _GetClientSize() const;
void _InvalidateRectangle(const til::rect& rc);
bool _IsAllInvalid() const noexcept;
[[nodiscard]] D2D1_COLOR_F _ColorFFromColorRef(const COLORREF color) noexcept;
// Routine Description:
// - Helps convert a Direct2D ColorF into a DXGI RGBA
// Arguments:
// - color - Direct2D Color F
// Return Value:
// - DXGI RGBA
[[nodiscard]] constexpr DXGI_RGBA s_RgbaFromColorF(const D2D1_COLOR_F color) noexcept
{
return { color.r, color.g, color.b, color.a };
}
};
}

View File

@@ -1,245 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "DxSoftFont.h"
#include "CustomTextRenderer.h"
#include <d2d1effects.h>
#pragma comment(lib, "dxguid.lib")
using namespace Microsoft::Console::Render;
using Microsoft::WRL::ComPtr;
// The soft font is rendered into a bitmap laid out in a 12x8 grid, which is
// enough space for the 96 characters expected in the font, and which minimizes
// the dimensions for a typical 2:1 cell size. Each position in the grid is
// surrounded by a 2 pixel border which helps avoid bleed across the character
// boundaries when the output is scaled.
constexpr size_t BITMAP_GRID_WIDTH = 12;
constexpr size_t BITMAP_GRID_HEIGHT = 8;
constexpr size_t PADDING = 2;
void DxSoftFont::SetFont(const std::span<const uint16_t> bitPattern,
const til::size sourceSize,
const til::size targetSize,
const size_t centeringHint)
{
Reset();
// If the font is being reset, just free up the memory and return.
if (bitPattern.empty())
{
_bitmapBits.clear();
return;
}
const auto maxGlyphCount = BITMAP_GRID_WIDTH * BITMAP_GRID_HEIGHT;
_glyphCount = std::min<size_t>(bitPattern.size() / sourceSize.height, maxGlyphCount);
_sourceSize = sourceSize;
_targetSize = targetSize;
_centeringHint = centeringHint;
const auto bitmapWidth = BITMAP_GRID_WIDTH * (_sourceSize.width + PADDING * 2);
const auto bitmapHeight = BITMAP_GRID_HEIGHT * (_sourceSize.height + PADDING * 2);
_bitmapBits = std::vector<byte>(bitmapWidth * bitmapHeight);
_bitmapSize = { gsl::narrow_cast<UINT32>(bitmapWidth), gsl::narrow_cast<UINT32>(bitmapHeight) };
const auto bitmapScanline = [=](const auto lineNumber) noexcept {
return _bitmapBits.begin() + lineNumber * bitmapWidth;
};
// The source bitPattern is just a list of the scanlines making up the
// glyphs one after the other, but we want to lay them out in a grid, so
// we need to process each glyph individually.
auto srcPointer = bitPattern.begin();
for (auto glyphNumber = 0u; glyphNumber < _glyphCount; glyphNumber++)
{
// We start by calculating the position in the bitmap where the glyph
// needs to be stored.
const auto xOffset = _xOffsetForGlyph<size_t>(glyphNumber);
const auto yOffset = _yOffsetForGlyph<size_t>(glyphNumber);
auto dstPointer = bitmapScanline(yOffset) + xOffset;
for (auto y = 0; y < sourceSize.height; y++)
{
// Then for each scanline in the source, we need to expand the bits
// into 8-bit values. For every bit that is set we write out an FF
// value, and if not set, we write out 00. In the end, all we care
// about is a single red component for the R8_UNORM bitmap format,
// since we'll later remap that to RGBA with a color matrix.
auto srcBits = *(srcPointer++);
for (auto x = 0; x < sourceSize.width; x++)
{
const auto srcBitIsSet = (srcBits & 0x8000) != 0;
*(dstPointer++) = srcBitIsSet ? 0xFF : 0x00;
srcBits <<= 1;
}
// When glyphs in this bitmap are output, they will typically need
// to scaled, and this can result in some bleed from the surrounding
// pixels. So to keep the borders clean, we pad the areas to the left
// and right by repeating the first and last pixels of each scanline.
std::fill_n(dstPointer, PADDING, til::at(dstPointer, -1));
dstPointer -= sourceSize.width;
std::fill_n(dstPointer - PADDING, PADDING, til::at(dstPointer, 0));
dstPointer += bitmapWidth;
}
}
// In the same way that we padded the left and right of each glyph in the
// code above, we also need to pad the top and bottom. But in this case we
// can simply do a whole row of glyphs from the grid at the same time.
for (auto gridRow = 0u; gridRow < BITMAP_GRID_HEIGHT; gridRow++)
{
const auto rowOffset = _yOffsetForGlyph<size_t>(gridRow);
const auto rowTop = bitmapScanline(rowOffset);
const auto rowBottom = bitmapScanline(rowOffset + _sourceSize.height - 1);
for (auto i = 1; i <= PADDING; i++)
{
std::copy_n(rowTop, bitmapWidth, rowTop - i * bitmapWidth);
std::copy_n(rowBottom, bitmapWidth, rowBottom + i * bitmapWidth);
}
}
}
HRESULT DxSoftFont::SetTargetSize(const til::size targetSize)
{
_targetSize = targetSize;
return _scaleEffect ? _scaleEffect->SetValue(D2D1_SCALE_PROP_SCALE, _scaleForTargetSize()) : S_OK;
}
HRESULT DxSoftFont::SetAntialiasing(const bool antialiased)
{
_interpolation = (antialiased ? ANTIALIASED_INTERPOLATION : ALIASED_INTERPOLATION);
return _scaleEffect ? _scaleEffect->SetValue(D2D1_SCALE_PROP_INTERPOLATION_MODE, _interpolation) : S_OK;
}
HRESULT DxSoftFont::SetColor(const D2D1_COLOR_F& color)
{
// Since our source image is monochrome, we don't care about the
// individual color components. We just multiply the red component
// by the active color value to get the output color. And note that
// the alpha matrix entry is already set to 1 in the constructor,
// so we don't need to keep updating it here.
_colorMatrix.m[0][0] = color.r;
_colorMatrix.m[0][1] = color.g;
_colorMatrix.m[0][2] = color.b;
return _colorEffect ? _colorEffect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, _colorMatrix) : S_OK;
}
HRESULT DxSoftFont::Draw(const DrawingContext& drawingContext,
const std::span<const Cluster> clusters,
const float originX,
const float originY)
{
ComPtr<ID2D1DeviceContext> d2dContext;
RETURN_IF_FAILED(drawingContext.renderTarget->QueryInterface(d2dContext.GetAddressOf()));
// We start by creating a clipping rectangle for the region we're going to
// draw, and this is initially filled with the active background color.
D2D1_RECT_F rect;
rect.top = originY + drawingContext.topClipOffset;
rect.bottom = originY + _targetSize.height - drawingContext.bottomClipOffset;
rect.left = originX;
rect.right = originX + _targetSize.width * clusters.size();
d2dContext->FillRectangle(rect, drawingContext.backgroundBrush);
d2dContext->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_ALIASED);
auto resetClippingRect = wil::scope_exit([&]() noexcept { d2dContext->PopAxisAlignedClip(); });
// The bitmap and associated scaling/coloring effects are created on demand
// so we need make sure they're generated now.
RETURN_IF_FAILED(_createResources(d2dContext.Get()));
// We use the CustomTextRenderer to draw the first pass of the cursor.
RETURN_IF_FAILED(CustomTextRenderer::DrawCursor(d2dContext.Get(), rect, drawingContext, true));
// Then we draw the associated glyph for each entry in the cluster list.
auto targetPoint = D2D1_POINT_2F{ originX, originY };
for (auto& cluster : clusters)
{
// For DRCS, we only care about the character's lower 7 bits, then
// codepoint 0x20 will be the first glyph in the set.
const auto glyphNumber = (cluster.GetTextAsSingle() & 0x7f) - 0x20;
const auto x = _xOffsetForGlyph<float>(glyphNumber);
const auto y = _yOffsetForGlyph<float>(glyphNumber);
const auto sourceRect = D2D1_RECT_F{ x, y, x + _targetSize.width, y + _targetSize.height };
LOG_IF_FAILED(_scaleEffect->SetValue(D2D1_SCALE_PROP_CENTER_POINT, D2D1::Point2F(x, y)));
d2dContext->DrawImage(_colorEffect.Get(), targetPoint, sourceRect);
targetPoint.x += _targetSize.width;
}
// We finish by the drawing the second pass of the cursor.
return CustomTextRenderer::DrawCursor(d2dContext.Get(), rect, drawingContext, false);
}
void DxSoftFont::Reset()
{
_colorEffect.Reset();
_scaleEffect.Reset();
_bitmap.Reset();
}
HRESULT DxSoftFont::_createResources(gsl::not_null<ID2D1DeviceContext*> d2dContext)
{
if (!_bitmap)
{
D2D1_BITMAP_PROPERTIES bitmapProperties{};
bitmapProperties.pixelFormat.format = DXGI_FORMAT_R8_UNORM;
bitmapProperties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_IGNORE;
const auto bitmapPitch = gsl::narrow_cast<UINT32>(_bitmapSize.width);
RETURN_IF_FAILED(d2dContext->CreateBitmap(_bitmapSize, _bitmapBits.data(), bitmapPitch, bitmapProperties, _bitmap.GetAddressOf()));
}
if (!_scaleEffect)
{
RETURN_IF_FAILED(d2dContext->CreateEffect(CLSID_D2D1Scale, _scaleEffect.GetAddressOf()));
RETURN_IF_FAILED(_scaleEffect->SetValue(D2D1_SCALE_PROP_INTERPOLATION_MODE, _interpolation));
RETURN_IF_FAILED(_scaleEffect->SetValue(D2D1_SCALE_PROP_SCALE, _scaleForTargetSize()));
_scaleEffect->SetInput(0, _bitmap.Get());
if (_colorEffect)
{
_colorEffect->SetInputEffect(0, _scaleEffect.Get());
}
}
if (!_colorEffect)
{
RETURN_IF_FAILED(d2dContext->CreateEffect(CLSID_D2D1ColorMatrix, _colorEffect.GetAddressOf()));
RETURN_IF_FAILED(_colorEffect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, _colorMatrix));
_colorEffect->SetInputEffect(0, _scaleEffect.Get());
}
return S_OK;
}
D2D1_VECTOR_2F DxSoftFont::_scaleForTargetSize() const noexcept
{
// If the text in the font is not perfectly centered, the _centeringHint
// gives us the offset needed to correct that misalignment. So to ensure
// the scaling is evenly balanced around the center point of the glyphs,
// we can use that hint to adjust the dimensions of our source and target
// widths when calculating the horizontal scale.
const auto targetCenteringHint = std::lround((float)_centeringHint * _targetSize.width / _sourceSize.width);
const auto xScale = gsl::narrow_cast<float>(_targetSize.width - targetCenteringHint) / (_sourceSize.width - _centeringHint);
const auto yScale = gsl::narrow_cast<float>(_targetSize.height) / _sourceSize.height;
return D2D1::Vector2F(xScale, yScale);
}
template<typename T>
T DxSoftFont::_xOffsetForGlyph(const size_t glyphNumber) const noexcept
{
const auto xOffsetInGrid = glyphNumber / BITMAP_GRID_HEIGHT;
const auto paddedGlyphWidth = _sourceSize.width + PADDING * 2;
return gsl::narrow_cast<T>(xOffsetInGrid * paddedGlyphWidth + PADDING);
}
template<typename T>
T DxSoftFont::_yOffsetForGlyph(const size_t glyphNumber) const noexcept
{
const auto yOffsetInGrid = glyphNumber % BITMAP_GRID_HEIGHT;
const auto paddedGlyphHeight = _sourceSize.height + PADDING * 2;
return gsl::narrow_cast<T>(yOffsetInGrid * paddedGlyphHeight + PADDING);
}

View File

@@ -1,56 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../inc/Cluster.hpp"
#include <vector>
#include <d2d1_1.h>
#include <wrl.h>
#include <wrl/client.h>
namespace Microsoft::Console::Render
{
struct DrawingContext;
class DxSoftFont
{
public:
void SetFont(const std::span<const uint16_t> bitPattern,
const til::size sourceSize,
const til::size targetSize,
const size_t centeringHint);
HRESULT SetTargetSize(const til::size targetSize);
HRESULT SetAntialiasing(const bool antialiased);
HRESULT SetColor(const D2D1_COLOR_F& color);
HRESULT Draw(const DrawingContext& drawingContext,
const std::span<const Cluster> clusters,
const float originX,
const float originY);
void Reset();
private:
static constexpr auto ANTIALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC;
static constexpr auto ALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_NEAREST_NEIGHBOR;
HRESULT _createResources(gsl::not_null<ID2D1DeviceContext*> d2dContext);
D2D1_VECTOR_2F _scaleForTargetSize() const noexcept;
template<typename T>
T _xOffsetForGlyph(const size_t glyphNumber) const noexcept;
template<typename T>
T _yOffsetForGlyph(const size_t glyphNumber) const noexcept;
size_t _glyphCount = 0;
til::size _sourceSize;
til::size _targetSize;
size_t _centeringHint = 0;
D2D1_SCALE_INTERPOLATION_MODE _interpolation = ALIASED_INTERPOLATION;
D2D1_MATRIX_5X4_F _colorMatrix{ ._14 = 1 };
D2D1_SIZE_U _bitmapSize{};
std::vector<byte> _bitmapBits;
::Microsoft::WRL::ComPtr<ID2D1Bitmap> _bitmap;
::Microsoft::WRL::ComPtr<ID2D1Effect> _scaleEffect;
::Microsoft::WRL::ComPtr<ID2D1Effect> _colorEffect;
};
}

View File

@@ -1,20 +0,0 @@
import "oaidl.idl";
import "ocidl.idl";
typedef struct BoxScale
{
float VerticalScale;
float VerticalTranslation;
float HorizontalScale;
float HorizontalTranslation;
} BoxScale;
[
uuid("C164926F-1A4D-470D-BB8A-3D2CC4B035E4"),
object,
local
]
interface IBoxDrawingEffect : IUnknown
{
HRESULT GetScale([out] BoxScale* scale);
};

View File

@@ -1,91 +0,0 @@
#pragma once
#if !TIL_FEATURE_DXENGINESHADERSUPPORT_ENABLED
constexpr std::string_view retroPixelShaderString{ "" };
#else
constexpr std::string_view retroPixelShaderString{ R"(
// The original retro pixel shader
Texture2D shaderTexture;
SamplerState samplerState;
cbuffer PixelShaderSettings {
float Time;
float Scale;
float2 Resolution;
float4 Background;
};
#define SCANLINE_FACTOR 0.5
#define SCALED_SCANLINE_PERIOD Scale
#define SCALED_GAUSSIAN_SIGMA (2.0*Scale)
static const float M_PI = 3.14159265f;
float Gaussian2D(float x, float y, float sigma)
{
return 1/(sigma*sqrt(2*M_PI)) * exp(-0.5*(x*x + y*y)/sigma/sigma);
}
float4 Blur(Texture2D input, float2 tex_coord, float sigma)
{
uint width, height;
shaderTexture.GetDimensions(width, height);
float texelWidth = 1.0f/width;
float texelHeight = 1.0f/height;
float4 color = { 0, 0, 0, 0 };
int sampleCount = 13;
for (int x = 0; x < sampleCount; x++)
{
float2 samplePos = { 0, 0 };
samplePos.x = tex_coord.x + (x - sampleCount/2) * texelWidth;
for (int y = 0; y < sampleCount; y++)
{
samplePos.y = tex_coord.y + (y - sampleCount/2) * texelHeight;
if (samplePos.x <= 0 || samplePos.y <= 0 || samplePos.x >= width || samplePos.y >= height) continue;
color += input.Sample(samplerState, samplePos) * Gaussian2D((x - sampleCount/2), (y - sampleCount/2), sigma);
}
}
return color;
}
float SquareWave(float y)
{
return 1 - (floor(y / SCALED_SCANLINE_PERIOD) % 2) * SCANLINE_FACTOR;
}
float4 Scanline(float4 color, float4 pos)
{
float wave = SquareWave(pos.y);
// TODO:GH#3929 make this configurable.
// Remove the && false to draw scanlines everywhere.
if (length(color.rgb) < 0.2 && false)
{
return color + wave*0.1;
}
else
{
return color * wave;
}
}
float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
Texture2D input = shaderTexture;
// TODO:GH#3930 Make these configurable in some way.
float4 color = input.Sample(samplerState, tex);
color += Blur(input, tex, SCALED_GAUSSIAN_SIGMA)*0.3;
color = Scanline(color, pos);
return color;
}
)" };
#endif

View File

@@ -1,20 +0,0 @@
#pragma once
#if !TIL_FEATURE_DXENGINESHADERSUPPORT_ENABLED
const char screenVertexShaderString[] = "";
#else
const char screenVertexShaderString[] = R"(
struct VS_OUTPUT
{
float4 pos : SV_POSITION;
float2 tex : TEXCOORD;
};
VS_OUTPUT main(float4 pos : POSITION, float2 tex : TEXCOORD)
{
VS_OUTPUT output;
output.pos = pos;
output.tex = tex;
return output;
}
)";
#endif

View File

@@ -1,2 +0,0 @@
DIRS= \
lib \

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{48D21369-3D7B-4431-9967-24E81292CF62}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>dx</RootNamespace>
<ProjectName>RendererDx</ProjectName>
<TargetName>ConRenderDx</TargetName>
<ConfigurationType>StaticLibrary</ConfigurationType>
</PropertyGroup>
<ItemDefinitionGroup>
<Midl>
<OutputDirectory>$(SolutionDir)src\renderer\dx\</OutputDirectory>
</Midl>
</ItemDefinitionGroup>
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.props" />
<ItemGroup>
<ClCompile Include="..\BoxDrawingEffect.cpp" />
<ClCompile Include="..\CustomTextLayout.cpp" />
<ClCompile Include="..\CustomTextRenderer.cpp" />
<ClCompile Include="..\DxFontInfo.cpp" />
<ClCompile Include="..\DxFontRenderData.cpp" />
<ClCompile Include="..\DxRenderer.cpp" />
<ClCompile Include="..\DxSoftFont.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\BoxDrawingEffect.h" />
<ClInclude Include="..\CustomTextLayout.h" />
<ClInclude Include="..\CustomTextRenderer.h" />
<ClInclude Include="..\DxFontInfo.h" />
<ClInclude Include="..\DxFontRenderData.h" />
<ClInclude Include="..\DxRenderer.hpp" />
<ClInclude Include="..\DxSoftFont.h" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\ScreenPixelShader.h" />
<ClInclude Include="..\ScreenVertexShader.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="..\IBoxDrawingEffect.idl" />
</ItemGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
</Project>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\BoxDrawingEffect.cpp" />
<ClCompile Include="..\CustomTextLayout.cpp" />
<ClCompile Include="..\CustomTextRenderer.cpp" />
<ClCompile Include="..\DxFontInfo.cpp" />
<ClCompile Include="..\DxFontRenderData.cpp" />
<ClCompile Include="..\DxRenderer.cpp" />
<ClCompile Include="..\DxSoftFont.cpp" />
<ClCompile Include="..\precomp.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\BoxDrawingEffect.h" />
<ClInclude Include="..\CustomTextLayout.h" />
<ClInclude Include="..\CustomTextRenderer.h" />
<ClInclude Include="..\DxFontInfo.h" />
<ClInclude Include="..\DxFontRenderData.h" />
<ClInclude Include="..\DxRenderer.hpp" />
<ClInclude Include="..\DxSoftFont.h" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\ScreenPixelShader.h" />
<ClInclude Include="..\ScreenVertexShader.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="..\IBoxDrawingEffect.idl" />
</ItemGroup>
</Project>

View File

@@ -1,8 +0,0 @@
!include ..\sources.inc
# -------------------------------------
# Program Information
# -------------------------------------
TARGETNAME = ConRenderDx
TARGETTYPE = LIBRARY

View File

@@ -1,41 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
// This includes support libraries from the CRT, STL, WIL, and GSL
#define BLOCK_TIL // We want to include it later, after DX.
#include "LibraryIncludes.h"
#include <windows.h>
#include <winmeta.h>
#include "../host/conddkrefs.h"
#include <condrv.h>
#include <cmath>
#include <exception>
#include <typeinfo>
#include <dcomp.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include <dxgi1_3.h>
#include <d3d11.h>
#include <d2d1.h>
#include <d2d1_1.h>
#include <d2d1_2.h>
#include <d2d1_3.h>
#include <d2d1helper.h>
#include <dwrite.h>
#include <dwrite_1.h>
#include <dwrite_2.h>
#include <dwrite_3.h>
// Re-include TIL at the bottom to gain DX superpowers.
#include "til.h"
#pragma hdrstop

View File

@@ -1,42 +0,0 @@
!include ..\..\..\project.inc
# -------------------------------------
# Windows Console
# - Console Renderer for DirectX
# -------------------------------------
# This module provides a rendering engine implementation that
# draws to a DirectX surface.
# -------------------------------------
# CRT Configuration
# -------------------------------------
BUILD_FOR_CORESYSTEM = 1
# -------------------------------------
# Sources, Headers, and Libraries
# -------------------------------------
PRECOMPILED_CXX = 1
PRECOMPILED_INCLUDE = ..\precomp.h
INCLUDES = \
$(INCLUDES); \
..; \
..\..\inc; \
..\..\..\inc; \
..\..\..\host; \
$(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \
$(MINWIN_RESTRICTED_PRIV_SDK_INC_PATH_L); \
SOURCES = \
$(SOURCES) \
..\DxRenderer.cpp \
..\DxFontInfo.cpp \
..\DxFontRenderData.cpp \
..\DxSoftFont.cpp \
..\CustomTextRenderer.cpp \
..\CustomTextLayout.cpp \
C_DEFINES=$(C_DEFINES) -D__INSIDE_WINDOWS

View File

@@ -1,100 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
#include "../../inc/consoletaeftemplates.hpp"
#include "../CustomTextLayout.h"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace Microsoft::Console::Render;
class Microsoft::Console::Render::CustomTextLayoutTests
{
TEST_CLASS(CustomTextLayoutTests);
TEST_METHOD(OrderRuns)
{
CustomTextLayout layout;
// Create linked list runs where a --> c --> b
CustomTextLayout::LinkedRun a;
a.nextRunIndex = 2;
a.textStart = 0;
CustomTextLayout::LinkedRun b;
b.nextRunIndex = 0;
b.textStart = 20;
CustomTextLayout::LinkedRun c;
c.nextRunIndex = 1;
c.textStart = 10;
// but insert them into the runs as a, b, c
layout._runs.push_back(a);
layout._runs.push_back(b);
layout._runs.push_back(c);
// Now order them.
layout._OrderRuns();
// Validate that they've been reordered to a, c, b by index so they can be iterated to go in order.
// The text starts should be in order 0, 10, 20.
// The next run indexes should point at each other.
VERIFY_ARE_EQUAL(a.textStart, layout._runs.at(0).textStart);
VERIFY_ARE_EQUAL(1u, layout._runs.at(0).nextRunIndex);
VERIFY_ARE_EQUAL(c.textStart, layout._runs.at(1).textStart);
VERIFY_ARE_EQUAL(2u, layout._runs.at(1).nextRunIndex);
VERIFY_ARE_EQUAL(b.textStart, layout._runs.at(2).textStart);
VERIFY_ARE_EQUAL(0u, layout._runs.at(2).nextRunIndex);
}
TEST_METHOD(SplitCurrentRunIncludingGlyphs)
{
CustomTextLayout layout;
// Put glyph data into the layout as if we've already gone through analysis.
// This data matches the verbose comment from the CustomTextLayout.cpp file
// and is derived from
// https://social.msdn.microsoft.com/Forums/en-US/993365bc-8689-45ff-a675-c5ed0c011788/dwriteglyphrundescriptionclustermap-explained
layout._text = L"fiñe";
layout._glyphIndices.push_back(19);
layout._glyphIndices.push_back(81);
layout._glyphIndices.push_back(23);
layout._glyphIndices.push_back(72);
layout._glyphClusters.push_back(0);
layout._glyphClusters.push_back(0);
layout._glyphClusters.push_back(1);
layout._glyphClusters.push_back(3);
// Set up the layout to have a run that already has glyph data inside of it.
CustomTextLayout::LinkedRun run;
run.textStart = 0;
run.textLength = 4;
run.glyphStart = 0;
run.glyphCount = 4;
layout._runs.push_back(run);
// Now split it in the middle per the comment example
layout._SetCurrentRun(2);
layout._SplitCurrentRun(2);
// And validate that the split state matches what we expected.
VERIFY_ARE_EQUAL(0u, layout._runs.at(0).textStart);
VERIFY_ARE_EQUAL(2u, layout._runs.at(0).textLength);
VERIFY_ARE_EQUAL(0u, layout._runs.at(0).glyphStart);
VERIFY_ARE_EQUAL(1u, layout._runs.at(0).glyphCount);
VERIFY_ARE_EQUAL(2u, layout._runs.at(1).textStart);
VERIFY_ARE_EQUAL(2u, layout._runs.at(1).textLength);
VERIFY_ARE_EQUAL(1u, layout._runs.at(1).glyphStart);
VERIFY_ARE_EQUAL(3u, layout._runs.at(1).glyphCount);
}
};

View File

@@ -1,12 +0,0 @@
//Autogenerated file name + version resource file for Device Guard whitelisting effort
#include <windows.h>
#include <ntverp.h>
#define VER_FILETYPE VFT_UNKNOWN
#define VER_FILESUBTYPE VFT2_UNKNOWN
#define VER_FILEDESCRIPTION_STR ___TARGETNAME
#define VER_INTERNALNAME_STR ___TARGETNAME
#define VER_ORIGINALFILENAME_STR ___TARGETNAME
#include "common.ver"

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{95B136F9-B238-490C-A7C5-5843C1FECAC4}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>DxUnitTests</RootNamespace>
<ProjectName>Dx.Unit.Tests</ProjectName>
<TargetName>Dx.Unit.Tests</TargetName>
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<Import Project="$(SolutionDir)\src\common.nugetversions.props" />
<ItemGroup>
<ClCompile Include="CustomTextLayoutTests.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\types\lib\types.vcxproj">
<Project>{18d09a24-8240-42d6-8cb6-236eee820263}</Project>
</ProjectReference>
<ProjectReference Include="..\..\base\lib\base.vcxproj">
<Project>{af0a096a-8b3a-4949-81ef-7df8f0fee91f}</Project>
</ProjectReference>
<ProjectReference Include="..\lib\dx.vcxproj">
<Project>{48D21369-3D7B-4431-9967-24E81292CF62}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h" />
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.build.tests.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
</Project>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="ProductBuild" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(NTMAKEENV)\UniversalTest\Microsoft.TestInfrastructure.UniversalTest.props" />
</Project>

View File

@@ -1,36 +0,0 @@
!include ..\..\..\project.unittest.inc
# -------------------------------------
# Program Information
# -------------------------------------
TARGETNAME = Microsoft.Console.Renderer.Dx.UnitTests
TARGETTYPE = DYNLINK
DLLDEF =
# -------------------------------------
# Sources, Headers, and Libraries
# -------------------------------------
SOURCES = \
$(SOURCES) \
CustomTextLayoutTests.cpp \
DefaultResource.rc \
INCLUDES = \
.. \
$(INCLUDES) \
TARGETLIBS = \
$(WINCORE_OBJ_PATH)\console\open\src\renderer\dx\lib\$(O)\ConRenderDx.lib \
$(WINCORE_OBJ_PATH)\console\open\src\renderer\base\lib\$(O)\ConRenderBase.lib \
$(WINCORE_OBJ_PATH)\console\open\src\types\lib\$(O)\ConTypes.lib \
$(TARGETLIBS) \
# -------------------------------------
# Localization
# -------------------------------------
# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe.
C_DEFINES = $(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)"""
MUI_VERIFY_NO_LOC_RESOURCE = 1

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "GdiEngine.h"
#include "../../types/inc/Viewport.hpp"
#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 : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
#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::Types;
using namespace Microsoft::Console::Render;
void GdiEngine::WaitUntilCanRender() noexcept
{
}
void GdiEngine::Render(const RenderingPayload& payload)
{
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../inc/IRenderEngine.hpp"
namespace Microsoft::Console::Render
{
class GdiEngine final : public IRenderEngine
{
public:
GdiEngine();
~GdiEngine() override;
void WaitUntilCanRender() noexcept override;
void Render(const RenderingPayload& payload) override;
private:
};
}

View File

@@ -1,215 +0,0 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- GdiRenderer.hpp
Abstract:
- This is the definition of the GDI specific implementation of the renderer.
Author(s):
- Michael Niksa (MiNiksa) 17-Nov-2015
--*/
#pragma once
#include "../inc/RenderEngineBase.hpp"
#include "../inc/FontResource.hpp"
namespace Microsoft::Console::Render
{
class GdiEngine final : public RenderEngineBase
{
public:
GdiEngine();
~GdiEngine() override;
[[nodiscard]] HRESULT SetHwnd(const HWND hwnd) noexcept;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT Invalidate(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
[[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override;
[[nodiscard]] HRESULT StartPaint() noexcept override;
[[nodiscard]] HRESULT EndPaint() noexcept override;
[[nodiscard]] HRESULT Present() noexcept override;
[[nodiscard]] HRESULT ScrollFrame() noexcept override;
[[nodiscard]] HRESULT ResetLineTransform() noexcept override;
[[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
const til::CoordType targetRow,
const til::CoordType viewportLeft) noexcept override;
[[nodiscard]] HRESULT PaintBackground() noexcept override;
[[nodiscard]] HRESULT PaintBufferLine(const std::span<const Cluster> clusters,
const til::point coord,
const bool trimLeft,
const bool lineWrapped) noexcept override;
[[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines,
const COLORREF color,
const size_t cchLine,
const til::point coordTarget) noexcept override;
[[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
[[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
[[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes,
const RenderSettings& renderSettings,
const gsl::not_null<IRenderData*> pData,
const bool usingSoftFont,
const bool isSettingDefaultBrushes) noexcept override;
[[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired,
_Out_ FontInfo& FontInfo) noexcept override;
[[nodiscard]] HRESULT UpdateSoftFont(const std::span<const uint16_t> bitPattern,
const til::size cellSize,
const size_t centeringHint) noexcept override;
[[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override;
[[nodiscard]] HRESULT UpdateViewport(const til::inclusive_rect& srNewViewport) noexcept override;
[[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontDesired,
_Out_ FontInfo& Font,
const int iDpi) noexcept override;
[[nodiscard]] HRESULT GetDirtyArea(std::span<const til::rect>& area) noexcept override;
[[nodiscard]] HRESULT GetFontSize(_Out_ til::size* const pFontSize) noexcept override;
[[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override;
protected:
[[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring_view newTitle) noexcept override;
private:
HWND _hwndTargetWindow;
[[nodiscard]] static HRESULT s_SetWindowLongWHelper(const HWND hWnd,
const int nIndex,
const LONG dwNewLong) noexcept;
static bool FontHasWesternScript(HDC hdc);
bool _fPaintStarted;
til::rect _invalidCharacters;
PAINTSTRUCT _psInvalidData;
HDC _hdcMemoryContext;
bool _isTrueTypeFont;
UINT _fontCodepage;
HFONT _hfont;
HFONT _hfontItalic;
TEXTMETRICW _tmFontMetrics;
FontResource _softFont;
static const size_t s_cPolyTextCache = 80;
POLYTEXTW _pPolyText[s_cPolyTextCache];
size_t _cPolyText;
[[nodiscard]] HRESULT _FlushBufferLines() noexcept;
std::vector<RECT> cursorInvertRects;
XFORM cursorInvertTransform;
struct LineMetrics
{
int gridlineWidth;
int underlineOffset;
int underlineOffset2;
int underlineWidth;
int strikethroughOffset;
int strikethroughWidth;
};
LineMetrics _lineMetrics;
til::size _coordFontLast;
int _iCurrentDpi;
static const int s_iBaseDpi = USER_DEFAULT_SCREEN_DPI;
til::size _szMemorySurface;
HBITMAP _hbitmapMemorySurface;
[[nodiscard]] HRESULT _PrepareMemoryBitmap(const HWND hwnd) noexcept;
til::size _szInvalidScroll;
til::rect _rcInvalid;
bool _fInvalidRectUsed;
COLORREF _lastFg;
COLORREF _lastBg;
enum class FontType : uint8_t
{
Undefined,
Default,
Italic,
Soft
};
FontType _lastFontType;
bool _fontHasWesternScript = false;
XFORM _currentLineTransform;
LineRendition _currentLineRendition;
// Memory pooling to save alloc/free work to the OS for things
// frequently created and dropped.
// It's important the pool is first so it can be given to the others on construction.
std::pmr::unsynchronized_pool_resource _pool;
std::pmr::vector<std::pmr::wstring> _polyStrings;
std::pmr::vector<std::pmr::basic_string<int>> _polyWidths;
[[nodiscard]] HRESULT _InvalidCombine(const til::rect* const prc) noexcept;
[[nodiscard]] HRESULT _InvalidOffset(const til::point* const ppt) noexcept;
[[nodiscard]] HRESULT _InvalidRestrict() noexcept;
[[nodiscard]] HRESULT _InvalidateRect(const til::rect* const prc) noexcept;
[[nodiscard]] HRESULT _PaintBackgroundColor(const RECT* const prc) noexcept;
static const ULONG s_ulMinCursorHeightPercent = 25;
static const ULONG s_ulMaxCursorHeightPercent = 100;
static int s_ScaleByDpi(const int iPx, const int iDpi);
static int s_ShrinkByDpi(const int iPx, const int iDpi);
til::point _GetInvalidRectPoint() const;
til::size _GetInvalidRectSize() const;
til::size _GetRectSize(const RECT* const pRect) const;
void _OrRect(_In_ til::rect* const pRectExisting, const til::rect* const pRectToOr) const;
bool _IsFontTrueType() const;
[[nodiscard]] HRESULT _GetProposedFont(const FontInfoDesired& FontDesired,
_Out_ FontInfo& Font,
const int iDpi,
_Inout_ wil::unique_hfont& hFont,
_Inout_ wil::unique_hfont& hFontItalic) noexcept;
til::size _GetFontSize() const;
bool _IsMinimized() const;
bool _IsWindowValid() const;
#ifdef DBG
// Helper functions to diagnose issues with painting from the in-memory buffer.
// These are only actually effective/on in Debug builds when the flag is set using an attached debugger.
bool _fDebug = false;
void _PaintDebugRect(const RECT* const prc) const;
void _DoDebugBlt(const RECT* const prc) const;
void _DebugBltAll() const;
HWND _debugWindow;
void _CreateDebugWindow();
HDC _debugContext;
#endif
};
constexpr XFORM IDENTITY_XFORM = { 1, 0, 0, 1 };
inline bool operator==(const XFORM& lhs, const XFORM& rhs) noexcept
{
return ::memcmp(&lhs, &rhs, sizeof(XFORM)) == 0;
};
}

View File

@@ -1,200 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "gdirenderer.hpp"
#include "../../types/inc/Viewport.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::Render;
// Routine Description:
// - Notifies us that the system has requested a particular pixel area of the client rectangle should be redrawn. (On WM_PAINT)
// Arguments:
// - prcDirtyClient - Pointer to pixel area (til::rect) of client region the system believes is dirty
// Return Value:
// - HRESULT S_OK, GDI-based error code, or safemath error
HRESULT GdiEngine::InvalidateSystem(const til::rect* const prcDirtyClient) noexcept
{
RETURN_HR(_InvalidCombine(prcDirtyClient));
}
// Routine Description:
// - Notifies us that the console is attempting to scroll the existing screen area
// Arguments:
// - pcoordDelta - Pointer to character dimension (til::point) of the distance the console would like us to move while scrolling.
// Return Value:
// - HRESULT S_OK, GDI-based error code, or safemath error
HRESULT GdiEngine::InvalidateScroll(const til::point* const pcoordDelta) noexcept
{
if (pcoordDelta->x != 0 || pcoordDelta->y != 0)
{
const auto ptDelta = *pcoordDelta * _GetFontSize();
RETURN_IF_FAILED(_InvalidOffset(&ptDelta));
_szInvalidScroll = _szInvalidScroll + ptDelta;
}
return S_OK;
}
// Routine Description:
// - Notifies us that the console has changed the selection region and would like it updated
// Arguments:
// - rectangles - Vector of rectangles to draw, line by line
// Return Value:
// - HRESULT S_OK or GDI-based error code
HRESULT GdiEngine::InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept
{
for (const auto& rect : rectangles)
{
RETURN_IF_FAILED(Invalidate(&rect));
}
return S_OK;
}
// Routine Description:
// - Notifies us that the console has changed the character region specified.
// - NOTE: This typically triggers on cursor or text buffer changes
// Arguments:
// - psrRegion - Character region (til::rect) that has been changed
// Return Value:
// - S_OK, GDI related failure, or safemath failure.
HRESULT GdiEngine::Invalidate(const til::rect* const psrRegion) noexcept
{
const auto rcRegion = psrRegion->scale_up(_GetFontSize());
RETURN_HR(_InvalidateRect(&rcRegion));
}
// Routine Description:
// - Notifies us that the console has changed the position of the cursor.
// Arguments:
// - psrRegion - the region covered by the cursor
// Return Value:
// - S_OK, else an appropriate HRESULT for failing to allocate or write.
HRESULT GdiEngine::InvalidateCursor(const til::rect* const psrRegion) noexcept
{
return this->Invalidate(psrRegion);
}
// Routine Description:
// - Notifies to repaint everything.
// - NOTE: Use sparingly. Only use when something that could affect the entire frame simultaneously occurs.
// Arguments:
// - <none>
// Return Value:
// - S_OK, S_FALSE (if no window yet), GDI related failure, or safemath failure.
HRESULT GdiEngine::InvalidateAll() noexcept
{
// If we don't have a window, don't bother.
if (!_IsWindowValid())
{
return S_FALSE;
}
til::rect rc;
RETURN_HR_IF(E_FAIL, !(GetClientRect(_hwndTargetWindow, rc.as_win32_rect())));
RETURN_HR(InvalidateSystem(&rc));
}
// Method Description:
// - Notifies us that we're about to be torn down. This gives us a last chance
// to force a repaint before the buffer contents are lost. The GDI renderer
// doesn't care if we lose text - we're only painting visible text anyways,
// so we return false.
// Arguments:
// - Receives a bool indicating if we should force the repaint.
// Return Value:
// - S_FALSE - we succeeded, but the result was false.
HRESULT GdiEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept
{
*pForcePaint = false;
return S_FALSE;
}
// Routine Description:
// - Helper to combine the given rectangle into the invalid region to be updated on the next paint
// Arguments:
// - prc - Pixel region (til::rect) that should be repainted on the next frame
// Return Value:
// - S_OK, GDI related failure, or safemath failure.
HRESULT GdiEngine::_InvalidCombine(const til::rect* const prc) noexcept
{
if (!_fInvalidRectUsed)
{
_rcInvalid = *prc;
_fInvalidRectUsed = true;
}
else
{
_OrRect(&_rcInvalid, prc);
}
// Ensure invalid areas remain within bounds of window.
RETURN_IF_FAILED(_InvalidRestrict());
return S_OK;
}
// Routine Description:
// - Helper to adjust the invalid region by the given offset such as when a scroll operation occurs.
// Arguments:
// - ppt - Distances by which we should move the invalid region in response to a scroll
// Return Value:
// - S_OK, GDI related failure, or safemath failure.
HRESULT GdiEngine::_InvalidOffset(const til::point* ppt) noexcept
{
if (_fInvalidRectUsed)
{
til::rect rcInvalidNew;
rcInvalidNew.left = _rcInvalid.left + ppt->x;
rcInvalidNew.right = _rcInvalid.right + ppt->x;
rcInvalidNew.top = _rcInvalid.top + ppt->y;
rcInvalidNew.bottom = _rcInvalid.bottom + ppt->y;
// Add the scrolled invalid rectangle to what was left behind to get the new invalid area.
// This is the equivalent of adding in the "update rectangle" that we would get out of ScrollWindowEx/ScrollDC.
_rcInvalid |= rcInvalidNew;
// Ensure invalid areas remain within bounds of window.
RETURN_IF_FAILED(_InvalidRestrict());
}
return S_OK;
}
// Routine Description:
// - Helper to ensure the invalid region remains within the bounds of the window.
// Arguments:
// - <none>
// Return Value:
// - S_OK, GDI related failure, or safemath failure.
HRESULT GdiEngine::_InvalidRestrict() noexcept
{
// Ensure that the invalid area remains within the bounds of the client area
til::rect rcClient;
// Do restriction only if retrieving the client rect was successful.
RETURN_HR_IF(E_FAIL, !(GetClientRect(_hwndTargetWindow, rcClient.as_win32_rect())));
_rcInvalid.left = rcClient.left;
_rcInvalid.right = rcClient.right;
_rcInvalid.top = std::clamp(_rcInvalid.top, rcClient.top, rcClient.bottom);
_rcInvalid.bottom = std::clamp(_rcInvalid.bottom, rcClient.top, rcClient.bottom);
return S_OK;
}
// Routine Description:
// - Helper to add a pixel rectangle to the invalid area
// Arguments:
// - prc - Pointer to pixel rectangle representing invalid area to add to next paint frame
// Return Value:
// - S_OK, GDI related failure, or safemath failure.
HRESULT GdiEngine::_InvalidateRect(const til::rect* const prc) noexcept
{
RETURN_HR(_InvalidCombine(prc));
}

View File

@@ -11,19 +11,19 @@
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.props" />
<ItemGroup>
<ClCompile Include="..\invalidate.cpp" />
<ClCompile Include="..\math.cpp" />
<ClCompile Include="..\paint.cpp" />
<ClCompile Include="..\state.cpp" />
<ClCompile Include="..\precomp.cpp">
<ClCompile Include="..\GdiEngine.cpp" />
<ClCompile Include="..\pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\gdirenderer.hpp" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\GdiEngine.h" />
<ClInclude Include="..\pch.h" />
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Lib>
<AdditionalDependencies>usp10.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Lib>

View File

@@ -1,42 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\invalidate.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\math.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\paint.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\state.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\precomp.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\GdiEngine.cpp" />
<ClCompile Include="..\pch.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\gdirenderer.hpp">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\precomp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\GdiEngine.h" />
<ClInclude Include="..\pch.h" />
</ItemGroup>
</Project>

View File

@@ -1,152 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "gdirenderer.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Render;
// Routine Description:
// - Gets the size in characters of the current dirty portion of the frame.
// Arguments:
// - area - The character dimensions of the current dirty area of the frame.
// This is an Inclusive rect.
// Return Value:
// - S_OK or math failure
[[nodiscard]] HRESULT GdiEngine::GetDirtyArea(std::span<const til::rect>& area) noexcept
{
_invalidCharacters = til::rect{ _psInvalidData.rcPaint }.scale_down(_GetFontSize());
area = { &_invalidCharacters, 1 };
return S_OK;
}
// Routine Description:
// - Uses the currently selected font to determine how wide the given character will be when rendered.
// - NOTE: Only supports determining half-width/full-width status for CJK-type languages (e.g. is it 1 character wide or 2. a.k.a. is it a rectangle or square.)
// Arguments:
// - glyph - utf16 encoded codepoint to check
// - pResult - receives return value, True if it is full-width (2 wide). False if it is half-width (1 wide).
// Return Value:
// - S_OK
[[nodiscard]] HRESULT GdiEngine::IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept
{
auto isFullWidth = false;
if (glyph.size() == 1)
{
const auto wch = glyph.front();
if (_IsFontTrueType())
{
ABC abc;
if (GetCharABCWidthsW(_hdcMemoryContext, wch, wch, &abc))
{
const int totalWidth = abc.abcA + abc.abcB + abc.abcC;
isFullWidth = totalWidth > _GetFontSize().width;
}
}
else
{
auto cpxWidth = 0;
if (GetCharWidth32W(_hdcMemoryContext, wch, wch, &cpxWidth))
{
isFullWidth = cpxWidth > _GetFontSize().width;
}
}
}
else
{
// can't find a way to make gdi measure the width of utf16 surrogate pairs.
// in the meantime, better to be too wide than too narrow.
isFullWidth = true;
}
*pResult = isFullWidth;
return S_OK;
}
// Routine Description:
// - Scales the given pixel measurement up from the typical system DPI (generally 96) to whatever the given DPI is.
// Arguments:
// - iPx - Pixel length measurement.
// - iDpi - Given DPI scalar value
// Return Value:
// - Pixel measurement scaled against the given DPI scalar.
int GdiEngine::s_ScaleByDpi(const int iPx, const int iDpi)
{
return MulDiv(iPx, iDpi, s_iBaseDpi);
}
// Routine Description:
// - Shrinks the given pixel measurement down from whatever the given DPI is to the typical system DPI (generally 96).
// Arguments:
// - iPx - Pixel measurement scaled against the given DPI.
// - iDpi - Given DPI for pixel scaling
// Return Value:
// - Pixel length measurement.
int GdiEngine::s_ShrinkByDpi(const int iPx, const int iDpi)
{
return MulDiv(iPx, s_iBaseDpi, iDpi);
}
// Routine Description:
// - Uses internal invalid structure to determine the top left pixel point of the invalid frame to be painted.
// Arguments:
// - <none>
// Return Value:
// - Top left corner in pixels of where to start repainting the frame.
til::point GdiEngine::_GetInvalidRectPoint() const
{
til::point pt;
pt.x = _psInvalidData.rcPaint.left;
pt.y = _psInvalidData.rcPaint.top;
return pt;
}
// Routine Description:
// - Uses internal invalid structure to determine the size of the invalid area of the frame to be painted.
// Arguments:
// - <none>
// Return Value:
// - Width and height in pixels of the invalid area of the frame.
til::size GdiEngine::_GetInvalidRectSize() const
{
return _GetRectSize(&_psInvalidData.rcPaint);
}
// Routine Description:
// - Converts a pixel region (til::rect) into its width/height (til::size)
// Arguments:
// - Pixel region (til::rect)
// Return Value:
// - Pixel dimensions (til::size)
til::size GdiEngine::_GetRectSize(const RECT* const pRect) const
{
til::size sz;
sz.width = pRect->right - pRect->left;
sz.height = pRect->bottom - pRect->top;
return sz;
}
// Routine Description:
// - Performs a "CombineRect" with the "OR" operation.
// - Basically extends the existing rect outward to also encompass the passed-in region.
// Arguments:
// - pRectExisting - Expand this rectangle to encompass the add rect.
// - pRectToOr - Add this rectangle to the existing one.
// Return Value:
// - <none>
void GdiEngine::_OrRect(_In_ til::rect* pRectExisting, const til::rect* pRectToOr) const
{
pRectExisting->left = std::min(pRectExisting->left, pRectToOr->left);
pRectExisting->top = std::min(pRectExisting->top, pRectToOr->top);
pRectExisting->right = std::max(pRectExisting->right, pRectToOr->right);
pRectExisting->bottom = std::max(pRectExisting->bottom, pRectToOr->bottom);
}

View File

@@ -1,858 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <vector>
#include "gdirenderer.hpp"
#include "../inc/unicode.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Render;
// This is an excerpt of GDI's FontHasWesternScript() as
// used by InternalTextOut() which is part of ExtTextOutW().
bool GdiEngine::FontHasWesternScript(HDC hdc)
{
WORD glyphs[4];
return (GetGlyphIndicesW(hdc, L"dMr\"", 4, glyphs, GGI_MARK_NONEXISTING_GLYPHS) == 4) &&
(glyphs[0] != 0xFFFF && glyphs[1] != 0xFFFF && glyphs[2] != 0xFFFF && glyphs[3] != 0xFFFF);
}
// Routine Description:
// - Prepares internal structures for a painting operation.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we started to paint. S_FALSE if we didn't need to paint. HRESULT error code if painting didn't start successfully.
[[nodiscard]] HRESULT GdiEngine::StartPaint() noexcept
{
// If we have no handle, we don't need to paint. Return quickly.
RETURN_HR_IF(S_FALSE, !_IsWindowValid());
// If we're already painting, we don't need to paint. Return quickly.
RETURN_HR_IF(S_FALSE, _fPaintStarted);
// If the window we're painting on is invisible, we don't need to paint. Return quickly.
// If the title changed, we will need to try and paint this frame. This will
// make sure the window's title is updated, even if the window isn't visible.
RETURN_HR_IF(S_FALSE, (!IsWindowVisible(_hwndTargetWindow) && !_titleChanged));
// At the beginning of a new frame, we have 0 lines ready for painting in PolyTextOut
_cPolyText = 0;
// Prepare our in-memory bitmap for double-buffered composition.
RETURN_IF_FAILED(_PrepareMemoryBitmap(_hwndTargetWindow));
// We must use Get and Release DC because BeginPaint/EndPaint can only be called in response to a WM_PAINT message (and may hang otherwise)
// We'll still use the PAINTSTRUCT for information because it's convenient.
_psInvalidData.hdc = GetDC(_hwndTargetWindow);
RETURN_HR_IF_NULL(E_FAIL, _psInvalidData.hdc);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(_psInvalidData.hdc, GM_ADVANCED);
// Signal that we're starting to paint.
_fPaintStarted = true;
_psInvalidData.fErase = TRUE;
_psInvalidData.rcPaint = _rcInvalid.to_win32_rect();
#if DBG
_debugContext = GetDC(_debugWindow);
#endif
_lastFontType = FontType::Undefined;
return S_OK;
}
// Routine Description:
// - Scrolls the existing data on the in-memory frame by the scroll region
// deltas we have collectively received through the Invalidate methods
// since the last time this was called.
// Arguments:
// - <none>
// Return Value:
// - S_OK, suitable GDI HRESULT error, error from Win32 windowing, or safemath error.
[[nodiscard]] HRESULT GdiEngine::ScrollFrame() noexcept
{
// If we don't have any scrolling to do, return early.
RETURN_HR_IF(S_OK, 0 == _szInvalidScroll.width && 0 == _szInvalidScroll.height);
// If we have an inverted cursor, we have to see if we have to clean it before we scroll to prevent
// left behind cursor copies in the scrolled region.
if (cursorInvertRects.size() > 0)
{
// We first need to apply the transform that was active at the time the cursor
// was rendered otherwise we won't be clearing the right area of the display.
// We don't need to do this if it was an identity transform though.
const auto identityTransform = cursorInvertTransform == IDENTITY_XFORM;
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &cursorInvertTransform));
LOG_HR_IF(E_FAIL, !SetWorldTransform(_psInvalidData.hdc, &cursorInvertTransform));
}
for (const auto& r : cursorInvertRects)
{
// Clean both the in-memory and actual window context.
LOG_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r)));
LOG_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r)));
}
// If we've applied a transform, then we need to reset it.
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY));
LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_psInvalidData.hdc, nullptr, MWT_IDENTITY));
}
cursorInvertRects.clear();
}
// We have to limit the region that can be scrolled to not include the gutters.
// Gutters are defined as sub-character width pixels at the bottom or right of the screen.
const auto coordFontSize = _GetFontSize();
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.width == 0 || coordFontSize.height == 0);
til::size szGutter;
szGutter.width = _szMemorySurface.width % coordFontSize.width;
szGutter.height = _szMemorySurface.height % coordFontSize.height;
RECT rcScrollLimit{};
RETURN_IF_FAILED(LongSub(_szMemorySurface.width, szGutter.width, &rcScrollLimit.right));
RETURN_IF_FAILED(LongSub(_szMemorySurface.height, szGutter.height, &rcScrollLimit.bottom));
// Scroll real window and memory buffer in-sync.
LOG_LAST_ERROR_IF(!ScrollWindowEx(_hwndTargetWindow,
_szInvalidScroll.width,
_szInvalidScroll.height,
&rcScrollLimit,
&rcScrollLimit,
nullptr,
nullptr,
0));
til::rect rcUpdate;
LOG_HR_IF(E_FAIL, !(ScrollDC(_hdcMemoryContext, _szInvalidScroll.width, _szInvalidScroll.height, &rcScrollLimit, &rcScrollLimit, nullptr, rcUpdate.as_win32_rect())));
LOG_IF_FAILED(_InvalidCombine(&rcUpdate));
// update invalid rect for the remainder of paint functions
_psInvalidData.rcPaint = _rcInvalid.to_win32_rect();
return S_OK;
}
// Routine Description:
// - BeginPaint helper to prepare the in-memory bitmap for double-buffering
// Arguments:
// - hwnd - Window handle to use for the DC properties when creating a memory DC and for checking the client area size.
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::_PrepareMemoryBitmap(const HWND hwnd) noexcept
{
RECT rcClient;
RETURN_HR_IF(E_FAIL, !(GetClientRect(hwnd, &rcClient)));
const auto szClient = _GetRectSize(&rcClient);
// Only do work if the existing memory surface is a different size from the client area.
// Return quickly if they're the same.
RETURN_HR_IF(S_OK, _szMemorySurface.width == szClient.width && _szMemorySurface.height == szClient.height);
wil::unique_hdc hdcRealWindow(GetDC(_hwndTargetWindow));
RETURN_HR_IF_NULL(E_FAIL, hdcRealWindow.get());
// If we already had a bitmap, Blt the old one onto the new one and clean up the old one.
if (nullptr != _hbitmapMemorySurface)
{
// Make a temporary DC for us to Blt with.
wil::unique_hdc hdcTemp(CreateCompatibleDC(hdcRealWindow.get()));
RETURN_HR_IF_NULL(E_FAIL, hdcTemp.get());
// Make the new bitmap we'll use going forward with the new size.
wil::unique_hbitmap hbitmapNew(CreateCompatibleBitmap(hdcRealWindow.get(), szClient.width, szClient.height));
RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get());
// Select it into the DC, but hold onto the junky one pixel bitmap (made by default) to give back when we need to Delete.
wil::unique_hbitmap hbitmapOnePixelJunk(SelectBitmap(hdcTemp.get(), hbitmapNew.get()));
RETURN_HR_IF_NULL(E_FAIL, hbitmapOnePixelJunk.get());
hbitmapNew.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object.
// Blt from the DC/bitmap we're already holding onto into the new one.
RETURN_HR_IF(E_FAIL, !(BitBlt(hdcTemp.get(), 0, 0, _szMemorySurface.width, _szMemorySurface.height, _hdcMemoryContext, 0, 0, SRCCOPY)));
// Put the junky bitmap back into the temp DC and get our new one out.
hbitmapNew.reset(SelectBitmap(hdcTemp.get(), hbitmapOnePixelJunk.get()));
RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get());
hbitmapOnePixelJunk.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object.
// Move our new bitmap into the long-standing DC we're holding onto.
wil::unique_hbitmap hbitmapOld(SelectBitmap(_hdcMemoryContext, hbitmapNew.get()));
RETURN_HR_IF_NULL(E_FAIL, hbitmapOld.get());
// Now save a pointer to our new bitmap into the class state.
_hbitmapMemorySurface = hbitmapNew.release(); // and prevent it from being freed now that GDI is holding onto it as well.
}
else
{
_hbitmapMemorySurface = CreateCompatibleBitmap(hdcRealWindow.get(), szClient.width, szClient.height);
RETURN_HR_IF_NULL(E_FAIL, _hbitmapMemorySurface);
wil::unique_hbitmap hOldBitmap(SelectBitmap(_hdcMemoryContext, _hbitmapMemorySurface)); // DC has a default junk bitmap, take it and delete it.
RETURN_HR_IF_NULL(E_FAIL, hOldBitmap.get());
}
// Save the new client size.
_szMemorySurface = szClient;
return S_OK;
}
// Routine Description:
// - EndPaint helper to perform the final BitBlt copy from the memory bitmap onto the final window bitmap (double-buffering.) Also cleans up structures used while painting.
// Arguments:
// - <none>
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::EndPaint() noexcept
{
// If we try to end a paint that wasn't started, it's invalid. Return.
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !(_fPaintStarted));
LOG_IF_FAILED(_FlushBufferLines());
const auto pt = _GetInvalidRectPoint();
const auto sz = _GetInvalidRectSize();
LOG_HR_IF(E_FAIL, !(BitBlt(_psInvalidData.hdc, pt.x, pt.y, sz.width, sz.height, _hdcMemoryContext, pt.x, pt.y, SRCCOPY)));
WHEN_DBG(_DebugBltAll());
_rcInvalid = {};
_fInvalidRectUsed = false;
_szInvalidScroll = {};
LOG_HR_IF(E_FAIL, !(GdiFlush()));
LOG_HR_IF(E_FAIL, !(ReleaseDC(_hwndTargetWindow, _psInvalidData.hdc)));
_psInvalidData.hdc = nullptr;
_fPaintStarted = false;
#if DBG
ReleaseDC(_debugWindow, _debugContext);
_debugContext = nullptr;
#endif
return S_OK;
}
// Routine Description:
// - Used to perform longer running presentation steps outside the lock so the other threads can continue.
// - Not currently used by GdiEngine.
// Arguments:
// - <none>
// Return Value:
// - S_FALSE since we do nothing.
[[nodiscard]] HRESULT GdiEngine::Present() noexcept
{
return S_FALSE;
}
// Routine Description:
// - Fills the given rectangle with the background color on the drawing context.
// Arguments:
// - prc - Rectangle to fill with color
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::_PaintBackgroundColor(const RECT* const prc) noexcept
{
wil::unique_hbrush hbr(GetStockBrush(DC_BRUSH));
RETURN_HR_IF_NULL(E_FAIL, hbr.get());
WHEN_DBG(_PaintDebugRect(prc));
LOG_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, prc, hbr.get())));
WHEN_DBG(_DoDebugBlt(prc));
return S_OK;
}
// Routine Description:
// - Paints the background of the invalid area of the frame.
// Arguments:
// - <none>
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::PaintBackground() noexcept
{
// We need to clear the cursorInvertRects at the start of a paint cycle so
// we don't inadvertently retain the invert region from the last paint after
// the cursor is hidden. If we don't, the ScrollFrame method may attempt to
// clean up a cursor that is no longer there, and instead leave a bunch of
// "ghost" cursor instances on the screen.
cursorInvertRects.clear();
if (_psInvalidData.fErase)
{
RETURN_IF_FAILED(_PaintBackgroundColor(&_psInvalidData.rcPaint));
}
return S_OK;
}
// Routine Description:
// - Draws one line of the buffer to the screen.
// - This will now be cached in a PolyText buffer and flushed periodically instead of drawing every individual segment. Note this means that the PolyText buffer must be flushed before some operations (changing the brush color, drawing lines on top of the characters, inverting for cursor/selection, etc.)
// Arguments:
// - clusters - text to be written and columns expected per cluster
// - coord - character coordinate target to render within viewport
// - trimLeft - This specifies whether to trim one character width off the left side of the output. Used for drawing the right-half only of a double-wide character.
// Return Value:
// - S_OK or suitable GDI HRESULT error.
// - HISTORICAL NOTES:
// ETO_OPAQUE will paint the background color before painting the text.
// ETO_CLIPPED required for ClearType fonts. Cleartype rendering can escape bounding rectangle unless clipped.
// Unclipped rectangles results in ClearType cutting off the right edge of the previous character when adding chars
// and in leaving behind artifacts when backspace/removing chars.
// This mainly applies to ClearType fonts like Lucida Console at small font sizes (10pt) or bolded.
// See: Win7: 390673, 447839 and then superseded by http://osgvsowi/638274 when FE/non-FE rendering condensed.
//#define CONSOLE_EXTTEXTOUT_FLAGS ETO_OPAQUE | ETO_CLIPPED
//#define MAX_POLY_LINES 80
[[nodiscard]] HRESULT GdiEngine::PaintBufferLine(const std::span<const Cluster> clusters,
const til::point coord,
const bool trimLeft,
const bool /*lineWrapped*/) noexcept
{
try
{
const auto cchLine = clusters.size();
// Exit early if there are no lines to draw.
RETURN_HR_IF(S_OK, 0 == cchLine);
const auto ptDraw = coord * _GetFontSize();
const auto pPolyTextLine = &_pPolyText[_cPolyText];
auto& polyString = _polyStrings.emplace_back();
polyString.reserve(cchLine);
const auto coordFontSize = _GetFontSize();
auto& polyWidth = _polyWidths.emplace_back();
polyWidth.reserve(cchLine);
// If we have a soft font, we only use the character's lower 7 bits.
const auto softFontCharMask = _lastFontType == FontType::Soft ? L'\x7F' : ~0;
// Sum up the total widths the entire line/run is expected to take while
// copying the pixel widths into a structure to direct GDI how many pixels to use per character.
size_t cchCharWidths = 0;
// Convert data from clusters into the text array and the widths array.
for (size_t i = 0; i < cchLine; i++)
{
const auto& cluster = til::at(clusters, i);
const auto text = cluster.GetText();
polyString += text;
polyString.back() &= softFontCharMask;
polyWidth.push_back(gsl::narrow<int>(cluster.GetColumns()) * coordFontSize.width);
cchCharWidths += polyWidth.back();
polyWidth.append(text.size() - 1, 0);
}
// Detect and convert for raster font...
if (!_isTrueTypeFont)
{
// dispatch conversion into our codepage
// Find out the bytes required
const auto cbRequired = WideCharToMultiByte(_fontCodepage, 0, polyString.data(), (int)cchLine, nullptr, 0, nullptr, nullptr);
if (cbRequired != 0)
{
// Allocate buffer for MultiByte
auto psConverted = std::make_unique<char[]>(cbRequired);
// Attempt conversion to current codepage
const auto cbConverted = WideCharToMultiByte(_fontCodepage, 0, polyString.data(), (int)cchLine, psConverted.get(), cbRequired, nullptr, nullptr);
// If successful...
if (cbConverted != 0)
{
// Now we have to convert back to Unicode but using the system ANSI codepage. Find buffer size first.
const auto cchRequired = MultiByteToWideChar(CP_ACP, 0, psConverted.get(), cbRequired, nullptr, 0);
if (cchRequired != 0)
{
std::pmr::wstring polyConvert(cchRequired, UNICODE_NULL, &_pool);
// Then do the actual conversion.
const auto cchConverted = MultiByteToWideChar(CP_ACP, 0, psConverted.get(), cbRequired, polyConvert.data(), cchRequired);
if (cchConverted != 0)
{
// If all successful, use this instead.
polyString.swap(polyConvert);
}
}
}
}
}
// If the line rendition is double height, we need to adjust the top or bottom
// of the clipping rect to clip half the height of the rendered characters.
const auto halfHeight = coordFontSize.height >> 1;
const auto topOffset = _currentLineRendition == LineRendition::DoubleHeightBottom ? halfHeight : 0;
const auto bottomOffset = _currentLineRendition == LineRendition::DoubleHeightTop ? halfHeight : 0;
pPolyTextLine->lpstr = polyString.data();
pPolyTextLine->n = gsl::narrow<UINT>(polyString.size());
pPolyTextLine->x = ptDraw.x;
pPolyTextLine->y = ptDraw.y;
pPolyTextLine->uiFlags = ETO_OPAQUE | ETO_CLIPPED;
pPolyTextLine->rcl.left = pPolyTextLine->x;
pPolyTextLine->rcl.top = pPolyTextLine->y + topOffset;
pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + (til::CoordType)cchCharWidths;
pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.height - bottomOffset;
pPolyTextLine->pdx = polyWidth.data();
if (trimLeft)
{
pPolyTextLine->rcl.left += coordFontSize.width;
}
_cPolyText++;
if (_cPolyText >= s_cPolyTextCache)
{
LOG_IF_FAILED(_FlushBufferLines());
}
return S_OK;
}
CATCH_RETURN();
}
// Routine Description:
// - Flushes any buffer lines in the PolyTextOut cache by drawing them and freeing the strings.
// - See also: PaintBufferLine
// Arguments:
// - <none>
// Return Value:
// - S_OK or E_FAIL if GDI failed.
[[nodiscard]] HRESULT GdiEngine::_FlushBufferLines() noexcept
{
auto hr = S_OK;
if (_cPolyText > 0)
{
for (size_t i = 0; i != _cPolyText; ++i)
{
const auto& t = _pPolyText[i];
// The following if/else replicates the essentials of how ExtTextOutW() without ETO_IGNORELANGUAGE works.
// See InternalTextOut().
//
// Unlike the original, we don't check for `GetTextCharacterExtra(hdc) != 0`,
// because we don't ever call SetTextCharacterExtra() anyways.
//
// GH#12294:
// Additionally we set ss.fOverrideDirection to TRUE, because we need to present RTL
// text in logical order in order to be compatible with applications like `vim -H`.
if (_fontHasWesternScript && ScriptIsComplex(t.lpstr, t.n, SIC_COMPLEX) == S_FALSE)
{
if (!ExtTextOutW(_hdcMemoryContext, t.x, t.y, t.uiFlags | ETO_IGNORELANGUAGE, &t.rcl, t.lpstr, t.n, t.pdx))
{
hr = E_FAIL;
break;
}
}
else
{
SCRIPT_STATE ss{};
ss.fOverrideDirection = TRUE;
SCRIPT_STRING_ANALYSIS ssa;
hr = ScriptStringAnalyse(_hdcMemoryContext, t.lpstr, t.n, 0, -1, SSA_GLYPHS | SSA_FALLBACK, 0, nullptr, &ss, t.pdx, nullptr, nullptr, &ssa);
if (FAILED(hr))
{
break;
}
hr = ScriptStringOut(ssa, t.x, t.y, t.uiFlags, &t.rcl, 0, 0, FALSE);
std::ignore = ScriptStringFree(&ssa);
if (FAILED(hr))
{
break;
}
}
}
_polyStrings.clear();
_polyWidths.clear();
ZeroMemory(_pPolyText, sizeof(_pPolyText));
_cPolyText = 0;
}
RETURN_HR(hr);
}
// Routine Description:
// - Draws up to one line worth of grid lines on top of characters.
// Arguments:
// - lines - Enum defining which edges of the rectangle to draw
// - color - The color to use for drawing the edges.
// - cchLine - How many characters we should draw the grid lines along (left to right in a row)
// - coordTarget - The starting X/Y position of the first character to draw on.
// Return Value:
// - S_OK or suitable GDI HRESULT error or E_FAIL for GDI errors in functions that don't reliably return a specific error code.
[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept
{
LOG_IF_FAILED(_FlushBufferLines());
// Convert the target from characters to pixels.
const auto ptTarget = coordTarget * _GetFontSize();
// Set the brush color as requested and save the previous brush to restore at the end.
wil::unique_hbrush hbr(CreateSolidBrush(color));
RETURN_HR_IF_NULL(E_FAIL, hbr.get());
wil::unique_hbrush hbrPrev(SelectBrush(_hdcMemoryContext, hbr.get()));
RETURN_HR_IF_NULL(E_FAIL, hbrPrev.get());
hbr.release(); // If SelectBrush was successful, GDI owns the brush. Release for now.
// On exit, be sure we try to put the brush back how it was originally.
auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); });
// Get the font size so we know the size of the rectangle lines we'll be inscribing.
const auto fontWidth = _GetFontSize().width;
const auto fontHeight = _GetFontSize().height;
const auto widthOfAllCells = fontWidth * gsl::narrow_cast<unsigned>(cchLine);
const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) {
return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY);
};
if (lines.test(GridLines::Left))
{
auto x = ptTarget.x;
for (size_t i = 0; i < cchLine; i++, x += fontWidth)
{
RETURN_HR_IF(E_FAIL, !DrawLine(x, ptTarget.y, _lineMetrics.gridlineWidth, fontHeight));
}
}
if (lines.test(GridLines::Right))
{
// NOTE: We have to subtract the stroke width from the cell width
// to ensure the x coordinate remains inside the clipping rectangle.
auto x = ptTarget.x + fontWidth - _lineMetrics.gridlineWidth;
for (size_t i = 0; i < cchLine; i++, x += fontWidth)
{
RETURN_HR_IF(E_FAIL, !DrawLine(x, ptTarget.y, _lineMetrics.gridlineWidth, fontHeight));
}
}
if (lines.test(GridLines::Top))
{
const auto y = ptTarget.y;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth));
}
if (lines.test(GridLines::Bottom))
{
// NOTE: We have to subtract the stroke width from the cell height
// to ensure the y coordinate remains inside the clipping rectangle.
const auto y = ptTarget.y + fontHeight - _lineMetrics.gridlineWidth;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth));
}
if (lines.any(GridLines::Underline, GridLines::DoubleUnderline))
{
const auto y = ptTarget.y + _lineMetrics.underlineOffset;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.underlineWidth));
if (lines.test(GridLines::DoubleUnderline))
{
const auto y2 = ptTarget.y + _lineMetrics.underlineOffset2;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y2, widthOfAllCells, _lineMetrics.underlineWidth));
}
}
if (lines.test(GridLines::Strikethrough))
{
const auto y = ptTarget.y + _lineMetrics.strikethroughOffset;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth));
}
return S_OK;
}
// Routine Description:
// - Draws the cursor on the screen
// Arguments:
// - options - Parameters that affect the way that the cursor is drawn
// Return Value:
// - S_OK, suitable GDI HRESULT error, or safemath error, or E_FAIL in a GDI error where a specific error isn't set.
[[nodiscard]] HRESULT GdiEngine::PaintCursor(const CursorOptions& options) noexcept
{
// if the cursor is off, do nothing - it should not be visible.
if (!options.isOn)
{
return S_FALSE;
}
LOG_IF_FAILED(_FlushBufferLines());
const auto coordFontSize = _GetFontSize();
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.width == 0 || coordFontSize.height == 0);
// First set up a block cursor the size of the font.
RECT rcBoundaries;
rcBoundaries.left = options.coordCursor.x * coordFontSize.width;
rcBoundaries.top = options.coordCursor.y * coordFontSize.height;
rcBoundaries.right = rcBoundaries.left + coordFontSize.width;
rcBoundaries.bottom = rcBoundaries.top + coordFontSize.height;
// If we're double-width cursor, make it an extra font wider.
if (options.fIsDoubleWidth)
{
rcBoundaries.right = rcBoundaries.right + coordFontSize.width;
}
// Make a set of RECTs to paint.
cursorInvertRects.clear();
auto rcInvert = rcBoundaries;
// depending on the cursorType, add rects to that set
switch (options.cursorType)
{
case CursorType::Legacy:
{
// Now adjust the cursor height
// enforce min/max cursor height
auto ulHeight = options.ulCursorHeightPercent;
ulHeight = std::max(ulHeight, s_ulMinCursorHeightPercent); // No smaller than 25%
ulHeight = std::min(ulHeight, s_ulMaxCursorHeightPercent); // No larger than 100%
ulHeight = MulDiv(coordFontSize.height, ulHeight, 100); // divide by 100 because percent.
// Reduce the height of the top to be relative to the bottom by the height we want.
rcInvert.top = rcInvert.bottom - ulHeight;
cursorInvertRects.push_back(rcInvert);
}
break;
case CursorType::VerticalBar:
LONG proposedWidth;
proposedWidth = rcInvert.left + options.cursorPixelWidth;
// It can't be wider than one cell or we'll have problems in invalidation, so restrict here.
// It's either the left + the proposed width from the ease of access setting, or
// it's the right edge of the block cursor as a maximum.
rcInvert.right = std::min(rcInvert.right, proposedWidth);
cursorInvertRects.push_back(rcInvert);
break;
case CursorType::Underscore:
rcInvert.top = rcInvert.bottom + -1;
cursorInvertRects.push_back(rcInvert);
break;
case CursorType::DoubleUnderscore:
{
RECT top, bottom;
top = bottom = rcBoundaries;
bottom.top = bottom.bottom + -1;
top.top = top.bottom + -3;
top.bottom = top.top + 1;
cursorInvertRects.push_back(top);
cursorInvertRects.push_back(bottom);
}
break;
case CursorType::EmptyBox:
{
RECT top, left, right, bottom;
top = left = right = bottom = rcBoundaries;
top.bottom = top.top + 1;
bottom.top = bottom.bottom + -1;
left.right = left.left + 1;
right.left = right.right + -1;
top.left = top.left + 1;
bottom.left = bottom.left + 1;
top.right = top.right + -1;
bottom.right = bottom.right + -1;
cursorInvertRects.push_back(top);
cursorInvertRects.push_back(left);
cursorInvertRects.push_back(right);
cursorInvertRects.push_back(bottom);
}
break;
case CursorType::FullBox:
cursorInvertRects.push_back(rcInvert);
break;
default:
return E_NOTIMPL;
}
// Prepare the appropriate line transform for the current row.
LOG_IF_FAILED(PrepareLineTransform(options.lineRendition, 0, options.viewportLeft));
auto resetLineTransform = wil::scope_exit([&]() {
LOG_IF_FAILED(ResetLineTransform());
});
// Either invert all the RECTs, or paint them.
if (options.fUseColor)
{
auto hCursorBrush = CreateSolidBrush(options.cursorColor);
for (auto r : cursorInvertRects)
{
RETURN_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, &r, hCursorBrush)));
}
DeleteObject(hCursorBrush);
// Clear out the inverted rects, so that we don't re-invert them next frame.
cursorInvertRects.clear();
}
else
{
// Save the current line transform in case we need to reapply these
// inverted rects to hide the cursor in the ScrollFrame method.
cursorInvertTransform = _currentLineTransform;
for (auto r : cursorInvertRects)
{
// Make sure the cursor is always readable (see gh-3647)
const auto PrevObject = SelectObject(_hdcMemoryContext, GetStockObject(LTGRAY_BRUSH));
const auto Result = PatBlt(_hdcMemoryContext, r.left, r.top, r.right - r.left, r.bottom - r.top, PATINVERT);
SelectObject(_hdcMemoryContext, PrevObject);
RETURN_HR_IF(E_FAIL, !Result);
}
}
return S_OK;
}
// Routine Description:
// - Inverts the selected region on the current screen buffer.
// - Reads the selected area, selection mode, and active screen buffer
// from the global properties and dispatches a GDI invert on the selected text area.
// Arguments:
// - rect - Rectangle to invert or highlight to make the selection area
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::PaintSelection(const til::rect& rect) noexcept
{
LOG_IF_FAILED(_FlushBufferLines());
const auto pixelRect = rect.scale_up(_GetFontSize()).to_win32_rect();
RETURN_HR_IF(E_FAIL, !InvertRect(_hdcMemoryContext, &pixelRect));
return S_OK;
}
#ifdef DBG
void GdiEngine::_CreateDebugWindow()
{
if (_fDebug)
{
const auto className = L"ConsoleGdiDebugWindow";
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_OWNDC;
wc.lpfnWndProc = DefWindowProcW;
wc.hInstance = nullptr;
wc.lpszClassName = className;
THROW_LAST_ERROR_IF(0 == RegisterClassExW(&wc));
_debugWindow = CreateWindowExW(0,
className,
L"ConhostGdiDebugWindow",
0,
0,
0,
0,
0,
nullptr,
nullptr,
nullptr,
nullptr);
THROW_LAST_ERROR_IF_NULL(_debugWindow);
ShowWindow(_debugWindow, SW_SHOWNORMAL);
}
}
// Routine Description:
// - Will fill a given rectangle with a gray shade to help identify which portion of the screen is being debugged.
// - Will attempt immediate BLT so you can see it.
// - NOTE: You must set _fDebug flag for this to operate using a debugger.
// - NOTE: This only works in Debug (DBG) builds.
// Arguments:
// - prc - Pointer to rectangle to fill
// Return Value:
// - <none>
void GdiEngine::_PaintDebugRect(const RECT* const prc) const
{
if (_fDebug)
{
if (!IsRectEmpty(prc))
{
wil::unique_hbrush hbr(GetStockBrush(GRAY_BRUSH));
if (nullptr != LOG_HR_IF_NULL(E_FAIL, hbr.get()))
{
LOG_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, prc, hbr.get())));
_DoDebugBlt(prc);
}
}
}
}
// Routine Description:
// - Will immediately Blt the given rectangle to the screen for aid in debugging when it is tough to see
// what is occurring with the in-memory DC.
// - This will pause the thread for 200ms when called to give you an opportunity to see the paint.
// - NOTE: You must set _fDebug flag for this to operate using a debugger.
// - NOTE: This only works in Debug (DBG) builds.
// Arguments:
// - prc - Pointer to region to immediately Blt to the real screen DC.
// Return Value:
// - <none>
void GdiEngine::_DoDebugBlt(const RECT* const prc) const
{
if (_fDebug)
{
if (!IsRectEmpty(prc))
{
LOG_HR_IF(E_FAIL, !(BitBlt(_debugContext, prc->left, prc->top, prc->right - prc->left, prc->bottom - prc->top, _hdcMemoryContext, prc->left, prc->top, SRCCOPY)));
Sleep(100);
}
}
}
void GdiEngine::_DebugBltAll() const
{
if (_fDebug)
{
BitBlt(_debugContext, 0, 0, _szMemorySurface.width, _szMemorySurface.height, _hdcMemoryContext, 0, 0, SRCCOPY);
Sleep(100);
}
}
#endif

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"

36
src/renderer/gdi/pch.h Normal file
View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <array>
#include <iomanip>
#include <optional>
#include <span>
#include <sstream>
#include <string_view>
#include <gsl/gsl_util>
#include <wil/com.h>
#include <wil/result_macros.h>
#include <wil/stl.h>
#include <wil/win32_helpers.h>
// Dynamic Bitset (optional dependency on LibPopCnt for perf at bit counting)
// Variable-size compressed-storage header-only bit flag storage library.
#pragma warning(push)
#pragma warning(disable : 4702) // unreachable code
#include <dynamic_bitset.hpp>
#pragma warning(pop)
// Chromium Numerics (safe math)
#pragma warning(push)
#pragma warning(disable : 4100) // '...': unreferenced formal parameter
#pragma warning(disable : 26812) // The enum type '...' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
#include <base/numerics/safe_math.h>
#pragma warning(pop)
#include <til.h>

View File

@@ -1,4 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"

View File

@@ -1,34 +0,0 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- precomp.h
Abstract:
- Contains external headers to include in the precompile phase of console build process.
- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building).
--*/
#include <cwchar>
#include <sal.h>
// This includes support libraries from the CRT, STL, WIL, and GSL
#include "LibraryIncludes.h"
#include <Windows.h>
#include <windowsx.h>
#include <usp10.h>
#if defined(DEBUG) || defined(_DEBUG) || defined(DBG)
#define WHEN_DBG(x) x
#else
#define WHEN_DBG(x)
#endif
// SafeMath
#pragma prefast(push)
#pragma prefast(disable : 26071, "Range violation in Intsafe. Not ours.")
#define ENABLE_INTSAFE_SIGNED_FUNCTIONS // Only unsigned intsafe math/casts available without this def
#include <intsafe.h>
#pragma prefast(pop)

View File

@@ -22,10 +22,7 @@ PRECOMPILED_CXX = 1
PRECOMPILED_INCLUDE = ..\precomp.h
SOURCES = \
..\invalidate.cpp \
..\math.cpp \
..\paint.cpp \
..\state.cpp \
..\GdiEngine.cpp \
INCLUDES = \
$(INCLUDES); \

View File

@@ -1,737 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "gdirenderer.hpp"
#include "../../inc/conattrs.hpp"
#include <winuserp.h> // for GWL_CONSOLE_BKCOLOR
#include "../../interactivity/win32/CustomWindowMessages.h"
#pragma hdrstop
using namespace Microsoft::Console::Render;
// Routine Description:
// - Creates a new GDI-based rendering engine
// - NOTE: Will throw if initialization failure. Caller must catch.
// Arguments:
// - <none>
// Return Value:
// - An instance of a Renderer.
GdiEngine::GdiEngine() :
_hwndTargetWindow((HWND)INVALID_HANDLE_VALUE),
#if DBG
_debugWindow((HWND)INVALID_HANDLE_VALUE),
#endif
_iCurrentDpi(s_iBaseDpi),
_hbitmapMemorySurface(nullptr),
_cPolyText(0),
_fInvalidRectUsed(false),
_lastFg(INVALID_COLOR),
_lastBg(INVALID_COLOR),
_lastFontType(FontType::Undefined),
_currentLineTransform(IDENTITY_XFORM),
_currentLineRendition(LineRendition::SingleWidth),
_fPaintStarted(false),
_invalidCharacters{},
_hfont(nullptr),
_hfontItalic(nullptr),
_pool{ til::pmr::get_default_resource() }, // It's important the pool is first so it can be given to the others on construction.
_polyStrings{ &_pool },
_polyWidths{ &_pool }
{
ZeroMemory(_pPolyText, sizeof(POLYTEXTW) * s_cPolyTextCache);
_hdcMemoryContext = CreateCompatibleDC(nullptr);
THROW_HR_IF_NULL(E_FAIL, _hdcMemoryContext);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(_hdcMemoryContext, GM_ADVANCED);
// On session zero, text GDI APIs might not be ready.
// Calling GetTextFace causes a wait that will be
// satisfied while GDI text APIs come online.
//
// (Session zero is the non-interactive session
// where long running services processes are hosted.
// this increase security and reliability as user
// applications in interactive session will not be
// able to interact with services through the common
// desktop (e.g., window messages)).
GetTextFaceW(_hdcMemoryContext, 0, nullptr);
#if DBG
if (_fDebug)
{
_CreateDebugWindow();
}
#endif
}
// Routine Description:
// - Destroys an instance of a GDI-based rendering engine
// Arguments:
// - <none>
// Return Value:
// - <none>
GdiEngine::~GdiEngine()
{
for (size_t iPoly = 0; iPoly < _cPolyText; iPoly++)
{
if (_pPolyText[iPoly].lpstr != nullptr)
{
delete[] _pPolyText[iPoly].lpstr;
}
}
if (_hbitmapMemorySurface != nullptr)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hbitmapMemorySurface)));
_hbitmapMemorySurface = nullptr;
}
if (_hfont != nullptr)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hfont)));
_hfont = nullptr;
}
if (_hfontItalic != nullptr)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hfontItalic)));
_hfontItalic = nullptr;
}
if (_hdcMemoryContext != nullptr)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hdcMemoryContext)));
_hdcMemoryContext = nullptr;
}
}
// Routine Description:
// - Updates the window to which this GDI renderer will be bound.
// - A window handle is required for determining the client area and other properties about the rendering surface and monitor.
// Arguments:
// - hwnd - Handle to the window on which we will be drawing.
// Return Value:
// - S_OK if set successfully or relevant GDI error via HRESULT.
[[nodiscard]] HRESULT GdiEngine::SetHwnd(const HWND hwnd) noexcept
{
// First attempt to get the DC and create an appropriate DC
const auto hdcRealWindow = GetDC(hwnd);
RETURN_HR_IF_NULL(E_FAIL, hdcRealWindow);
const auto hdcNewMemoryContext = CreateCompatibleDC(hdcRealWindow);
RETURN_HR_IF_NULL(E_FAIL, hdcNewMemoryContext);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(hdcNewMemoryContext, GM_ADVANCED);
// If we had an existing memory context stored, release it before proceeding.
if (nullptr != _hdcMemoryContext)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hdcMemoryContext)));
_hdcMemoryContext = nullptr;
}
// Store new window handle and memory context
_hwndTargetWindow = hwnd;
_hdcMemoryContext = hdcNewMemoryContext;
if (nullptr != hdcRealWindow)
{
LOG_HR_IF(E_FAIL, !(ReleaseDC(_hwndTargetWindow, hdcRealWindow)));
}
#if DBG
if (_debugWindow != INVALID_HANDLE_VALUE && _debugWindow != nullptr)
{
RECT rc{};
THROW_IF_WIN32_BOOL_FALSE(GetWindowRect(_hwndTargetWindow, &rc));
THROW_IF_WIN32_BOOL_FALSE(SetWindowPos(_debugWindow, nullptr, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOMOVE));
}
#endif
return S_OK;
}
// Routine Description:
// - This routine will help call SetWindowLongW with the correct semantics to retrieve the appropriate error code.
// Arguments:
// - hWnd - Window handle to use for setting
// - nIndex - Window handle item offset
// - dwNewLong - Value to update in window structure
// Return Value:
// - S_OK or converted HRESULT from last Win32 error from SetWindowLongW
[[nodiscard]] HRESULT GdiEngine::s_SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept
{
// SetWindowLong has strange error handling. On success, it returns the previous Window Long value and doesn't modify the Last Error state.
// To deal with this, we set the last error to 0/S_OK first, call it, and if the previous long was 0, we check if the error was non-zero before reporting.
// Otherwise, we'll get an "Error: The operation has completed successfully." and there will be another screenshot on the internet making fun of Windows.
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx
SetLastError(0);
const auto lResult = SetWindowLongW(hWnd, nIndex, dwNewLong);
if (0 == lResult)
{
RETURN_LAST_ERROR_IF(0 != GetLastError());
}
return S_OK;
}
// Routine Description
// - Resets the world transform to the identity matrix.
// Arguments:
// - <none>
// Return Value:
// - S_OK if successful. S_FALSE if already reset. E_FAIL if there was an error.
[[nodiscard]] HRESULT GdiEngine::ResetLineTransform() noexcept
{
// Return early if the current transform is already the identity matrix.
RETURN_HR_IF(S_FALSE, _currentLineTransform == IDENTITY_XFORM);
// Flush any buffer lines which would be expecting to use the current transform.
LOG_IF_FAILED(_FlushBufferLines());
// Reset the active transform to the identity matrix.
RETURN_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY));
// Reset the current state.
_currentLineTransform = IDENTITY_XFORM;
_currentLineRendition = LineRendition::SingleWidth;
return S_OK;
}
// Routine Description
// - Applies an appropriate transform for the given line rendition and viewport offset.
// Arguments:
// - lineRendition - The line rendition specifying the scaling of the line.
// - targetRow - The row on which the line is expected to be rendered.
// - viewportLeft - The left offset of the current viewport.
// Return Value:
// - S_OK if successful. S_FALSE if already set. E_FAIL if there was an error.
[[nodiscard]] HRESULT GdiEngine::PrepareLineTransform(const LineRendition lineRendition,
const til::CoordType targetRow,
const til::CoordType viewportLeft) noexcept
{
XFORM lineTransform = {};
// The X delta is to account for the horizontal viewport offset.
lineTransform.eDx = viewportLeft ? -1.0f * viewportLeft * _GetFontSize().width : 0.0f;
switch (lineRendition)
{
case LineRendition::SingleWidth:
lineTransform.eM11 = 1; // single width
lineTransform.eM22 = 1; // single height
break;
case LineRendition::DoubleWidth:
lineTransform.eM11 = 2; // double width
lineTransform.eM22 = 1; // single height
break;
case LineRendition::DoubleHeightTop:
lineTransform.eM11 = 2; // double width
lineTransform.eM22 = 2; // double height
// The Y delta is to negate the offset caused by the scaled height.
lineTransform.eDy = -1.0f * targetRow * _GetFontSize().height;
break;
case LineRendition::DoubleHeightBottom:
lineTransform.eM11 = 2; // double width
lineTransform.eM22 = 2; // double height
// The Y delta is to negate the offset caused by the scaled height.
// An extra row is added because we need the bottom half of the line.
lineTransform.eDy = -1.0f * (targetRow + 1) * _GetFontSize().height;
break;
}
// Return early if the new matrix is the same as the current transform.
RETURN_HR_IF(S_FALSE, _currentLineRendition == lineRendition && _currentLineTransform == lineTransform);
// Flush any buffer lines which would be expecting to use the current transform.
LOG_IF_FAILED(_FlushBufferLines());
// Set the active transform with the new matrix.
RETURN_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &lineTransform));
// Save the current state.
_currentLineTransform = lineTransform;
_currentLineRendition = lineRendition;
return S_OK;
}
// Routine Description:
// - This method will set the GDI brushes in the drawing context (and update the hung-window background color)
// Arguments:
// - textAttributes - Text attributes to use for the brush color
// - renderSettings - The color table and modes required for rendering
// - pData - The interface to console data structures required for rendering
// - usingSoftFont - Whether we're rendering characters from a soft font
// - isSettingDefaultBrushes - Lets us know that the default brushes are being set so we can update the DC background
// and the hung app background painting color
// Return Value:
// - S_OK if set successfully or relevant GDI error via HRESULT.
[[nodiscard]] HRESULT GdiEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes,
const RenderSettings& renderSettings,
const gsl::not_null<IRenderData*> /*pData*/,
const bool usingSoftFont,
const bool isSettingDefaultBrushes) noexcept
{
RETURN_IF_FAILED(_FlushBufferLines());
RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), _hdcMemoryContext);
// Set the colors for painting text
const auto [colorForeground, colorBackground] = renderSettings.GetAttributeColors(textAttributes);
if (colorForeground != _lastFg)
{
RETURN_HR_IF(E_FAIL, CLR_INVALID == SetTextColor(_hdcMemoryContext, colorForeground));
_lastFg = colorForeground;
}
if (colorBackground != _lastBg)
{
RETURN_HR_IF(E_FAIL, CLR_INVALID == SetBkColor(_hdcMemoryContext, colorBackground));
_lastBg = colorBackground;
}
if (isSettingDefaultBrushes)
{
// Set the color for painting the extra DC background area
RETURN_HR_IF(E_FAIL, CLR_INVALID == SetDCBrushColor(_hdcMemoryContext, colorBackground));
// Set the hung app background painting color
RETURN_IF_FAILED(s_SetWindowLongWHelper(_hwndTargetWindow, GWL_CONSOLE_BKCOLOR, colorBackground));
}
// If the font type has changed, select an appropriate font variant or soft font.
const auto usingItalicFont = textAttributes.IsItalic();
const auto fontType = usingSoftFont ? FontType::Soft :
usingItalicFont ? FontType::Italic :
FontType::Default;
if (fontType != _lastFontType)
{
switch (fontType)
{
case FontType::Soft:
SelectFont(_hdcMemoryContext, _softFont);
break;
case FontType::Italic:
SelectFont(_hdcMemoryContext, _hfontItalic);
break;
case FontType::Default:
default:
SelectFont(_hdcMemoryContext, _hfont);
break;
}
_lastFontType = fontType;
_fontHasWesternScript = FontHasWesternScript(_hdcMemoryContext);
}
return S_OK;
}
// Routine Description:
// - This method will update the active font on the current device context
// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure.
// Arguments:
// - FontDesired - reference to font information we should use while instantiating a font.
// - Font - reference to font information where the chosen font information will be populated.
// Return Value:
// - S_OK if set successfully or relevant GDI error via HRESULT.
[[nodiscard]] HRESULT GdiEngine::UpdateFont(const FontInfoDesired& FontDesired, _Out_ FontInfo& Font) noexcept
{
wil::unique_hfont hFont, hFontItalic;
RETURN_IF_FAILED(_GetProposedFont(FontDesired, Font, _iCurrentDpi, hFont, hFontItalic));
// Select into DC
RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, hFont.get()));
// Save off the font metrics for various other calculations
RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(_hdcMemoryContext, &_tmFontMetrics)));
// There is no font metric for the grid line width, so we use a small
// multiple of the font size, which typically rounds to a pixel.
const auto fontSize = _tmFontMetrics.tmHeight - _tmFontMetrics.tmInternalLeading;
_lineMetrics.gridlineWidth = std::lround(fontSize * 0.025);
OUTLINETEXTMETRICW outlineMetrics;
if (GetOutlineTextMetricsW(_hdcMemoryContext, sizeof(outlineMetrics), &outlineMetrics))
{
// For TrueType fonts, the other line metrics can be obtained from
// the font's outline text metric structure.
_lineMetrics.underlineOffset = outlineMetrics.otmsUnderscorePosition;
_lineMetrics.underlineWidth = outlineMetrics.otmsUnderscoreSize;
_lineMetrics.strikethroughOffset = outlineMetrics.otmsStrikeoutPosition;
_lineMetrics.strikethroughWidth = outlineMetrics.otmsStrikeoutSize;
}
else
{
// If we can't obtain the outline metrics for the font, we just pick
// some reasonable values for the offsets and widths.
_lineMetrics.underlineOffset = -std::lround(fontSize * 0.05);
_lineMetrics.underlineWidth = _lineMetrics.gridlineWidth;
_lineMetrics.strikethroughOffset = std::lround(_tmFontMetrics.tmAscent / 3.0);
_lineMetrics.strikethroughWidth = _lineMetrics.gridlineWidth;
}
// We always want the lines to be visible, so if a stroke width ends
// up being zero, we need to make it at least 1 pixel.
_lineMetrics.gridlineWidth = std::max(_lineMetrics.gridlineWidth, 1);
_lineMetrics.underlineWidth = std::max(_lineMetrics.underlineWidth, 1);
_lineMetrics.strikethroughWidth = std::max(_lineMetrics.strikethroughWidth, 1);
// Offsets are relative to the base line of the font, so we subtract
// from the ascent to get an offset relative to the top of the cell.
const auto ascent = _tmFontMetrics.tmAscent;
_lineMetrics.underlineOffset = ascent - _lineMetrics.underlineOffset;
_lineMetrics.strikethroughOffset = ascent - _lineMetrics.strikethroughOffset;
// For double underlines we need a second offset, just below the first,
// but with a bit of a gap (about double the grid line width).
_lineMetrics.underlineOffset2 = _lineMetrics.underlineOffset +
_lineMetrics.underlineWidth +
std::lround(fontSize * 0.05);
// However, we don't want the underline to extend past the bottom of the
// cell, so we clamp the offset to fit just inside.
const auto maxUnderlineOffset = Font.GetSize().height - _lineMetrics.underlineWidth;
_lineMetrics.underlineOffset2 = std::min(_lineMetrics.underlineOffset2, maxUnderlineOffset);
// But if the resulting gap isn't big enough even to register as a thicker
// line, it's better to place the second line slightly above the first.
if (_lineMetrics.underlineOffset2 < _lineMetrics.underlineOffset + _lineMetrics.gridlineWidth)
{
_lineMetrics.underlineOffset2 = _lineMetrics.underlineOffset - _lineMetrics.gridlineWidth;
}
// Now find the size of a 0 in this current font and save it for conversions done later.
_coordFontLast = Font.GetSize();
// Persist font for cleanup (and free existing if necessary)
if (_hfont != nullptr)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hfont)));
_hfont = nullptr;
}
// Save the font.
_hfont = hFont.release();
// Persist italic font for cleanup (and free existing if necessary)
if (_hfontItalic != nullptr)
{
LOG_HR_IF(E_FAIL, !(DeleteObject(_hfontItalic)));
_hfontItalic = nullptr;
}
// Save the italic font.
_hfontItalic = hFontItalic.release();
// Save raster vs. TrueType and codepage data in case we need to convert.
_isTrueTypeFont = Font.IsTrueTypeFont();
_fontCodepage = Font.GetCodePage();
// Inform the soft font of the change in size.
_softFont.SetTargetSize(_GetFontSize());
LOG_IF_FAILED(InvalidateAll());
return S_OK;
}
// Routine Description:
// - This method will replace the active soft font with the given bit pattern.
// Arguments:
// - bitPattern - An array of scanlines representing all the glyphs in the font.
// - cellSize - The cell size for an individual glyph.
// - centeringHint - The horizontal extent that glyphs are offset from center.
// Return Value:
// - S_OK if successful. E_FAIL if there was an error.
[[nodiscard]] HRESULT GdiEngine::UpdateSoftFont(const std::span<const uint16_t> bitPattern,
const til::size cellSize,
const size_t centeringHint) noexcept
{
// If we previously called SelectFont(_hdcMemoryContext, _softFont), it will
// still hold a reference to the _softFont object we're planning to overwrite.
// --> First revert back to the standard _hfont, lest we have dangling pointers.
if (_lastFontType == FontType::Soft)
{
RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont));
_lastFontType = FontType::Default;
}
// Create a new font resource with the updated pattern, or delete if empty.
_softFont = FontResource{ bitPattern, cellSize, _GetFontSize(), centeringHint };
return S_OK;
}
// Routine Description:
// - This method will modify the DPI we're using for scaling calculations.
// Arguments:
// - iDpi - The Dots Per Inch to use for scaling. We will use this relative to the system default DPI defined in Windows headers as a constant.
// Return Value:
// - HRESULT S_OK, GDI-based error code, or safemath error
[[nodiscard]] HRESULT GdiEngine::UpdateDpi(const int iDpi) noexcept
{
_iCurrentDpi = iDpi;
return S_OK;
}
// Method Description:
// - This method will update our internal reference for how big the viewport is.
// Does nothing for GDI.
// Arguments:
// - srNewViewport - The bounds of the new viewport.
// Return Value:
// - HRESULT S_OK
[[nodiscard]] HRESULT GdiEngine::UpdateViewport(const til::inclusive_rect& /*srNewViewport*/) noexcept
{
return S_OK;
}
// Routine Description:
// - This method will figure out what the new font should be given the starting font information and a DPI.
// - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match.
// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure.
// - If the intent is to immediately turn around and use this font, pass the optional handle parameter and use it immediately.
// Arguments:
// - FontDesired - reference to font information we should use while instantiating a font.
// - Font - reference to font information where the chosen font information will be populated.
// - iDpi - The DPI we will have when rendering
// Return Value:
// - S_OK if set successfully or relevant GDI error via HRESULT.
[[nodiscard]] HRESULT GdiEngine::GetProposedFont(const FontInfoDesired& FontDesired, _Out_ FontInfo& Font, const int iDpi) noexcept
{
wil::unique_hfont hFont, hFontItalic;
return _GetProposedFont(FontDesired, Font, iDpi, hFont, hFontItalic);
}
// Method Description:
// - Updates the window's title string. For GDI, this does nothing, because the
// title must be updated on the main window's windowproc thread.
// Arguments:
// - newTitle: the new string to use for the title of the window
// Return Value:
// - S_OK if PostMessageW succeeded, otherwise E_FAIL
[[nodiscard]] HRESULT GdiEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept
{
// the CM_UPDATE_TITLE handler in windowproc will query the updated title.
return PostMessageW(_hwndTargetWindow, CM_UPDATE_TITLE, 0, (LPARAM) nullptr) ? S_OK : E_FAIL;
}
// Routine Description:
// - This method will figure out what the new font should be given the starting font information and a DPI.
// - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match.
// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure.
// - If the intent is to immediately turn around and use this font, pass the optional handle parameter and use it immediately.
// Arguments:
// - FontDesired - reference to font information we should use while instantiating a font.
// - Font - the actual font
// - iDpi - The DPI we will have when rendering
// - hFont - A smart pointer to receive a handle to a ready-to-use GDI font.
// - hFontItalic - A smart pointer to receive a handle to an italic variant of the font.
// Return Value:
// - S_OK if set successfully or relevant GDI error via HRESULT.
[[nodiscard]] HRESULT GdiEngine::_GetProposedFont(const FontInfoDesired& FontDesired,
_Out_ FontInfo& Font,
const int iDpi,
_Inout_ wil::unique_hfont& hFont,
_Inout_ wil::unique_hfont& hFontItalic) noexcept
{
wil::unique_hdc hdcTemp(CreateCompatibleDC(_hdcMemoryContext));
RETURN_HR_IF_NULL(E_FAIL, hdcTemp.get());
// Get a special engine size because TT fonts can't specify X or we'll get weird scaling under some circumstances.
auto coordFontRequested = FontDesired.GetEngineSize();
// First, check to see if we're asking for the default raster font.
if (FontDesired.IsDefaultRasterFont())
{
// We're being asked for the default raster font, which gets special handling. In particular, it's the font
// returned by GetStockObject(OEM_FIXED_FONT).
// We do this because, for instance, if we ask GDI for an 8x12 OEM_FIXED_FONT,
// it may very well decide to choose Courier New instead of the Terminal raster.
#pragma prefast(suppress : 38037, "raster fonts get special handling, we need to get it this way")
hFont.reset((HFONT)GetStockObject(OEM_FIXED_FONT));
hFontItalic.reset((HFONT)GetStockObject(OEM_FIXED_FONT));
}
else
{
// For future reference, here is the engine weighting and internal details on Windows Font Mapping:
// https://msdn.microsoft.com/en-us/library/ms969909.aspx
// More relevant links:
// https://support.microsoft.com/en-us/kb/94646
// IMPORTANT: Be very careful when modifying the values being passed in below. Even the slightest change can cause
// GDI to return a font other than the one being requested. If you must change the below for any reason, make sure
// these fonts continue to work correctly, as they've been known to break:
// * Monofur
// * Iosevka Extralight
//
// While you're at it, make sure that the behavior matches what happens in the Fonts property sheet. Pay very close
// attention to the font previews to ensure that the font being selected by GDI is exactly the font requested --
// some monospace fonts look very similar.
LOGFONTW lf = { 0 };
lf.lfHeight = s_ScaleByDpi(coordFontRequested.height, iDpi);
lf.lfWidth = s_ScaleByDpi(coordFontRequested.width, iDpi);
lf.lfWeight = FontDesired.GetWeight();
// If we're searching for Terminal, our supported Raster Font, then we must use OEM_CHARSET.
// If the System's Non-Unicode Setting is set to English (United States) which is 437
// and we try to enumerate Terminal with the console codepage as 932, that will turn into SHIFTJIS_CHARSET.
// Despite C:\windows\fonts\vga932.fon always being present, GDI will refuse to load the Terminal font
// that doesn't correspond to the current System Non-Unicode Setting. It will then fall back to a TrueType
// font that does support the SHIFTJIS_CHARSET (because Terminal with CP 437 a.k.a. C:\windows\fonts\vgaoem.fon does NOT support it.)
// This is OK for display purposes (things will render properly) but not OK for API purposes.
// Because the API is affected by the raster/TT status of the actively selected font, we can't have
// GDI choosing a TT font for us when we ask for Raster. We have to settle for forcing the current system
// Terminal font to load even if it doesn't have the glyphs necessary such that the APIs continue to work fine.
if (FontDesired.GetFaceName() == DEFAULT_RASTER_FONT_FACENAME)
{
lf.lfCharSet = OEM_CHARSET;
}
else
{
CHARSETINFO csi;
if (!TranslateCharsetInfo((DWORD*)IntToPtr(FontDesired.GetCodePage()), &csi, TCI_SRCCODEPAGE))
{
// if we failed to translate from codepage to charset, choose our charset depending on what kind of font we're
// dealing with. Raster Fonts need to be presented with the OEM charset, while TT fonts need to be ANSI.
csi.ciCharset = FontDesired.IsTrueTypeFont() ? ANSI_CHARSET : OEM_CHARSET;
}
lf.lfCharSet = (BYTE)csi.ciCharset;
}
lf.lfQuality = DRAFT_QUALITY;
// NOTE: not using what GDI gave us because some fonts don't quite roundtrip (e.g. MS Gothic and VL Gothic)
lf.lfPitchAndFamily = (FIXED_PITCH | FF_MODERN);
FontDesired.FillLegacyNameBuffer(lf.lfFaceName);
// Create font.
hFont.reset(CreateFontIndirectW(&lf));
RETURN_HR_IF_NULL(E_FAIL, hFont.get());
// Create italic variant of the font.
lf.lfItalic = TRUE;
hFontItalic.reset(CreateFontIndirectW(&lf));
RETURN_HR_IF_NULL(E_FAIL, hFontItalic.get());
}
// Select into DC
wil::unique_hfont hFontOld(SelectFont(hdcTemp.get(), hFont.get()));
RETURN_HR_IF_NULL(E_FAIL, hFontOld.get());
// Save off the font metrics for various other calculations
TEXTMETRICW tm;
RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(hdcTemp.get(), &tm)));
// Now find the size of a 0 in this current font and save it for conversions done later.
SIZE sz;
RETURN_HR_IF(E_FAIL, !(GetTextExtentPoint32W(hdcTemp.get(), L"0", 1, &sz)));
til::size coordFont;
coordFont.width = sz.cx;
coordFont.height = sz.cy;
// The extent point won't necessarily be perfect for the width, so get the ABC metrics for the 0 if possible to improve the measurement.
// This will fail for non-TrueType fonts and we'll fall back to what GetTextExtentPoint said.
{
ABC abc;
if (0 != GetCharABCWidthsW(hdcTemp.get(), '0', '0', &abc))
{
const auto abcTotal = abc.abcA + abc.abcB + abc.abcC;
// No negatives or zeros or we'll have bad character-to-pixel math later.
if (abcTotal > 0)
{
coordFont.width = abcTotal;
}
}
}
// Now fill up the FontInfo we were passed with the full details of which font we actually chose
{
// Get the actual font face that we chose
const auto faceNameLength{ gsl::narrow<size_t>(GetTextFaceW(hdcTemp.get(), 0, nullptr)) };
std::wstring currentFaceName{};
currentFaceName.resize(faceNameLength);
RETURN_HR_IF(E_FAIL, !(GetTextFaceW(hdcTemp.get(), gsl::narrow_cast<int>(faceNameLength), currentFaceName.data())));
currentFaceName.resize(faceNameLength - 1); // remove the null terminator (wstring!)
if (FontDesired.IsDefaultRasterFont())
{
coordFontRequested = coordFont;
}
else if (coordFontRequested.width == 0)
{
coordFontRequested.width = s_ShrinkByDpi(coordFont.width, iDpi);
}
Font.SetFromEngine(currentFaceName,
tm.tmPitchAndFamily,
gsl::narrow_cast<unsigned int>(tm.tmWeight),
FontDesired.IsDefaultRasterFont(),
coordFont,
coordFontRequested);
}
return S_OK;
}
// Routine Description:
// - Retrieves the current pixel size of the font we have selected for drawing.
// Arguments:
// - pFontSize - receives the current X by Y size of the font.
// Return Value:
// - S_OK
[[nodiscard]] HRESULT GdiEngine::GetFontSize(_Out_ til::size* pFontSize) noexcept
{
*pFontSize = _GetFontSize();
return S_OK;
}
// Routine Description:
// - Retrieves the current pixel size of the font we have selected for drawing.
// Arguments:
// - <none>
// Return Value:
// - X by Y size of the font.
til::size GdiEngine::_GetFontSize() const
{
return _coordFontLast;
}
// Routine Description:
// - Retrieves whether or not the window is currently minimized.
// Arguments:
// - <none>
// Return Value:
// - True if minimized (don't need to draw anything). False otherwise.
bool GdiEngine::_IsMinimized() const
{
return !!IsIconic(_hwndTargetWindow);
}
// Routine Description:
// - Determines whether or not we have a TrueType font selected.
// - Intended only for determining whether we need to perform special raster font scaling.
// Arguments:
// - <none>
// Return Value:
// - True if TrueType. False otherwise (and generally assumed to be raster font type.)
bool GdiEngine::_IsFontTrueType() const
{
return !!(_tmFontMetrics.tmPitchAndFamily & TMPF_TRUETYPE);
}
// Routine Description:
// - Helper to determine whether our window handle is valid.
// Allows us to skip operations if we don't have a window.
// Return Value:
// - True if it is valid.
// - False if it is known invalid.
bool GdiEngine::_IsWindowValid() const
{
return _hwndTargetWindow != INVALID_HANDLE_VALUE &&
_hwndTargetWindow != nullptr;
}

View File

@@ -1,78 +1,116 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- IRenderData.hpp
Abstract:
- This serves as the interface defining all information needed to render to the screen.
Author(s):
- Michael Niksa (MiNiksa) 17-Nov-2015
--*/
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../../host/conimeinfo.h"
#include "../../buffer/out/TextAttribute.hpp"
#include <til/generational.h>
#include "CSSLengthPercentage.h"
#include "../../buffer/out/textBuffer.hpp"
class Cursor;
namespace Microsoft::Console::Render
{
struct RenderOverlay final
enum class CursorStyle
{
// This is where the data is stored
const TextBuffer& buffer;
// This is where the top left of the stored buffer should be overlaid on the screen
// (relative to the current visible viewport)
const til::point origin;
// This is the area of the buffer that is actually used for overlay.
// Anything outside of this is considered empty by the overlay and shouldn't be used
// for painting purposes.
const Microsoft::Console::Types::Viewport region;
Invert = 0,
Color,
};
class IRenderData
enum class TextAntialiasMode
{
Default = 0,
ClearType,
Grayscale,
Aliased,
};
struct TargetSettings
{
COLORREF paddingColor = 0;
bool forceFullRepaint = false;
bool transparentBackground = false;
bool softwareRendering = false;
};
struct FontSettings
{
std::wstring faceName;
int family = 0;
int weight = 0;
int codePage = 0;
float fontSize = 0;
CSSLengthPercentage cellWidth;
CSSLengthPercentage cellHeight;
std::unordered_map<std::wstring_view, uint32_t> features;
std::unordered_map<std::wstring_view, float> axes;
TextAntialiasMode antialiasingMode = TextAntialiasMode::Default;
float dpi = 0;
};
struct CursorSettings
{
CursorType type = CursorType::Legacy;
CursorStyle style = CursorStyle::Invert;
COLORREF color = 0xffffffff;
float heightPercentage = 0.2f;
float widthInDIP = 1.0f;
};
struct SelectionSettings
{
COLORREF selectionColor = 0x7fffffff;
};
struct ShaderSettings
{
std::wstring shaderPath;
bool retroTerminalEffect = false;
};
struct Settings
{
til::generational<TargetSettings> target;
til::generational<FontSettings> font;
til::generational<CursorSettings> cursor;
til::generational<SelectionSettings> selection;
til::generational<ShaderSettings> shader;
til::size targetSizeInPixel;
};
struct RenderingLayer
{
TextBuffer* source;
til::rect sourceRegion;
til::point targetOrigin;
};
struct RenderData
{
til::generational<Settings> settings;
std::vector<til::rect> selections;
uint16_t hoveredHyperlinkId = 0;
std::vector<RenderingLayer> layers;
};
struct RenderingPayload
{
til::generational<Settings> settings;
std::vector<til::rect> selections;
uint16_t hoveredHyperlinkId = 0;
TextBuffer buffer;
};
struct IRenderData
{
public:
virtual ~IRenderData() = default;
// This block used to be IBaseData.
virtual Microsoft::Console::Types::Viewport GetViewport() noexcept = 0;
virtual til::point GetTextBufferEndPosition() const noexcept = 0;
virtual const TextBuffer& GetTextBuffer() const noexcept = 0;
virtual const FontInfo& GetFontInfo() const noexcept = 0;
virtual std::vector<Microsoft::Console::Types::Viewport> GetSelectionRects() noexcept = 0;
virtual void LockConsole() noexcept = 0;
virtual void UnlockConsole() noexcept = 0;
// This block used to be the original IRenderData.
virtual til::point GetCursorPosition() const noexcept = 0;
virtual bool IsCursorVisible() const noexcept = 0;
virtual bool IsCursorOn() const noexcept = 0;
virtual ULONG GetCursorHeight() const noexcept = 0;
virtual CursorType GetCursorStyle() const noexcept = 0;
virtual ULONG GetCursorPixelWidth() const noexcept = 0;
virtual bool IsCursorDoubleWidth() const = 0;
virtual const std::vector<RenderOverlay> GetOverlays() const noexcept = 0;
virtual const bool IsGridLineDrawingAllowed() noexcept = 0;
virtual const std::wstring_view GetConsoleTitle() const noexcept = 0;
virtual const std::wstring GetHyperlinkUri(uint16_t id) const = 0;
virtual const std::wstring GetHyperlinkCustomId(uint16_t id) const = 0;
virtual const std::vector<size_t> GetPatternId(const til::point location) const = 0;
// This block used to be IUiaData.
virtual std::pair<COLORREF, COLORREF> GetAttributeColors(const TextAttribute& attr) const noexcept = 0;
virtual const bool IsSelectionActive() const = 0;
virtual const bool IsBlockSelection() const = 0;
virtual void ClearSelection() = 0;
virtual void SelectNewRegion(const til::point coordStart, const til::point coordEnd) = 0;
virtual const til::point GetSelectionAnchor() const noexcept = 0;
virtual const til::point GetSelectionEnd() const noexcept = 0;
virtual const bool IsUiaDataInitialized() const noexcept = 0;
virtual void UpdateRenderData(RenderData& data) = 0;
};
}

Some files were not shown because too many files have changed in this diff Show More