Skip to content

Commit

Permalink
AtlasEngine: Scale down glyphs with overhangs
Browse files Browse the repository at this point in the history
  • Loading branch information
lhecker committed Jul 22, 2022
1 parent bb40efc commit 151eba0
Show file tree
Hide file tree
Showing 7 changed files with 517 additions and 194 deletions.
67 changes: 38 additions & 29 deletions src/renderer/atlas/AtlasEngine.api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ CATCH_RETURN()
DWRITE_TEXT_METRICS metrics;
RETURN_IF_FAILED(textLayout->GetMetrics(&metrics));

*pResult = static_cast<unsigned int>(std::ceil(metrics.width)) > _api.fontMetrics.cellSize.x;
*pResult = static_cast<unsigned int>(std::ceilf(metrics.width)) > _api.fontMetrics.cellSize.x;
return S_OK;
}

Expand Down Expand Up @@ -605,57 +605,66 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
// Point sizes are commonly treated at a 72 DPI scale
// (including by OpenType), whereas DirectWrite uses 96 DPI.
// Since we want the height in px we multiply by the display's DPI.
const auto fontSizeInPx = std::ceil(requestedSize.Y / 72.0 * _api.dpi);

const auto designUnitsPerPx = fontSizeInPx / static_cast<double>(metrics.designUnitsPerEm);
const auto ascentInPx = static_cast<double>(metrics.ascent) * designUnitsPerPx;
const auto descentInPx = static_cast<double>(metrics.descent) * designUnitsPerPx;
const auto lineGapInPx = static_cast<double>(metrics.lineGap) * designUnitsPerPx;
const auto advanceWidthInPx = static_cast<double>(glyphMetrics.advanceWidth) * designUnitsPerPx;

const auto halfGapInPx = lineGapInPx / 2.0;
const auto baseline = std::ceil(ascentInPx + halfGapInPx);
const auto cellWidth = gsl::narrow<u16>(std::ceil(advanceWidthInPx));
const auto cellHeight = gsl::narrow<u16>(std::ceil(baseline + descentInPx + halfGapInPx));
const auto fontSizeInPx = std::ceilf(requestedSize.Y / 72.0f * _api.dpi);

const auto designUnitsPerPx = fontSizeInPx / static_cast<float>(metrics.designUnitsPerEm);
const auto ascentInPx = static_cast<float>(metrics.ascent) * designUnitsPerPx;
const auto descentInPx = static_cast<float>(metrics.descent) * designUnitsPerPx;
const auto lineGapInPx = static_cast<float>(metrics.lineGap) * designUnitsPerPx;
const auto advanceWidthInPx = static_cast<float>(glyphMetrics.advanceWidth) * designUnitsPerPx;

const auto halfGapInPx = lineGapInPx / 2.0f;
const auto baselineInPx = std::ceilf(ascentInPx + halfGapInPx);
const auto lineHeightInPx = baselineInPx + descentInPx + halfGapInPx;

// How large a Terminal "cell" would like to be in fractional pixels.
const f32x2 theoreticalSize{ advanceWidthInPx, lineHeightInPx };
// Since there are no fractional pixels, we need to round the cell size.
const f32x2 practicalSize{ std::ceilf(theoreticalSize.x), std::ceilf(theoreticalSize.y) };
const u16x2 cellSize{ gsl::narrow<u16>(practicalSize.x), gsl::narrow<u16>(practicalSize.y) };
// Given a glyph size in fractional units, multiplying it by scale will yield a glyph size in
// cell-sized units. It basically compensates for us rounding theoreticalSize to practicalSize.
const f32x2 scale{ practicalSize.x / theoreticalSize.x, practicalSize.y / theoreticalSize.y };

{
til::size coordSize;
coordSize.X = cellWidth;
coordSize.Y = cellHeight;
coordSize.X = cellSize.x;
coordSize.Y = cellSize.y;

if (requestedSize.X == 0)
{
// The coordSizeUnscaled parameter to SetFromEngine is used for API functions like GetConsoleFontSize.
// Since clients expect that settings the font height to Y yields back a font height of Y,
// we're scaling the X relative/proportional to the actual cellWidth/cellHeight ratio.
// we're scaling the X relative/proportional to the actual cellSize.x/.y ratio.
// The code below uses a poor form of integer rounding.
requestedSize.X = (requestedSize.Y * cellWidth + cellHeight / 2) / cellHeight;
requestedSize.X = (requestedSize.Y * cellSize.x + cellSize.y / 2) / cellSize.y;
}

fontInfo.SetFromEngine(requestedFaceName, requestedFamily, requestedWeight, false, coordSize, requestedSize);
}

if (fontMetrics)
{
const auto underlineOffsetInPx = static_cast<double>(-metrics.underlinePosition) * designUnitsPerPx;
const auto underlineThicknessInPx = static_cast<double>(metrics.underlineThickness) * designUnitsPerPx;
const auto strikethroughOffsetInPx = static_cast<double>(-metrics.strikethroughPosition) * designUnitsPerPx;
const auto strikethroughThicknessInPx = static_cast<double>(metrics.strikethroughThickness) * designUnitsPerPx;
const auto lineThickness = gsl::narrow<u16>(std::round(std::min(underlineThicknessInPx, strikethroughThicknessInPx)));
const auto underlinePos = gsl::narrow<u16>(std::ceil(baseline + underlineOffsetInPx - lineThickness / 2.0));
const auto strikethroughPos = gsl::narrow<u16>(std::round(baseline + strikethroughOffsetInPx - lineThickness / 2.0));

auto fontName = wil::make_process_heap_string(requestedFaceName);
const auto underlineOffsetInPx = static_cast<float>(-metrics.underlinePosition) * designUnitsPerPx;
const auto underlineThicknessInPx = static_cast<float>(metrics.underlineThickness) * designUnitsPerPx;
const auto strikethroughOffsetInPx = static_cast<float>(-metrics.strikethroughPosition) * designUnitsPerPx;
const auto strikethroughThicknessInPx = static_cast<float>(metrics.strikethroughThickness) * designUnitsPerPx;
const auto lineThickness = gsl::narrow<u16>(std::roundf(std::min(underlineThicknessInPx, strikethroughThicknessInPx)));
const auto underlinePos = gsl::narrow<u16>(std::ceilf(baselineInPx + underlineOffsetInPx - lineThickness / 2.0f));
const auto strikethroughPos = gsl::narrow<u16>(std::roundf(baselineInPx + strikethroughOffsetInPx - lineThickness / 2.0f));

std::wstring fontName{ requestedFaceName };
const auto fontWeight = gsl::narrow<u16>(requestedWeight);

// NOTE: From this point onward no early returns or throwing code should exist,
// as we might cause _api to be in an inconsistent state otherwise.

fontMetrics->fontCollection = std::move(fontCollection);
fontMetrics->fontName = std::move(fontName);
fontMetrics->fontSizeInDIP = static_cast<float>(fontSizeInPx / static_cast<double>(_api.dpi) * 96.0);
fontMetrics->baselineInDIP = static_cast<float>(baseline / static_cast<double>(_api.dpi) * 96.0);
fontMetrics->cellSize = { cellWidth, cellHeight };
fontMetrics->fontSizeInDIP = fontSizeInPx / static_cast<float>(_api.dpi) * 96.0f;
fontMetrics->baselineInDIP = baselineInPx / static_cast<float>(_api.dpi) * 96.0f;
fontMetrics->scale = scale;
fontMetrics->cellSize = cellSize;
fontMetrics->fontWeight = fontWeight;
fontMetrics->underlinePos = underlinePos;
fontMetrics->strikethroughPos = strikethroughPos;
Expand Down
161 changes: 17 additions & 144 deletions src/renderer/atlas/AtlasEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,134 +25,6 @@

using namespace Microsoft::Console::Render;

struct TextAnalyzer final : IDWriteTextAnalysisSource, IDWriteTextAnalysisSink
{
constexpr TextAnalyzer(const std::vector<wchar_t>& text, std::vector<AtlasEngine::TextAnalyzerResult>& results) noexcept :
_text{ text }, _results{ results }
{
Ensures(_text.size() <= UINT32_MAX);
}

// TextAnalyzer will be allocated on the stack and reference counting is pointless because of that.
// The debug version will assert that we don't leak any references though.
#ifdef NDEBUG
ULONG __stdcall AddRef() noexcept override
{
return 1;
}

ULONG __stdcall Release() noexcept override
{
return 1;
}
#else
ULONG _refCount = 1;

~TextAnalyzer()
{
assert(_refCount == 1);
}

ULONG __stdcall AddRef() noexcept override
{
return ++_refCount;
}

ULONG __stdcall Release() noexcept override
{
return --_refCount;
}
#endif

HRESULT __stdcall QueryInterface(const IID& riid, void** ppvObject) noexcept override
{
__assume(ppvObject != nullptr);

if (IsEqualGUID(riid, __uuidof(IDWriteTextAnalysisSource)) || IsEqualGUID(riid, __uuidof(IDWriteTextAnalysisSink)))
{
*ppvObject = this;
return S_OK;
}

*ppvObject = nullptr;
return E_NOINTERFACE;
}

HRESULT __stdcall GetTextAtPosition(UINT32 textPosition, const WCHAR** textString, UINT32* textLength) noexcept override
{
// Writing to address 0 is a crash in practice. Just what we want.
__assume(textString != nullptr);
__assume(textLength != nullptr);

const auto size = gsl::narrow_cast<UINT32>(_text.size());
textPosition = std::min(textPosition, size);
*textString = _text.data() + textPosition;
*textLength = size - textPosition;
return S_OK;
}

HRESULT __stdcall GetTextBeforePosition(UINT32 textPosition, const WCHAR** textString, UINT32* textLength) noexcept override
{
// Writing to address 0 is a crash in practice. Just what we want.
__assume(textString != nullptr);
__assume(textLength != nullptr);

const auto size = gsl::narrow_cast<UINT32>(_text.size());
textPosition = std::min(textPosition, size);
*textString = _text.data();
*textLength = textPosition;
return S_OK;
}

DWRITE_READING_DIRECTION __stdcall GetParagraphReadingDirection() noexcept override
{
return DWRITE_READING_DIRECTION_LEFT_TO_RIGHT;
}

HRESULT __stdcall GetLocaleName(UINT32 textPosition, UINT32* textLength, const WCHAR** localeName) noexcept override
{
// Writing to address 0 is a crash in practice. Just what we want.
__assume(textLength != nullptr);
__assume(localeName != nullptr);

*textLength = gsl::narrow_cast<UINT32>(_text.size()) - textPosition;
*localeName = nullptr;
return S_OK;
}

HRESULT __stdcall GetNumberSubstitution(UINT32 textPosition, UINT32* textLength, IDWriteNumberSubstitution** numberSubstitution) noexcept override
{
return E_NOTIMPL;
}

HRESULT __stdcall SetScriptAnalysis(UINT32 textPosition, UINT32 textLength, const DWRITE_SCRIPT_ANALYSIS* scriptAnalysis) noexcept override
try
{
_results.emplace_back(AtlasEngine::TextAnalyzerResult{ textPosition, textLength, scriptAnalysis->script, static_cast<UINT8>(scriptAnalysis->shapes), 0 });
return S_OK;
}
CATCH_RETURN()

HRESULT __stdcall SetLineBreakpoints(UINT32 textPosition, UINT32 textLength, const DWRITE_LINE_BREAKPOINT* lineBreakpoints) noexcept override
{
return E_NOTIMPL;
}

HRESULT __stdcall SetBidiLevel(UINT32 textPosition, UINT32 textLength, UINT8 explicitLevel, UINT8 resolvedLevel) noexcept override
{
return E_NOTIMPL;
}

HRESULT __stdcall SetNumberSubstitution(UINT32 textPosition, UINT32 textLength, IDWriteNumberSubstitution* numberSubstitution) noexcept override
{
return E_NOTIMPL;
}

private:
const std::vector<wchar_t>& _text;
std::vector<AtlasEngine::TextAnalyzerResult>& _results;
};

#pragma warning(suppress : 26455) // Default constructor may not throw. Declare it 'noexcept' (f.6).
AtlasEngine::AtlasEngine()
{
Expand Down Expand Up @@ -976,10 +848,10 @@ void AtlasEngine::_recreateFontDependentResources()

_r.cellSizeDIP.x = static_cast<float>(_api.fontMetrics.cellSize.x) / scaling;
_r.cellSizeDIP.y = static_cast<float>(_api.fontMetrics.cellSize.y) / scaling;
_r.cellSize = _api.fontMetrics.cellSize;
_r.fontMetrics = _api.fontMetrics;
_r.cellCount = _api.cellCount;
_r.atlasSizeInPixel = { 0, 0 };
_r.tileAllocator = TileAllocator{ _r.cellSize, _api.sizeInPixel };
_r.tileAllocator = TileAllocator{ _r.fontMetrics.cellSize, _api.sizeInPixel };

_r.glyphs = {};
_r.glyphQueue = {};
Expand All @@ -1001,10 +873,8 @@ void AtlasEngine::_recreateFontDependentResources()

// D2D
{
_r.underlinePos = _api.fontMetrics.underlinePos;
_r.strikethroughPos = _api.fontMetrics.strikethroughPos;
_r.lineThickness = _api.fontMetrics.lineThickness;
_r.dpi = _api.dpi;
_r.dipPerPixel = static_cast<float>(USER_DEFAULT_SCREEN_DPI) / static_cast<float>(_r.dpi);
}
{
// See AtlasEngine::UpdateFont.
Expand Down Expand Up @@ -1036,19 +906,21 @@ void AtlasEngine::_recreateFontDependentResources()
const auto fontStyle = italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
auto& textFormat = _r.textFormats[italic][bold];

THROW_IF_FAILED(_sr.dwriteFactory->CreateTextFormat(_api.fontMetrics.fontName.get(), _api.fontMetrics.fontCollection.get(), fontWeight, fontStyle, DWRITE_FONT_STRETCH_NORMAL, _api.fontMetrics.fontSizeInDIP, L"", textFormat.put()));
textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);
THROW_IF_FAILED(_sr.dwriteFactory->CreateTextFormat(_api.fontMetrics.fontName.data(), _api.fontMetrics.fontCollection.get(), fontWeight, fontStyle, DWRITE_FONT_STRETCH_NORMAL, _api.fontMetrics.fontSizeInDIP, L"", textFormat.put()));
THROW_IF_FAILED(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER));
THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));

// DWRITE_LINE_SPACING_METHOD_UNIFORM:
// > Lines are explicitly set to uniform spacing, regardless of contained font sizes.
// > This can be useful to avoid the uneven appearance that can occur from font fallback.
// We want that. Otherwise fallback fonts might be rendered with an incorrect baseline and get cut off vertically.
THROW_IF_FAILED(textFormat->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, _r.cellSizeDIP.y, _api.fontMetrics.baselineInDIP));

if (!_api.fontAxisValues.empty())
if (const auto textFormat3 = textFormat.try_query<IDWriteTextFormat3>())
{
if (const auto textFormat3 = textFormat.try_query<IDWriteTextFormat3>())
THROW_IF_FAILED(textFormat3->SetAutomaticFontAxes(DWRITE_AUTOMATIC_FONT_AXES_OPTICAL_SIZE));

if (!_api.fontAxisValues.empty())
{
// The wght axis defaults to the font weight.
_api.fontAxisValues[0].value = bold || standardAxes[0].value == -1.0f ? static_cast<float>(fontWeight) : standardAxes[0].value;
Expand Down Expand Up @@ -1181,7 +1053,8 @@ void AtlasEngine::_flushBufferLine()
const auto textFormat = _getTextFormat(_api.attributes.bold, _api.attributes.italic);
const auto& textFormatAxis = _getTextFormatAxis(_api.attributes.bold, _api.attributes.italic);

TextAnalyzer atlasAnalyzer{ _api.bufferLine, _api.analysisResults };
TextAnalysisSource analysisSource{ _api.bufferLine.data(), gsl::narrow<UINT32>(_api.bufferLine.size()) };
TextAnalysisSink analysisSink{ _api.analysisResults };

wil::com_ptr<IDWriteFontCollection> fontCollection;
THROW_IF_FAILED(textFormat->GetFontCollection(fontCollection.addressof()));
Expand All @@ -1200,11 +1073,11 @@ void AtlasEngine::_flushBufferLine()
{
wil::com_ptr<IDWriteFontFace5> fontFace5;
THROW_IF_FAILED(_sr.systemFontFallback.query<IDWriteFontFallback1>()->MapCharacters(
/* analysisSource */ &atlasAnalyzer,
/* analysisSource */ &analysisSource,
/* textPosition */ idx,
/* textLength */ gsl::narrow_cast<u32>(_api.bufferLine.size()) - idx,
/* baseFontCollection */ fontCollection.get(),
/* baseFamilyName */ _api.fontMetrics.fontName.get(),
/* baseFamilyName */ _api.fontMetrics.fontName.data(),
/* fontAxisValues */ textFormatAxis.data(),
/* fontAxisValueCount */ gsl::narrow_cast<u32>(textFormatAxis.size()),
/* mappedLength */ &mappedLength,
Expand All @@ -1219,11 +1092,11 @@ void AtlasEngine::_flushBufferLine()
wil::com_ptr<IDWriteFont> font;

THROW_IF_FAILED(_sr.systemFontFallback->MapCharacters(
/* analysisSource */ &atlasAnalyzer,
/* analysisSource */ &analysisSource,
/* textPosition */ idx,
/* textLength */ gsl::narrow_cast<u32>(_api.bufferLine.size()) - idx,
/* baseFontCollection */ fontCollection.get(),
/* baseFamilyName */ _api.fontMetrics.fontName.get(),
/* baseFamilyName */ _api.fontMetrics.fontName.data(),
/* baseWeight */ baseWeight,
/* baseStyle */ baseStyle,
/* baseStretch */ DWRITE_FONT_STRETCH_NORMAL,
Expand Down Expand Up @@ -1305,7 +1178,7 @@ void AtlasEngine::_flushBufferLine()
else
{
_api.analysisResults.clear();
THROW_IF_FAILED(_sr.textAnalyzer->AnalyzeScript(&atlasAnalyzer, idx, complexityLength, &atlasAnalyzer));
THROW_IF_FAILED(_sr.textAnalyzer->AnalyzeScript(&analysisSource, idx, complexityLength, &analysisSink));
//_sr.textAnalyzer->AnalyzeBidi(&atlasAnalyzer, idx, complexityLength, &atlasAnalyzer);

for (const auto& a : _api.analysisResults)
Expand Down
Loading

0 comments on commit 151eba0

Please sign in to comment.