From 90ff261c35a1b3e50267576f5cbdc66733731101 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 6 Aug 2021 21:41:02 +0100 Subject: [PATCH] Add support for downloadable soft fonts (#10011) This PR adds conhost support for downloadable soft fonts - also known as dynamically redefinable character sets (DRCS) - using the `DECDLD` escape sequence. These fonts are typically designed to work on a specific terminal model, and each model tends to have a different character cell size. So in order to support as many models as possible, the code attempts to detect the original target size of the font, and then scale the glyphs to fit our current cell size. Once a font has been downloaded to the terminal, it can be designated in the same way you would a standard character set, using an `SCS` escape sequence. The identification string for the set is defined by the `DECDLD` sequence. Internally we map the characters in this set to code points `U+EF20` to `U+EF7F` in the Unicode private use are (PUA). Then in the renderer, any characters in that range are split off into separate runs, which get painted with a special font. The font itself is dynamically generated as an in-memory resource, constructed from the downloaded character bitmaps which have been scaled to the appropriate size. If no soft fonts are in use, then no mapping of the PUA code points will take place, so this shouldn't interfere with anyone using those code points for something else, as along as they aren't also trying to use soft fonts. I also tried to pick a PUA range that hadn't already been snatched up by Nerd Fonts, but if we do receive reports of a conflict, it's easy enough to change. ## Validation Steps Performed I added an adapter test that runs through a bunch of parameter variations for the `DECDLD` sequence, to make sure we're correctly detecting the font sizes for most of the known DEC terminal models. I've also tested manually on a wide range of existing fonts, of varying dimensions, and from multiple sources, and made sure they all worked reasonably well. Closes #9164 --- .github/actions/spelling/allow/apis.txt | 1 + .github/actions/spelling/expect/expect.txt | 14 + src/host/getset.cpp | 24 + src/host/getset.h | 4 + src/host/outputStream.cpp | 15 + src/host/outputStream.hpp | 4 + src/host/ut_host/VtRendererTests.cpp | 50 +- src/interactivity/onecore/BgfxEngine.cpp | 1 + src/interactivity/onecore/BgfxEngine.hpp | 1 + src/renderer/base/FontResource.cpp | 285 +++++++++ src/renderer/base/RenderEngineBase.cpp | 7 + src/renderer/base/lib/base.vcxproj | 2 + src/renderer/base/lib/base.vcxproj.filters | 6 + src/renderer/base/renderer.cpp | 52 +- src/renderer/base/renderer.hpp | 13 +- src/renderer/base/sources.inc | 1 + src/renderer/dx/DxRenderer.cpp | 2 + src/renderer/dx/DxRenderer.hpp | 1 + src/renderer/gdi/gdirenderer.hpp | 15 +- src/renderer/gdi/paint.cpp | 4 + src/renderer/gdi/state.cpp | 63 +- src/renderer/inc/FontResource.hpp | 45 ++ src/renderer/inc/IRenderEngine.hpp | 4 + src/renderer/inc/IRenderer.hpp | 4 + src/renderer/inc/RenderEngineBase.hpp | 4 + src/renderer/uia/UiaRenderer.cpp | 2 + src/renderer/uia/UiaRenderer.hpp | 1 + src/renderer/vt/Xterm256Engine.cpp | 2 + src/renderer/vt/Xterm256Engine.hpp | 1 + src/renderer/vt/XtermEngine.cpp | 2 + src/renderer/vt/XtermEngine.hpp | 1 + src/renderer/vt/vtrenderer.hpp | 1 + src/renderer/wddmcon/WddmConRenderer.cpp | 1 + src/renderer/wddmcon/WddmConRenderer.hpp | 1 + src/terminal/adapter/DispatchTypes.hpp | 40 ++ src/terminal/adapter/FontBuffer.cpp | 603 ++++++++++++++++++ src/terminal/adapter/FontBuffer.hpp | 95 +++ src/terminal/adapter/ITermDispatch.hpp | 11 + src/terminal/adapter/adaptDispatch.cpp | 87 +++ src/terminal/adapter/adaptDispatch.hpp | 11 + src/terminal/adapter/charsets.hpp | 11 + src/terminal/adapter/conGetSet.hpp | 4 + src/terminal/adapter/lib/adapter.vcxproj | 2 + .../adapter/lib/adapter.vcxproj.filters | 6 + src/terminal/adapter/sources.inc | 1 + src/terminal/adapter/termDispatch.hpp | 9 + src/terminal/adapter/terminalOutput.cpp | 234 ++++--- src/terminal/adapter/terminalOutput.hpp | 7 + .../adapter/ut_adapter/adapterTest.cpp | 238 +++++++ .../parser/OutputStateMachineEngine.cpp | 19 +- .../parser/OutputStateMachineEngine.hpp | 7 +- 51 files changed, 1903 insertions(+), 116 deletions(-) create mode 100644 src/renderer/base/FontResource.cpp create mode 100644 src/renderer/inc/FontResource.hpp create mode 100644 src/terminal/adapter/FontBuffer.cpp create mode 100644 src/terminal/adapter/FontBuffer.hpp diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 87d479f7ff2..85121e065bf 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -133,6 +133,7 @@ SRWLOCK STDCPP STDMETHOD strchr +strcpy streambuf strtoul Stubless diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 94f8fc91164..05ccc895a55 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -169,6 +169,7 @@ brandings BRK Browsable bsearch +Bspace bstr BTNFACE buf @@ -270,10 +271,12 @@ cmder CMDEXT Cmdlet cmdline +cmh CMOUSEBUTTONS cmp cmpeq cmt +cmw cmyk CNL cnt @@ -406,11 +409,13 @@ csbiex csharp CSHORT CSIDL +Cspace csproj Csr csrmsg CSRSS csrutil +css cstdarg cstddef cstdio @@ -509,6 +514,7 @@ DECAWM DECCKM DECCOLM DECDHL +DECDLD DECDWL DECEKBD DECID @@ -789,6 +795,7 @@ FONTENUMPROC FONTFACE FONTFAMILY FONTHEIGHT +FONTINFO fontlist FONTOK FONTSIZE @@ -902,6 +909,7 @@ github gitlab gle globals +GLYPHENTRY gmail GMEM GNUC @@ -950,6 +958,7 @@ hdrstop HEIGHTSCROLL hfile hfont +hfontresource hglobal hhh HHmm @@ -1272,6 +1281,7 @@ locsrc locstudio Loewen LOGFONT +LOGFONTA LOGFONTW logissue lowercased @@ -1935,6 +1945,7 @@ realloc reamapping rects redef +redefinable Redir redirector redist @@ -1980,6 +1991,7 @@ rfc rftp rgb rgba +RGBCOLOR rgbi rgci rgfae @@ -2149,6 +2161,7 @@ SIGDN SINGLEFLAG SINGLETHREADED siup +sixel SIZEBOX sizeof SIZESCROLL @@ -2754,6 +2767,7 @@ WTo wtof wtoi WTs +WTSOFTFONT wtw wtypes Wubi diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 6e9e7019101..9c3ccde4456 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1639,6 +1639,30 @@ void DoSrvEndHyperlink(SCREEN_INFORMATION& screenInfo) screenInfo.GetTextBuffer().SetCurrentAttributes(attr); } +// Routine Description: +// - A private API call for updating the active soft font. +// 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 we succeeded, otherwise the HRESULT of the failure. +[[nodiscard]] HRESULT DoSrvUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + try + { + auto* pRender = ServiceLocator::LocateGlobals().pRender; + if (pRender) + { + pRender->UpdateSoftFont(bitPattern, cellSize, centeringHint); + } + return S_OK; + } + CATCH_RETURN(); +} + // Routine Description: // - A private API call for forcing the renderer to repaint the screen. If the // input screen buffer is not the active one, then just do nothing. We only diff --git a/src/host/getset.h b/src/host/getset.h index 43b8d5fefe5..1fb48e286a9 100644 --- a/src/host/getset.h +++ b/src/host/getset.h @@ -55,6 +55,10 @@ void DoSrvAddHyperlink(SCREEN_INFORMATION& screenInfo, void DoSrvEndHyperlink(SCREEN_INFORMATION& screenInfo); +[[nodiscard]] HRESULT DoSrvUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept; + void DoSrvPrivateRefreshWindow(const SCREEN_INFORMATION& screenInfo); [[nodiscard]] HRESULT DoSrvSetConsoleOutputCodePage(const unsigned int codepage); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 3129f0a1ad9..a70b48afd0c 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -818,3 +818,18 @@ bool ConhostInternalGetSet::PrivateEndHyperlink() const DoSrvEndHyperlink(_io.GetActiveOutputBuffer()); return true; } + +// Routine Description: +// - Replaces 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: +// - true if successful (see DoSrvUpdateSoftFont). false otherwise. +bool ConhostInternalGetSet::PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + return SUCCEEDED(DoSrvUpdateSoftFont(bitPattern, cellSize, centeringHint)); +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 34048e0260a..99889baca46 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -147,6 +147,10 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: bool PrivateAddHyperlink(const std::wstring_view uri, const std::wstring_view params) const override; bool PrivateEndHyperlink() const override; + bool PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; + private: Microsoft::Console::IIoProvider& _io; }; diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index b1785380587..a12dd86739e 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -420,6 +420,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;5;6;7m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x00030201, 0x00070605 }, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -428,6 +429,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;7;8;9m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x00030201, 0x00090807 }, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -435,6 +437,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;2;10;11;12m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x000c0b0a, 0x00090807 }, &renderData, + false, false)); }); @@ -444,6 +447,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({ 0x000c0b0a, 0x00090807 }, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -458,6 +462,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -469,6 +474,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -477,6 +483,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -485,6 +492,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;2;19;161;14m"); // Background RGB(19,161,14) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -493,6 +501,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;2;193;156;0m"); // Foreground RGB(193,156,0) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -501,6 +510,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[49m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -509,6 +519,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[38;5;7m"); // Foreground DARK_WHITE (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -517,6 +528,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[48;5;1m"); // Background DARK_RED (256-Color Index) VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -525,6 +537,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[39m"); // Background default VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -533,6 +546,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); }); @@ -542,6 +556,7 @@ void VtRendererTest::Xterm256TestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -795,7 +810,7 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { @@ -841,29 +856,29 @@ void VtRendererTest::Xterm256TestAttributesAcrossReset() break; } qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[32m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[42m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } @@ -1081,6 +1096,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); TestPaint(*engine, [&]() { @@ -1092,6 +1108,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1100,6 +1117,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1108,6 +1126,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[42m"); // Background DARK_GREEN VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1116,6 +1135,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[33m"); // Foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1125,6 +1145,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[33m"); // Reapply foreground DARK_YELLOW VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1133,6 +1154,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1141,6 +1163,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1150,6 +1173,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[41m"); // Reapply background DARK_RED VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); Log::Comment(NoThrowString().Format( @@ -1158,6 +1182,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back("\x1b[m"); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, + false, false)); }); @@ -1167,6 +1192,7 @@ void VtRendererTest::XtermTestColors() qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes({}, &renderData, + false, false)); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback }); @@ -1304,7 +1330,7 @@ void VtRendererTest::XtermTestAttributesAcrossReset() Log::Comment(L"----Start With All Attributes Reset----"); TextAttribute textAttributes = {}; qExpectedInput.push_back("\x1b[m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); switch (renditionAttribute) { @@ -1322,29 +1348,29 @@ void VtRendererTest::XtermTestAttributesAcrossReset() break; } qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Foreground----"); textAttributes.SetIndexedForeground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[32m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Foreground and Retain Rendition----"); textAttributes.SetDefaultForeground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Set Green Background----"); textAttributes.SetIndexedBackground(FOREGROUND_GREEN); qExpectedInput.push_back("\x1b[42m"); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); Log::Comment(L"----Reset Default Background and Retain Rendition----"); textAttributes.SetDefaultBackground(); qExpectedInput.push_back("\x1b[m"); qExpectedInput.push_back(renditionSequence.str()); - VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false)); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(textAttributes, &renderData, false, false)); VerifyExpectedInputsDrained(); } diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index cf690ecd398..dcd3f923e7f 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -196,6 +196,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid [[nodiscard]] HRESULT BgfxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, bool const /*isSettingDefaultBrushes*/) noexcept { _currentLegacyColorAttribute = textAttributes.GetLegacyAttributes(); diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 4fddcfb0b39..2b4e787a31e 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -60,6 +60,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, bool const isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/renderer/base/FontResource.cpp b/src/renderer/base/FontResource.cpp new file mode 100644 index 00000000000..38263033a2e --- /dev/null +++ b/src/renderer/base/FontResource.cpp @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "../inc/FontResource.hpp" + +using namespace Microsoft::Console::Render; + +namespace +{ + // The structures below are based on the Windows 3.0 font file format, which + // was documented in Microsoft Knowledge Base article Q65123. Although no + // longer hosted by Microsoft, it can still be found at the following URL: + // https://web.archive.org/web/20140820153410/http://support.microsoft.com/kb/65123 + + // For now we're only using fixed pitch single color fonts, but the rest + // of the flags are included here for completeness. + static constexpr DWORD DFF_FIXED = 0x0001; + static constexpr DWORD DFF_PROPORTIONAL = 0x0002; + static constexpr DWORD DFF_1COLOR = 0x0010; + static constexpr DWORD DFF_16COLOR = 0x0020; + static constexpr DWORD DFF_256COLOR = 0x0040; + static constexpr DWORD DFF_RGBCOLOR = 0x0080; + + // DRCS soft fonts only require 96 characters at most. + static constexpr size_t CHAR_COUNT = 96; + +#pragma pack(push, 1) + struct GLYPHENTRY + { + WORD geWidth; + DWORD geOffset; + }; + + struct FONTINFO + { + WORD dfVersion; + DWORD dfSize; + CHAR dfCopyright[60]; + WORD dfType; + WORD dfPoints; + WORD dfVertRes; + WORD dfHorizRes; + WORD dfAscent; + WORD dfInternalLeading; + WORD dfExternalLeading; + BYTE dfItalic; + BYTE dfUnderline; + BYTE dfStrikeOut; + WORD dfWeight; + BYTE dfCharSet; + WORD dfPixWidth; + WORD dfPixHeight; + BYTE dfPitchAndFamily; + WORD dfAvgWidth; + WORD dfMaxWidth; + BYTE dfFirstChar; + BYTE dfLastChar; + BYTE dfDefaultChar; + BYTE dfBreakChar; + WORD dfWidthBytes; + DWORD dfDevice; + DWORD dfFace; + DWORD dfBitsPointer; + DWORD dfBitsOffset; + BYTE dfReserved; + DWORD dfFlags; + WORD dfAspace; + WORD dfBspace; + WORD dfCspace; + DWORD dfColorPointer; + DWORD dfReserved1[4]; + GLYPHENTRY dfCharTable[CHAR_COUNT]; + CHAR szFaceName[LF_FACESIZE]; + }; +#pragma pack(pop) +} + +FontResource::FontResource(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint) : + _bitPattern{ bitPattern.begin(), bitPattern.end() }, + _sourceSize{ sourceSize }, + _targetSize{ targetSize }, + _centeringHint{ centeringHint } +{ +} + +void FontResource::SetTargetSize(const til::size targetSize) +{ + if (_targetSize != targetSize) + { + _targetSize = targetSize; + _fontHandle = nullptr; + } +} + +FontResource::operator HFONT() +{ + if (!_fontHandle && !_bitPattern.empty()) + { + _regenerateFont(); + } + return _fontHandle.get(); +} + +void FontResource::_regenerateFont() +{ + const auto targetWidth = _targetSize.width(); + const auto targetHeight = _targetSize.height(); + const auto charSizeInBytes = (targetWidth + 7) / 8 * targetHeight; + + const DWORD fontBitmapSize = charSizeInBytes * CHAR_COUNT; + const DWORD fontResourceSize = sizeof(FONTINFO) + fontBitmapSize; + + auto fontResourceBuffer = std::vector(fontResourceSize); + void* fontResourceBufferPointer = fontResourceBuffer.data(); + auto& fontResource = *static_cast(fontResourceBufferPointer); + + fontResource.dfVersion = 0x300; + fontResource.dfSize = fontResourceSize; + fontResource.dfWeight = FW_NORMAL; + fontResource.dfCharSet = OEM_CHARSET; + fontResource.dfPixWidth = targetWidth; + fontResource.dfPixHeight = targetHeight; + fontResource.dfPitchAndFamily = FIXED_PITCH | FF_DONTCARE; + fontResource.dfAvgWidth = targetWidth; + fontResource.dfMaxWidth = targetWidth; + fontResource.dfFirstChar = L' '; + fontResource.dfLastChar = fontResource.dfFirstChar + CHAR_COUNT - 1; + fontResource.dfFace = offsetof(FONTINFO, szFaceName); + fontResource.dfBitsOffset = sizeof(FONTINFO); + fontResource.dfFlags = DFF_FIXED | DFF_1COLOR; + + // We use an atomic counter to create a locally-unique name for the font. + static std::atomic faceNameCounter; + sprintf_s(fontResource.szFaceName, "WTSOFTFONT%016llX", faceNameCounter++); + + // Each character has a fixed size and position in the font bitmap, but we + // still need to fill in the header table with that information. + for (auto i = 0u; i < std::size(fontResource.dfCharTable); i++) + { + const auto charOffset = fontResource.dfBitsOffset + charSizeInBytes * i; + fontResource.dfCharTable[i].geOffset = charOffset; + fontResource.dfCharTable[i].geWidth = targetWidth; + } + + // Raster fonts aren't generally scalable, so we need to resize the bit + // patterns for the character glyphs to the requested target size, and + // copy the results into the resource structure. + auto fontResourceSpan = gsl::span(fontResourceBuffer); + _resizeBitPattern(fontResourceSpan.subspan(fontResource.dfBitsOffset)); + + DWORD fontCount = 0; + _resourceHandle.reset(AddFontMemResourceEx(&fontResource, fontResourceSize, nullptr, &fontCount)); + LOG_HR_IF_NULL(E_FAIL, _resourceHandle.get()); + + // Once the resource has been registered, we should be able to create the + // font by using the same name and attributes as were set in the resource. + LOGFONTA logFont = {}; + logFont.lfHeight = fontResource.dfPixHeight; + logFont.lfWidth = fontResource.dfPixWidth; + logFont.lfCharSet = fontResource.dfCharSet; + logFont.lfOutPrecision = OUT_RASTER_PRECIS; + logFont.lfPitchAndFamily = fontResource.dfPitchAndFamily; + strcpy_s(logFont.lfFaceName, fontResource.szFaceName); + _fontHandle.reset(CreateFontIndirectA(&logFont)); + LOG_HR_IF_NULL(E_FAIL, _fontHandle.get()); +} + +void FontResource::_resizeBitPattern(gsl::span targetBuffer) +{ + auto sourceWidth = _sourceSize.width(); + auto targetWidth = _targetSize.width(); + const auto sourceHeight = _sourceSize.height(); + const auto targetHeight = _targetSize.height(); + + // If the text in the font is not perfectly centered, the _centeringHint + // gives us the offset needed to correct that misalignment. So to ensure + // that any inserted or deleted columns are evenly spaced around the center + // point of the glyphs, we need to adjust the source and target widths by + // that amount (proportionally) before calculating the scaling increments. + targetWidth -= std::lround((double)_centeringHint * targetWidth / sourceWidth); + sourceWidth -= gsl::narrow_cast(_centeringHint); + + // The way the scaling works is by iterating over the target range, and + // calculating the source offsets that correspond to each target position. + // We achieve that by incrementing the source offset every iteration by an + // integer value that is the quotient of the source and target dimensions. + // Because this is an integer division, we're going to be off by a certain + // fraction on each iteration, so we need to keep track of that accumulated + // error using the modulus of the division. Once the error total exceeds + // the target dimension (more or less), we add another pixel to compensate + // for the error, and reset the error total. + const auto createIncrementFunction = [](const auto sourceDimension, const auto targetDimension) { + const auto increment = sourceDimension / targetDimension; + const auto errorIncrement = sourceDimension % targetDimension * 2; + const auto errorThreshold = targetDimension * 2 - std::min(sourceDimension, targetDimension); + const auto errorReset = targetDimension * 2; + + return [=](auto& errorTotal) { + errorTotal += errorIncrement; + if (errorTotal > errorThreshold) + { + errorTotal -= errorReset; + return increment + 1; + } + return increment; + }; + }; + const auto columnIncrement = createIncrementFunction(sourceWidth, targetWidth); + const auto lineIncrement = createIncrementFunction(sourceHeight, targetHeight); + + // Once we've calculated the scaling increments, taking the centering hint + // into account, we reset the target width back to its original value. + targetWidth = _targetSize.width(); + + auto targetBufferPointer = targetBuffer.begin(); + for (auto ch = 0; ch < CHAR_COUNT; ch++) + { + // Bits are read from the source from left to right - MSB to LSB. The source + // column is a single bit representing the 1-based position. The reason for + // this will become clear in the mask calculation below. + auto sourceColumn = 1 << 16; + auto sourceColumnError = 0; + + // The target format expects the character bitmaps to be laid out in columns + // of 8 bits. So we generate 8 bits from each scanline until we've covered + // the full target height. Then we start again from the top with the next 8 + // bits of the line, until we've covered the full target width. + for (auto targetX = 0; targetX < targetWidth; targetX += 8) + { + auto sourceLine = std::next(_bitPattern.begin(), ch * sourceHeight); + auto sourceLineError = 0; + + // Since we're going to be reading from the same horizontal offset for each + // target line, we save the state here so we can reset it every iteration. + const auto initialSourceColumn = sourceColumn; + const auto initialSourceColumnError = sourceColumnError; + + for (auto targetY = 0; targetY < targetHeight; targetY++) + { + sourceColumn = initialSourceColumn; + sourceColumnError = initialSourceColumnError; + + // For a particular target line, we calculate the span of source lines from + // which it is derived, then OR those values together. We don't want the + // source value to be zero, though, so we must read at least one line. + const auto lineSpan = lineIncrement(sourceLineError); + auto sourceValue = 0; + for (auto i = 0; i < std::max(lineSpan, 1); i++) + { + sourceValue |= sourceLine[i]; + } + std::advance(sourceLine, lineSpan); + + // From the combined value of the source lines, we now need to extract eight + // bits to make up the next byte in the target at the current X offset. + byte targetValue = 0; + for (auto targetBit = 0; targetBit < 8; targetBit++) + { + targetValue <<= 1; + if (targetX + targetBit < targetWidth) + { + // As with the line iteration, we first need to calculate the span of source + // columns from which the target bit is derived. We shift our source column + // position right by that amount to determine the next column position, then + // subtract those two values to obtain a mask. For example, if we're reading + // from columns 6 to 3 (exclusively), the initial column position is 1<<6, + // the next column position is 1<<3, so the mask is 64-8=56, or 00111000. + // Again we don't want this mask to be zero, so if the span is zero, we need + // to shift an additional bit to make sure we cover at least one column. + const auto columnSpan = columnIncrement(sourceColumnError); + const auto nextSourceColumn = sourceColumn >> columnSpan; + const auto sourceMask = sourceColumn - (nextSourceColumn >> (columnSpan ? 0 : 1)); + sourceColumn = nextSourceColumn; + targetValue |= (sourceValue & sourceMask) ? 1 : 0; + } + } + *(targetBufferPointer++) = targetValue; + } + } + } +} diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index f31fff625d7..a32b4b5251c 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -36,6 +36,13 @@ HRESULT RenderEngineBase::UpdateTitle(const std::wstring_view newTitle) noexcept return hr; } +HRESULT RenderEngineBase::UpdateSoftFont(const gsl::span /*bitPattern*/, + const SIZE /*cellSize*/, + const size_t /*centeringHint*/) noexcept +{ + return S_FALSE; +} + HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noexcept { return S_FALSE; diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj index f38398cfc9c..2a9ee32cbb8 100644 --- a/src/renderer/base/lib/base.vcxproj +++ b/src/renderer/base/lib/base.vcxproj @@ -15,6 +15,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters index 1ec046c4332..baff5035236 100644 --- a/src/renderer/base/lib/base.vcxproj.filters +++ b/src/renderer/base/lib/base.vcxproj.filters @@ -27,6 +27,9 @@ Source Files + + Source Files + Source Files @@ -65,6 +68,9 @@ Header Files\inc + + Header Files\inc + Header Files\inc diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index b53bc6eadf6..863bcd24fe1 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -144,7 +144,7 @@ try }); // A. Prep Colors - RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), true)); + RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), false, true)); // B. Perform Scroll Operations RETURN_IF_FAILED(_PerformScrolling(pEngine)); @@ -526,6 +526,35 @@ void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfo _NotifyPaintFrame(); } +// Routine Description: +// - Called when the active soft font has been updated. +// 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: +// - +void Renderer::UpdateSoftFont(const gsl::span bitPattern, const SIZE cellSize, const size_t centeringHint) +{ + // We reserve PUA code points U+EF20 to U+EF7F for soft fonts, but the range + // that we test for in _IsSoftFontChar will depend on the size of the active + // bitPattern. If it's empty (i.e. no soft font is set), then nothing will + // match, and those code points will be treated the same as everything else. + const auto softFontCharCount = cellSize.cy ? bitPattern.size() / cellSize.cy : 0; + _lastSoftFontChar = _firstSoftFontChar + softFontCharCount - 1; + + for (const auto pEngine : _rgpEngines) + { + LOG_IF_FAILED(pEngine->UpdateSoftFont(bitPattern, cellSize, centeringHint)); + } + TriggerRedrawAll(); +} + +bool Renderer::s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar) +{ + return v.size() == 1 && v.front() >= firstSoftFontChar && v.front() <= lastSoftFontChar; +} + // Routine Description: // - Get the information on what font we would be using if we decided to create a font with the given parameters // - This is for use with speculative calculations. @@ -740,6 +769,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, auto color = it->TextAttr(); // Retrieve the first pattern id auto patternIds = _pData->GetPatternId(target); + // Determine whether we're using a soft font. + auto usingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); // And hold the point where we should start drawing. auto screenPoint = target; @@ -756,8 +787,8 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, // Hold onto the current pattern id as well const auto currentPatternId = patternIds; - // Update the drawing brushes with our color. - THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, false)); + // Update the drawing brushes with our color and font usage. + THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, usingSoftFont, false)); // Advance the point by however many columns we've just outputted and reset the accumulator. screenPoint.X += gsl::narrow(cols); @@ -786,15 +817,18 @@ void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, { COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; const auto thisPointPatterns = _pData->GetPatternId(thisPoint); - if (color != it->TextAttr() || patternIds != thisPointPatterns) + const auto thisUsingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar); + const auto changedPatternOrFont = patternIds != thisPointPatterns || usingSoftFont != thisUsingSoftFont; + if (color != it->TextAttr() || changedPatternOrFont) { auto newAttr{ it->TextAttr() }; // foreground doesn't matter for runs of spaces (!) // if we trick it . . . we call Paint far fewer times for cmatrix - if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || patternIds != thisPointPatterns) + if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || changedPatternOrFont) { color = newAttr; patternIds = thisPointPatterns; + usingSoftFont = thisUsingSoftFont; break; // vend this run } } @@ -1183,17 +1217,21 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) // Arguments: // - pEngine - Which engine is being updated // - textAttributes - The 16 color foreground/background combination to set +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Alerts that the default brushes are being set which will // impact whether or not to include the hung window/erase window brushes in this operation // and can affect other draw state that wants to know the default color scheme. // (Usually only happens when the default is changed, not when each individual color is swapped in a multi-color run.) // Return Value: // - -[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute textAttributes, const bool isSettingDefaultBrushes) +[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, + const TextAttribute textAttributes, + const bool usingSoftFont, + const bool isSettingDefaultBrushes) { // The last color needs to be each engine's responsibility. If it's local to this function, // then on the next engine we might not update the color. - return pEngine->UpdateDrawingBrushes(textAttributes, _pData, isSettingDefaultBrushes); + return pEngine->UpdateDrawingBrushes(textAttributes, _pData, usingSoftFont, isSettingDefaultBrushes); } // Routine Description: diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 6f8f6d7a8c4..eb8eadf62ed 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -65,6 +65,10 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) override; + void UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) override; + [[nodiscard]] HRESULT GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) override; @@ -120,7 +124,10 @@ namespace Microsoft::Console::Render 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 isSettingDefaultBrushes); + [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, + const TextAttribute attr, + const bool usingSoftFont, + const bool isSettingDefaultBrushes); [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); @@ -138,6 +145,10 @@ namespace Microsoft::Console::Render [[nodiscard]] std::optional _GetCursorInfo(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); + const size_t _firstSoftFontChar = 0xEF20; + size_t _lastSoftFontChar = 0; + static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); + // Helper functions to diagnose issues with painting and layout. // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. bool _fDebug = false; diff --git a/src/renderer/base/sources.inc b/src/renderer/base/sources.inc index 9aeaf8c656f..e3f3d217142 100644 --- a/src/renderer/base/sources.inc +++ b/src/renderer/base/sources.inc @@ -29,6 +29,7 @@ SOURCES = \ ..\FontInfo.cpp \ ..\FontInfoBase.cpp \ ..\FontInfoDesired.cpp \ + ..\FontResource.cpp \ ..\RenderEngineBase.cpp \ ..\renderer.cpp \ ..\thread.cpp \ diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 8d8f238e158..e40a40ffd1c 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1907,11 +1907,13 @@ CATCH_RETURN() // Arguments: // - textAttributes - Text attributes to use for the brush color // - 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 these are the default brushes to paint the swapchain background or selection // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool /*usingSoftFont*/, const bool isSettingDefaultBrushes) noexcept { // GH#5098: If we're rendering with cleartype text, we need to always render diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 1351e61de79..6fdf2fe7f55 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -107,6 +107,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null 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& features, const std::unordered_map& axes) noexcept; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 230b09c4f6c..4caf62ad6ac 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -15,6 +15,7 @@ Author(s): #pragma once #include "../inc/RenderEngineBase.hpp" +#include "../inc/FontResource.hpp" namespace Microsoft::Console::Render { @@ -61,9 +62,13 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; @@ -95,6 +100,7 @@ namespace Microsoft::Console::Render HFONT _hfont; HFONT _hfontItalic; TEXTMETRICW _tmFontMetrics; + FontResource _softFont; static const size_t s_cPolyTextCache = 80; POLYTEXTW _pPolyText[s_cPolyTextCache]; @@ -130,7 +136,14 @@ namespace Microsoft::Console::Render COLORREF _lastFg; COLORREF _lastBg; - bool _lastFontItalic; + + enum class FontType : size_t + { + Default, + Italic, + Soft + }; + FontType _lastFontType; XFORM _currentLineTransform; LineRendition _currentLineRendition; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 089e9777e69..3471359740a 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -336,6 +336,9 @@ using namespace Microsoft::Console::Render; 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; @@ -347,6 +350,7 @@ using namespace Microsoft::Console::Render; const auto text = cluster.GetText(); polyString += text; + polyString.back() &= softFontCharMask; polyWidth.push_back(gsl::narrow(cluster.GetColumns()) * coordFontSize.X); cchCharWidths += polyWidth.back(); polyWidth.append(text.size() - 1, 0); diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 3311e75858e..04a5df7d5cc 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -29,7 +29,7 @@ GdiEngine::GdiEngine() : _fInvalidRectUsed(false), _lastFg(INVALID_COLOR), _lastBg(INVALID_COLOR), - _lastFontItalic(false), + _lastFontType(FontType::Default), _currentLineTransform(IDENTITY_XFORM), _currentLineRendition(LineRendition::SingleWidth), _fPaintStarted(false), @@ -148,8 +148,8 @@ GdiEngine::~GdiEngine() LOG_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont)); } - // Record the fact that the selected font is not italic. - _lastFontItalic = false; + // Record the fact that the selected font is the default. + _lastFontType = FontType::Default; if (nullptr != hdcRealWindow) { @@ -269,12 +269,14 @@ GdiEngine::~GdiEngine() // Arguments: // - textAttributes - Text attributes to use for the brush color // - 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 gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept { RETURN_IF_FAILED(_FlushBufferLines()); @@ -304,12 +306,25 @@ GdiEngine::~GdiEngine() RETURN_IF_FAILED(s_SetWindowLongWHelper(_hwndTargetWindow, GWL_CONSOLE_BKCOLOR, colorBackground)); } - // If the italic attribute has changed, select an appropriate font variant. - const auto fontItalic = textAttributes.IsItalic(); - if (fontItalic != _lastFontItalic) + // 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) { - SelectFont(_hdcMemoryContext, fontItalic ? _hfontItalic : _hfont); - _lastFontItalic = fontItalic; + 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; } return S_OK; @@ -331,8 +346,8 @@ GdiEngine::~GdiEngine() // Select into DC RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, hFont.get())); - // Record the fact that the selected font is not italic. - _lastFontItalic = false; + // Record the fact that the selected font is the default. + _lastFontType = FontType::Default; // Save off the font metrics for various other calculations RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(_hdcMemoryContext, &_tmFontMetrics))); @@ -419,11 +434,39 @@ GdiEngine::~GdiEngine() _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 gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept +{ + // If the soft font is currently selected, replace it with the default font. + 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 = { bitPattern, cellSize, _GetFontSize(), centeringHint }; + + return S_OK; +} + // Routine Description: // - This method will modify the DPI we're using for scaling calculations. // Arguments: diff --git a/src/renderer/inc/FontResource.hpp b/src/renderer/inc/FontResource.hpp new file mode 100644 index 00000000000..aa159ed3461 --- /dev/null +++ b/src/renderer/inc/FontResource.hpp @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontResource.hpp + +Abstract: +- This manages the construction of in-memory font resources for the VT soft fonts. +--*/ + +#pragma once + +namespace wil +{ + typedef unique_any unique_hfontresource; +} + +namespace Microsoft::Console::Render +{ + class FontResource + { + public: + FontResource(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint); + FontResource() = default; + ~FontResource() = default; + FontResource& operator=(FontResource&&) = default; + void SetTargetSize(const til::size targetSize); + operator HFONT(); + + private: + void _regenerateFont(); + void _resizeBitPattern(gsl::span targetBuffer); + + std::vector _bitPattern; + til::size _sourceSize; + til::size _targetSize; + size_t _centeringHint{ 0 }; + wil::unique_hfontresource _resourceHandle; + wil::unique_hfont _fontHandle; + }; +} diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 3d6631e410e..bcc98dfc4df 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -96,9 +96,13 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept = 0; + [[nodiscard]] virtual HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDpi(const int iDpi) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept = 0; diff --git a/src/renderer/inc/IRenderer.hpp b/src/renderer/inc/IRenderer.hpp index 79058c5ae5a..a3894bdb74c 100644 --- a/src/renderer/inc/IRenderer.hpp +++ b/src/renderer/inc/IRenderer.hpp @@ -50,6 +50,10 @@ namespace Microsoft::Console::Render const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) = 0; + virtual void UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) = 0; + [[nodiscard]] virtual HRESULT GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 725347d8215..6ad59fec343 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -38,6 +38,10 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index ba7ca8e5f64..2d5d171969b 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -364,11 +364,13 @@ CATCH_RETURN(); // Arguments: // - textAttributes - // - pData - +// - usingSoftFont - // - isSettingDefaultBrushes - // Return Value: // - S_FALSE since we do nothing [[nodiscard]] HRESULT UiaEngine::UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { return S_FALSE; diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 3ead4673b70..6591579c50c 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -62,6 +62,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp index 91e220c33f2..4edece71563 100644 --- a/src/renderer/vt/Xterm256Engine.cpp +++ b/src/renderer/vt/Xterm256Engine.cpp @@ -20,12 +20,14 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe, // Arguments: // - textAttributes - Text attributes to use for the colors and character rendition // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes: indicates if we should change the background color of // the window. Unused for VT // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT Xterm256Engine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { RETURN_IF_FAILED(VtEngine::_RgbUpdateDrawingBrushes(textAttributes)); diff --git a/src/renderer/vt/Xterm256Engine.hpp b/src/renderer/vt/Xterm256Engine.hpp index b1ed02332eb..4e5913e07e1 100644 --- a/src/renderer/vt/Xterm256Engine.hpp +++ b/src/renderer/vt/Xterm256Engine.hpp @@ -30,6 +30,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT ManuallyClearScrollback() noexcept override; diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index a948acd827e..1c09225facd 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -137,12 +137,14 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // Arguments: // - textAttributes - Text attributes to use for the colors and character rendition // - pData - The interface to console data structures required for rendering +// - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes: indicates if we should change the background color of // the window. Unused for VT // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT XtermEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, const bool /*isSettingDefaultBrushes*/) noexcept { // The base xterm mode only knows about 16 colors diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index 1dcf2d17349..a46458ad64e 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -40,6 +40,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index f7b0a788b66..bb8f0413f14 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -75,6 +75,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, _Out_ FontInfo& pfiFontInfo) noexcept override; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index ed2c027ee4a..ba67819abde 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -307,6 +307,7 @@ bool WddmConEngine::IsInitialized() [[nodiscard]] HRESULT WddmConEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, + const bool /*usingSoftFont*/, bool const /*isSettingDefaultBrushes*/) noexcept { _currentLegacyColorAttribute = textAttributes.GetLegacyAttributes(); diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 6898f1ecef8..57663d5cb95 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -52,6 +52,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, + const bool usingSoftFont, bool const isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 1938791d474..b226346379b 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -434,6 +434,46 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes DependsOnMode }; + enum class DrcsEraseControl : size_t + { + AllChars = 0, + ReloadedChars = 1, + AllRenditions = 2 + }; + + enum class DrcsCellMatrix : size_t + { + Default = 0, + Invalid = 1, + Size5x10 = 2, + Size6x10 = 3, + Size7x10 = 4 + }; + + enum class DrcsFontSet : size_t + { + Default = 0, + Size80x24 = 1, + Size132x24 = 2, + Size80x36 = 11, + Size132x36 = 12, + Size80x48 = 21, + Size132x48 = 22 + }; + + enum class DrcsFontUsage : size_t + { + Default = 0, + Text = 1, + FullCell = 2 + }; + + enum class DrcsCharsetSize : size_t + { + Size94 = 0, + Size96 = 1 + }; + constexpr short s_sDECCOLMSetColumns = 132; constexpr short s_sDECCOLMResetColumns = 80; diff --git a/src/terminal/adapter/FontBuffer.cpp b/src/terminal/adapter/FontBuffer.cpp new file mode 100644 index 00000000000..688325b27ec --- /dev/null +++ b/src/terminal/adapter/FontBuffer.cpp @@ -0,0 +1,603 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "FontBuffer.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +FontBuffer::FontBuffer() noexcept +{ + SetEraseControl(DispatchTypes::DrcsEraseControl::AllRenditions); +}; + +bool FontBuffer::SetEraseControl(const DispatchTypes::DrcsEraseControl eraseControl) noexcept +{ + switch (eraseControl) + { + case DispatchTypes::DrcsEraseControl::AllChars: + case DispatchTypes::DrcsEraseControl::AllRenditions: + // Setting the current cell matrix to an invalid value will guarantee + // that it's different from the pending cell matrix, and any change in + // the font attributes will force the buffer to be cleared. + _cellMatrix = DispatchTypes::DrcsCellMatrix::Invalid; + return true; + case DispatchTypes::DrcsEraseControl::ReloadedChars: + return true; + default: + return false; + } +} + +bool FontBuffer::SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix, + const VTParameter cellHeight, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage) noexcept +{ + auto valid = true; + + if (valid) + { + // We don't yet support screen sizes in which the font is horizontally + // or vertically compressed, so there is not much value in storing a + // separate font for each of the screen sizes. However, we still need + // to use these values to determine the cell size for which the font + // was originally targeted, so we can resize it appropriately. + switch (fontSet) + { + case DispatchTypes::DrcsFontSet::Default: + case DispatchTypes::DrcsFontSet::Size80x24: + _columnsPerPage = 80; + _linesPerPage = 24; + break; + case DispatchTypes::DrcsFontSet::Size80x36: + _columnsPerPage = 80; + _linesPerPage = 36; + break; + case DispatchTypes::DrcsFontSet::Size80x48: + _columnsPerPage = 80; + _linesPerPage = 48; + break; + case DispatchTypes::DrcsFontSet::Size132x24: + _columnsPerPage = 132; + _linesPerPage = 24; + break; + case DispatchTypes::DrcsFontSet::Size132x36: + _columnsPerPage = 132; + _linesPerPage = 36; + break; + case DispatchTypes::DrcsFontSet::Size132x48: + _columnsPerPage = 132; + _linesPerPage = 48; + break; + default: + valid = false; + break; + } + } + + if (valid) + { + switch (fontUsage) + { + case DispatchTypes::DrcsFontUsage::Default: + case DispatchTypes::DrcsFontUsage::Text: + _isTextFont = true; + break; + case DispatchTypes::DrcsFontUsage::FullCell: + _isTextFont = false; + break; + default: + valid = false; + break; + } + } + + if (valid) + { + switch (cellMatrix) + { + case DispatchTypes::DrcsCellMatrix::Invalid: + valid = false; + break; + case DispatchTypes::DrcsCellMatrix::Size5x10: + // Size 5x10 is only valid for text fonts. + valid = _isTextFont; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 5; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Size6x10: + // Size 6x10 is only valid for text fonts, + // unless it's a VT240 in 132-column mode. + valid = _isTextFont || _columnsPerPage == 132; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 6; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Size7x10: + // Size 7x10 is only valid for text fonts. + valid = _isTextFont; + _sizeDeclaredAsMatrix = true; + _declaredWidth = 7; + _declaredHeight = 10; + break; + case DispatchTypes::DrcsCellMatrix::Default: + default: + // If we aren't given one of the predefined matrix sizes, then the + // matrix parameter is a pixel width, and height is obtained from the + // height parameter. This also applies for the default of 0, since a + // 0 width is treated as unknown (we'll try and estimate the expected + // width), and the height parameter can still give us the height. + _sizeDeclaredAsMatrix = false; + _declaredWidth = static_cast(cellMatrix); + _declaredHeight = cellHeight.value_or(0); + valid = (_declaredWidth <= MAX_WIDTH && _declaredHeight <= MAX_HEIGHT); + break; + } + } + + // Save the pending attributes, but don't update the current values until we + // are sure we have a valid sequence that can replace the current buffer. + _pendingCellMatrix = cellMatrix; + _pendingCellHeight = cellHeight.value_or(0); + _pendingFontSet = fontSet; + _pendingFontUsage = fontUsage; + + // Reset the used dimensions. These values will be determined by the extent + // of the sixel data that we receive in the following string sequence. + _usedWidth = 0; + _usedHeight = 0; + + return valid; +} + +bool FontBuffer::SetStartChar(const VTParameter startChar, + const DispatchTypes::DrcsCharsetSize charsetSize) noexcept +{ + switch (charsetSize) + { + case DispatchTypes::DrcsCharsetSize::Size94: + _startChar = startChar.value_or(1); + break; + case DispatchTypes::DrcsCharsetSize::Size96: + _startChar = startChar.value_or(0); + break; + default: + return false; + } + + _currentChar = _startChar; + _pendingCharsetSize = charsetSize; + _charsetIdInitialized = false; + _charsetIdBuilder.Clear(); + + return true; +} + +void FontBuffer::AddSixelData(const wchar_t ch) +{ + if (!_charsetIdInitialized) + { + _buildCharsetId(ch); + } + else if (ch >= L'?' && ch <= L'~') + { + _addSixelValue(ch - L'?'); + } + else if (ch == L'/') + { + _endOfSixelLine(); + } + else if (ch == L';') + { + _endOfCharacter(); + } +} + +bool FontBuffer::FinalizeSixelData() +{ + // If the charset ID hasn't been initialized this isn't a valid update. + RETURN_BOOL_IF_FALSE(_charsetIdInitialized); + + // Flush the current line to make sure we take all the used positions + // into account when calculating the font dimensions. + _endOfSixelLine(); + + // If the buffer has been cleared, we'll need to recalculate the dimensions + // using the latest attributes, adjust the character bit patterns to fit + // their true size, and fill in unused buffer positions with an error glyph. + if (_bufferCleared) + { + std::tie(_fullWidth, _fullHeight, _textWidth) = _calculateDimensions(); + _packAndCenterBitPatterns(); + _fillUnusedCharacters(); + } + + return true; +} + +gsl::span FontBuffer::GetBitPattern() const noexcept +{ + return { _buffer.data(), MAX_CHARS * _fullHeight }; +} + +til::size FontBuffer::GetCellSize() const +{ + return { _fullWidth, _fullHeight }; +} + +size_t FontBuffer::GetTextCenteringHint() const noexcept +{ + return _textCenteringHint; +} + +VTID FontBuffer::GetDesignation() const noexcept +{ + return _charsetId; +} + +void FontBuffer::_buildCharsetId(const wchar_t ch) +{ + // Note that we ignore any characters that are not valid in this state. + if (ch >= 0x20 && ch <= 0x2F) + { + _charsetIdBuilder.AddIntermediate(ch); + } + else if (ch >= 0x30 && ch <= 0x7E) + { + _pendingCharsetId = _charsetIdBuilder.Finalize(ch); + _charsetIdInitialized = true; + _prepareCharacterBuffer(); + } +} + +void FontBuffer::_prepareCharacterBuffer() +{ + // If any of the attributes have changed since the last time characters + // were downloaded, the font dimensions will need to be recalculated, and + // the buffer will need to be cleared. Otherwise we'll just be adding to + // the existing font, assuming the current dimensions. + if (_cellMatrix != _pendingCellMatrix || + _cellHeight != _pendingCellHeight || + _fontSet != _pendingFontSet || + _fontUsage != _pendingFontUsage || + _charsetSize != _pendingCharsetSize || + _charsetId != _pendingCharsetId) + { + // Replace the current attributes with the pending values. + _cellMatrix = _pendingCellMatrix; + _cellHeight = _pendingCellHeight; + _fontSet = _pendingFontSet; + _fontUsage = _pendingFontUsage; + _charsetSize = _pendingCharsetSize; + _charsetId = _pendingCharsetId; + + // Reset the font dimensions to the maximum supported size, since we + // can't be certain of the intended size until we've received all of + // the sixel data. These values will be recalculated once we can work + // out the terminal type that the font was originally designed for. + _fullWidth = MAX_WIDTH; + _fullHeight = MAX_HEIGHT; + _textWidth = MAX_WIDTH; + _textOffset = 0; + + // Clear the buffer. + _buffer.fill(0); + _bufferCleared = true; + } + else + { + _bufferCleared = false; + } + + _prepareNextCharacter(); +} + +void FontBuffer::_prepareNextCharacter() +{ + _lastChar = _currentChar; + _currentCharBuffer = std::next(_buffer.begin(), _currentChar * _fullHeight); + _sixelColumn = 0; + _sixelRow = 0; + + // If the buffer hasn't been cleared, we'll need to clear each character + // position individually, before adding any new sixel data. + if (!_bufferCleared && _currentChar < MAX_CHARS) + { + std::fill_n(_currentCharBuffer, _fullHeight, uint16_t{ 0 }); + } +} + +void FontBuffer::_addSixelValue(const size_t value) noexcept +{ + if (_currentChar < MAX_CHARS && _sixelColumn < _textWidth) + { + // Each sixel updates six pixels of a single column, so we setup a bit + // mask for the column we want to update, and then set that bit in each + // row for which there is a corresponding "on" bit in the input value. + const auto outputColumnBit = (0x8000 >> (_sixelColumn + _textOffset)); + auto outputIterator = _currentCharBuffer; + auto inputValueMask = 1; + for (size_t i = 0; i < 6 && _sixelRow + i < _fullHeight; i++) + { + *outputIterator |= (value & inputValueMask) ? outputColumnBit : 0; + outputIterator++; + inputValueMask <<= 1; + } + } + _sixelColumn++; +} + +void FontBuffer::_endOfSixelLine() +{ + // Move down six rows to the get to the next sixel position. + std::advance(_currentCharBuffer, 6); + _sixelRow += 6; + + // Keep track of the maximum width and height covered by the sixel data. + _usedWidth = std::max(_usedWidth, _sixelColumn); + _usedHeight = std::max(_usedHeight, _sixelRow); + + // Reset the column number to the start of the next line. + _sixelColumn = 0; +} + +void FontBuffer::_endOfCharacter() +{ + _endOfSixelLine(); + _currentChar++; + _prepareNextCharacter(); +} + +std::tuple FontBuffer::_calculateDimensions() const +{ + // If the size is declared as a matrix, this is most likely a VT2xx font, + // typically with a cell size of 10x10. However, in 132-column mode, the + // VT240 has a cell size of 6x10, but that's only for widths of 6 or less. + if (_sizeDeclaredAsMatrix) + { + if (_columnsPerPage == 132 && _declaredWidth <= 6) + { + // 6x10 cell with no clipping. + return { 6, 10, 0 }; + } + else + { + // 10x10 cell with text clipped to 8 pixels. + return { 10, 10, 8 }; + } + } + + // If we've been given explicit dimensions, and this is not a text font, + // then we assume those dimensions are the exact cell size. + if (_declaredWidth && _declaredHeight && !_isTextFont) + { + // Since this is not a text font, no clipping is required. + return { _declaredWidth, _declaredHeight, 0 }; + } + + // For most of the cases that follow, a text font will be clipped within + // the bounds of the declared width (if given). There are only a few cases + // where we'll need to use a hard-coded text width, and that's when the + // font appears to be targeting a VT2xx. + const auto textWidth = _isTextFont ? _declaredWidth : 0; + + // If the lines per page isn't 24, this must be targeting a VT420 or VT5xx. + // The cell width is 6 for 132 columns, and 10 for 80 columns. + // The cell height is 8 for 48 lines and 10 for 36 lines. + if (_linesPerPage != 24) + { + const auto cellWidth = _columnsPerPage == 132 ? 6 : 10; + const auto cellHeight = _linesPerPage == 48 ? 8 : 10; + return { cellWidth, cellHeight, textWidth }; + } + + // Now we're going to test whether the dimensions are in range for a number + // of known terminals. We use the declared dimensions if given, otherwise + // estimate the size from the used sixel values. If comparing a sixel-based + // height, though, we need to round up the target cell height to account for + // the fact that our used height will always be a multiple of six. + const auto inRange = [=](const size_t cellWidth, const size_t cellHeight) { + const auto sixelHeight = (cellHeight + 5) / 6 * 6; + const auto heightInRange = _declaredHeight ? _declaredHeight <= cellHeight : _usedHeight <= sixelHeight; + const auto widthInRange = _declaredWidth ? _declaredWidth <= cellWidth : _usedWidth <= cellWidth; + return heightInRange && widthInRange; + }; + + // In the case of a VT2xx font, you could only use a matrix size (which + // we've dealt with above), or a default size, so the tests below are only + // applicable for a VT2xx when no explicit dimensions have been declared. + const auto noDeclaredSize = _declaredWidth == 0 && _declaredHeight == 0; + + if (_columnsPerPage == 80) + { + if (inRange(8, 10) && noDeclaredSize) + { + // VT2xx - 10x10 cell with text clipped to 8 pixels. + return { 10, 10, 8 }; + } + else if (inRange(15, 12)) + { + // VT320 - 15x12 cell with default text width. + return { 15, 12, textWidth }; + } + else if (inRange(10, 16)) + { + // VT420 & VT5xx - 10x16 cell with default text width. + return { 10, 16, textWidth }; + } + else if (inRange(10, 20)) + { + // VT340 - 10x20 cell with default text width. + return { 10, 20, textWidth }; + } + else if (inRange(12, 30)) + { + // VT382 - 12x30 cell with default text width. + return { 12, 30, textWidth }; + } + else + { + // If all else fails, assume the maximum size. + return { MAX_WIDTH, MAX_HEIGHT, textWidth }; + } + } + else + { + if (inRange(6, 10) && noDeclaredSize) + { + // VT240 - 6x10 cell with no clipping. + return { 6, 10, 0 }; + } + else if (inRange(9, 12)) + { + // VT320 - 9x12 cell with default text width. + return { 9, 12, textWidth }; + } + else if (inRange(6, 16)) + { + // VT420 & VT5xx - 6x16 cell with default text width. + return { 6, 16, textWidth }; + } + else if (inRange(6, 20)) + { + // VT340 - 6x20 cell with default text width. + return { 6, 20, textWidth }; + } + else if (inRange(7, 30)) + { + // VT382 - 7x30 cell with default text width. + return { 7, 30, textWidth }; + } + else + { + // If all else fails, assume the maximum size. + return { MAX_WIDTH, MAX_HEIGHT, textWidth }; + } + } +} + +void FontBuffer::_packAndCenterBitPatterns() +{ + // If this is a text font, we'll clip the bits up to the text width and + // center them within the full cell width. For a full cell font we'll just + // use all of the bits, and no offset will be required. + _textWidth = _textWidth ? _textWidth : _fullWidth; + _textWidth = std::min(_textWidth, _fullWidth); + _textOffset = (_fullWidth - _textWidth) / 2; + const auto textClippingMask = ~(0xFFFF >> _textWidth); + + // If the text is given an explicit width, we check to what extent the + // content is offset from center. Knowing that information will enable the + // renderer to scale the font more symmetrically. + _textCenteringHint = _declaredWidth ? _fullWidth - (_declaredWidth + _textOffset * 2) : 0; + + // Initially the characters are written to the buffer assuming the maximum + // cell height, but now that we know the true height, we need to pack the + // buffer data so that each character occupies the exact number of scanlines + // that are required. + for (auto srcLine = 0u, dstLine = 0u; srcLine < _buffer.size(); srcLine++) + { + if ((srcLine % MAX_HEIGHT) < _fullHeight) + { + auto characterScanline = til::at(_buffer, srcLine); + characterScanline &= textClippingMask; + characterScanline >>= _textOffset; + til::at(_buffer, dstLine++) = characterScanline; + } + } +} + +void FontBuffer::_fillUnusedCharacters() +{ + // Every character in the buffer that hasn't been uploaded will be replaced + // with an error glyph (a reverse question mark). This includes every + // character prior to the start char, or after the last char. + const auto errorPattern = _generateErrorGlyph(); + for (auto ch = 0u; ch < MAX_CHARS; ch++) + { + if (ch < _startChar || ch > _lastChar) + { + auto charBuffer = std::next(_buffer.begin(), ch * _fullHeight); + std::copy_n(errorPattern.begin(), _fullHeight, charBuffer); + } + } +} + +std::array FontBuffer::_generateErrorGlyph() +{ + // We start with a bit pattern for a reverse question mark covering the + // maximum font resolution that we might need. + constexpr std::array inputBitPattern = { + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b000000000000000, + 0b001111111110000, + 0b011111111111000, + 0b111000000011100, + 0b111000000011100, + 0b111000000000000, + 0b111000000000000, + 0b111100000000000, + 0b011111000000000, + 0b000011110000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + 0b000000000000000, + 0b000001110000000, + 0b000001110000000, + 0b000001110000000, + }; + + // Then for each possible width and height, we have hard-coded bit masks + // indicating a range of columns and rows to select from the base bitmap + // to produce a scaled down version of reasonable quality. + constexpr std::array widthMasks = { + // clang-format off + 0, 1, 3, 32771, 8457, 9481, 9545, 9673, 42441, 26061, + 58829, 28141, 60909, 63453, 63485, 65533, 65535 + // clang-format on + }; + constexpr std::array heightMasks = { + // clang-format off + 0, 1, 3, 7, 15, 1613952, 10002560, 10002816, 10068352, 10068353, + 26845569, 26847617, 26847619, 26864003, 28961155, 28961219, + 28961731, 62516163, 62516167, 129625031, 129625039, 129756111, + 263973839, 263974863, 268169167, 536604623, 536608719, 536608735, + 536870879, 1073741791, 2147483615, 2147483647, 4294967295 + // clang-format on + }; + + const auto widthMask = widthMasks.at(_fullWidth); + const auto heightMask = heightMasks.at(_fullHeight); + + auto outputBitPattern = std::array{}; + auto outputIterator = outputBitPattern.begin(); + for (auto y = 0; y < MAX_HEIGHT; y++) + { + const auto yBit = (1 << y); + if (heightMask & yBit) + { + const uint16_t inputScanline = til::at(inputBitPattern, y); + uint16_t outputScanline = 0; + for (auto x = MAX_WIDTH; x-- > 0;) + { + const auto xBit = 1 << x; + if (widthMask & xBit) + { + outputScanline <<= 1; + outputScanline |= (inputScanline & xBit) ? 1 : 0; + } + } + outputScanline <<= (MAX_WIDTH - _fullWidth); + *(outputIterator++) = outputScanline; + } + } + return outputBitPattern; +} diff --git a/src/terminal/adapter/FontBuffer.hpp b/src/terminal/adapter/FontBuffer.hpp new file mode 100644 index 00000000000..bdd6fff7204 --- /dev/null +++ b/src/terminal/adapter/FontBuffer.hpp @@ -0,0 +1,95 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontBuffer.hpp + +Abstract: +- This manages the construction and storage of font definitions for the VT DECDLD control sequence. +--*/ + +#pragma once + +#include "DispatchTypes.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class FontBuffer + { + public: + FontBuffer() noexcept; + ~FontBuffer() = default; + bool SetEraseControl(const DispatchTypes::DrcsEraseControl eraseControl) noexcept; + bool SetAttributes(const DispatchTypes::DrcsCellMatrix cellMatrix, + const VTParameter cellHeight, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage) noexcept; + bool SetStartChar(const VTParameter startChar, + const DispatchTypes::DrcsCharsetSize charsetSize) noexcept; + void AddSixelData(const wchar_t ch); + bool FinalizeSixelData(); + + gsl::span GetBitPattern() const noexcept; + til::size GetCellSize() const; + size_t GetTextCenteringHint() const noexcept; + VTID GetDesignation() const noexcept; + + private: + static constexpr size_t MAX_WIDTH = 16; + static constexpr size_t MAX_HEIGHT = 32; + static constexpr size_t MAX_CHARS = 96; + + void _buildCharsetId(const wchar_t ch); + void _prepareCharacterBuffer(); + void _prepareNextCharacter(); + void _addSixelValue(const size_t value) noexcept; + void _endOfSixelLine(); + void _endOfCharacter(); + + std::tuple _calculateDimensions() const; + void _packAndCenterBitPatterns(); + void _fillUnusedCharacters(); + std::array _generateErrorGlyph(); + + DispatchTypes::DrcsCellMatrix _cellMatrix; + DispatchTypes::DrcsCellMatrix _pendingCellMatrix; + size_t _cellHeight; + size_t _pendingCellHeight; + bool _sizeDeclaredAsMatrix; + size_t _declaredWidth; + size_t _declaredHeight; + size_t _usedWidth; + size_t _usedHeight; + size_t _fullWidth; + size_t _fullHeight; + size_t _textWidth; + size_t _textOffset; + size_t _textCenteringHint; + + DispatchTypes::DrcsFontSet _fontSet; + DispatchTypes::DrcsFontSet _pendingFontSet; + DispatchTypes::DrcsFontUsage _fontUsage; + DispatchTypes::DrcsFontUsage _pendingFontUsage; + size_t _linesPerPage; + size_t _columnsPerPage; + bool _isTextFont; + + DispatchTypes::DrcsCharsetSize _charsetSize; + DispatchTypes::DrcsCharsetSize _pendingCharsetSize; + VTID _charsetId{ 0 }; + VTID _pendingCharsetId{ 0 }; + bool _charsetIdInitialized; + VTIDBuilder _charsetIdBuilder; + size_t _startChar; + size_t _lastChar; + size_t _currentChar; + + using buffer_type = std::array; + buffer_type _buffer; + buffer_type::iterator _currentCharBuffer; + bool _bufferCleared; + size_t _sixelColumn; + size_t _sixelRow; + }; +} diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index fe53e49d8b9..b0efd81d892 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -23,6 +23,8 @@ namespace Microsoft::Console::VirtualTerminal class Microsoft::Console::VirtualTerminal::ITermDispatch { public: + using StringHandler = std::function; + #pragma warning(push) #pragma warning(disable : 26432) // suppress rule of 5 violation on interface because tampering with this is fraught with peril virtual ~ITermDispatch() = 0; @@ -130,6 +132,15 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool EndHyperlink() = 0; virtual bool DoConEmuAction(const std::wstring_view string) = 0; + + virtual StringHandler DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD }; inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() {} #pragma warning(pop) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 4bd30cbc847..50db520c0a1 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -1884,6 +1884,7 @@ bool AdaptDispatch::SoftReset() // - Performs a communications line disconnect. // - Clears UDKs. // - Clears a down-line-loaded character set. +// * The soft font is reset in the renderer and the font buffer is deleted. // - Clears the screen. // * This is like Erase in Display (3), also clearing scrollback, as well as ED(2) // - Returns the cursor to the upper-left corner of the screen. @@ -1929,6 +1930,10 @@ bool AdaptDispatch::HardReset() // Delete all current tab stops and reapply _ResetTabStops(); + // Clear the soft font in the renderer and delete the font buffer. + success = _pConApi->PrivateUpdateSoftFont({}, {}, false) && success; + _fontBuffer = nullptr; + // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this RIS sequence to the connected // terminal application. We've reset our state, but the connected terminal @@ -2440,6 +2445,88 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view /*string*/) noexcept return false; } +// Method Description: +// - DECDLD - Downloads one or more characters of a dynamically redefinable +// character set (DRCS) with a specified pixel pattern. The pixel array is +// transmitted in sixel format via the returned StringHandler function. +// Arguments: +// - fontNumber - The buffer number into which the font will be loaded. +// - startChar - The first character in the set that will be replaced. +// - eraseControl - Which characters to erase before loading the new data. +// - cellMatrix - The character cell width (sometimes also height in legacy formats). +// - fontSet - The screen size for which the font is designed. +// - fontUsage - Whether it is a text font or a full-cell font. +// - cellHeight - The character cell height (if not defined by cellMatrix). +// - charsetSize - Whether the character set is 94 or 96 characters. +// Return Value: +// - a function to receive the pixel data or nullptr if parameters are invalid +ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) +{ + // If we're a conpty, we're just going to ignore the operation for now. + // There's no point in trying to pass it through without also being able + // to pass through the character set designations. + if (_pConApi->IsConsolePty()) + { + return nullptr; + } + + // The font buffer is created on demand. + if (!_fontBuffer) + { + _fontBuffer = std::make_unique(); + } + + // Only one font buffer is supported, so only 0 (default) and 1 are valid. + auto success = fontNumber <= 1; + success = success && _fontBuffer->SetEraseControl(eraseControl); + success = success && _fontBuffer->SetAttributes(cellMatrix, cellHeight, fontSet, fontUsage); + success = success && _fontBuffer->SetStartChar(startChar, charsetSize); + + // If any of the parameters are invalid, we return a null handler to let + // the state machine know we want to ignore the subsequent data string. + if (!success) + { + return nullptr; + } + + return [=](const auto ch) { + // We pass the data string straight through to the font buffer class + // until we receive an ESC, indicating the end of the string. At that + // point we can finalize the buffer, and if valid, update the renderer + // with the constructed bit pattern. + if (ch != AsciiChars::ESC) + { + _fontBuffer->AddSixelData(ch); + } + else if (_fontBuffer->FinalizeSixelData()) + { + // We also need to inform the character set mapper of the ID that + // will map to this font (we only support one font buffer so there + // will only ever be one active dynamic character set). + if (charsetSize == DispatchTypes::DrcsCharsetSize::Size96) + { + _termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation()); + } + else + { + _termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation()); + } + const auto bitPattern = _fontBuffer->GetBitPattern(); + const auto cellSize = _fontBuffer->GetCellSize(); + const auto centeringHint = _fontBuffer->GetTextCenteringHint(); + _pConApi->PrivateUpdateSoftFont(bitPattern, cellSize, centeringHint); + } + return true; + }; +} + // Routine Description: // - Determines whether we should pass any sequence that manipulates // TerminalInput's input generator through the PTY. It encapsulates diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 5ea5823430c..7eeade5cb9a 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -18,6 +18,7 @@ Author(s): #include "DispatchCommon.hpp" #include "conGetSet.hpp" #include "adaptDefaults.hpp" +#include "FontBuffer.hpp" #include "terminalOutput.hpp" #include "..\..\types\inc\sgrStack.hpp" @@ -130,6 +131,15 @@ namespace Microsoft::Console::VirtualTerminal bool DoConEmuAction(const std::wstring_view string) noexcept override; + StringHandler DownloadDRCS(const size_t fontNumber, + const VTParameter startChar, + const DispatchTypes::DrcsEraseControl eraseControl, + const DispatchTypes::DrcsCellMatrix cellMatrix, + const DispatchTypes::DrcsFontSet fontSet, + const DispatchTypes::DrcsFontUsage fontUsage, + const VTParameter cellHeight, + const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD + private: enum class ScrollDirection { @@ -187,6 +197,7 @@ namespace Microsoft::Console::VirtualTerminal std::unique_ptr _pConApi; std::unique_ptr _pDefaults; TerminalOutput _termOutput; + std::unique_ptr _fontBuffer; std::optional _initialCodePage; // We have two instances of the saved cursor state, because we need diff --git a/src/terminal/adapter/charsets.hpp b/src/terminal/adapter/charsets.hpp index 4ead5f14bce..0a3a649ffd8 100644 --- a/src/terminal/adapter/charsets.hpp +++ b/src/terminal/adapter/charsets.hpp @@ -43,6 +43,11 @@ namespace Microsoft::Console::VirtualTerminal return rhs == lhs; } + // Note that the 94-character sets are deliberately defined with a size of + // 95 to avoid having to test the lower bound. We just alway leave the first + // entry - which is not meant to be mapped - as a SPACE or NBSP, which is at + // least visually equivalent to leaving it untranslated. + typedef CharSet AsciiBasedCharSet; typedef CharSet Latin1BasedCharSet94; typedef CharSet Latin1BasedCharSet96; @@ -1051,5 +1056,11 @@ namespace Microsoft::Console::VirtualTerminal { L'\x7e', L'\u00fc' }, // Latin Small Letter U With Diaeresis }; + // We're reserving 96 characters (U+EF20 to U+EF7F) from the Unicode + // Private Use Area for our dynamically redefinable characters sets. + static constexpr auto DRCS_BASE_CHAR = L'\uEF20'; + static constexpr auto Drcs94 = CharSet{ { DRCS_BASE_CHAR, '\x20' } }; + static constexpr auto Drcs96 = CharSet{}; + #pragma warning(pop) } diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index d604d8e06ec..e69ab753fca 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -107,5 +107,9 @@ namespace Microsoft::Console::VirtualTerminal virtual bool PrivateAddHyperlink(const std::wstring_view uri, const std::wstring_view params) const = 0; virtual bool PrivateEndHyperlink() const = 0; + + virtual bool PrivateUpdateSoftFont(const gsl::span bitPattern, + const SIZE cellSize, + const size_t centeringHint) = 0; }; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index eeb6f4574bc..1626e7a7de3 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -12,6 +12,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 4675a7e3a20..fb644781d84 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + @@ -83,6 +86,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index 0fadc810f99..8b3ae4f4339 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -32,6 +32,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\adaptDispatch.cpp \ ..\DispatchCommon.cpp \ + ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index d82a28c225f..1fcb83a4e4c 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -123,4 +123,13 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool EndHyperlink() noexcept override { return false; } bool DoConEmuAction(const std::wstring_view /*string*/) noexcept override { return false; } + + StringHandler DownloadDRCS(const size_t /*fontNumber*/, + const VTParameter /*startChar*/, + const DispatchTypes::DrcsEraseControl /*eraseControl*/, + const DispatchTypes::DrcsCellMatrix /*cellMatrix*/, + const DispatchTypes::DrcsFontSet /*fontSet*/, + const DispatchTypes::DrcsFontUsage /*fontUsage*/, + const VTParameter /*cellHeight*/, + const DispatchTypes::DrcsCharsetSize /*charsetSize*/) noexcept override { return nullptr; } }; diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp index 14de84e1393..944ff88b71f 100644 --- a/src/terminal/adapter/terminalOutput.cpp +++ b/src/terminal/adapter/terminalOutput.cpp @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#include -#include +#include "precomp.h" #include "charsets.hpp" #include "terminalOutput.hpp" #include "strsafe.h" @@ -19,91 +18,30 @@ TerminalOutput::TerminalOutput() noexcept bool TerminalOutput::Designate94Charset(size_t gsetNumber, const VTID charset) { - switch (charset) - { - case VTID("B"): // US ASCII - case VTID("1"): // Alternate Character ROM - return _SetTranslationTable(gsetNumber, Ascii); - case VTID("0"): // DEC Special Graphics - case VTID("2"): // Alternate Character ROM Special Graphics - return _SetTranslationTable(gsetNumber, DecSpecialGraphics); - case VTID("<"): // DEC Supplemental - return _SetTranslationTable(gsetNumber, DecSupplemental); - case VTID("A"): // British NRCS - return _SetTranslationTable(gsetNumber, BritishNrcs); - case VTID("4"): // Dutch NRCS - return _SetTranslationTable(gsetNumber, DutchNrcs); - case VTID("5"): // Finnish NRCS - case VTID("C"): // (fallback) - return _SetTranslationTable(gsetNumber, FinnishNrcs); - case VTID("R"): // French NRCS - return _SetTranslationTable(gsetNumber, FrenchNrcs); - case VTID("f"): // French NRCS (ISO update) - return _SetTranslationTable(gsetNumber, FrenchNrcsIso); - case VTID("9"): // French Canadian NRCS - case VTID("Q"): // (fallback) - return _SetTranslationTable(gsetNumber, FrenchCanadianNrcs); - case VTID("K"): // German NRCS - return _SetTranslationTable(gsetNumber, GermanNrcs); - case VTID("Y"): // Italian NRCS - return _SetTranslationTable(gsetNumber, ItalianNrcs); - case VTID("6"): // Norwegian/Danish NRCS - case VTID("E"): // (fallback) - return _SetTranslationTable(gsetNumber, NorwegianDanishNrcs); - case VTID("`"): // Norwegian/Danish NRCS (ISO standard) - return _SetTranslationTable(gsetNumber, NorwegianDanishNrcsIso); - case VTID("Z"): // Spanish NRCS - return _SetTranslationTable(gsetNumber, SpanishNrcs); - case VTID("7"): // Swedish NRCS - case VTID("H"): // (fallback) - return _SetTranslationTable(gsetNumber, SwedishNrcs); - case VTID("="): // Swiss NRCS - return _SetTranslationTable(gsetNumber, SwissNrcs); - case VTID("&4"): // DEC Cyrillic - return _SetTranslationTable(gsetNumber, DecCyrillic); - case VTID("&5"): // Russian NRCS - return _SetTranslationTable(gsetNumber, RussianNrcs); - case VTID("\"?"): // DEC Greek - return _SetTranslationTable(gsetNumber, DecGreek); - case VTID("\">"): // Greek NRCS - return _SetTranslationTable(gsetNumber, GreekNrcs); - case VTID("\"4"): // DEC Hebrew - return _SetTranslationTable(gsetNumber, DecHebrew); - case VTID("%="): // Hebrew NRCS - return _SetTranslationTable(gsetNumber, HebrewNrcs); - case VTID("%0"): // DEC Turkish - return _SetTranslationTable(gsetNumber, DecTurkish); - case VTID("%2"): // Turkish NRCS - return _SetTranslationTable(gsetNumber, TurkishNrcs); - case VTID("%5"): // DEC Supplemental - return _SetTranslationTable(gsetNumber, DecSupplemental); - case VTID("%6"): // Portuguese NRCS - return _SetTranslationTable(gsetNumber, PortugueseNrcs); - default: - return false; - } + const auto translationTable = _LookupTranslationTable94(charset); + RETURN_BOOL_IF_FALSE(!translationTable.empty()); + return _SetTranslationTable(gsetNumber, translationTable); } bool TerminalOutput::Designate96Charset(size_t gsetNumber, const VTID charset) { - switch (charset) - { - case VTID("A"): // ISO Latin-1 Supplemental - case VTID("<"): // (UPSS when assigned to Latin-1) - return _SetTranslationTable(gsetNumber, Latin1); - case VTID("B"): // ISO Latin-2 Supplemental - return _SetTranslationTable(gsetNumber, Latin2); - case VTID("L"): // ISO Latin-Cyrillic Supplemental - return _SetTranslationTable(gsetNumber, LatinCyrillic); - case VTID("F"): // ISO Latin-Greek Supplemental - return _SetTranslationTable(gsetNumber, LatinGreek); - case VTID("H"): // ISO Latin-Hebrew Supplemental - return _SetTranslationTable(gsetNumber, LatinHebrew); - case VTID("M"): // ISO Latin-5 Supplemental - return _SetTranslationTable(gsetNumber, Latin5); - default: - return false; - } + const auto translationTable = _LookupTranslationTable96(charset); + RETURN_BOOL_IF_FALSE(!translationTable.empty()); + return _SetTranslationTable(gsetNumber, translationTable); +} + +void TerminalOutput::SetDrcs94Designation(const VTID charset) +{ + _ReplaceDrcsTable(_LookupTranslationTable94(charset), Drcs94); + _drcsId = charset; + _drcsTranslationTable = Drcs94; +} + +void TerminalOutput::SetDrcs96Designation(const VTID charset) +{ + _ReplaceDrcsTable(_LookupTranslationTable96(charset), Drcs96); + _drcsId = charset; + _drcsTranslationTable = Drcs96; } #pragma warning(suppress : 26440) // Suppress spurious "function can be declared noexcept" warning @@ -186,9 +124,139 @@ wchar_t TerminalOutput::TranslateKey(const wchar_t wch) const noexcept return wchFound; } +const std::wstring_view TerminalOutput::_LookupTranslationTable94(const VTID charset) const +{ + // Note that the DRCS set can be designated with either a 94 or 96 sequence, + // regardless of the actual size of the set. This isn't strictly correct, + // but there is existing software that depends on this behavior. + if (charset == _drcsId) + { + return _drcsTranslationTable; + } + switch (charset) + { + case VTID("B"): // US ASCII + case VTID("1"): // Alternate Character ROM + return Ascii; + case VTID("0"): // DEC Special Graphics + case VTID("2"): // Alternate Character ROM Special Graphics + return DecSpecialGraphics; + case VTID("<"): // DEC Supplemental + return DecSupplemental; + case VTID("A"): // British NRCS + return BritishNrcs; + case VTID("4"): // Dutch NRCS + return DutchNrcs; + case VTID("5"): // Finnish NRCS + case VTID("C"): // (fallback) + return FinnishNrcs; + case VTID("R"): // French NRCS + return FrenchNrcs; + case VTID("f"): // French NRCS (ISO update) + return FrenchNrcsIso; + case VTID("9"): // French Canadian NRCS + case VTID("Q"): // (fallback) + return FrenchCanadianNrcs; + case VTID("K"): // German NRCS + return GermanNrcs; + case VTID("Y"): // Italian NRCS + return ItalianNrcs; + case VTID("6"): // Norwegian/Danish NRCS + case VTID("E"): // (fallback) + return NorwegianDanishNrcs; + case VTID("`"): // Norwegian/Danish NRCS (ISO standard) + return NorwegianDanishNrcsIso; + case VTID("Z"): // Spanish NRCS + return SpanishNrcs; + case VTID("7"): // Swedish NRCS + case VTID("H"): // (fallback) + return SwedishNrcs; + case VTID("="): // Swiss NRCS + return SwissNrcs; + case VTID("&4"): // DEC Cyrillic + return DecCyrillic; + case VTID("&5"): // Russian NRCS + return RussianNrcs; + case VTID("\"?"): // DEC Greek + return DecGreek; + case VTID("\">"): // Greek NRCS + return GreekNrcs; + case VTID("\"4"): // DEC Hebrew + return DecHebrew; + case VTID("%="): // Hebrew NRCS + return HebrewNrcs; + case VTID("%0"): // DEC Turkish + return DecTurkish; + case VTID("%2"): // Turkish NRCS + return TurkishNrcs; + case VTID("%5"): // DEC Supplemental + return DecSupplemental; + case VTID("%6"): // Portuguese NRCS + return PortugueseNrcs; + default: + return {}; + } +} + +const std::wstring_view TerminalOutput::_LookupTranslationTable96(const VTID charset) const +{ + // Note that the DRCS set can be designated with either a 94 or 96 sequence, + // regardless of the actual size of the set. This isn't strictly correct, + // but there is existing software that depends on this behavior. + if (charset == _drcsId) + { + return _drcsTranslationTable; + } + switch (charset) + { + case VTID("A"): // ISO Latin-1 Supplemental + case VTID("<"): // (UPSS when assigned to Latin-1) + return Latin1; + case VTID("B"): // ISO Latin-2 Supplemental + return Latin2; + case VTID("L"): // ISO Latin-Cyrillic Supplemental + return LatinCyrillic; + case VTID("F"): // ISO Latin-Greek Supplemental + return LatinGreek; + case VTID("H"): // ISO Latin-Hebrew Supplemental + return LatinHebrew; + case VTID("M"): // ISO Latin-5 Supplemental + return Latin5; + default: + return {}; + } +} + bool TerminalOutput::_SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable) { _gsetTranslationTables.at(gsetNumber) = translationTable; // We need to reapply the locking shifts in case the underlying G-sets have changed. return LockingShift(_glSetNumber) && LockingShiftRight(_grSetNumber); } + +void TerminalOutput::_ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable) +{ + if (newTable.data() != oldTable.data()) + { + for (size_t gsetNumber = 0; gsetNumber < 4; gsetNumber++) + { + // Get the current translation table for this G-set. + auto gsetTable = _gsetTranslationTables.at(gsetNumber); + // If it's already a DRCS, replace it with a default charset. + if (Drcs94 == gsetTable || Drcs96 == gsetTable) + { + gsetTable = gsetNumber < 2 ? (std::wstring_view)Ascii : (std::wstring_view)Latin1; + } + // If it matches the old table, replace it with the new table. + if (gsetTable.data() == oldTable.data()) + { + gsetTable = newTable; + } + // Update the G-set entry with the new translation table. + _gsetTranslationTables.at(gsetNumber) = gsetTable; + } + // Reapply the locking shifts in case the underlying G-sets have changed. + LockingShift(_glSetNumber); + LockingShiftRight(_grSetNumber); + } +} diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp index e82e067a7c2..aabc12657bf 100644 --- a/src/terminal/adapter/terminalOutput.hpp +++ b/src/terminal/adapter/terminalOutput.hpp @@ -28,6 +28,8 @@ namespace Microsoft::Console::VirtualTerminal wchar_t TranslateKey(const wchar_t wch) const noexcept; bool Designate94Charset(const size_t gsetNumber, const VTID charset); bool Designate96Charset(const size_t gsetNumber, const VTID charset); + void SetDrcs94Designation(const VTID charset); + void SetDrcs96Designation(const VTID charset); bool LockingShift(const size_t gsetNumber); bool LockingShiftRight(const size_t gsetNumber); bool SingleShift(const size_t gsetNumber); @@ -35,7 +37,10 @@ namespace Microsoft::Console::VirtualTerminal void EnableGrTranslation(boolean enabled); private: + const std::wstring_view _LookupTranslationTable94(const VTID charset) const; + const std::wstring_view _LookupTranslationTable96(const VTID charset) const; bool _SetTranslationTable(const size_t gsetNumber, const std::wstring_view translationTable); + void _ReplaceDrcsTable(const std::wstring_view oldTable, const std::wstring_view newTable); std::array _gsetTranslationTables; size_t _glSetNumber = 0; @@ -44,5 +49,7 @@ namespace Microsoft::Console::VirtualTerminal std::wstring_view _grTranslationTable; mutable std::wstring_view _ssTranslationTable; boolean _grTranslationEnabled = false; + VTID _drcsId = 0; + std::wstring_view _drcsTranslationTable; }; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index ce120ea3b65..13f9476aef3 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -567,6 +567,19 @@ class TestGetSet final : public ConGetSet return TRUE; } + bool PrivateUpdateSoftFont(const gsl::span /*bitPattern*/, + const SIZE cellSize, + const size_t /*centeringHint*/) noexcept override + { + Log::Comment(L"PrivateUpdateSoftFont MOCK called..."); + + Log::Comment(NoThrowString().Format(L"Cell size: %dx%d", cellSize.cx, cellSize.cy)); + VERIFY_ARE_EQUAL(_expectedCellSize.cx, cellSize.cx); + VERIFY_ARE_EQUAL(_expectedCellSize.cy, cellSize.cy); + + return TRUE; + } + void PrepData() { PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter. @@ -810,6 +823,8 @@ class TestGetSet final : public ConGetSet bool _privateSetDefaultBackgroundResult = false; COLORREF _expectedDefaultBackgroundColorValue = INVALID_COLOR; + SIZE _expectedCellSize = {}; + private: HANDLE _hCon; }; @@ -2353,6 +2368,229 @@ class AdapterTest VERIFY_IS_FALSE(_pDispatch.get()->SetColorTableEntry(15, testColor)); } + TEST_METHOD(SoftFontSizeDetection) + { + using CellMatrix = DispatchTypes::DrcsCellMatrix; + using FontSet = DispatchTypes::DrcsFontSet; + using FontUsage = DispatchTypes::DrcsFontUsage; + + const auto decdld = [=](const auto cmw, const auto cmh, const auto ss, const auto u, const std::wstring_view data = {}) { + const auto ec = DispatchTypes::DrcsEraseControl::AllChars; + const auto css = DispatchTypes::DrcsCharsetSize::Size94; + const auto cellMatrix = static_cast(cmw); + const auto stringHandler = _pDispatch.get()->DownloadDRCS(0, 0, ec, cellMatrix, ss, u, cmh, css); + if (stringHandler) + { + stringHandler(L'B'); // Charset identifier + for (auto ch : data) + { + stringHandler(ch); + } + stringHandler(L'\033'); // String terminator + } + return stringHandler != nullptr; + }; + + // Matrix sizes at 80x24 should always use a 10x10 cell size (VT2xx). + Log::Comment(L"Matrix 5x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size5x10, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 6x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 7x10 for 80x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 0, FontSet::Size80x24, FontUsage::Text)); + + // At 132x24 the cell size is typically 6x10 (VT240), but could be 10x10 (VT220) + Log::Comment(L"Matrix 5x10 for 132x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size5x10, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 6x10 for 132x24 font set with text usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 7x10 for 132x24 font set with text usage (VT220 only)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 0, FontSet::Size132x24, FontUsage::Text)); + + // Full cell usage is invalid for all matrix sizes except 6x10 at 132x24. + Log::Comment(L"Matrix 5x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size5x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 6x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size6x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 7x10 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size7x10, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 5x10 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size5x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 6x10 for 132x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size6x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 7x10 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Size7x10, 0, FontSet::Size132x24, FontUsage::FullCell)); + + // Matrix size 1 is always invalid. + Log::Comment(L"Matrix 1 for 80x24 font set with text usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Matrix 1 for 132x24 font set with text usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Matrix 1 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Matrix 1 for 132x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(CellMatrix::Invalid, 0, FontSet::Size132x24, FontUsage::FullCell)); + + // The height parameter has no effect when a matrix size is used. + Log::Comment(L"Matrix 7x10 with unused height parameter"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Size7x10, 20, FontSet::Size80x24, FontUsage::Text)); + + // Full cell fonts with explicit dimensions are accepted as their given cell size. + Log::Comment(L"Explicit 13x17 for 80x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 13, 17 }; + VERIFY_IS_TRUE(decdld(13, 17, FontSet::Size80x24, FontUsage::FullCell)); + Log::Comment(L"Explicit 9x25 for 132x24 font set with full cell usage"); + _testGetSet->_expectedCellSize = { 9, 25 }; + VERIFY_IS_TRUE(decdld(9, 25, FontSet::Size132x24, FontUsage::FullCell)); + + // Cell sizes outside the maximum supported range (16x32) are invalid. + Log::Comment(L"Explicit 18x38 for 80x24 font set with full cell usage (invalid)"); + VERIFY_IS_FALSE(decdld(18, 38, FontSet::Size80x24, FontUsage::FullCell)); + + // Text fonts with explicit dimensions are interpreted as their closest matching device. + Log::Comment(L"Explicit 12x12 for 80x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + VERIFY_IS_TRUE(decdld(12, 12, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 9x20 for 80x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + VERIFY_IS_TRUE(decdld(9, 20, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 10x30 for 80x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + VERIFY_IS_TRUE(decdld(10, 30, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 8x16 for 80x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + VERIFY_IS_TRUE(decdld(8, 16, FontSet::Size80x24, FontUsage::Text)); + Log::Comment(L"Explicit 7x12 for 132x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + VERIFY_IS_TRUE(decdld(7, 12, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 5x20 for 132x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + VERIFY_IS_TRUE(decdld(5, 20, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 6x30 for 132x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + VERIFY_IS_TRUE(decdld(6, 30, FontSet::Size132x24, FontUsage::Text)); + Log::Comment(L"Explicit 5x16 for 132x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + VERIFY_IS_TRUE(decdld(5, 16, FontSet::Size132x24, FontUsage::Text)); + + // Font sets with more than 24 lines must be VT420/VT5xx. + Log::Comment(L"80x36 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x36, FontUsage::Text)); + Log::Comment(L"80x48 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x48, FontUsage::Text)); + Log::Comment(L"132x36 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x36, FontUsage::Text)); + Log::Comment(L"132x48 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x48, FontUsage::Text)); + Log::Comment(L"80x36 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x36, FontUsage::FullCell)); + Log::Comment(L"80x48 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x48, FontUsage::FullCell)); + Log::Comment(L"132x36 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x36, FontUsage::FullCell)); + Log::Comment(L"132x48 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 8 }; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x48, FontUsage::FullCell)); + + // Without an explicit size, the cell size is estimated from the number of sixels + // used in the character bitmaps. But note that sixel heights are always a multiple + // of 6, so will often be larger than the cell size for which they were intended. + Log::Comment(L"8x12 bitmap for 80x24 font set with text usage (VT2xx)"); + _testGetSet->_expectedCellSize = { 10, 10 }; + const auto bitmapOf8x12 = L"????????/????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf8x12)); + Log::Comment(L"12x12 bitmap for 80x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + const auto bitmapOf12x12 = L"????????????/????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf12x12)); + Log::Comment(L"9x24 bitmap for 80x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + const auto bitmapOf9x24 = L"?????????/?????????/?????????/?????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf9x24)); + Log::Comment(L"10x30 bitmap for 80x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + const auto bitmapOf10x30 = L"??????????/??????????/??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf10x30)); + Log::Comment(L"8x18 bitmap for 80x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + const auto bitmapOf8x18 = L"????????/????????/????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::Text, bitmapOf8x18)); + + Log::Comment(L"5x12 bitmap for 132x24 font set with text usage (VT240)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + const auto bitmapOf5x12 = L"?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x12)); + Log::Comment(L"7x12 bitmap for 132x24 font set with text usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + const auto bitmapOf7x12 = L"???????/???????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf7x12)); + Log::Comment(L"5x24 bitmap for 132x24 font set with text usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + const auto bitmapOf5x24 = L"?????/?????/?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x24)); + Log::Comment(L"6x30 bitmap for 132x24 font set with text usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + const auto bitmapOf6x30 = L"??????/??????/??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf6x30)); + Log::Comment(L"5x18 bitmap for 132x24 font set with text usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + const auto bitmapOf5x18 = L"?????/?????/?????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::Text, bitmapOf5x18)); + + Log::Comment(L"15x12 bitmap for 80x24 font set with full cell usage (VT320)"); + _testGetSet->_expectedCellSize = { 15, 12 }; + const auto bitmapOf15x12 = L"???????????????/???????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf15x12)); + Log::Comment(L"10x24 bitmap for 80x24 font set with full cell usage (VT340)"); + _testGetSet->_expectedCellSize = { 10, 20 }; + const auto bitmapOf10x24 = L"??????????/??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf10x24)); + Log::Comment(L"12x30 bitmap for 80x24 font set with full cell usage (VT382)"); + _testGetSet->_expectedCellSize = { 12, 30 }; + const auto bitmapOf12x30 = L"????????????/????????????/????????????/????????????/????????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf12x30)); + Log::Comment(L"10x18 bitmap for 80x24 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 10, 16 }; + const auto bitmapOf10x18 = L"??????????/??????????/??????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size80x24, FontUsage::FullCell, bitmapOf10x18)); + + Log::Comment(L"6x12 bitmap for 132x24 font set with full cell usage (VT240)"); + _testGetSet->_expectedCellSize = { 6, 10 }; + const auto bitmapOf6x12 = L"??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x12)); + Log::Comment(L"9x12 bitmap for 132x24 font set with full cell usage (VT320)"); + _testGetSet->_expectedCellSize = { 9, 12 }; + const auto bitmapOf9x12 = L"?????????/?????????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf9x12)); + Log::Comment(L"6x24 bitmap for 132x24 font set with full cell usage (VT340)"); + _testGetSet->_expectedCellSize = { 6, 20 }; + const auto bitmapOf6x24 = L"??????/??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x24)); + Log::Comment(L"7x30 bitmap for 132x24 font set with full cell usage (VT382)"); + _testGetSet->_expectedCellSize = { 7, 30 }; + const auto bitmapOf7x30 = L"???????/???????/???????/???????/???????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf7x30)); + Log::Comment(L"6x18 bitmap for 132x24 font set with full cell usage (VT420/VT5xx)"); + _testGetSet->_expectedCellSize = { 6, 16 }; + const auto bitmapOf6x18 = L"??????/??????/??????"; + VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x18)); + } + private: TestGetSet* _testGetSet; // non-ownership pointer std::unique_ptr _pDispatch; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 0ecd5ea4da1..953fce4154b 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -645,10 +645,27 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete // - parameters - set of numeric parameters collected while parsing the sequence. // Return Value: // - the data string handler function or nullptr if the sequence is not supported -IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(const VTID /*id*/, const VTParameters /*parameters*/) noexcept +IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(const VTID id, const VTParameters parameters) { StringHandler handler = nullptr; + switch (id) + { + case DcsActionCodes::DECDLD_DownloadDRCS: + handler = _dispatch->DownloadDRCS(parameters.at(0), + parameters.at(1), + parameters.at(2), + parameters.at(3), + parameters.at(4), + parameters.at(5), + parameters.at(6), + parameters.at(7)); + break; + default: + handler = nullptr; + break; + } + _ClearLastChar(); return handler; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 1f79795a91b..8ef83116046 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -39,7 +39,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionCsiDispatch(const VTID id, const VTParameters parameters) override; - StringHandler ActionDcsDispatch(const VTID id, const VTParameters parameters) noexcept override; + StringHandler ActionDcsDispatch(const VTID id, const VTParameters parameters) override; bool ActionClear() noexcept override; @@ -144,6 +144,11 @@ namespace Microsoft::Console::VirtualTerminal DECSCPP_SetColumnsPerPage = VTID("$|"), }; + enum DcsActionCodes : uint64_t + { + DECDLD_DownloadDRCS = VTID("{"), + }; + enum Vt52ActionCodes : uint64_t { CursorUp = VTID("A"),