Skip to content

Commit

Permalink
AtlasEngine: Improve dotted, dashed and curly underlines (#16719)
Browse files Browse the repository at this point in the history
This changeset makes 3 improvements:
* Dotted lines now use a 2:1 ratio between gaps and dots (from 1:1).
  This makes the dots a lot easier to spot at small font sizes.
* Dashed lines use a 1:2 ratio and a cells-size independent stride.
  By being cell-size independent it works more consistently with a
  wider variety of fonts with weird cell aspect ratios.
* Curly lines are now cell-size independent as well and have a
  height that equals the double-underline size.
  This ensures that the curve isn't cut off anymore and just like
  with dashed lines, that it works under weird aspect ratios.

Closes #16712

## Validation Steps Performed
This was tested using RenderingTests using Cascadia Mono, Consolas,
Courier New, Lucida Console and MS Gothic.

(cherry picked from commit 9c8058c)
Service-Card-Id: 91922826
Service-Version: 1.20
  • Loading branch information
lhecker authored and DHowett committed Feb 26, 2024
1 parent 07c6168 commit 4706697
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 45 deletions.
38 changes: 13 additions & 25 deletions src/renderer/atlas/BackendD3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,29 +310,18 @@ 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.
{
// initialize curlyline peak height to a desired value. Clamp it to at
// least 1.
constexpr auto curlyLinePeakHeightEm = 0.075f;
_curlyLinePeakHeight = std::max(1.0f, std::roundf(curlyLinePeakHeightEm * font.fontSize));

// calc the limit we need to apply
const auto strokeHalfWidth = std::floor(font.underline.height / 2.0f);
const auto underlineMidY = font.underline.position + strokeHalfWidth;
const auto maxDrawableCurlyLinePeakHeight = font.cellSize.y - underlineMidY - font.underline.height;

// if the limit is <= 0 (no height at all), stick with the desired height.
// This is how we force a curlyline even when there's no space, though it
// might be clipped at the bottom.
if (maxDrawableCurlyLinePeakHeight > 0.0f)
{
_curlyLinePeakHeight = std::min(_curlyLinePeakHeight, maxDrawableCurlyLinePeakHeight);
}
const auto cellHeight = static_cast<f32>(font.cellSize.y);
const auto strokeWidth = static_cast<f32>(font.thinLineWidth);

// 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, static_cast<f32>(font.doubleUnderline[1].position + font.doubleUnderline[1].height - font.doubleUnderline[0].position));
const auto top = std::min(static_cast<f32>(font.doubleUnderline[0].position), floorf(cellHeight - height - strokeWidth));

const auto curlyUnderlinePos = underlineMidY - _curlyLinePeakHeight - font.underline.height;
const auto curlyUnderlineWidth = 2.0f * (_curlyLinePeakHeight + font.underline.height);
const auto curlyUnderlinePosU16 = gsl::narrow_cast<u16>(lrintf(curlyUnderlinePos));
const auto curlyUnderlineWidthU16 = gsl::narrow_cast<u16>(lrintf(curlyUnderlineWidth));
_curlyUnderline = { curlyUnderlinePosU16, curlyUnderlineWidthU16 };
_curlyLineHalfHeight = height * 0.5f;
_curlyUnderline.position = gsl::narrow_cast<u16>(lrintf(top));
_curlyUnderline.height = gsl::narrow_cast<u16>(lrintf(height));
}

DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put());
Expand Down Expand Up @@ -573,9 +562,8 @@ void BackendD3D::_recreateConstBuffer(const RenderingPayload& p) const
DWrite_GetGammaRatios(_gamma, data.gammaRatios);
data.enhancedContrast = p.s->font->antialiasingMode == AntialiasingMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast;
data.underlineWidth = p.s->font->underline.height;
data.curlyLineWaveFreq = 2.0f * 3.14f / p.s->font->cellSize.x;
data.curlyLinePeakHeight = _curlyLinePeakHeight;
data.curlyLineCellOffset = p.s->font->underline.position + p.s->font->underline.height / 2.0f;
data.thinLineWidth = p.s->font->thinLineWidth;
data.curlyLineHalfHeight = _curlyLineHalfHeight;
p.deviceContext->UpdateSubresource(_psConstantBuffer.get(), 0, nullptr, &data, 0, 0);
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/renderer/atlas/BackendD3D.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ namespace Microsoft::Console::Render::Atlas
alignas(sizeof(f32x4)) f32 gammaRatios[4]{};
alignas(sizeof(f32)) f32 enhancedContrast = 0;
alignas(sizeof(f32)) f32 underlineWidth = 0;
alignas(sizeof(f32)) f32 curlyLinePeakHeight = 0;
alignas(sizeof(f32)) f32 curlyLineWaveFreq = 0;
alignas(sizeof(f32)) f32 curlyLineCellOffset = 0;
alignas(sizeof(f32)) f32 thinLineWidth = 0;
alignas(sizeof(f32)) f32 curlyLineHalfHeight = 0;
#pragma warning(suppress : 4324) // 'PSConstBuffer': structure was padded due to alignment specifier
};

Expand Down Expand Up @@ -291,7 +290,7 @@ namespace Microsoft::Console::Render::Atlas
// The bounding rect of _cursorRects in pixels.
til::rect _cursorPosition;

f32 _curlyLinePeakHeight = 0.0f;
f32 _curlyLineHalfHeight = 0.0f;
FontDecorationPosition _curlyUnderline;

bool _requiresContinuousRedraw = false;
Expand Down
25 changes: 9 additions & 16 deletions src/renderer/atlas/shader_ps.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ cbuffer ConstBuffer : register(b0)
float4 gammaRatios;
float enhancedContrast;
float underlineWidth;
float curlyLinePeakHeight;
float curlyLineWaveFreq;
float curlyLineCellOffset;
float thinLineWidth;
float curlyLineHalfHeight;
}

Texture2D<float4> background : register(t0);
Expand Down Expand Up @@ -76,31 +75,25 @@ Output main(PSData data) : SV_Target
}
case SHADING_TYPE_DOTTED_LINE:
{
const bool on = frac(data.position.x / (2.0f * underlineWidth * data.renditionScale.x)) < 0.5f;
const bool on = frac(data.position.x / (3.0f * underlineWidth * data.renditionScale.x)) < (1.0f / 3.0f);
color = on * premultiplyColor(data.color);
weights = color.aaaa;
break;
}
case SHADING_TYPE_DASHED_LINE:
{
const bool on = frac(data.position.x / (backgroundCellSize.x * data.renditionScale.x)) < 0.5f;
const bool on = frac(data.position.x / (6.0f * underlineWidth * data.renditionScale.x)) < (4.0f / 6.0f);
color = on * premultiplyColor(data.color);
weights = color.aaaa;
break;
}
case SHADING_TYPE_CURLY_LINE:
{
uint cellRow = floor(data.position.y / backgroundCellSize.y);
// Use the previous cell when drawing 'Double Height' curly line.
cellRow -= data.renditionScale.y - 1;
const float cellTop = cellRow * backgroundCellSize.y;
const float centerY = cellTop + curlyLineCellOffset * data.renditionScale.y;
const float strokeWidthHalf = underlineWidth * data.renditionScale.y / 2.0f;
const float amp = curlyLinePeakHeight * data.renditionScale.y;
const float freq = curlyLineWaveFreq / data.renditionScale.x;

const float s = sin(data.position.x * freq);
const float d = abs(centerY - (s * amp) - data.position.y);
const float strokeWidthHalf = thinLineWidth * data.renditionScale.y * 0.5f;
const float amp = (curlyLineHalfHeight - strokeWidthHalf) * data.renditionScale.y;
const float freq = data.renditionScale.x / curlyLineHalfHeight * 1.57079632679489661923f;
const float s = sin(data.position.x * freq) * amp;
const float d = abs(curlyLineHalfHeight - data.texcoord.y - s);
const float a = 1 - saturate(d - strokeWidthHalf);
color = a * premultiplyColor(data.color);
weights = color.aaaa;
Expand Down

1 comment on commit 4706697

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

See the 📜action log or 📝 job summary for details.

Unrecognized words (15)
ahicon
commoncontrols
COPYFROMRESOURCE
EXACTSIZEONLY
HIGHQUALITYSCALE
ICONINFO
IImage
ILC
ILCo
ILD
phico
phicon
piml
snapcx
snapcy
Previously acknowledged words that are now absent chcbpat DESTINATIONNAME inputrc kcub kcud kcuf kcuu khome Mbxy QUESTIONMARK reallocs reamapping RTFTo xff 🫥
To accept these unrecognized words as correct and remove the previously acknowledged and now absent words, you could run the following commands

... in a clone of the [email protected]:microsoft/terminal.git repository
on the release-1.20 branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.22/apply.pl' |
perl - 'https://github.com/microsoft/terminal/actions/runs/8053362739/attempts/1'
Available 📚 dictionaries could cover words (expected and unrecognized) not in the 📘 dictionary

This includes both expected items (2244) from .github/actions/spelling/expect/04cdb9b77d6827c0202f51acd4205b017015bfff.txt
.github/actions/spelling/expect/alphabet.txt
.github/actions/spelling/expect/expect.txt
.github/actions/spelling/expect/web.txt and unrecognized words (15)

Dictionary Entries Covers Uniquely
cspell:k8s/dict/k8s.txt 153 2 2
cspell:swift/src/swift.txt 53 1 1
cspell:gaming-terms/dict/gaming-terms.txt 59 1 1
cspell:monkeyc/src/monkeyc_keywords.txt 123 1 1
cspell:cryptocurrencies/cryptocurrencies.txt 125 1 1

Consider adding them (in .github/workflows/spelling2.yml) for uses: check-spelling/[email protected] in its with:

      with:
        extra_dictionaries:
          cspell:k8s/dict/k8s.txt
          cspell:swift/src/swift.txt
          cspell:gaming-terms/dict/gaming-terms.txt
          cspell:monkeyc/src/monkeyc_keywords.txt
          cspell:cryptocurrencies/cryptocurrencies.txt

To stop checking additional dictionaries, add (in .github/workflows/spelling2.yml) for uses: check-spelling/[email protected] in its with:

check_extra_dictionaries: ''
Errors (1)

See the 📜action log or 📝 job summary for details.

❌ Errors Count
❌ ignored-expect-variant 3

See ❌ Event descriptions for more information.

✏️ Contributor please read this

By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.

If the listed items are:

  • ... misspelled, then please correct them instead of using the command.
  • ... names, please add them to .github/actions/spelling/allow/names.txt.
  • ... APIs, you can add them to a file in .github/actions/spelling/allow/.
  • ... just things you're using, please add them to an appropriate file in .github/actions/spelling/expect/.
  • ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in .github/actions/spelling/patterns/.

See the README.md in each directory for more information.

🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Please sign in to comment.