From acb3e299623018f0134b6b7388b33adb6c505eee Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 15:41:00 +0200 Subject: [PATCH 1/6] AtlasEngine: Implement builtin glyphs for D2D --- src/renderer/atlas/AtlasEngine.cpp | 11 +- src/renderer/atlas/AtlasEngine.h | 12 - src/renderer/atlas/AtlasEngine.r.cpp | 8 +- src/renderer/atlas/BackendD2D.cpp | 347 ++++++++++++++++++++++----- src/renderer/atlas/BackendD2D.h | 15 +- src/renderer/atlas/BackendD3D.cpp | 32 ++- src/renderer/atlas/BuiltinGlyphs.cpp | 54 ++--- src/renderer/atlas/BuiltinGlyphs.h | 12 +- src/tools/RenderingTests/main.cpp | 60 ++--- 9 files changed, 392 insertions(+), 159 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 28295aeadd2..146d13e98e2 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() @@ -771,12 +768,6 @@ void AtlasEngine::_flushBufferLine() size_t segmentEnd = 0; bool custom = false; - if (!_hackWantsBuiltinGlyphs) - { - _mapRegularText(0, len); - return; - } - while (segmentBeg < len) { segmentEnd = segmentBeg; 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..33360005c81 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) + 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) { - while (ColorGlyphRunMoveNext(enumerator.get())) - { - const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); - ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); - ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); - } + row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); } - else + if (row->lineRendition != LineRendition::DoubleHeightBottom) { - _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); - GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); - } - - 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) - { - 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,138 @@ 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) + { + u32 ch = row->glyphIndices[i]; + if (til::is_leading_surrogate(ch)) + { + i += 1; + ch = til::combine_surrogates(ch, row->glyphIndices[i]); + } + + 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 (!_builtinGlyphBatch || _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). + // This has 2 benefits: GPUs like power-of-2 textures and it ensures that we don't resize the texture + // every time you resize the window by a pixel. Instead it only grows/shrinks by a factor of 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 }; + + 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); + 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 (!_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 +550,117 @@ 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; - - auto posX = from * cellSize.x + pos.position; - const auto end = to * cellSize.x; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + auto x = from + pos.position; - 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())); + + 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); + 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 +694,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..4ead4324eb3 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,13 @@ 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; + // We need to round the coordinates to avoid antialiasing. + // In order to get a consistent rounding behavior across different glyphs, across different target coordinates, + // it's important that we first round them only then add the target coordinate. + 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 +1146,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 +1177,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(); From e8f0e91b949d60e3d7e4b8dd6a1198ab9e6142bd Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 16:44:22 +0200 Subject: [PATCH 2/6] Fix constness --- src/renderer/atlas/BackendD2D.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 33360005c81..fb74598fe58 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -407,7 +407,7 @@ D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t } const auto brush = _brushWithColor(0xffffffff); - D2D1_RECT_F rectF{ + const D2D1_RECT_F rectF{ static_cast(rectU.left), static_cast(rectU.top), static_cast(rectU.right), @@ -656,7 +656,7 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro THROW_IF_FAILED(sink->Close()); const auto brush = _brushWithColor(r.underlineColor); - D2D1_RECT_F clipRect{ from, rowTop, to, rowBottom }; + 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(); From b5c79ab846182b114df6db6be2f73e9d3c479027 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 23:13:15 +0200 Subject: [PATCH 3/6] Fix the builtinGlyph setting --- src/renderer/atlas/AtlasEngine.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 146d13e98e2..d4b6b3cdb69 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -768,6 +768,12 @@ void AtlasEngine::_flushBufferLine() size_t segmentEnd = 0; bool custom = false; + if (!_p.s->font->builtinGlyphs) + { + _mapRegularText(0, len); + return; + } + while (segmentBeg < len) { segmentEnd = segmentBeg; From 26dabafac8c92fa5858733545e7c141e86b7acb7 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 23:38:30 +0200 Subject: [PATCH 4/6] Address feedback --- src/renderer/atlas/AtlasEngine.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index d4b6b3cdb69..d661a14f0ca 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -762,6 +762,7 @@ 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; @@ -786,7 +787,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; From 313be146f6b181af3f530c1db4fd72ad40836bf2 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 23:42:30 +0200 Subject: [PATCH 5/6] Forgot to stage this --- src/renderer/atlas/AtlasEngine.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index d661a14f0ca..b5a89f6c09d 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -769,12 +769,6 @@ void AtlasEngine::_flushBufferLine() size_t segmentEnd = 0; bool custom = false; - if (!_p.s->font->builtinGlyphs) - { - _mapRegularText(0, len); - return; - } - while (segmentBeg < len) { segmentEnd = segmentBeg; From e015a0c3fbb2fae765765ab0d04b0d2d625c8744 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 17 May 2024 01:11:26 +0200 Subject: [PATCH 6/6] Add comments --- src/renderer/atlas/BackendD2D.cpp | 29 +++++++++++++++++++++++++--- src/renderer/atlas/BuiltinGlyphs.cpp | 11 ++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index fb74598fe58..cf7bd8a2bec 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -318,6 +318,8 @@ f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* r 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)) { @@ -325,6 +327,9 @@ f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* r 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) @@ -344,7 +349,17 @@ f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* r void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p) { - if (!_builtinGlyphBatch || _builtinGlyphsRenderTarget) + // 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; } @@ -356,8 +371,11 @@ void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p) // 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). - // This has 2 benefits: GPUs like power-of-2 textures and it ensures that we don't resize the texture - // every time you resize the window by a pixel. Instead it only grows/shrinks by a factor of 2. + // 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); @@ -388,6 +406,7 @@ D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t 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; @@ -421,6 +440,8 @@ D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t 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; @@ -431,6 +452,7 @@ void BackendD2D::_flushBuiltinGlyphs() THROW_IF_FAILED(_builtinGlyphsRenderTarget->EndDraw()); _builtinGlyphsRenderTargetActive = false; } + if (const auto count = _builtinGlyphBatch->GetSpriteCount(); count > 0) { _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); @@ -632,6 +654,7 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro 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;) { diff --git a/src/renderer/atlas/BuiltinGlyphs.cpp b/src/renderer/atlas/BuiltinGlyphs.cpp index 4ead4324eb3..92d87bc0b45 100644 --- a/src/renderer/atlas/BuiltinGlyphs.cpp +++ b/src/renderer/atlas/BuiltinGlyphs.cpp @@ -1131,9 +1131,14 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* const auto lineOffsetX = isHollowRect || isLineX ? lineWidthHalf : 0.0f; const auto lineOffsetY = isHollowRect || isLineY ? lineWidthHalf : 0.0f; - // We need to round the coordinates to avoid antialiasing. - // In order to get a consistent rounding behavior across different glyphs, across different target coordinates, - // it's important that we first round them only then add the target coordinate. + // 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;