From 3486111722296f287158e0340789c607642c1067 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 17 May 2024 01:41:48 +0200 Subject: [PATCH] AtlasEngine: Implement remaining underlines and builtin glyphs for D2D (#17278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements builtin glyphs for our Direct2D renderer, as well as dashed and curly underlines. With this in place the only two features it doesn't support are inverted cursors and VT soft fonts. This allows us to remove the `_hack*` members introduced in a6a0e44. The implementation of dashed underlines is trivial, while curly underlines use quadratic bezier curves. Caching the curve as a sprite is possible, however I feel like that can be done in the future. Builtin glyphs on the other hand require a cache, because otherwise filling the entire viewport with shaded glyphs would result in poor performance. This is why it's built on top of `ID2D1SpriteBatch`. Unfortunately the API causes an eager flush of other pending graphics instructions, which is why there's still a decent perf hit. Finally, as a little extra, this fixes the rounded powerline glyph shapes being slightly cut off. The fix is to simply don't round the position and radius of the ellipsis/semi-circle. Closes #17224 ## Validation Steps Performed * RenderingTests.exe updated ✅ * All supported builtin glyphs look sorta right at different sizes ✅ --- src/renderer/atlas/AtlasEngine.cpp | 14 +- src/renderer/atlas/AtlasEngine.h | 12 - src/renderer/atlas/AtlasEngine.r.cpp | 8 +- src/renderer/atlas/BackendD2D.cpp | 370 ++++++++++++++++++++++----- src/renderer/atlas/BackendD2D.h | 15 +- src/renderer/atlas/BackendD3D.cpp | 32 ++- src/renderer/atlas/BuiltinGlyphs.cpp | 59 +++-- src/renderer/atlas/BuiltinGlyphs.h | 12 +- src/tools/RenderingTests/main.cpp | 60 ++--- 9 files changed, 422 insertions(+), 160 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 28295aeadd2..b5a89f6c09d 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -74,9 +74,8 @@ try _handleSettingsUpdate(); } - if (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION || _hackTriggerRedrawAll) + if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION) { - _hackTriggerRedrawAll = false; _api.invalidatedRows = invalidatedRowsAll; _api.scrollOffset = 0; } @@ -703,8 +702,6 @@ void AtlasEngine::_recreateFontDependentResources() _api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() }; } } - - _hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D; } void AtlasEngine::_recreateCellCountDependentResources() @@ -765,18 +762,13 @@ void AtlasEngine::_flushBufferLine() // This would seriously blow us up otherwise. Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1); + const auto builtinGlyphs = _p.s->font->builtinGlyphs; const auto beg = _api.bufferLine.data(); const auto len = _api.bufferLine.size(); size_t segmentBeg = 0; size_t segmentEnd = 0; bool custom = false; - if (!_hackWantsBuiltinGlyphs) - { - _mapRegularText(0, len); - return; - } - while (segmentBeg < len) { segmentEnd = segmentBeg; @@ -789,7 +781,7 @@ void AtlasEngine::_flushBufferLine() codepoint = til::combine_surrogates(codepoint, beg[i++]); } - const auto c = BuiltinGlyphs::IsBuiltinGlyph(codepoint) || BuiltinGlyphs::IsSoftFontChar(codepoint); + const auto c = (builtinGlyphs && BuiltinGlyphs::IsBuiltinGlyph(codepoint)) || BuiltinGlyphs::IsSoftFontChar(codepoint); if (custom != c) { break; diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 1f26644ebe6..ccb4da9fb4e 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -127,18 +127,6 @@ namespace Microsoft::Console::Render::Atlas std::unique_ptr _b; RenderingPayload _p; - // _p.s->font->builtinGlyphs is the setting which decides whether we should map box drawing glyphs to - // our own builtin versions. There's just one problem: BackendD2D doesn't have this functionality. - // But since AtlasEngine shapes the text before it's handed to the backends, it would need to know - // whether BackendD2D is in use, before BackendD2D even exists. These two flags solve the issue - // by triggering a complete, immediate redraw whenever the backend type changes. - // - // The proper solution is to move text shaping into the backends. - // Someone just needs to write a generic "TextBuffer to DWRITE_GLYPH_RUN" function. - bool _hackIsBackendD2D = false; - bool _hackWantsBuiltinGlyphs = true; - bool _hackTriggerRedrawAll = false; - struct ApiState { GenerationalSettings s = DirtyGenerationalSettings(); diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index a0dbdcc5470..3591abe7905 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -77,7 +77,7 @@ CATCH_RETURN() [[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept { - return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()) || _hackTriggerRedrawAll; + return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()); } void AtlasEngine::WaitUntilCanRender() noexcept @@ -282,21 +282,15 @@ void AtlasEngine::_recreateBackend() { case GraphicsAPI::Direct2D: _b = std::make_unique(); - _hackIsBackendD2D = true; break; default: _b = std::make_unique(_p); - _hackIsBackendD2D = false; break; } // This ensures that the backends redraw their entire viewports whenever a new swap chain is created, // EVEN IF we got called when no actual settings changed (i.e. rendering failure, etc.). _p.MarkAllAsDirty(); - - const auto hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D; - _hackTriggerRedrawAll = _hackWantsBuiltinGlyphs != hackWantsBuiltinGlyphs; - _hackWantsBuiltinGlyphs = hackWantsBuiltinGlyphs; } void AtlasEngine::_handleSwapChainUpdate() diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 5a663b49d04..cf7bd8a2bec 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -4,6 +4,8 @@ #include "pch.h" #include "BackendD2D.h" +#include + #if ATLAS_DEBUG_SHOW_DIRTY #include "colorbrewer.h" #endif @@ -94,11 +96,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) .dpiY = static_cast(p.s->font->dpi), }; // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not. - THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.addressof()))); - _renderTarget.try_query_to(_renderTarget4.addressof()); + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.put()))); _renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); - _renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + + _renderTarget.try_query_to(_renderTarget4.put()); + if (_renderTarget4) + { + THROW_IF_FAILED(_renderTarget4->CreateSpriteBatch(_builtinGlyphBatch.put())); + } } { static constexpr D2D1_COLOR_F color{}; @@ -108,18 +114,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) } } - if (!_dottedStrokeStyle) - { - static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; - static constexpr FLOAT dashes[2]{ 1, 1 }; - THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); - } - if (renderTargetChanged || fontChanged) { const auto dpi = static_cast(p.s->font->dpi); _renderTarget->SetDpi(dpi, dpi); _renderTarget->SetTextAntialiasMode(static_cast(p.s->font->antialiasingMode)); + + _builtinGlyphsRenderTarget.reset(); + _builtinGlyphsBitmap.reset(); + _builtinGlyphsRenderTargetActive = false; } if (renderTargetChanged || fontChanged || cellCountChanged) @@ -199,6 +202,12 @@ void BackendD2D::_drawText(RenderingPayload& p) for (const auto& m : row->mappings) { + if (!m.fontFace) + { + baselineX = _drawBuiltinGlyphs(p, row, m, baselineY, baselineX); + continue; + } + const auto colorsBegin = row->colors.begin(); auto it = colorsBegin + m.glyphsFrom; const auto end = colorsBegin + m.glyphsTo; @@ -228,42 +237,39 @@ void BackendD2D::_drawText(RenderingPayload& p) baselineY, }; - if (glyphRun.fontFace) + D2D1_RECT_F bounds = GlyphRunEmptyBounds; + wil::com_ptr enumerator; + + if (p.s->font->colorGlyphs) { - D2D1_RECT_F bounds = GlyphRunEmptyBounds; - wil::com_ptr enumerator; + enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + } - if (p.s->font->colorGlyphs) + if (enumerator) + { + while (ColorGlyphRunMoveNext(enumerator.get())) { - enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); + ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); + ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); } + } + else + { + _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); + GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + } - if (enumerator) - { - while (ColorGlyphRunMoveNext(enumerator.get())) - { - const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); - ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); - ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); - } - } - else + if (bounds.top < bounds.bottom) + { + // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. + if (row->lineRendition != LineRendition::DoubleHeightTop) { - _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); - GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); } - - if (bounds.top < bounds.bottom) + if (row->lineRendition != LineRendition::DoubleHeightBottom) { - // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. - if (row->lineRendition != LineRendition::DoubleHeightTop) - { - row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); - } - if (row->lineRendition != LineRendition::DoubleHeightBottom) - { - row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); - } + row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); } } @@ -274,6 +280,8 @@ void BackendD2D::_drawText(RenderingPayload& p) } } + _flushBuiltinGlyphs(); + if (!row->gridLineRanges.empty()) { _drawGridlineRow(p, row, y); @@ -300,6 +308,160 @@ void BackendD2D::_drawText(RenderingPayload& p) } } +f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX) +{ + const f32 cellTop = baselineY - p.s->font->baseline; + const f32 cellBottom = cellTop + p.s->font->cellSize.y; + const f32 cellWidth = p.s->font->cellSize.x; + + _prepareBuiltinGlyphRenderTarget(p); + + for (size_t i = m.glyphsFrom; i < m.glyphsTo; ++i) + { + // This code runs when fontFace == nullptr. This is only the case for builtin glyphs which then use the glyphIndices + // to store UTF16 code points. In other words, this doesn't accidentally corrupt any actual glyph indices. + u32 ch = row->glyphIndices[i]; + if (til::is_leading_surrogate(ch)) + { + i += 1; + ch = til::combine_surrogates(ch, row->glyphIndices[i]); + } + + // If we don't have support for ID2D1SpriteBatch we don't support builtin glyphs. + // But we do still need to account for the glyphAdvances, which is why we can't just skip everything. + // It's very unlikely for a target device to not support ID2D1SpriteBatch as it's very old at this point. + if (_builtinGlyphBatch) + { + if (const auto off = BuiltinGlyphs::GetBitmapCellIndex(ch); off >= 0) + { + const D2D1_RECT_F dst{ baselineX, cellTop, baselineX + cellWidth, cellBottom }; + const auto src = _prepareBuiltinGlyph(p, ch, off); + const auto color = colorFromU32(row->colors[i]); + THROW_IF_FAILED(_builtinGlyphBatch->AddSprites(1, &dst, &src, &color, nullptr, sizeof(D2D1_RECT_F), sizeof(D2D1_RECT_U), sizeof(D2D1_COLOR_F), sizeof(D2D1_MATRIX_3X2_F))); + } + } + + baselineX += row->glyphAdvances[i]; + } + + return baselineX; +} + +void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p) +{ + // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used. + // We can just early-return in that case. + if (!_builtinGlyphBatch) + { + return; + } + + // If the render target is already created, all of the below has already been done in a previous frame. + // Once the relevant settings change for some reason (primarily the font->cellSize), then _handleSettingsUpdate() + // will reset the render target which will cause us to skip this condition and re-initialize it below. + if (_builtinGlyphsRenderTarget) + { + return; + } + + const auto cellWidth = static_cast(p.s->font->cellSize.x); + const auto cellHeight = static_cast(p.s->font->cellSize.y); + const auto cellArea = cellWidth * cellHeight; + const auto area = cellArea * BuiltinGlyphs::TotalCharCount; + + // This block of code calculates the size of a power-of-2 texture that has an area larger than the given `area`. + // For instance, for an area of 985x1946 = 1916810 it would result in a u/v of 2048x1024 (area = 2097152). + // We throw the "v" in this case away, because we don't really need power-of-2 textures here, + // but you can find the complete code over in BackendD3D. If someone deleted it in the meantime: + // const auto index = bitness_of_area_minus_1 - std::countl_zero(area - 1); // aka: _BitScanReverse + // const auto u = 1u << ((index + 2) / 2); + // const auto v = 1u << ((index + 1) / 2); + unsigned long index; + _BitScanReverse(&index, area - 1); + const auto potWidth = 1u << ((index + 2) / 2); + + const auto cellCountU = potWidth / cellWidth; + const auto cellCountV = (BuiltinGlyphs::TotalCharCount + cellCountU - 1) / cellCountU; + const auto u = cellCountU * cellWidth; + const auto v = cellCountV * cellHeight; + + const D2D1_SIZE_F sizeF{ static_cast(u), static_cast(v) }; + const D2D1_SIZE_U sizeU{ gsl::narrow_cast(u), gsl::narrow_cast(v) }; + static constexpr D2D1_PIXEL_FORMAT format{ DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }; + wil::com_ptr target; + THROW_IF_FAILED(_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, &format, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, target.addressof())); + + THROW_IF_FAILED(target->GetBitmap(_builtinGlyphsBitmap.put())); + _builtinGlyphsRenderTarget = target.query(); + _builtinGlyphsBitmapCellCountU = cellCountU; + _builtinGlyphsRenderTargetActive = false; + memset(&_builtinGlyphsReady[0], 0, sizeof(_builtinGlyphsReady)); +} + +D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off) +{ + const u32 w = p.s->font->cellSize.x; + const u32 h = p.s->font->cellSize.y; + const u32 l = (off % _builtinGlyphsBitmapCellCountU) * w; + const u32 t = (off / _builtinGlyphsBitmapCellCountU) * h; + D2D1_RECT_U rectU{ l, t, l + w, t + h }; + + // Check if we previously cached this glyph already. + if (_builtinGlyphsReady[off]) + { + return rectU; + } + + static constexpr D2D1_COLOR_F shadeColorMap[] = { + { 1, 1, 1, 0.25f }, // Shape_Filled025 + { 1, 1, 1, 0.50f }, // Shape_Filled050 + { 1, 1, 1, 0.75f }, // Shape_Filled075 + { 1, 1, 1, 1.00f }, // Shape_Filled100 + }; + + if (!_builtinGlyphsRenderTargetActive) + { + _builtinGlyphsRenderTarget->BeginDraw(); + _builtinGlyphsRenderTargetActive = true; + } + + const auto brush = _brushWithColor(0xffffffff); + const D2D1_RECT_F rectF{ + static_cast(rectU.left), + static_cast(rectU.top), + static_cast(rectU.right), + static_cast(rectU.bottom), + }; + BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _builtinGlyphsRenderTarget.get(), brush, shadeColorMap, rectF, ch); + + _builtinGlyphsReady[off] = true; + return rectU; +} + +void BackendD2D::_flushBuiltinGlyphs() +{ + // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used. + // We can just early-return in that case. + if (!_builtinGlyphBatch) + { + return; + } + + if (_builtinGlyphsRenderTargetActive) + { + THROW_IF_FAILED(_builtinGlyphsRenderTarget->EndDraw()); + _builtinGlyphsRenderTargetActive = false; + } + + if (const auto count = _builtinGlyphBatch->GetSpriteCount(); count > 0) + { + _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + _renderTarget4->DrawSpriteBatch(_builtinGlyphBatch.get(), 0, count, _builtinGlyphsBitmap.get(), D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_SPRITE_OPTIONS_NONE); + _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + _builtinGlyphBatch->Clear(); + } +} + f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept { const auto lineRendition = row->lineRendition; @@ -410,44 +572,118 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y) { - const auto widthShift = gsl::narrow_cast(row->lineRendition != LineRendition::SingleWidth); - const auto cellSize = p.s->font->cellSize; - const auto rowTop = gsl::narrow_cast(cellSize.y * y); - const auto rowBottom = gsl::narrow_cast(rowTop + cellSize.y); - const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + const auto cellWidth = static_cast(p.s->font->cellSize.x); + const auto cellHeight = static_cast(p.s->font->cellSize.y); + const auto rowTop = cellHeight * y; + const auto rowBottom = rowTop + cellHeight; + const auto cellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + const auto scaleHorizontal = row->lineRendition != LineRendition::SingleWidth ? 0.5f : 1.0f; + const auto scaledCellWidth = cellWidth * scaleHorizontal; const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) { - const auto from = r.from >> widthShift; - const auto to = r.to >> widthShift; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + auto x = from + pos.position; - auto posX = from * cellSize.x + pos.position; - const auto end = to * cellSize.x; - - D2D1_POINT_2F point0{ 0, static_cast(textCellCenter) }; - D2D1_POINT_2F point1{ 0, static_cast(textCellCenter + cellSize.y) }; + D2D1_POINT_2F point0{ 0, cellCenter }; + D2D1_POINT_2F point1{ 0, cellCenter + cellHeight }; const auto brush = _brushWithColor(r.gridlineColor); const f32 w = pos.height; const f32 hw = w * 0.5f; - for (; posX < end; posX += cellSize.x) + for (; x < to; x += cellWidth) { - const auto centerX = posX + hw; + const auto centerX = x + hw; point0.x = centerX; point1.x = centerX; _renderTarget->DrawLine(point0, point1, brush, w, nullptr); } }; const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle, const u32 color) { - const auto from = r.from >> widthShift; - const auto to = r.to >> widthShift; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; const auto brush = _brushWithColor(color); const f32 w = pos.height; - const f32 centerY = textCellCenter + pos.position + w * 0.5f; - const D2D1_POINT_2F point0{ static_cast(from * cellSize.x), centerY }; - const D2D1_POINT_2F point1{ static_cast(to * cellSize.x), centerY }; + const f32 centerY = cellCenter + pos.position + w * 0.5f; + const D2D1_POINT_2F point0{ from, centerY }; + const D2D1_POINT_2F point1{ to, centerY }; _renderTarget->DrawLine(point0, point1, brush, w, strokeStyle); }; + const auto appendCurlyLine = [&](const GridLineRange& r) { + const auto& font = *p.s->font; + + const auto duTop = static_cast(font.doubleUnderline[0].position); + const auto duBottom = static_cast(font.doubleUnderline[1].position); + // The double-underline height is also our target line width. + const auto duHeight = static_cast(font.doubleUnderline[0].height); + + // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from + // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. + // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). + const auto height = std::max(3.0f, duBottom + duHeight - duTop); + const auto position = std::min(duTop, cellHeight - height - duHeight); + + // The amplitude of the wave needs to account for the stroke width, so that the final height including + // antialiasing isn't larger than our target `height`. That's why we calculate `(height - duHeight)`. + // + // In other words, Direct2D draws strokes centered on the path. This also means that (for instance) + // for a line width of 1px, we need to ensure that the amplitude passes through the center of a pixel. + // Because once the path gets stroked, it'll occupy half a pixel on either side of the path. + // This results in a "crisp" look. That's why we do `round(amp + half) - half`. + const auto halfLineWidth = 0.5f * duHeight; + const auto amplitude = roundf((height - duHeight) * 0.5f + halfLineWidth) - halfLineWidth; + // While the amplitude needs to account for the stroke width, the vertical center of the wave needs + // to be at an integer pixel position of course. Otherwise, the wave won't be vertically symmetric. + const auto center = cellCenter + position + amplitude + halfLineWidth; + + const auto top = center - 2.0f * amplitude; + const auto bottom = center + 2.0f * amplitude; + const auto step = 0.5f * height; + const auto period = 4.0f * step; + + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + // Align the start of the wave to the nearest preceding period boundary. + // This ensures that the wave is continuous across color and cell changes. + auto x = floorf(from / period) * period; + + wil::com_ptr geometry; + THROW_IF_FAILED(p.d2dFactory->CreatePathGeometry(geometry.addressof())); + + wil::com_ptr sink; + THROW_IF_FAILED(geometry->Open(sink.addressof())); + + // This adds complete periods of the wave until we reach the end of the range. + sink->BeginFigure({ x, center }, D2D1_FIGURE_BEGIN_HOLLOW); + for (D2D1_QUADRATIC_BEZIER_SEGMENT segment; x < to;) + { + x += step; + segment.point1.x = x; + segment.point1.y = top; + x += step; + segment.point2.x = x; + segment.point2.y = center; + sink->AddQuadraticBezier(&segment); + + x += step; + segment.point1.x = x; + segment.point1.y = bottom; + x += step; + segment.point2.x = x; + segment.point2.y = center; + sink->AddQuadraticBezier(&segment); + } + sink->EndFigure(D2D1_FIGURE_END_OPEN); + + THROW_IF_FAILED(sink->Close()); + + const auto brush = _brushWithColor(r.underlineColor); + const D2D1_RECT_F clipRect{ from, rowTop, to, rowBottom }; + _renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); + _renderTarget->DrawGeometry(geometry.get(), brush, duHeight, nullptr); + _renderTarget->PopAxisAlignedClip(); + }; for (const auto& r : row->gridLineRanges) { @@ -481,8 +717,28 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro } else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { + if (!_dottedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 1, 1 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); + } appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get(), r.underlineColor); } + else if (r.lines.test(GridLines::DashedUnderline)) + { + if (!_dashedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 2, 2 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dashedStrokeStyle.addressof())); + } + appendHorizontalLine(r, p.s->font->underline, _dashedStrokeStyle.get(), r.underlineColor); + } + else if (r.lines.test(GridLines::CurlyUnderline)) + { + appendCurlyLine(r); + } else if (r.lines.test(GridLines::DoubleUnderline)) { for (const auto pos : p.s->font->doubleUnderline) diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h index e6993d60603..4206390ea52 100644 --- a/src/renderer/atlas/BackendD2D.h +++ b/src/renderer/atlas/BackendD2D.h @@ -3,9 +3,8 @@ #pragma once -#include - #include "Backend.h" +#include "BuiltinGlyphs.h" namespace Microsoft::Console::Render::Atlas { @@ -19,6 +18,10 @@ namespace Microsoft::Console::Render::Atlas ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); void _drawBackground(const RenderingPayload& p); void _drawText(RenderingPayload& p); + ATLAS_ATTR_COLD f32 _drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX); + void _prepareBuiltinGlyphRenderTarget(const RenderingPayload& p); + D2D1_RECT_U _prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off); + void _flushBuiltinGlyphs(); ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept; ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept; ATLAS_ATTR_COLD f32r _getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY); @@ -37,10 +40,18 @@ namespace Microsoft::Console::Render::Atlas wil::com_ptr _renderTarget; wil::com_ptr _renderTarget4; // Optional. Supported since Windows 10 14393. wil::com_ptr _dottedStrokeStyle; + wil::com_ptr _dashedStrokeStyle; wil::com_ptr _backgroundBitmap; wil::com_ptr _backgroundBrush; til::generation_t _backgroundBitmapGeneration; + wil::com_ptr _builtinGlyphsRenderTarget; + wil::com_ptr _builtinGlyphsBitmap; + wil::com_ptr _builtinGlyphBatch; + u32 _builtinGlyphsBitmapCellCountU = 0; + bool _builtinGlyphsRenderTargetActive = false; + bool _builtinGlyphsReady[BuiltinGlyphs::TotalCharCount]{}; + wil::com_ptr _cursorBitmap; til::size _cursorBitmapSize; // in columns/rows diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index fe07271c16a..d85d6e0365f 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -295,20 +295,20 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p) // baseline of curlyline is at the middle of singly underline. When there's // limited space to draw a curlyline, we apply a limit on the peak height. { - const auto cellHeight = static_cast(font.cellSize.y); - const auto duTop = static_cast(font.doubleUnderline[0].position); - const auto duBottom = static_cast(font.doubleUnderline[1].position); - const auto duHeight = static_cast(font.doubleUnderline[0].height); + const int cellHeight = font.cellSize.y; + const int duTop = font.doubleUnderline[0].position; + const int duBottom = font.doubleUnderline[1].position; + const int duHeight = font.doubleUnderline[0].height; // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. - // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom. - const auto height = std::max(3.0f, duBottom + duHeight - duTop); - const auto top = std::min(duTop, floorf(cellHeight - height - duHeight)); + // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). + const auto height = std::max(3, duBottom + duHeight - duTop); + const auto position = std::min(duTop, cellHeight - height - duHeight); _curlyLineHalfHeight = height * 0.5f; - _curlyUnderline.position = gsl::narrow_cast(lrintf(top)); - _curlyUnderline.height = gsl::narrow_cast(lrintf(height)); + _curlyUnderline.position = gsl::narrow_cast(position); + _curlyUnderline.height = gsl::narrow_cast(height); } DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put()); @@ -1509,7 +1509,19 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa } else { - BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), r, glyphIndex); + // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader. + // Unless someone removed it, it should have a lengthy comment visually explaining + // what each of the 3 RGB components do. The short version is: + // R: stretch the checkerboard pattern (Shape_Filled050) horizontally + // G: invert the pixels + // B: overrides the above and fills it + static constexpr D2D1_COLOR_F shadeColorMap[] = { + { 1, 0, 0, 1 }, // Shape_Filled025 + { 0, 0, 0, 1 }, // Shape_Filled050 + { 1, 1, 0, 1 }, // Shape_Filled075 + { 1, 1, 1, 1 }, // Shape_Filled100 + }; + BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), shadeColorMap, r, glyphIndex); shadingType = ShadingType::TextBuiltinGlyph; } diff --git a/src/renderer/atlas/BuiltinGlyphs.cpp b/src/renderer/atlas/BuiltinGlyphs.cpp index c46bfc01fc0..92d87bc0b45 100644 --- a/src/renderer/atlas/BuiltinGlyphs.cpp +++ b/src/renderer/atlas/BuiltinGlyphs.cpp @@ -135,8 +135,6 @@ inline constexpr f32 Pos_Lut[][2] = { /* Pos_11_12 */ { 11.0f / 12.0f, 0.0f }, }; -static constexpr char32_t BoxDrawing_FirstChar = 0x2500; -static constexpr u32 BoxDrawing_CharCount = 0xA0; static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGlyph] = { // U+2500 ─ BOX DRAWINGS LIGHT HORIZONTAL { @@ -964,8 +962,6 @@ static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGly }, }; -static constexpr char32_t Powerline_FirstChar = 0xE0B0; -static constexpr u32 Powerline_CharCount = 0x10; static constexpr Instruction Powerline[Powerline_CharCount][InstructionsPerGlyph] = { // U+E0B0 Right triangle solid { @@ -1071,7 +1067,20 @@ static const Instruction* GetInstructions(char32_t codepoint) noexcept return nullptr; } -void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint) +i32 BuiltinGlyphs::GetBitmapCellIndex(char32_t codepoint) noexcept +{ + if (BoxDrawing_IsMapped(codepoint)) + { + return codepoint - BoxDrawing_FirstChar; + } + if (Powerline_IsMapped(codepoint)) + { + return codepoint - Powerline_FirstChar + BoxDrawing_CharCount; + } + return -1; +} + +void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint) { renderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); const auto restoreD2D = wil::scope_exit([&]() { @@ -1122,15 +1131,18 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* const auto lineOffsetX = isHollowRect || isLineX ? lineWidthHalf : 0.0f; const auto lineOffsetY = isHollowRect || isLineY ? lineWidthHalf : 0.0f; - begX = roundf(begX - lineOffsetX) + lineOffsetX; - begY = roundf(begY - lineOffsetY) + lineOffsetY; - endX = roundf(endX + lineOffsetX) - lineOffsetX; - endY = roundf(endY + lineOffsetY) - lineOffsetY; - - const auto begXabs = begX + rectX; - const auto begYabs = begY + rectY; - const auto endXabs = endX + rectX; - const auto endYabs = endY + rectY; + // Direct2D draws strokes centered on the path. In order to make them pixel-perfect we need to round the + // coordinates to whole pixels, but offset by half the stroke width (= the radius of the stroke). + // + // All floats up to this point will be highly "consistent" between different `rect`s of identical size and + // different shapes, because the above calculations work with only a small set of constant floats. + // However, the addition of a potentially fractional begX/Y with a highly variable `rect` position is different. + // Rounding beg/endX/Y first ensures that we continue to get a consistent behavior between calls. + // This is particularly noticeable at smaller font sizes, where the line width is just a pixel or two. + const auto begXabs = rectX + roundf(begX - lineOffsetX) + lineOffsetX; + const auto begYabs = rectY + roundf(begY - lineOffsetY) + lineOffsetY; + const auto endXabs = rectX + roundf(endX + lineOffsetX) - lineOffsetX; + const auto endYabs = rectY + roundf(endY + lineOffsetY) - lineOffsetY; switch (shape) { @@ -1139,21 +1151,8 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* case Shape_Filled075: case Shape_Filled100: { - // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader. - // Unless someone removed it, it should have a lengthy comment visually explaining - // what each of the 3 RGB components do. The short version is: - // R: stretch the checkerboard pattern (Shape_Filled050) horizontally - // G: invert the pixels - // B: overrides the above and fills it - static constexpr D2D1_COLOR_F colors[] = { - { 1, 0, 0, 1 }, // Shape_Filled025 - { 0, 0, 0, 1 }, // Shape_Filled050 - { 1, 1, 0, 1 }, // Shape_Filled075 - { 1, 1, 1, 1 }, // Shape_Filled100 - }; - const auto brushColor = brush->GetColor(); - brush->SetColor(&colors[shape]); + brush->SetColor(&shadeColorMap[shape]); const D2D1_RECT_F r{ begXabs, begYabs, endXabs, endYabs }; renderTarget->FillRectangle(&r, brush); @@ -1183,13 +1182,13 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* } case Shape_FilledEllipsis: { - const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY }; + const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY }; renderTarget->FillEllipse(&e, brush); break; } case Shape_EmptyEllipsis: { - const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY }; + const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY }; renderTarget->DrawEllipse(&e, brush, lineWidth, nullptr); break; } diff --git a/src/renderer/atlas/BuiltinGlyphs.h b/src/renderer/atlas/BuiltinGlyphs.h index f399653893c..b6cce3a3974 100644 --- a/src/renderer/atlas/BuiltinGlyphs.h +++ b/src/renderer/atlas/BuiltinGlyphs.h @@ -8,7 +8,17 @@ namespace Microsoft::Console::Render::Atlas::BuiltinGlyphs { bool IsBuiltinGlyph(char32_t codepoint) noexcept; - void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint); + void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint); + + inline constexpr char32_t BoxDrawing_FirstChar = 0x2500; + inline constexpr u32 BoxDrawing_CharCount = 0xA0; + + inline constexpr char32_t Powerline_FirstChar = 0xE0B0; + inline constexpr u32 Powerline_CharCount = 0x10; + + inline constexpr u32 TotalCharCount = BoxDrawing_CharCount + Powerline_CharCount; + + i32 GetBitmapCellIndex(char32_t codepoint) noexcept; // This is just an extra. It's not actually implemented as part of BuiltinGlyphs.cpp. constexpr bool IsSoftFontChar(char32_t ch) noexcept diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp index 45dfcf66ea7..2d0ef8612a4 100644 --- a/src/tools/RenderingTests/main.cpp +++ b/src/tools/RenderingTests/main.cpp @@ -108,15 +108,15 @@ static void printfUTF16(_In_z_ _Printf_format_string_ wchar_t const* const forma static void wait() { - printUTF16(L"\x1B[9999;1HPress any key to continue..."); + printUTF16(L"\x1b[9999;1HPress any key to continue..."); _getch(); } static void clear() { printUTF16( - L"\x1B[H" // move cursor to 0,0 - L"\x1B[2J" // clear screen + L"\x1b[H" // move cursor to 0,0 + L"\x1b[2J" // clear screen ); } @@ -166,7 +166,7 @@ int main() for (const auto& t : consoleAttributeTests) { const auto length = static_cast(wcslen(t.text)); - printfUTF16(L"\x1B[%d;5H%s", row + 1, t.text); + printfUTF16(L"\x1b[%d;5H%s", row + 1, t.text); WORD attributes[32]; std::fill_n(&attributes[0], length, static_cast(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | t.attribute)); @@ -190,16 +190,16 @@ int main() { L"overlined", 53 }, }; - printfUTF16(L"\x1B[3;39HANSI escape SGR:"); + printfUTF16(L"\x1b[3;39HANSI escape SGR:"); int row = 5; for (const auto& t : basicSGR) { - printfUTF16(L"\x1B[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text); + printfUTF16(L"\x1b[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text); row += 2; } - printfUTF16(L"\x1B[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); + printfUTF16(L"\x1b[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); } { @@ -211,18 +211,18 @@ int main() { L"dashed", 5 }, }; - printfUTF16(L"\x1B[3;63HStyled Underlines:"); + printfUTF16(L"\x1b[3;63HStyled Underlines:"); int row = 5; for (const auto& t : styledUnderlines) { - printfUTF16(L"\x1B[%d;63H\x1b[4:%dm", row, t.attribute); + printfUTF16(L"\x1b[%d;63H\x1b[4:%dm", row, t.attribute); const auto len = wcslen(t.text); for (size_t i = 0; i < len; ++i) { const auto color = colorbrewer::pastel1[i % std::size(colorbrewer::pastel1)]; - printfUTF16(L"\x1B[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]); + printfUTF16(L"\x1b[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]); } printfUTF16(L"\x1b[m"); @@ -236,19 +236,19 @@ int main() { printUTF16( - L"\x1B[3;5HDECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" - L"\x1B[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" - L"\x1B[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" - L"\x1B[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m" - L"\x1B[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" - L"\x1B[13;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[15;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[17;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[19;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[21;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[22;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[24;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[25;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); + L"\x1b[3;5HDECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F" + L"\x1b[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F" + L"\x1b[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F" + L"\x1b[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m" + L"\x1b[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F" + L"\x1b[12;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[14;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[16;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[18;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[20;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[21;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[23;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[24;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); static constexpr WORD attributes[]{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL, @@ -264,7 +264,7 @@ int main() DWORD numberOfAttrsWritten; DWORD offset = 0; - for (const auto r : { 12, 14, 16, 18, 20, 21, 23, 24 }) + for (const auto r : { 11, 13, 15, 17, 19, 20, 22, 23 }) { COORD coord; coord.X = r > 14 ? 2 : 4; @@ -338,14 +338,14 @@ int main() #define DRCS_SEQUENCE L"\x1b( @#\x1b(A" printUTF16( - L"\x1B[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" - L"\x1B[5;5HRegular: " DRCS_SEQUENCE L"" - L"\x1B[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" - L"\x1B[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" - L"\x1B[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" + L"\x1b[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" + L"\x1b[5;5HRegular: " DRCS_SEQUENCE L"" + L"\x1b[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" + L"\x1b[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" + L"\x1b[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" // We map soft fonts into the private use area starting at U+EF20. This test ensures // that we correctly map actual fallback glyphs mixed into the DRCS glyphs. - L"\x1B[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); + L"\x1b[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); #undef DRCS_SEQUENCE wait();