diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 320a4bc1976a..2e8c968c28fd 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -292,6 +292,7 @@ codeproject coinit COLLECTIONURI colorizing +COLORMATRIX colororacle colorref colorscheme @@ -682,6 +683,7 @@ dwrite dwriteglyphrundescriptionclustermap dxgi dxgidwm +dxguid dxinterop dxp dxsm diff --git a/src/renderer/dx/CustomTextRenderer.cpp b/src/renderer/dx/CustomTextRenderer.cpp index 6d4eb36d6005..f3a894a43965 100644 --- a/src/renderer/dx/CustomTextRenderer.cpp +++ b/src/renderer/dx/CustomTextRenderer.cpp @@ -238,10 +238,10 @@ using namespace Microsoft::Console::Render; // - firstPass - true if we're being called before the text is drawn, false afterwards. // Return Value: // - S_FALSE if we did nothing, S_OK if we successfully painted, otherwise an appropriate HRESULT -[[nodiscard]] HRESULT _drawCursor(gsl::not_null d2dContext, - D2D1_RECT_F textRunBounds, - const DrawingContext& drawingContext, - const bool firstPass) +[[nodiscard]] HRESULT CustomTextRenderer::DrawCursor(gsl::not_null d2dContext, + D2D1_RECT_F textRunBounds, + const DrawingContext& drawingContext, + const bool firstPass) try { if (!drawingContext.cursorInfo.has_value()) @@ -374,7 +374,7 @@ try // = = ===== // ===== ===== // - // Then, outside of _drawCursor, the glyph is drawn: + // Then, outside of DrawCursor, the glyph is drawn: // // EMPTY BOX FILLED BOX // ==A== ==A== @@ -556,7 +556,7 @@ CATCH_RETURN() d2dContext->FillRectangle(rect, drawingContext->backgroundBrush); - RETURN_IF_FAILED(_drawCursor(d2dContext.Get(), rect, *drawingContext, true)); + RETURN_IF_FAILED(DrawCursor(d2dContext.Get(), rect, *drawingContext, true)); // GH#5098: If we're rendering with cleartype text, we need to always render // onto an opaque background. If our background _isn't_ opaque, then we need @@ -746,7 +746,7 @@ CATCH_RETURN() clientDrawingEffect)); } - RETURN_IF_FAILED(_drawCursor(d2dContext.Get(), rect, *drawingContext, false)); + RETURN_IF_FAILED(DrawCursor(d2dContext.Get(), rect, *drawingContext, false)); return S_OK; } diff --git a/src/renderer/dx/CustomTextRenderer.h b/src/renderer/dx/CustomTextRenderer.h index a30eb833b308..188b843d950f 100644 --- a/src/renderer/dx/CustomTextRenderer.h +++ b/src/renderer/dx/CustomTextRenderer.h @@ -112,6 +112,11 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT STDMETHODCALLTYPE EndClip(void* clientDrawingContext) noexcept; + [[nodiscard]] static HRESULT DrawCursor(gsl::not_null d2dContext, + D2D1_RECT_F textRunBounds, + const DrawingContext& drawingContext, + const bool firstPass); + private: [[nodiscard]] HRESULT _FillRectangle(void* clientDrawingContext, IUnknown* clientDrawingEffect, diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 386c8fb7925d..b0a349ff26e4 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -881,6 +881,8 @@ void DxEngine::_ReleaseDeviceResources() noexcept _d2dBitmap.Reset(); + _softFont.Reset(); + if (nullptr != _d2dDeviceContext.Get() && _isPainting) { _d2dDeviceContext->EndDraw(); @@ -1689,12 +1691,22 @@ try // Calculate positioning of our origin. const auto origin = (coord * _fontRenderData->GlyphCell()).to_d2d_point(); - // Create the text layout - RETURN_IF_FAILED(_customLayout->Reset()); - RETURN_IF_FAILED(_customLayout->AppendClusters(clusters)); + if (_usingSoftFont) + { + // We need to reset the clipping rect applied by the CustomTextRenderer, + // since the soft font will want to set its own clipping rect. + RETURN_IF_FAILED(_customRenderer->EndClip(_drawingContext.get())); + RETURN_IF_FAILED(_softFont.Draw(*_drawingContext, clusters, origin.x, origin.y)); + } + else + { + // Create the text layout + RETURN_IF_FAILED(_customLayout->Reset()); + RETURN_IF_FAILED(_customLayout->AppendClusters(clusters)); - // Layout then render the text - RETURN_IF_FAILED(_customLayout->Draw(_drawingContext.get(), _customRenderer.Get(), origin.x, origin.y)); + // Layout then render the text + RETURN_IF_FAILED(_customLayout->Draw(_drawingContext.get(), _customRenderer.Get(), origin.x, origin.y)); + } return S_OK; } @@ -1933,8 +1945,9 @@ CATCH_RETURN() [[nodiscard]] HRESULT DxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, const gsl::not_null /*pData*/, - const bool /*usingSoftFont*/, + const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept +try { const auto [colorForeground, colorBackground] = renderSettings.GetAttributeColorsWithAlpha(textAttributes); @@ -1951,6 +1964,12 @@ CATCH_RETURN() _d2dBrushForeground->SetColor(_foregroundColor); _d2dBrushBackground->SetColor(_backgroundColor); + _usingSoftFont = usingSoftFont; + if (_usingSoftFont) + { + _softFont.SetColor(_foregroundColor); + } + // If this flag is set, then we need to update the default brushes too and the swap chain background. if (isSettingDefaultBrushes) { @@ -1990,6 +2009,7 @@ CATCH_RETURN() return S_OK; } +CATCH_RETURN(); // Routine Description: // - Updates the font used for drawing @@ -2021,7 +2041,8 @@ try // Prepare the text layout. _customLayout = WRL::Make(_fontRenderData.get()); - return S_OK; + // Inform the soft font of the new cell size so it can scale appropriately. + return _softFont.SetTargetSize(_fontRenderData->GlyphCell()); } CATCH_RETURN(); @@ -2233,6 +2254,7 @@ try { _antialiasingMode = antialiasingMode; _recreateDeviceRequested = true; + LOG_IF_FAILED(_softFont.SetAntialiasing(antialiasingMode != D2D1_TEXT_ANTIALIAS_MODE_ALIASED)); LOG_IF_FAILED(InvalidateAll()); } } @@ -2275,6 +2297,24 @@ void DxEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept _hyperlinkHoveredId = hoveredId; } +// 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. +HRESULT DxEngine::UpdateSoftFont(const gsl::span bitPattern, + const til::size cellSize, + const size_t centeringHint) noexcept +try +{ + _softFont.SetFont(bitPattern, cellSize, _fontRenderData->GlyphCell(), centeringHint); + return S_OK; +} +CATCH_RETURN(); + // Method Description: // - Informs this render engine about certain state for this frame at the // beginning of this frame. We'll use it to get information about the cursor diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index fb1e4618d772..7d49fe141cb2 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -27,6 +27,7 @@ #include "CustomTextLayout.h" #include "CustomTextRenderer.h" #include "DxFontRenderData.h" +#include "DxSoftFont.h" #include "../../types/inc/Viewport.hpp" @@ -91,6 +92,10 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ScrollFrame() noexcept override; + [[nodiscard]] HRESULT UpdateSoftFont(const gsl::span bitPattern, + const til::size cellSize, + const size_t centeringHint) noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; [[nodiscard]] HRESULT ResetLineTransform() noexcept override; @@ -204,6 +209,8 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _hyperlinkStrokeStyle; std::unique_ptr _fontRenderData; + DxSoftFont _softFont; + bool _usingSoftFont; D2D1_STROKE_STYLE_PROPERTIES _strokeStyleProperties; D2D1_STROKE_STYLE_PROPERTIES _dashStrokeStyleProperties; diff --git a/src/renderer/dx/DxSoftFont.cpp b/src/renderer/dx/DxSoftFont.cpp new file mode 100644 index 000000000000..e9a3a6388910 --- /dev/null +++ b/src/renderer/dx/DxSoftFont.cpp @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DxSoftFont.h" +#include "CustomTextRenderer.h" + +#include + +#pragma comment(lib, "dxguid.lib") + +using namespace Microsoft::Console::Render; +using Microsoft::WRL::ComPtr; + +// The soft font is rendered into a bitmap laid out in a 12x8 grid, which is +// enough space for the 96 characters expected in the font, and which minimizes +// the dimensions for a typical 2:1 cell size. Each position in the grid is +// surrounded by a 2 pixel border which helps avoid bleed across the character +// boundaries when the output is scaled. + +constexpr size_t BITMAP_GRID_WIDTH = 12; +constexpr size_t BITMAP_GRID_HEIGHT = 8; +constexpr size_t PADDING = 2; + +constexpr auto ANTIALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; +constexpr auto ALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_NEAREST_NEIGHBOR; + +DxSoftFont::DxSoftFont() noexcept : + _centeringHint{}, + _interpolation{ ANTIALIASED_INTERPOLATION }, + _colorMatrix{} +{ + _colorMatrix.m[0][3] = 1; +} + +void DxSoftFont::SetFont(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint) +{ + Reset(); + + // If the font is being reset, just free up the memory and return. + if (bitPattern.empty()) + { + _bitmapBits.clear(); + return; + } + + const auto maxGlyphCount = BITMAP_GRID_WIDTH * BITMAP_GRID_HEIGHT; + _glyphCount = std::min(bitPattern.size() / sourceSize.height, maxGlyphCount); + _sourceSize = sourceSize; + _targetSize = targetSize; + _centeringHint = centeringHint; + + const auto bitmapWidth = BITMAP_GRID_WIDTH * (_sourceSize.width + PADDING * 2); + const auto bitmapHeight = BITMAP_GRID_HEIGHT * (_sourceSize.height + PADDING * 2); + _bitmapBits = std::vector(bitmapWidth * bitmapHeight); + _bitmapSize = { gsl::narrow_cast(bitmapWidth), gsl::narrow_cast(bitmapHeight) }; + + const auto bitmapScanline = [=](const auto lineNumber) noexcept { + return _bitmapBits.begin() + lineNumber * bitmapWidth; + }; + + // The source bitPattern is just a list of the scanlines making up the + // glyphs one after the other, but we want to lay them out in a grid, so + // we need to process each glyph individually. + auto srcPointer = bitPattern.begin(); + for (auto glyphNumber = 0u; glyphNumber < _glyphCount; glyphNumber++) + { + // We start by calculating the position in the bitmap where the glyph + // needs to be stored. + const auto xOffset = _xOffsetForGlyph(glyphNumber); + const auto yOffset = _yOffsetForGlyph(glyphNumber); + auto dstPointer = bitmapScanline(yOffset) + xOffset; + for (auto y = 0; y < sourceSize.height; y++) + { + // Then for each scanline in the source, we need to expand the bits + // into 8-bit values. For every bit that is set we write out an FF + // value, and if not set, we write out 00. In the end, all we care + // about is a single red component for the R8_UNORM bitmap format, + // since we'll later remap that to RGBA with a color matrix. + auto srcBits = *(srcPointer++); + for (auto x = 0; x < sourceSize.width; x++) + { + const auto srcBitIsSet = (srcBits & 0x8000) != 0; + *(dstPointer++) = srcBitIsSet ? 0xFF : 0x00; + srcBits <<= 1; + } + // When glyphs in this bitmap are output, they will typically need + // to scaled, and this can result in some bleed from the surrounding + // pixels. So to keep the borders clean, we pad the areas to the left + // and right by repeating the first and last pixels of each scanline. + std::fill_n(dstPointer, PADDING, til::at(dstPointer, -1)); + dstPointer -= sourceSize.width; + std::fill_n(dstPointer - PADDING, PADDING, til::at(dstPointer, 0)); + dstPointer += bitmapWidth; + } + } + + // In the same way that we padded the left and right of each glyph in the + // code above, we also need to pad the top and bottom. But in this case we + // can simply do a whole row of glyphs from the grid at the same time. + for (auto gridRow = 0u; gridRow < BITMAP_GRID_HEIGHT; gridRow++) + { + const auto rowOffset = _yOffsetForGlyph(gridRow); + const auto rowTop = bitmapScanline(rowOffset); + const auto rowBottom = bitmapScanline(rowOffset + _sourceSize.height - 1); + for (auto i = 1; i <= PADDING; i++) + { + std::copy_n(rowTop, bitmapWidth, rowTop - i * bitmapWidth); + std::copy_n(rowBottom, bitmapWidth, rowBottom + i * bitmapWidth); + } + } +} + +HRESULT DxSoftFont::SetTargetSize(const til::size targetSize) +{ + _targetSize = targetSize; + return _scaleEffect ? _scaleEffect->SetValue(D2D1_SCALE_PROP_SCALE, _scaleForTargetSize()) : S_OK; +} + +HRESULT DxSoftFont::SetAntialiasing(const bool antialiased) +{ + _interpolation = (antialiased ? ANTIALIASED_INTERPOLATION : ALIASED_INTERPOLATION); + return _scaleEffect ? _scaleEffect->SetValue(D2D1_SCALE_PROP_INTERPOLATION_MODE, _interpolation) : S_OK; +} + +HRESULT DxSoftFont::SetColor(const D2D1_COLOR_F& color) +{ + // Since our source image is monochrome, we don't care about the + // individual color components. We just multiply the red component + // by the active color value to get the output color. And note that + // the alpha matrix entry is already set to 1 in the constructor, + // so we don't need to keep updating it here. + _colorMatrix.m[0][0] = color.r; + _colorMatrix.m[0][1] = color.g; + _colorMatrix.m[0][2] = color.b; + return _colorEffect ? _colorEffect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, _colorMatrix) : S_OK; +} + +HRESULT DxSoftFont::Draw(const DrawingContext& drawingContext, + const gsl::span clusters, + const float originX, + const float originY) +{ + ComPtr d2dContext; + RETURN_IF_FAILED(drawingContext.renderTarget->QueryInterface(d2dContext.GetAddressOf())); + + // We start by creating a clipping rectangle for the region we're going to + // draw, and this is initially filled with the active background color. + D2D1_RECT_F rect; + rect.top = originY + drawingContext.topClipOffset; + rect.bottom = originY + _targetSize.height - drawingContext.bottomClipOffset; + rect.left = originX; + rect.right = originX + _targetSize.width * clusters.size(); + d2dContext->FillRectangle(rect, drawingContext.backgroundBrush); + d2dContext->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_ALIASED); + auto resetClippingRect = wil::scope_exit([&]() noexcept { d2dContext->PopAxisAlignedClip(); }); + + // The bitmap and associated scaling/coloring effects are created on demand + // so we need make sure they're generated now. + RETURN_IF_FAILED(_createResources(d2dContext.Get())); + + // We use the the CustomTextRenderer to draw the first pass of the cursor. + RETURN_IF_FAILED(CustomTextRenderer::DrawCursor(d2dContext.Get(), rect, drawingContext, true)); + + // Then we draw the associated glyph for each entry in the cluster list. + auto targetPoint = D2D1_POINT_2F{ originX, originY }; + for (auto& cluster : clusters) + { + // For DRCS, we only care about the character's lower 7 bits, then + // codepoint 0x20 will be the first glyph in the set. + const auto glyphNumber = (cluster.GetTextAsSingle() & 0x7f) - 0x20; + const auto x = _xOffsetForGlyph(glyphNumber); + const auto y = _yOffsetForGlyph(glyphNumber); + const auto sourceRect = D2D1_RECT_F{ x, y, x + _targetSize.width, y + _targetSize.height }; + LOG_IF_FAILED(_scaleEffect->SetValue(D2D1_SCALE_PROP_CENTER_POINT, D2D1::Point2F(x, y))); + d2dContext->DrawImage(_colorEffect.Get(), targetPoint, sourceRect); + targetPoint.x += _targetSize.width; + } + + // We finish by the drawing the second pass of the cursor. + return CustomTextRenderer::DrawCursor(d2dContext.Get(), rect, drawingContext, false); +} + +void DxSoftFont::Reset() +{ + _colorEffect.Reset(); + _scaleEffect.Reset(); + _bitmap.Reset(); +} + +HRESULT DxSoftFont::_createResources(gsl::not_null d2dContext) +{ + if (!_bitmap) + { + D2D1_BITMAP_PROPERTIES bitmapProperties{}; + bitmapProperties.pixelFormat.format = DXGI_FORMAT_R8_UNORM; + bitmapProperties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_IGNORE; + const auto bitmapPitch = gsl::narrow_cast(_bitmapSize.width); + RETURN_IF_FAILED(d2dContext->CreateBitmap(_bitmapSize, _bitmapBits.data(), bitmapPitch, bitmapProperties, _bitmap.GetAddressOf())); + } + + if (!_scaleEffect) + { + RETURN_IF_FAILED(d2dContext->CreateEffect(CLSID_D2D1Scale, _scaleEffect.GetAddressOf())); + RETURN_IF_FAILED(_scaleEffect->SetValue(D2D1_SCALE_PROP_INTERPOLATION_MODE, _interpolation)); + RETURN_IF_FAILED(_scaleEffect->SetValue(D2D1_SCALE_PROP_SCALE, _scaleForTargetSize())); + _scaleEffect->SetInput(0, _bitmap.Get()); + + if (_colorEffect) + { + _colorEffect->SetInputEffect(0, _scaleEffect.Get()); + } + } + + if (!_colorEffect) + { + RETURN_IF_FAILED(d2dContext->CreateEffect(CLSID_D2D1ColorMatrix, _colorEffect.GetAddressOf())); + RETURN_IF_FAILED(_colorEffect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, _colorMatrix)); + _colorEffect->SetInputEffect(0, _scaleEffect.Get()); + } + + return S_OK; +} + +D2D1_VECTOR_2F DxSoftFont::_scaleForTargetSize() const noexcept +{ + // If the text in the font is not perfectly centered, the _centeringHint + // gives us the offset needed to correct that misalignment. So to ensure + // the scaling is evenly balanced around the center point of the glyphs, + // we can use that hint to adjust the dimensions of our source and target + // widths when calculating the horizontal scale. + const auto targetCenteringHint = std::lround((float)_centeringHint * _targetSize.width / _sourceSize.width); + const auto xScale = gsl::narrow_cast(_targetSize.width - targetCenteringHint) / (_sourceSize.width - _centeringHint); + const auto yScale = gsl::narrow_cast(_targetSize.height) / _sourceSize.height; + return D2D1::Vector2F(xScale, yScale); +} + +template +T DxSoftFont::_xOffsetForGlyph(const size_t glyphNumber) const noexcept +{ + const auto xOffsetInGrid = glyphNumber / BITMAP_GRID_HEIGHT; + const auto paddedGlyphWidth = _sourceSize.width + PADDING * 2; + return gsl::narrow_cast(xOffsetInGrid * paddedGlyphWidth + PADDING); +} + +template +T DxSoftFont::_yOffsetForGlyph(const size_t glyphNumber) const noexcept +{ + const auto yOffsetInGrid = glyphNumber % BITMAP_GRID_HEIGHT; + const auto paddedGlyphHeight = _sourceSize.height + PADDING * 2; + return gsl::narrow_cast(yOffsetInGrid * paddedGlyphHeight + PADDING); +} diff --git a/src/renderer/dx/DxSoftFont.h b/src/renderer/dx/DxSoftFont.h new file mode 100644 index 000000000000..718861902a9f --- /dev/null +++ b/src/renderer/dx/DxSoftFont.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "../inc/Cluster.hpp" + +#include +#include +#include +#include +#include + +namespace Microsoft::Console::Render +{ + struct DrawingContext; + + class DxSoftFont + { + public: + DxSoftFont() noexcept; + void SetFont(const gsl::span bitPattern, + const til::size sourceSize, + const til::size targetSize, + const size_t centeringHint); + HRESULT SetTargetSize(const til::size targetSize); + HRESULT SetAntialiasing(const bool antialiased); + HRESULT SetColor(const D2D1_COLOR_F& color); + HRESULT Draw(const DrawingContext& drawingContext, + const gsl::span clusters, + const float originX, + const float originY); + void Reset(); + + private: + HRESULT _createResources(gsl::not_null d2dContext); + D2D1_VECTOR_2F _scaleForTargetSize() const noexcept; + template + T _xOffsetForGlyph(const size_t glyphNumber) const noexcept; + template + T _yOffsetForGlyph(const size_t glyphNumber) const noexcept; + + size_t _glyphCount; + til::size _sourceSize; + til::size _targetSize; + size_t _centeringHint; + D2D1_SCALE_INTERPOLATION_MODE _interpolation; + D2D1_MATRIX_5X4_F _colorMatrix; + D2D1_SIZE_U _bitmapSize; + std::vector _bitmapBits; + ::Microsoft::WRL::ComPtr _bitmap; + ::Microsoft::WRL::ComPtr _scaleEffect; + ::Microsoft::WRL::ComPtr _colorEffect; + }; +} diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj index d54407c5b84b..c416c9bbbc03 100644 --- a/src/renderer/dx/lib/dx.vcxproj +++ b/src/renderer/dx/lib/dx.vcxproj @@ -22,6 +22,7 @@ + Create @@ -33,6 +34,7 @@ + diff --git a/src/renderer/dx/lib/dx.vcxproj.filters b/src/renderer/dx/lib/dx.vcxproj.filters index a351c8e019cd..db30c6d1ae22 100644 --- a/src/renderer/dx/lib/dx.vcxproj.filters +++ b/src/renderer/dx/lib/dx.vcxproj.filters @@ -10,6 +10,7 @@ + @@ -19,6 +20,7 @@ + diff --git a/src/renderer/dx/sources.inc b/src/renderer/dx/sources.inc index f94f2d5ba325..35b2fbc70352 100644 --- a/src/renderer/dx/sources.inc +++ b/src/renderer/dx/sources.inc @@ -35,6 +35,7 @@ SOURCES = \ ..\DxRenderer.cpp \ ..\DxFontInfo.cpp \ ..\DxFontRenderData.cpp \ + ..\DxSoftFont.cpp \ ..\CustomTextRenderer.cpp \ ..\CustomTextLayout.cpp \