Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make shaded block glyphs look even betterer #16760

Merged
merged 5 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/renderer/atlas/AtlasEngine.api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -688,29 +688,30 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
const auto underlineWidth = std::max(1.0f, std::roundf(underlineThickness));
const auto strikethroughPos = std::roundf(baseline + strikethroughPosition);
const auto strikethroughWidth = std::max(1.0f, std::roundf(strikethroughThickness));
const auto thinLineWidth = std::max(1.0f, std::roundf(underlineThickness / 2.0f));
const auto doubleUnderlineWidth = std::max(1.0f, std::roundf(underlineThickness / 2.0f));
lhecker marked this conversation as resolved.
Show resolved Hide resolved
const auto thinLineWidth = std::max(1.0f, std::roundf(std::max(adjustedWidth / 16.0f, adjustedHeight / 32.0f)));

// For double underlines we loosely follow what Word does:
// 1. The lines are half the width of an underline (= thinLineWidth)
// 1. The lines are half the width of an underline (= doubleUnderlineWidth)
// 2. Ideally the bottom line is aligned with the bottom of the underline
// 3. The top underline is vertically in the middle between baseline and ideal bottom underline
// 4. If the top line gets too close to the baseline the underlines are shifted downwards
// 5. The minimum gap between the two lines appears to be similar to Tex (1.2pt)
// (Additional notes below.)

// 2.
auto doubleUnderlinePosBottom = underlinePos + underlineWidth - thinLineWidth;
auto doubleUnderlinePosBottom = underlinePos + underlineWidth - doubleUnderlineWidth;
// 3. Since we don't align the center of our two lines, but rather the top borders
// we need to subtract half a line width from our center point.
auto doubleUnderlinePosTop = std::roundf((baseline + doubleUnderlinePosBottom - thinLineWidth) / 2.0f);
auto doubleUnderlinePosTop = std::roundf((baseline + doubleUnderlinePosBottom - doubleUnderlineWidth) / 2.0f);
// 4.
doubleUnderlinePosTop = std::max(doubleUnderlinePosTop, baseline + thinLineWidth);
doubleUnderlinePosTop = std::max(doubleUnderlinePosTop, baseline + doubleUnderlineWidth);
// 5. The gap is only the distance _between_ the lines, but we need the distance from the
// top border of the top and bottom lines, which includes an additional line width.
const auto doubleUnderlineGap = std::max(1.0f, std::roundf(1.2f / 72.0f * dpi));
doubleUnderlinePosBottom = std::max(doubleUnderlinePosBottom, doubleUnderlinePosTop + doubleUnderlineGap + thinLineWidth);
doubleUnderlinePosBottom = std::max(doubleUnderlinePosBottom, doubleUnderlinePosTop + doubleUnderlineGap + doubleUnderlineWidth);
// Our cells can't overlap each other so we additionally clamp the bottom line to be inside the cell boundaries.
doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, adjustedHeight - thinLineWidth);
doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, adjustedHeight - doubleUnderlineWidth);

const auto cellWidth = gsl::narrow<u16>(lrintf(adjustedWidth));
const auto cellHeight = gsl::narrow<u16>(lrintf(adjustedHeight));
Expand Down Expand Up @@ -749,6 +750,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
const auto strikethroughWidthU16 = gsl::narrow_cast<u16>(lrintf(strikethroughWidth));
const auto doubleUnderlinePosTopU16 = gsl::narrow_cast<u16>(lrintf(doubleUnderlinePosTop));
const auto doubleUnderlinePosBottomU16 = gsl::narrow_cast<u16>(lrintf(doubleUnderlinePosBottom));
const auto doubleUnderlineWidthU16 = gsl::narrow_cast<u16>(lrintf(doubleUnderlineWidth));

// 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.
Expand All @@ -771,8 +773,8 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo

fontMetrics->underline = { underlinePosU16, underlineWidthU16 };
fontMetrics->strikethrough = { strikethroughPosU16, strikethroughWidthU16 };
fontMetrics->doubleUnderline[0] = { doubleUnderlinePosTopU16, thinLineWidthU16 };
fontMetrics->doubleUnderline[1] = { doubleUnderlinePosBottomU16, thinLineWidthU16 };
fontMetrics->doubleUnderline[0] = { doubleUnderlinePosTopU16, doubleUnderlineWidthU16 };
fontMetrics->doubleUnderline[1] = { doubleUnderlinePosBottomU16, doubleUnderlineWidthU16 };
fontMetrics->overline = { 0, underlineWidthU16 };

fontMetrics->builtinGlyphs = fontInfoDesired.GetEnableBuiltinGlyphs();
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/atlas/Backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@

namespace Microsoft::Console::Render::Atlas
{
// Don't use this definition in the code elsewhere.
// It only exists to make the definitions below possible.
#ifdef NDEBUG
#define ATLAS_DEBUG__IS_DEBUG 0
#else
#define ATLAS_DEBUG__IS_DEBUG 1
#endif

// If set to 1, this will cause the entire viewport to be invalidated at all times.
// Helpful for benchmarking our text shaping code based on DirectWrite.
#define ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION 0

// Redraw at display refresh rate at all times. This helps with shader debugging.
#define ATLAS_DEBUG_CONTINUOUS_REDRAW 0

// Hot reload the builtin .hlsl files whenever they change on disk.
// Enabled by default in debug builds.
#define ATLAS_DEBUG_SHADER_HOT_RELOAD ATLAS_DEBUG__IS_DEBUG

// Disables the use of DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT.
// This helps with benchmarking the application as it'll run beyond display refresh rate.
#define ATLAS_DEBUG_DISABLE_FRAME_LATENCY_WAITABLE_OBJECT 0
Expand Down
53 changes: 35 additions & 18 deletions src/renderer/atlas/BackendD3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

TIL_FAST_MATH_BEGIN

#pragma warning(disable : 4100) // '...': unreferenced formal parameter
#pragma warning(disable : 26440) // Function '...' can be declared 'noexcept'(f.6).
// This code packs various data into smaller-than-int types to save both CPU and GPU memory. This warning would force
// us to add dozens upon dozens of gsl::narrow_cast<>s throughout the file which is more annoying than helpful.
#pragma warning(disable : 4242) // '=': conversion from '...' to '...', possible loss of data
Expand Down Expand Up @@ -158,7 +160,7 @@ BackendD3D::BackendD3D(const RenderingPayload& p)
THROW_IF_FAILED(p.device->CreateBlendState(&desc, _blendState.addressof()));
}

#ifndef NDEBUG
#if ATLAS_DEBUG_SHADER_HOT_RELOAD
_sourceDirectory = std::filesystem::path{ __FILE__ }.parent_path();
_sourceCodeWatcher = wil::make_folder_change_reader_nothrow(_sourceDirectory.c_str(), false, wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime, [this](wil::FolderChangeEvent, PCWSTR path) {
if (til::ends_with(path, L".hlsl"))
Expand Down Expand Up @@ -186,9 +188,7 @@ void BackendD3D::Render(RenderingPayload& p)
_handleSettingsUpdate(p);
}

#ifndef NDEBUG
_debugUpdateShaders(p);
#endif

// After a Present() the render target becomes unbound.
p.deviceContext->OMSetRenderTargets(1, _customRenderTargetView ? _customRenderTargetView.addressof() : _renderTargetView.addressof(), nullptr);
Expand All @@ -205,19 +205,15 @@ void BackendD3D::Render(RenderingPayload& p)
_drawCursorBackground(p);
_drawText(p);
_drawSelection(p);
#if ATLAS_DEBUG_SHOW_DIRTY
_debugShowDirty(p);
#endif
_flushQuads(p);

if (_customPixelShader)
{
_executeCustomShader(p);
}

#if ATLAS_DEBUG_DUMP_RENDER_TARGET
_debugDumpRenderTarget(p);
#endif
}

bool BackendD3D::RequiresContinuousRedraw() noexcept
Expand Down Expand Up @@ -277,13 +273,15 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
// limited space to draw a curlyline, we apply a limit on the peak height.
{
const auto cellHeight = static_cast<f32>(font.cellSize.y);
const auto strokeWidth = static_cast<f32>(font.thinLineWidth);
const auto duTop = static_cast<f32>(font.doubleUnderline[0].position);
const auto duBottom = static_cast<f32>(font.doubleUnderline[1].position);
const auto duHeight = static_cast<f32>(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, 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 height = std::max(3.0f, duBottom + duHeight - duTop);
const auto top = std::min(duTop, floorf(cellHeight - height - duHeight));

_curlyLineHalfHeight = height * 0.5f;
_curlyUnderline.position = gsl::narrow_cast<u16>(lrintf(top));
Expand Down Expand Up @@ -532,8 +530,9 @@ 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.thinLineWidth = p.s->font->thinLineWidth;
data.doubleUnderlineWidth = p.s->font->doubleUnderline[0].height;
data.curlyLineHalfHeight = _curlyLineHalfHeight;
data.shadedGlyphDotSize = std::max(1.0f, std::roundf(std::max(p.s->font->cellSize.x / 16.0f, p.s->font->cellSize.y / 32.0f)));
p.deviceContext->UpdateSubresource(_psConstantBuffer.get(), 0, nullptr, &data, 0, 0);
}
}
Expand Down Expand Up @@ -570,8 +569,9 @@ void BackendD3D::_setupDeviceContextState(const RenderingPayload& p)
p.deviceContext->OMSetRenderTargets(1, _customRenderTargetView ? _customRenderTargetView.addressof() : _renderTargetView.addressof(), nullptr);
}

#ifndef NDEBUG
void BackendD3D::_debugUpdateShaders(const RenderingPayload& p) noexcept
{
#if ATLAS_DEBUG_SHADER_HOT_RELOAD
Copy link
Member Author

Choose a reason for hiding this comment

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

try
{
const auto invalidationTime = _sourceCodeInvalidationTime.load(std::memory_order_relaxed);
Expand All @@ -583,6 +583,13 @@ try

_sourceCodeInvalidationTime.store(INT64_MAX, std::memory_order_relaxed);

static constexpr auto flags =
D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS
#ifndef NDEBUG
| D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION
#endif
;

static const auto compile = [](const std::filesystem::path& path, const char* target) {
wil::com_ptr<ID3DBlob> error;
wil::com_ptr<ID3DBlob> blob;
Expand All @@ -592,7 +599,7 @@ try
/* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE,
/* pEntrypoint */ "main",
/* pTarget */ target,
/* Flags1 */ D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION | D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS,
/* Flags1 */ flags,
/* Flags2 */ 0,
/* ppCode */ blob.addressof(),
/* ppErrorMsgs */ error.addressof());
Expand Down Expand Up @@ -656,6 +663,7 @@ try
}
CATCH_LOG()
#endif
}

void BackendD3D::_d2dBeginDrawing() noexcept
{
Expand Down Expand Up @@ -980,6 +988,11 @@ void BackendD3D::_drawText(RenderingPayload& p)
}
}

const u8x2 renditionScale{
static_cast<u8>(row->lineRendition != LineRendition::SingleWidth ? 2 : 1),
static_cast<u8>(row->lineRendition >= LineRendition::DoubleHeightTop ? 2 : 1),
};

for (const auto& m : row->mappings)
{
auto x = m.glyphsFrom;
Expand Down Expand Up @@ -1028,6 +1041,7 @@ void BackendD3D::_drawText(RenderingPayload& p)

_appendQuad() = {
.shadingType = static_cast<u16>(glyphEntry->shadingType),
.renditionScale = renditionScale,
.position = { static_cast<i16>(l), static_cast<i16>(t) },
.size = glyphEntry->size,
.texcoord = glyphEntry->texcoord,
Expand Down Expand Up @@ -1394,6 +1408,8 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa
_drawGlyphAtlasAllocate(p, rect);
_d2dBeginDrawing();

auto shadingType = ShadingType::TextGrayscale;

if (BuiltinGlyphs::IsSoftFontChar(glyphIndex))
{
_drawSoftFontGlyph(p, rect, glyphIndex);
Expand All @@ -1407,10 +1423,11 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa
static_cast<f32>(rect.y + rect.h),
};
BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), r, glyphIndex);
shadingType = ShadingType::TextBuiltinGlyph;
}

const auto glyphEntry = _drawGlyphAllocateEntry(row, fontFaceEntry, glyphIndex);
glyphEntry->shadingType = ShadingType::TextGrayscale;
glyphEntry->shadingType = shadingType;
glyphEntry->overlapSplit = 0;
glyphEntry->offset.x = 0;
glyphEntry->offset.y = -baseline;
Expand Down Expand Up @@ -2023,9 +2040,9 @@ void BackendD3D::_drawSelection(const RenderingPayload& p)
}
}

#if ATLAS_DEBUG_SHOW_DIRTY
void BackendD3D::_debugShowDirty(const RenderingPayload& p)
{
#if ATLAS_DEBUG_SHOW_DIRTY
_presentRects[_presentRectsPos] = p.dirtyRectInPx;
_presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects);

Expand All @@ -2048,12 +2065,12 @@ void BackendD3D::_debugShowDirty(const RenderingPayload& p)
};
}
}
}
#endif
}

#if ATLAS_DEBUG_DUMP_RENDER_TARGET
void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p)
{
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
if (_dumpRenderTargetCounter == 0)
{
ExpandEnvironmentStringsW(ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH, &_dumpRenderTargetBasePath[0], gsl::narrow_cast<DWORD>(std::size(_dumpRenderTargetBasePath)));
Expand All @@ -2064,8 +2081,8 @@ void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p)
swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter);
SaveTextureToPNG(p.deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]);
_dumpRenderTargetCounter++;
}
#endif
}

void BackendD3D::_executeCustomShader(RenderingPayload& p)
{
Expand Down
24 changes: 13 additions & 11 deletions src/renderer/atlas/BackendD3D.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ 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 thinLineWidth = 0;
alignas(sizeof(f32)) f32 doubleUnderlineWidth = 0;
alignas(sizeof(f32)) f32 curlyLineHalfHeight = 0;
alignas(sizeof(f32)) f32 shadedGlyphDotSize = 0;
#pragma warning(suppress : 4324) // 'PSConstBuffer': structure was padded due to alignment specifier
};

Expand All @@ -64,17 +65,18 @@ namespace Microsoft::Console::Render::Atlas

// This block of values will be used for the TextDrawingFirst/Last range and need to stay together.
// This is used to quickly check if an instance is related to a "text drawing primitive".
TextGrayscale = 1,
TextClearType = 2,
TextPassthrough = 3,
DottedLine = 4,
DashedLine = 5,
CurlyLine = 6,
TextGrayscale,
TextClearType,
TextBuiltinGlyph,
TextPassthrough,
DottedLine,
DashedLine,
CurlyLine,
// All items starting here will be drawing as a solid RGBA color
SolidLine = 7,
SolidLine,

Cursor = 8,
Selection = 9,
Cursor,
Selection,

TextDrawingFirst = TextGrayscale,
TextDrawingLast = SolidLine,
Expand Down Expand Up @@ -305,7 +307,7 @@ namespace Microsoft::Console::Render::Atlas
size_t _colorizeGlyphAtlasCounter = 0;
#endif

#ifndef NDEBUG
#if ATLAS_DEBUG_SHADER_HOT_RELOAD
std::filesystem::path _sourceDirectory;
wil::unique_folder_change_reader_nothrow _sourceCodeWatcher;
std::atomic<int64_t> _sourceCodeInvalidationTime{ INT64_MAX };
Expand Down
Loading
Loading