mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-12 08:40:37 -06:00
Make shaded block glyphs look even betterer (#16760)
Shaded glyphs (U+2591..3, etc.) all have one problem in common: The cell size may not be evenly divisible by the pixel/dot size in the glyph. This either results in blurring, or in moiré-like patterns at the edges of the cells with its neighbors, because they happen to start with a pattern that overlaps with the end of the previous cell. This PR solves the issue by moving the pixel/dot pattern generation into the shader. That way the pixel/dot location can be made dependent on the viewport-position of the actual underlying pixels, which avoids repeating patterns between cells. The PR contains some additional modifications, all of which either extend or improve the existing debug facilities in AtlasEngine. Suppressing whitespaces changes makes the diff way smaller.
This commit is contained in:
parent
badc00e83b
commit
94e74d22c6
@ -688,10 +688,11 @@ 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));
|
||||
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
|
||||
@ -699,18 +700,18 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
|
||||
// (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));
|
||||
@ -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.
|
||||
@ -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();
|
||||
|
||||
@ -7,6 +7,14 @@
|
||||
|
||||
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
|
||||
@ -14,6 +22,10 @@ namespace Microsoft::Console::Render::Atlas
|
||||
// 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
|
||||
|
||||
@ -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
|
||||
@ -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"))
|
||||
@ -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);
|
||||
@ -205,9 +205,7 @@ void BackendD3D::Render(RenderingPayload& p)
|
||||
_drawCursorBackground(p);
|
||||
_drawText(p);
|
||||
_drawSelection(p);
|
||||
#if ATLAS_DEBUG_SHOW_DIRTY
|
||||
_debugShowDirty(p);
|
||||
#endif
|
||||
_flushQuads(p);
|
||||
|
||||
if (_customPixelShader)
|
||||
@ -215,9 +213,7 @@ void BackendD3D::Render(RenderingPayload& p)
|
||||
_executeCustomShader(p);
|
||||
}
|
||||
|
||||
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
|
||||
_debugDumpRenderTarget(p);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool BackendD3D::RequiresContinuousRedraw() noexcept
|
||||
@ -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));
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -570,92 +569,101 @@ 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
|
||||
try
|
||||
{
|
||||
const auto invalidationTime = _sourceCodeInvalidationTime.load(std::memory_order_relaxed);
|
||||
|
||||
if (invalidationTime == INT64_MAX || invalidationTime > std::chrono::steady_clock::now().time_since_epoch().count())
|
||||
#if ATLAS_DEBUG_SHADER_HOT_RELOAD
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
const auto invalidationTime = _sourceCodeInvalidationTime.load(std::memory_order_relaxed);
|
||||
|
||||
_sourceCodeInvalidationTime.store(INT64_MAX, std::memory_order_relaxed);
|
||||
|
||||
static const auto compile = [](const std::filesystem::path& path, const char* target) {
|
||||
wil::com_ptr<ID3DBlob> error;
|
||||
wil::com_ptr<ID3DBlob> blob;
|
||||
const auto hr = D3DCompileFromFile(
|
||||
/* pFileName */ path.c_str(),
|
||||
/* pDefines */ nullptr,
|
||||
/* 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,
|
||||
/* Flags2 */ 0,
|
||||
/* ppCode */ blob.addressof(),
|
||||
/* ppErrorMsgs */ error.addressof());
|
||||
|
||||
if (error)
|
||||
if (invalidationTime == INT64_MAX || invalidationTime > std::chrono::steady_clock::now().time_since_epoch().count())
|
||||
{
|
||||
std::thread t{ [error = std::move(error)]() noexcept {
|
||||
MessageBoxA(nullptr, static_cast<const char*>(error->GetBufferPointer()), "Compilation error", MB_ICONERROR | MB_OK);
|
||||
} };
|
||||
t.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
THROW_IF_FAILED(hr);
|
||||
return blob;
|
||||
};
|
||||
_sourceCodeInvalidationTime.store(INT64_MAX, std::memory_order_relaxed);
|
||||
|
||||
struct FileVS
|
||||
{
|
||||
std::wstring_view filename;
|
||||
wil::com_ptr<ID3D11VertexShader> BackendD3D::*target;
|
||||
};
|
||||
struct FilePS
|
||||
{
|
||||
std::wstring_view filename;
|
||||
wil::com_ptr<ID3D11PixelShader> BackendD3D::*target;
|
||||
};
|
||||
|
||||
static constexpr std::array filesVS{
|
||||
FileVS{ L"shader_vs.hlsl", &BackendD3D::_vertexShader },
|
||||
};
|
||||
static constexpr std::array filesPS{
|
||||
FilePS{ L"shader_ps.hlsl", &BackendD3D::_pixelShader },
|
||||
};
|
||||
|
||||
std::array<wil::com_ptr<ID3D11VertexShader>, filesVS.size()> compiledVS;
|
||||
std::array<wil::com_ptr<ID3D11PixelShader>, filesPS.size()> compiledPS;
|
||||
|
||||
// Compile our files before moving them into `this` below to ensure we're
|
||||
// always in a consistent state where all shaders are seemingly valid.
|
||||
for (size_t i = 0; i < filesVS.size(); ++i)
|
||||
{
|
||||
const auto blob = compile(_sourceDirectory / filesVS[i].filename, "vs_4_0");
|
||||
THROW_IF_FAILED(p.device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, compiledVS[i].addressof()));
|
||||
}
|
||||
for (size_t i = 0; i < filesPS.size(); ++i)
|
||||
{
|
||||
const auto blob = compile(_sourceDirectory / filesPS[i].filename, "ps_4_0");
|
||||
THROW_IF_FAILED(p.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, compiledPS[i].addressof()));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < filesVS.size(); ++i)
|
||||
{
|
||||
this->*filesVS[i].target = std::move(compiledVS[i]);
|
||||
}
|
||||
for (size_t i = 0; i < filesPS.size(); ++i)
|
||||
{
|
||||
this->*filesPS[i].target = std::move(compiledPS[i]);
|
||||
}
|
||||
|
||||
_setupDeviceContextState(p);
|
||||
}
|
||||
CATCH_LOG()
|
||||
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;
|
||||
const auto hr = D3DCompileFromFile(
|
||||
/* pFileName */ path.c_str(),
|
||||
/* pDefines */ nullptr,
|
||||
/* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE,
|
||||
/* pEntrypoint */ "main",
|
||||
/* pTarget */ target,
|
||||
/* Flags1 */ flags,
|
||||
/* Flags2 */ 0,
|
||||
/* ppCode */ blob.addressof(),
|
||||
/* ppErrorMsgs */ error.addressof());
|
||||
|
||||
if (error)
|
||||
{
|
||||
std::thread t{ [error = std::move(error)]() noexcept {
|
||||
MessageBoxA(nullptr, static_cast<const char*>(error->GetBufferPointer()), "Compilation error", MB_ICONERROR | MB_OK);
|
||||
} };
|
||||
t.detach();
|
||||
}
|
||||
|
||||
THROW_IF_FAILED(hr);
|
||||
return blob;
|
||||
};
|
||||
|
||||
struct FileVS
|
||||
{
|
||||
std::wstring_view filename;
|
||||
wil::com_ptr<ID3D11VertexShader> BackendD3D::*target;
|
||||
};
|
||||
struct FilePS
|
||||
{
|
||||
std::wstring_view filename;
|
||||
wil::com_ptr<ID3D11PixelShader> BackendD3D::*target;
|
||||
};
|
||||
|
||||
static constexpr std::array filesVS{
|
||||
FileVS{ L"shader_vs.hlsl", &BackendD3D::_vertexShader },
|
||||
};
|
||||
static constexpr std::array filesPS{
|
||||
FilePS{ L"shader_ps.hlsl", &BackendD3D::_pixelShader },
|
||||
};
|
||||
|
||||
std::array<wil::com_ptr<ID3D11VertexShader>, filesVS.size()> compiledVS;
|
||||
std::array<wil::com_ptr<ID3D11PixelShader>, filesPS.size()> compiledPS;
|
||||
|
||||
// Compile our files before moving them into `this` below to ensure we're
|
||||
// always in a consistent state where all shaders are seemingly valid.
|
||||
for (size_t i = 0; i < filesVS.size(); ++i)
|
||||
{
|
||||
const auto blob = compile(_sourceDirectory / filesVS[i].filename, "vs_4_0");
|
||||
THROW_IF_FAILED(p.device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, compiledVS[i].addressof()));
|
||||
}
|
||||
for (size_t i = 0; i < filesPS.size(); ++i)
|
||||
{
|
||||
const auto blob = compile(_sourceDirectory / filesPS[i].filename, "ps_4_0");
|
||||
THROW_IF_FAILED(p.device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, compiledPS[i].addressof()));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < filesVS.size(); ++i)
|
||||
{
|
||||
this->*filesVS[i].target = std::move(compiledVS[i]);
|
||||
}
|
||||
for (size_t i = 0; i < filesPS.size(); ++i)
|
||||
{
|
||||
this->*filesPS[i].target = std::move(compiledPS[i]);
|
||||
}
|
||||
|
||||
_setupDeviceContextState(p);
|
||||
}
|
||||
CATCH_LOG()
|
||||
#endif
|
||||
}
|
||||
|
||||
void BackendD3D::_d2dBeginDrawing() noexcept
|
||||
{
|
||||
@ -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;
|
||||
@ -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,
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
@ -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)));
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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,
|
||||
@ -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 };
|
||||
|
||||
@ -1071,57 +1071,6 @@ static const Instruction* GetInstructions(char32_t codepoint) noexcept
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static wil::com_ptr<ID2D1BitmapBrush> createShadedBitmapBrush(ID2D1DeviceContext* renderTarget, Shape shape)
|
||||
{
|
||||
static constexpr u32 _ = 0;
|
||||
static constexpr u32 w = 0xffffffff;
|
||||
static constexpr u32 size = 4;
|
||||
// clang-format off
|
||||
static constexpr u32 shades[3][size * size] = {
|
||||
{
|
||||
w, _, _, _,
|
||||
w, _, _, _,
|
||||
_, _, w, _,
|
||||
_, _, w, _,
|
||||
},
|
||||
{
|
||||
w, _, w, _,
|
||||
_, w, _, w,
|
||||
w, _, w, _,
|
||||
_, w, _, w,
|
||||
},
|
||||
{
|
||||
_, w, w, w,
|
||||
_, w, w, w,
|
||||
w, w, _, w,
|
||||
w, w, _, w,
|
||||
},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static constexpr D2D1_SIZE_U bitmapSize{ size, size };
|
||||
static constexpr D2D1_BITMAP_PROPERTIES bitmapProps{
|
||||
.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
|
||||
.dpiX = 96,
|
||||
.dpiY = 96,
|
||||
};
|
||||
static constexpr D2D1_BITMAP_BRUSH_PROPERTIES bitmapBrushProps{
|
||||
.extendModeX = D2D1_EXTEND_MODE_WRAP,
|
||||
.extendModeY = D2D1_EXTEND_MODE_WRAP,
|
||||
.interpolationMode = D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR
|
||||
};
|
||||
|
||||
assert(shape < ARRAYSIZE(shades));
|
||||
|
||||
wil::com_ptr<ID2D1Bitmap> bitmap;
|
||||
THROW_IF_FAILED(renderTarget->CreateBitmap(bitmapSize, &shades[shape][0], sizeof(u32) * size, &bitmapProps, bitmap.addressof()));
|
||||
|
||||
wil::com_ptr<ID2D1BitmapBrush> bitmapBrush;
|
||||
THROW_IF_FAILED(renderTarget->CreateBitmapBrush(bitmap.get(), &bitmapBrushProps, nullptr, bitmapBrush.addressof()));
|
||||
|
||||
return bitmapBrush;
|
||||
}
|
||||
|
||||
void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint)
|
||||
{
|
||||
renderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
|
||||
@ -1188,16 +1137,28 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext*
|
||||
case Shape_Filled025:
|
||||
case Shape_Filled050:
|
||||
case Shape_Filled075:
|
||||
{
|
||||
const D2D1_RECT_F r{ begXabs, begYabs, endXabs, endYabs };
|
||||
const auto bitmapBrush = createShadedBitmapBrush(renderTarget, shape);
|
||||
renderTarget->FillRectangle(&r, bitmapBrush.get());
|
||||
break;
|
||||
}
|
||||
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]);
|
||||
|
||||
const D2D1_RECT_F r{ begXabs, begYabs, endXabs, endYabs };
|
||||
renderTarget->FillRectangle(&r, brush);
|
||||
|
||||
brush->SetColor(&brushColor);
|
||||
break;
|
||||
}
|
||||
case Shape_LightLine:
|
||||
|
||||
@ -37,12 +37,12 @@ float3 DWrite_EnhanceContrast3(float3 alpha, float k)
|
||||
|
||||
float DWrite_ApplyAlphaCorrection(float a, float f, float4 g)
|
||||
{
|
||||
return a + a * (1 - a) * ((g.x * f + g.y) * a + (g.z * f + g.w));
|
||||
return a + a * (1.0f - a) * ((g.x * f + g.y) * a + (g.z * f + g.w));
|
||||
}
|
||||
|
||||
float3 DWrite_ApplyAlphaCorrection3(float3 a, float3 f, float4 g)
|
||||
{
|
||||
return a + a * (1 - a) * ((g.x * f + g.y) * a + (g.z * f + g.w));
|
||||
return a + a * (1.0f - a) * ((g.x * f + g.y) * a + (g.z * f + g.w));
|
||||
}
|
||||
|
||||
// Call this function to get the same gamma corrected alpha blending effect
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
// clang-format off
|
||||
// Depends on the background texture
|
||||
#define SHADING_TYPE_TEXT_BACKGROUND 0
|
||||
|
||||
// Depends on the glyphAtlas texture
|
||||
#define SHADING_TYPE_TEXT_GRAYSCALE 1
|
||||
#define SHADING_TYPE_TEXT_CLEARTYPE 2
|
||||
#define SHADING_TYPE_TEXT_PASSTHROUGH 3
|
||||
#define SHADING_TYPE_DOTTED_LINE 4
|
||||
#define SHADING_TYPE_DASHED_LINE 5
|
||||
#define SHADING_TYPE_CURLY_LINE 6
|
||||
// clang-format on
|
||||
#define SHADING_TYPE_TEXT_BUILTIN_GLYPH 3
|
||||
#define SHADING_TYPE_TEXT_PASSTHROUGH 4
|
||||
|
||||
// Independent of any textures
|
||||
#define SHADING_TYPE_DOTTED_LINE 5
|
||||
#define SHADING_TYPE_DASHED_LINE 6
|
||||
#define SHADING_TYPE_CURLY_LINE 7
|
||||
#define SHADING_TYPE_SOLID_LINE 8
|
||||
#define SHADING_TYPE_CURSOR 9
|
||||
#define SHADING_TYPE_SELECTION 10
|
||||
|
||||
struct VSData
|
||||
{
|
||||
@ -27,7 +34,7 @@ struct PSData
|
||||
float4 position : SV_Position;
|
||||
float2 texcoord : texcoord;
|
||||
nointerpolation uint shadingType : shadingType;
|
||||
nointerpolation uint2 renditionScale : renditionScale;
|
||||
nointerpolation float2 renditionScale : renditionScale;
|
||||
nointerpolation float4 color : color;
|
||||
};
|
||||
|
||||
|
||||
@ -12,8 +12,9 @@ cbuffer ConstBuffer : register(b0)
|
||||
float4 gammaRatios;
|
||||
float enhancedContrast;
|
||||
float underlineWidth;
|
||||
float thinLineWidth;
|
||||
float doubleUnderlineWidth;
|
||||
float curlyLineHalfHeight;
|
||||
float shadedGlyphDotSize;
|
||||
}
|
||||
|
||||
Texture2D<float4> background : register(t0);
|
||||
@ -67,6 +68,87 @@ Output main(PSData data) : SV_Target
|
||||
color = weights * data.color;
|
||||
break;
|
||||
}
|
||||
case SHADING_TYPE_TEXT_BUILTIN_GLYPH:
|
||||
{
|
||||
// The RGB components of builtin glyphs are used to control the generation of pixel patterns in this shader.
|
||||
// Below you can see their intended effects where # indicates lit pixels.
|
||||
//
|
||||
// .r = stretch
|
||||
// 0: #_#_#_#_
|
||||
// _#_#_#_#
|
||||
// #_#_#_#_
|
||||
// _#_#_#_#
|
||||
//
|
||||
// 1: #___#___
|
||||
// __#___#_
|
||||
// #___#___
|
||||
// __#___#_
|
||||
//
|
||||
// .g = invert
|
||||
// 0: #_#_#_#_
|
||||
// _#_#_#_#
|
||||
// #_#_#_#_
|
||||
// _#_#_#_#
|
||||
//
|
||||
// 1: _#_#_#_#
|
||||
// #_#_#_#_
|
||||
// _#_#_#_#
|
||||
// #_#_#_#_
|
||||
//
|
||||
// .r = fill
|
||||
// 0: #_#_#_#_
|
||||
// _#_#_#_#
|
||||
// #_#_#_#_
|
||||
// _#_#_#_#
|
||||
//
|
||||
// 1: ########
|
||||
// ########
|
||||
// ########
|
||||
// ########
|
||||
//
|
||||
float4 glyph = glyphAtlas[data.texcoord];
|
||||
float2 pos = floor(data.position.xy / (shadedGlyphDotSize * data.renditionScale));
|
||||
|
||||
// A series of on/off/on/off/on/off pixels can be generated with:
|
||||
// step(frac(x * 0.5f), 0)
|
||||
// The inner frac(x * 0.5f) will generate a series of
|
||||
// 0, 0.5, 0, 0.5, 0, 0.5
|
||||
// and the step() will transform that to
|
||||
// 1, 0, 1, 0, 1, 0
|
||||
//
|
||||
// We can now turn that into a checkerboard pattern quite easily,
|
||||
// if we imagine the fields of the checkerboard like this:
|
||||
// +---+---+---+
|
||||
// | 0 | 1 | 2 |
|
||||
// +---+---+---+
|
||||
// | 1 | 2 | 3 |
|
||||
// +---+---+---+
|
||||
// | 2 | 3 | 4 |
|
||||
// +---+---+---+
|
||||
//
|
||||
// Because this means we just need to set
|
||||
// x = pos.x + pos.y
|
||||
// and so we end up with
|
||||
// step(frac(dot(pos, 0.5f)), 0)
|
||||
//
|
||||
// Finally, we need to implement the "stretch" explained above, which can
|
||||
// be easily achieved by simply replacing the factor 0.5 with 0.25 like so
|
||||
// step(frac(x * 0.25f), 0)
|
||||
// as this gets us
|
||||
// 0, 0.25, 0.5, 0.75, 0, 0.25, 0.5, 0.75
|
||||
// = 1, 0, 0, 0, 1, 0, 0, 0
|
||||
//
|
||||
// Of course we only want to apply that to the X axis, which means
|
||||
// below we end up having 2 different multipliers for the dot().
|
||||
float stretched = step(frac(dot(pos, float2(glyph.r * -0.25f + 0.5f, 0.5f))), 0) * glyph.a;
|
||||
// Thankfully the remaining 2 operations are a lot simpler.
|
||||
float inverted = abs(glyph.g - stretched);
|
||||
float filled = max(glyph.b, inverted);
|
||||
|
||||
color = premultiplyColor(data.color) * filled;
|
||||
weights = color.aaaa;
|
||||
break;
|
||||
}
|
||||
case SHADING_TYPE_TEXT_PASSTHROUGH:
|
||||
{
|
||||
color = glyphAtlas[data.texcoord];
|
||||
@ -89,7 +171,7 @@ Output main(PSData data) : SV_Target
|
||||
}
|
||||
case SHADING_TYPE_CURLY_LINE:
|
||||
{
|
||||
const float strokeWidthHalf = thinLineWidth * data.renditionScale.y * 0.5f;
|
||||
const float strokeWidthHalf = doubleUnderlineWidth * 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;
|
||||
|
||||
@ -117,11 +117,11 @@ namespace Microsoft::Console::Render
|
||||
struct LineMetrics
|
||||
{
|
||||
int gridlineWidth;
|
||||
int thinLineWidth;
|
||||
int underlineCenter;
|
||||
int underlineWidth;
|
||||
int doubleUnderlinePosTop;
|
||||
int doubleUnderlinePosBottom;
|
||||
int doubleUnderlineWidth;
|
||||
int strikethroughOffset;
|
||||
int strikethroughWidth;
|
||||
int curlyLineCenter;
|
||||
|
||||
@ -632,7 +632,7 @@ try
|
||||
DWORD underlineWidth = _lineMetrics.underlineWidth;
|
||||
if (lines.any(GridLines::DoubleUnderline, GridLines::CurlyUnderline))
|
||||
{
|
||||
underlineWidth = _lineMetrics.thinLineWidth;
|
||||
underlineWidth = _lineMetrics.doubleUnderlineWidth;
|
||||
}
|
||||
|
||||
const LOGBRUSH brushProp{ .lbStyle = BS_SOLID, .lbColor = underlineColor };
|
||||
|
||||
@ -389,20 +389,20 @@ GdiEngine::~GdiEngine()
|
||||
// (Additional notes below.)
|
||||
|
||||
// 1.
|
||||
const auto thinLineWidth = std::max(1.0f, roundf(idealUnderlineWidth / 2.0f));
|
||||
const auto doubleUnderlineWidth = std::max(1.0f, roundf(idealUnderlineWidth / 2.0f));
|
||||
// 2.
|
||||
auto doubleUnderlinePosBottom = underlineCenter + underlineWidth - thinLineWidth;
|
||||
auto doubleUnderlinePosBottom = underlineCenter + 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 = roundf((baseline + doubleUnderlinePosBottom - thinLineWidth) / 2.0f);
|
||||
auto doubleUnderlinePosTop = 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, roundf(1.2f / 72.0f * _iCurrentDpi));
|
||||
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, cellHeight - thinLineWidth);
|
||||
doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, cellHeight - doubleUnderlineWidth);
|
||||
|
||||
// The wave line is drawn using a cubic Bézier curve (PolyBezier), because that happens to be cheap with GDI.
|
||||
// We use a Bézier curve where, if the start (a) and end (c) points are at (0,0) and (1,0), the control points are
|
||||
@ -431,13 +431,13 @@ GdiEngine::~GdiEngine()
|
||||
const auto curlyLineControlPointOffset = roundf(curlyLineIdealAmplitude * (1.0f / 0.140625f) * 0.5f);
|
||||
const auto curlyLinePeriod = curlyLineControlPointOffset * 2.0f;
|
||||
// We can reverse the above to get back the actual amplitude of our Bézier curve. The line
|
||||
// will be drawn with a width of thinLineWidth in the center of the curve (= 0.5x padding).
|
||||
const auto curlyLineAmplitude = 0.140625f * curlyLinePeriod + 0.5f * thinLineWidth;
|
||||
// will be drawn with a width of doubleUnderlineWidth in the center of the curve (= 0.5x padding).
|
||||
const auto curlyLineAmplitude = 0.140625f * curlyLinePeriod + 0.5f * doubleUnderlineWidth;
|
||||
// To make the wavy line with its double-underline amplitude look consistent with the double-underline we position it at its center.
|
||||
const auto curlyLineOffset = std::min(roundf(doubleUnderlineCenter), floorf(cellHeight - curlyLineAmplitude));
|
||||
|
||||
_lineMetrics.gridlineWidth = lroundf(idealGridlineWidth);
|
||||
_lineMetrics.thinLineWidth = lroundf(thinLineWidth);
|
||||
_lineMetrics.doubleUnderlineWidth = lroundf(doubleUnderlineWidth);
|
||||
_lineMetrics.underlineCenter = lroundf(underlineCenter);
|
||||
_lineMetrics.underlineWidth = lroundf(underlineWidth);
|
||||
_lineMetrics.doubleUnderlinePosTop = lroundf(doubleUnderlinePosTop);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user