diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index fa7290371e..30cac1da93 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -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(lrintf(adjustedWidth)); const auto cellHeight = gsl::narrow(lrintf(adjustedHeight)); @@ -749,6 +750,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo const auto strikethroughWidthU16 = gsl::narrow_cast(lrintf(strikethroughWidth)); const auto doubleUnderlinePosTopU16 = gsl::narrow_cast(lrintf(doubleUnderlinePosTop)); const auto doubleUnderlinePosBottomU16 = gsl::narrow_cast(lrintf(doubleUnderlinePosBottom)); + const auto doubleUnderlineWidthU16 = gsl::narrow_cast(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(); diff --git a/src/renderer/atlas/Backend.h b/src/renderer/atlas/Backend.h index 9f2736ecee..ad22c1b20d 100644 --- a/src/renderer/atlas/Backend.h +++ b/src/renderer/atlas/Backend.h @@ -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 diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index dd4dcbf0c2..308518ec27 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -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(font.cellSize.y); - const auto strokeWidth = static_cast(font.thinLineWidth); + 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); // 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(font.doubleUnderline[1].position + font.doubleUnderline[1].height - font.doubleUnderline[0].position)); - const auto top = std::min(static_cast(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(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 error; - wil::com_ptr 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(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 BackendD3D::*target; - }; - struct FilePS - { - std::wstring_view filename; - wil::com_ptr 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, filesVS.size()> compiledVS; - std::array, 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 error; + wil::com_ptr 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(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 BackendD3D::*target; + }; + struct FilePS + { + std::wstring_view filename; + wil::com_ptr 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, filesVS.size()> compiledVS; + std::array, 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(row->lineRendition != LineRendition::SingleWidth ? 2 : 1), + static_cast(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(glyphEntry->shadingType), + .renditionScale = renditionScale, .position = { static_cast(l), static_cast(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(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(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) { diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index 3fddb161d4..1ae6b139b4 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -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 _sourceCodeInvalidationTime{ INT64_MAX }; diff --git a/src/renderer/atlas/BuiltinGlyphs.cpp b/src/renderer/atlas/BuiltinGlyphs.cpp index 541d569032..c46bfc01fc 100644 --- a/src/renderer/atlas/BuiltinGlyphs.cpp +++ b/src/renderer/atlas/BuiltinGlyphs.cpp @@ -1071,57 +1071,6 @@ static const Instruction* GetInstructions(char32_t codepoint) noexcept return nullptr; } -static wil::com_ptr 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 bitmap; - THROW_IF_FAILED(renderTarget->CreateBitmap(bitmapSize, &shades[shape][0], sizeof(u32) * size, &bitmapProps, bitmap.addressof())); - - wil::com_ptr 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: diff --git a/src/renderer/atlas/dwrite.hlsl b/src/renderer/atlas/dwrite.hlsl index ad7493275b..955b9f57e5 100644 --- a/src/renderer/atlas/dwrite.hlsl +++ b/src/renderer/atlas/dwrite.hlsl @@ -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 diff --git a/src/renderer/atlas/shader_common.hlsl b/src/renderer/atlas/shader_common.hlsl index d712f081f2..51eb524b86 100644 --- a/src/renderer/atlas/shader_common.hlsl +++ b/src/renderer/atlas/shader_common.hlsl @@ -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; }; diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl index d3c0bfac6c..d8d894be13 100644 --- a/src/renderer/atlas/shader_ps.hlsl +++ b/src/renderer/atlas/shader_ps.hlsl @@ -12,8 +12,9 @@ cbuffer ConstBuffer : register(b0) float4 gammaRatios; float enhancedContrast; float underlineWidth; - float thinLineWidth; + float doubleUnderlineWidth; float curlyLineHalfHeight; + float shadedGlyphDotSize; } Texture2D 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; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 13ecdd2537..601085c35f 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -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; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 4dbb82d9c6..29f8dfc43a 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -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 }; diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index f993c6f74e..7563a68b7d 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -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);