diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index fea31c0fb2d..f7276428193 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -160,100 +160,105 @@ try } #endif - if (_api.invalidatedRows == invalidatedRowsAll) + if constexpr (debugGlyphGenerationPerformance) + { + _r.glyphs = {}; + _r.tileAllocator = TileAllocator{ _api.fontMetrics.cellSize, _api.sizeInPixel }; + } + if constexpr (debugTextParsingPerformance) { - // Skip all the partial updates, since we redraw everything anyways. - _api.invalidatedCursorArea = invalidatedAreaNone; - _api.invalidatedRows = { 0, _api.cellCount.y }; + _api.invalidatedRows = invalidatedRowsAll; _api.scrollOffset = 0; } - else + + // Clamp invalidation rects into valid value ranges. { - // Clamp invalidation rects into valid value ranges. - { - _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _api.cellCount.x); - _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _api.cellCount.y); - _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _api.cellCount.x); - _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _api.cellCount.y); - } - { - _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, _api.cellCount.y); - _api.invalidatedRows.y = clamp(_api.invalidatedRows.y, _api.invalidatedRows.x, _api.cellCount.y); - } + _api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _api.cellCount.x); + _api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _api.cellCount.y); + _api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _api.cellCount.x); + _api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _api.cellCount.y); + } + { + _api.invalidatedRows.x = std::min(_api.invalidatedRows.x, _api.cellCount.y); + _api.invalidatedRows.y = clamp(_api.invalidatedRows.y, _api.invalidatedRows.x, _api.cellCount.y); + } + { + const auto limit = gsl::narrow_cast(_api.cellCount.y & 0x7fff); + _api.scrollOffset = gsl::narrow_cast(clamp(_api.scrollOffset, -limit, limit)); + } + + // Scroll the buffer by the given offset and mark the newly uncovered rows as "invalid". + if (_api.scrollOffset != 0) + { + const auto nothingInvalid = _api.invalidatedRows.x == _api.invalidatedRows.y; + const auto offset = static_cast(_api.scrollOffset) * _api.cellCount.x; + + if (_api.scrollOffset < 0) { - const auto limit = gsl::narrow_cast(_api.cellCount.y & 0x7fff); - _api.scrollOffset = gsl::narrow_cast(clamp(_api.scrollOffset, -limit, limit)); + // Scroll up (for instance when new text is being written at the end of the buffer). + const u16 endRow = _api.cellCount.y + _api.scrollOffset; + _api.invalidatedRows.x = nothingInvalid ? endRow : std::min(_api.invalidatedRows.x, endRow); + _api.invalidatedRows.y = _api.cellCount.y; + + // scrollOffset/offset = -1 + // +----------+ +----------+ + // | | | xxxxxxxxx| + dst < beg + // | xxxxxxxxx| -> |xxxxxxx | + src | < beg - offset + // |xxxxxxx | | | | v + // +----------+ +----------+ v < end + { + const auto beg = _r.cells.begin(); + const auto end = _r.cells.end(); + std::move(beg - offset, end, beg); + } + { + const auto beg = _r.cellGlyphMapping.begin(); + const auto end = _r.cellGlyphMapping.end(); + std::move(beg - offset, end, beg); + } } - - // Scroll the buffer by the given offset and mark the newly uncovered rows as "invalid". - if (_api.scrollOffset != 0) + else { - const auto nothingInvalid = _api.invalidatedRows.x == _api.invalidatedRows.y; - const auto offset = static_cast(_api.scrollOffset) * _api.cellCount.x; - auto count = _r.cells.size(); - - if (_api.scrollOffset < 0) + // Scroll down. + _api.invalidatedRows.x = 0; + _api.invalidatedRows.y = nothingInvalid ? _api.scrollOffset : std::max(_api.invalidatedRows.y, _api.scrollOffset); + + // scrollOffset/offset = 1 + // +----------+ +----------+ + // | xxxxxxxxx| | | + src < beg + // |xxxxxxx | -> | xxxxxxxxx| | ^ + // | | |xxxxxxx | v | < end - offset + // +----------+ +----------+ + dst < end { - // Scroll up (for instance when new text is being written at the end of the buffer). - const u16 endRow = _api.cellCount.y + _api.scrollOffset; - _api.invalidatedRows.x = nothingInvalid ? endRow : std::min(_api.invalidatedRows.x, endRow); - _api.invalidatedRows.y = _api.cellCount.y; - - // scrollOffset/offset = -1 - // +----------+ +----------+ - // | | | xxxxxxxxx| + dst < beg - // | xxxxxxxxx| -> |xxxxxxx | + src | < beg - offset - // |xxxxxxx | | | | v - // +----------+ +----------+ v < end - { - const auto beg = _r.cells.begin(); - const auto end = beg + count; - std::move(beg - offset, end, beg); - } - { - const auto beg = _r.cellGlyphMapping.begin(); - const auto end = beg + count; - std::move(beg - offset, end, beg); - } + const auto beg = _r.cells.begin(); + const auto end = _r.cells.end(); + std::move_backward(beg, end - offset, end); } - else { - // Scroll down. - _api.invalidatedRows.x = 0; - _api.invalidatedRows.y = nothingInvalid ? _api.scrollOffset : std::max(_api.invalidatedRows.y, _api.scrollOffset); - - // scrollOffset/offset = 1 - // +----------+ +----------+ - // | xxxxxxxxx| | | + src < beg - // |xxxxxxx | -> | xxxxxxxxx| | ^ - // | | |xxxxxxx | v | < end - offset - // +----------+ +----------+ + dst < end - { - const auto beg = _r.cells.begin(); - const auto end = beg + count; - std::move_backward(beg, end - offset, end); - } - { - const auto beg = _r.cellGlyphMapping.begin(); - const auto end = beg + count; - std::move_backward(beg, end - offset, end); - } + const auto beg = _r.cellGlyphMapping.begin(); + const auto end = _r.cellGlyphMapping.end(); + std::move_backward(beg, end - offset, end); } } } - if constexpr (debugGlyphGenerationPerformance) - { - _r.glyphs = {}; - _r.tileAllocator = TileAllocator{ _api.fontMetrics.cellSize, _api.sizeInPixel }; - } - if constexpr (debugTextParsingPerformance) + _api.dirtyRect = til::rect{ 0, _api.invalidatedRows.x, _api.cellCount.x, _api.invalidatedRows.y }; + + // Skip partial updates in the renderer if we redraw everything. + if (_api.invalidatedRows == u16x2{ 0, _r.cellCount.y }) { - _api.dirtyRect = til::rect{ 0, 0, _api.cellCount.x, _api.cellCount.y }; + _r.dirtyRect = {}; + _r.scrollOffset = 0; } else { - _api.dirtyRect = til::rect{ 0, _api.invalidatedRows.x, _api.cellCount.x, _api.invalidatedRows.y }; + _r.dirtyRect = _api.dirtyRect | til::rect{ + _api.invalidatedCursorArea.left, + _api.invalidatedCursorArea.top, + _api.invalidatedCursorArea.right, + _api.invalidatedCursorArea.bottom, + }; + _r.scrollOffset = _api.scrollOffset; } // This is an important block of code for our TileHashMap. @@ -319,7 +324,7 @@ void AtlasEngine::WaitUntilCanRender() noexcept { if constexpr (!debugGeneralPerformance) { - WaitForSingleObjectEx(_r.frameLatencyWaitableObject.get(), 100, true); + WaitForSingleObjectEx(_r.frameLatencyWaitableObject.get(), INFINITE, true); #ifndef NDEBUG _r.frameLatencyWaitableObjectUsed = true; #endif @@ -636,6 +641,13 @@ void AtlasEngine::_createResources() _r.deviceContext = deviceContext.query(); } + // > You should not use GetSystemMetrics(SM_REMOTESESSION) to determine if your application is running + // > in a remote session in Windows 8 and later or Windows Server 2012 and later if the remote session + // > may also be using the RemoteFX vGPU improvements to the Microsoft Remote Display Protocol (RDP). + // > In this case, GetSystemMetrics(SM_REMOTESESSION) will identify the remote session as a local session. + // This actually sounds great for us. The non-d2dMode of AtlasEngine has more features, but requires a GPU. + _r.d2dMode = debugForceD2DMode || GetSystemMetrics(SM_REMOTESESSION); + #ifndef NDEBUG // D3D debug messages if (deviceFlags & D3D11_CREATE_DEVICE_DEBUG) @@ -648,17 +660,20 @@ void AtlasEngine::_createResources() } #endif // NDEBUG - // Our constant buffer will never get resized + if (!_r.d2dMode) { - D3D11_BUFFER_DESC desc{}; - desc.ByteWidth = sizeof(ConstBuffer); - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.constantBuffer.put())); - } + // Our constant buffer will never get resized + { + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = sizeof(ConstBuffer); + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.constantBuffer.put())); + } - THROW_IF_FAILED(_r.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _r.vertexShader.put())); - THROW_IF_FAILED(_r.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _r.pixelShader.put())); + THROW_IF_FAILED(_r.device->CreateVertexShader(&shader_vs[0], sizeof(shader_vs), nullptr, _r.vertexShader.put())); + THROW_IF_FAILED(_r.device->CreatePixelShader(&shader_ps[0], sizeof(shader_ps), nullptr, _r.pixelShader.put())); + } WI_ClearFlag(_api.invalidations, ApiInvalidations::Device); WI_SetAllFlags(_api.invalidations, ApiInvalidations::SwapChain); @@ -673,6 +688,10 @@ void AtlasEngine::_releaseSwapChain() // no views are bound to pipeline state), and then call Flush on the immediate context. if (_r.swapChain && _r.deviceContext) { + if (_r.d2dMode) + { + _r.d2dRenderTarget.reset(); + } _r.frameLatencyWaitableObject.reset(); _r.swapChain.reset(); _r.renderTargetView.reset(); @@ -694,9 +713,9 @@ void AtlasEngine::_createSwapChain() desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; desc.SampleDesc.Count = 1; desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - desc.BufferCount = 2; // TODO: 3? + desc.BufferCount = 2; desc.Scaling = DXGI_SCALING_NONE; - desc.SwapEffect = _sr.isWindows10OrGreater ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; + desc.SwapEffect = _sr.isWindows10OrGreater && !_r.d2dMode ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // * HWND swap chains can't do alpha. // * If our background is opaque we can enable "independent" flips by setting DXGI_SWAP_EFFECT_FLIP_DISCARD and DXGI_ALPHA_MODE_IGNORE. // As our swap chain won't have to compose with DWM anymore it reduces the display latency dramatically. @@ -754,43 +773,29 @@ void AtlasEngine::_recreateSizeDependentResources() // ResizeBuffer() docs: // Before you call ResizeBuffers, ensure that the application releases all references [...]. // You can use ID3D11DeviceContext::ClearState to ensure that all [internal] references are released. - if (_r.renderTargetView) + // The _r.cells check exists simply to prevent us from calling ResizeBuffers() on startup (i.e. when `_r` is empty). + if (_r.cells) { + if (_r.d2dMode) + { + _r.d2dRenderTarget.reset(); + } _r.renderTargetView.reset(); _r.deviceContext->ClearState(); _r.deviceContext->Flush(); - THROW_IF_FAILED(_r.swapChain->ResizeBuffers(0, _api.sizeInPixel.x, _api.sizeInPixel.y, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT)); - } - - // The RenderTargetView is later used with OMSetRenderTargets - // to tell D3D where stuff is supposed to be rendered at. - { - wil::com_ptr buffer; - THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void())); - THROW_IF_FAILED(_r.device->CreateRenderTargetView(buffer.get(), nullptr, _r.renderTargetView.put())); + THROW_IF_FAILED(_r.swapChain->ResizeBuffers(0, _api.sizeInPixel.x, _api.sizeInPixel.y, DXGI_FORMAT_UNKNOWN, debugGeneralPerformance ? 0 : DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT)); } - // Tell D3D which parts of the render target will be visible. - // Everything outside of the viewport will be black. - // - // In the future this should cover the entire _api.sizeInPixel.x/_api.sizeInPixel.y. - // The pixel shader should draw the remaining content in the configured background color. - { - D3D11_VIEWPORT viewport{}; - viewport.Width = static_cast(_api.sizeInPixel.x); - viewport.Height = static_cast(_api.sizeInPixel.y); - _r.deviceContext->RSSetViewports(1, &viewport); - } + const auto totalCellCount = static_cast(_api.cellCount.x) * static_cast(_api.cellCount.y); + const auto resize = _api.cellCount != _r.cellCount; - if (_api.cellCount != _r.cellCount) + if (resize) { - const auto totalCellCount = static_cast(_api.cellCount.x) * static_cast(_api.cellCount.y); // Let's guess that every cell consists of a surrogate pair. const auto projectedTextSize = static_cast(_api.cellCount.x) * 2; // IDWriteTextAnalyzer::GetGlyphs says: // The recommended estimate for the per-glyph output buffers is (3 * textLength / 2 + 16). - // We already set the textLength to twice the cell count. - const auto projectedGlyphSize = 3 * projectedTextSize + 16; + const auto projectedGlyphSize = 3 * projectedTextSize / 2 + 16; // This buffer is a bit larger than the others (multiple MB). // Prevent a memory usage spike, by first deallocating and then allocating. @@ -818,21 +823,47 @@ void AtlasEngine::_recreateSizeDependentResources() _api.glyphProps = Buffer{ projectedGlyphSize }; _api.glyphAdvances = Buffer{ projectedGlyphSize }; _api.glyphOffsets = Buffer{ projectedGlyphSize }; - - D3D11_BUFFER_DESC desc; - desc.ByteWidth = gsl::narrow(totalCellCount * sizeof(Cell)); // totalCellCount can theoretically be UINT32_MAX! - desc.Usage = D3D11_USAGE_DYNAMIC; - desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; - desc.StructureByteStride = sizeof(Cell); - THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.cellBuffer.put())); - THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.cellBuffer.get(), nullptr, _r.cellView.put())); } - // We have called _r.deviceContext->ClearState() in the beginning and lost all D3D state. - // This forces us to set up everything up from scratch again. - _setShaderResources(); + if (!_r.d2dMode) + { + // The RenderTargetView is later used with OMSetRenderTargets + // to tell D3D where stuff is supposed to be rendered at. + { + wil::com_ptr buffer; + THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void())); + THROW_IF_FAILED(_r.device->CreateRenderTargetView(buffer.get(), nullptr, _r.renderTargetView.put())); + } + + // Tell D3D which parts of the render target will be visible. + // Everything outside of the viewport will be black. + // + // In the future this should cover the entire _api.sizeInPixel.x/_api.sizeInPixel.y. + // The pixel shader should draw the remaining content in the configured background color. + { + D3D11_VIEWPORT viewport{}; + viewport.Width = static_cast(_api.sizeInPixel.x); + viewport.Height = static_cast(_api.sizeInPixel.y); + _r.deviceContext->RSSetViewports(1, &viewport); + } + + if (resize) + { + D3D11_BUFFER_DESC desc; + desc.ByteWidth = gsl::narrow(totalCellCount * sizeof(Cell)); // totalCellCount can theoretically be UINT32_MAX! + desc.Usage = D3D11_USAGE_DYNAMIC; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; + desc.StructureByteStride = sizeof(Cell); + THROW_IF_FAILED(_r.device->CreateBuffer(&desc, nullptr, _r.cellBuffer.put())); + THROW_IF_FAILED(_r.device->CreateShaderResourceView(_r.cellBuffer.get(), nullptr, _r.cellView.put())); + } + + // We have called _r.deviceContext->ClearState() in the beginning and lost all D3D state. + // This forces us to set up everything up from scratch again. + _setShaderResources(); + } WI_ClearFlag(_api.invalidations, ApiInvalidations::Size); WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer); @@ -1387,7 +1418,7 @@ bool AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si } it = _r.glyphs.insert(std::move(key), std::move(value)); - _r.glyphQueue.emplace_back(&it->first, &it->second); + _r.glyphQueue.emplace_back(it); } const auto valueData = it->second.data(); diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 78c3a02df4a..0b24c5a503f 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -99,8 +99,10 @@ namespace Microsoft::Console::Render friend constexpr type operator~(type v) noexcept { return static_cast(~static_cast(v)); } \ friend constexpr type operator|(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) | static_cast(rhs)); } \ friend constexpr type operator&(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) & static_cast(rhs)); } \ + friend constexpr type operator^(type lhs, type rhs) noexcept { return static_cast(static_cast(lhs) ^ static_cast(rhs)); } \ friend constexpr void operator|=(type& lhs, type rhs) noexcept { lhs = lhs | rhs; } \ - friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; } + friend constexpr void operator&=(type& lhs, type rhs) noexcept { lhs = lhs & rhs; } \ + friend constexpr void operator^=(type& lhs, type rhs) noexcept { lhs = lhs ^ rhs; } template struct vec2 @@ -132,7 +134,7 @@ namespace Microsoft::Console::Render ATLAS_POD_OPS(rect) - constexpr bool non_empty() noexcept + constexpr bool non_empty() const noexcept { return (left < right) & (top < bottom); } @@ -347,6 +349,7 @@ namespace Microsoft::Console::Render SmallObjectOptimizer& operator=(SmallObjectOptimizer&& other) noexcept { + this->~SmallObjectOptimizer(); return *new (this) SmallObjectOptimizer(other); } @@ -507,6 +510,21 @@ namespace Microsoft::Console::Render } }; + struct CachedGlyphLayout + { + wil::com_ptr textLayout; + f32x2 halfSize; + f32x2 offset; + f32x2 scale; + D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE; + bool scalingRequired = false; + + explicit operator bool() const noexcept; + void reset() noexcept; + void applyScaling(ID2D1RenderTarget* d2dRenderTarget, D2D1_POINT_2F origin) const noexcept; + void undoScaling(ID2D1RenderTarget* d2dRenderTarget) const noexcept; + }; + struct AtlasValueData { CellFlags flags = CellFlags::None; @@ -530,6 +548,8 @@ namespace Microsoft::Console::Render return _data.data(); } + CachedGlyphLayout cachedLayout; + private: SmallObjectOptimizer _data; @@ -539,12 +559,6 @@ namespace Microsoft::Console::Render } }; - struct AtlasQueueItem - { - const AtlasKey* key; - const AtlasValue* value; - }; - struct AtlasKeyHasher { using is_transparent = int; @@ -899,9 +913,24 @@ namespace Microsoft::Console::Render void _updateConstantBuffer() const noexcept; void _adjustAtlasSize(); void _processGlyphQueue(); - void _drawGlyph(const AtlasQueueItem& item) const; - void _drawCursor(); - + void _drawGlyph(const TileHashMap::iterator& it) const; + CachedGlyphLayout _getCachedGlyphLayout(const wchar_t* chars, u16 charsLength, u16 cellCount, IDWriteTextFormat* textFormat, bool coloredGlyph) const; + void _drawCursor(u16r rect, u32 color, bool clear); + ID2D1Brush* _brushWithColor(u32 color); + void _d2dPresent(); + void _d2dCreateRenderTarget(); + void _d2dDrawDirtyArea(); + u16 _d2dDrawGlyph(const TileHashMap::iterator& it, u16x2 coord, u32 color); + void _d2dDrawLine(u16r rect, u16 pos, u16 width, u32 color, ID2D1StrokeStyle* strokeStyle = nullptr); + void _d2dFillRectangle(u16r rect, u32 color); + void _d2dCellFlagRendererCursor(u16r rect, u32 color); + void _d2dCellFlagRendererSelected(u16r rect, u32 color); + void _d2dCellFlagRendererUnderline(u16r rect, u32 color); + void _d2dCellFlagRendererUnderlineDotted(u16r rect, u32 color); + void _d2dCellFlagRendererUnderlineDouble(u16r rect, u32 color); + void _d2dCellFlagRendererStrikethrough(u16r rect, u32 color); + + static constexpr bool debugForceD2DMode = false; static constexpr bool debugGlyphGenerationPerformance = false; static constexpr bool debugTextParsingPerformance = false || debugGlyphGenerationPerformance; static constexpr bool debugGeneralPerformance = false || debugTextParsingPerformance; @@ -947,10 +976,11 @@ namespace Microsoft::Console::Render wil::com_ptr atlasBuffer; wil::com_ptr atlasView; wil::com_ptr d2dRenderTarget; - wil::com_ptr brush; + wil::com_ptr brush; wil::com_ptr textFormats[2][2]; Buffer textFormatAxes[2][2]; wil::com_ptr typography; + wil::com_ptr dottedStrokeStyle; Buffer cells; // invalidated by ApiInvalidations::Size Buffer cellGlyphMapping; // invalidated by ApiInvalidations::Size @@ -963,17 +993,23 @@ namespace Microsoft::Console::Render u16x2 atlasSizeInPixel; // invalidated by ApiInvalidations::Font TileHashMap glyphs; TileAllocator tileAllocator; - std::vector glyphQueue; + std::vector glyphQueue; f32 gamma = 0; f32 cleartypeEnhancedContrast = 0; f32 grayscaleEnhancedContrast = 0; u32 backgroundColor = 0xff000000; u32 selectionColor = 0x7fffffff; + u32 brushColor = 0xffffffff; CachedCursorOptions cursorOptions; RenderInvalidations invalidations = RenderInvalidations::None; + til::rect previousDirtyRectInPx; + til::rect dirtyRect; + i16 scrollOffset = 0; + bool d2dMode = false; + #ifndef NDEBUG // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method: // > For every frame it renders, the app should wait on this handle before starting any rendering operations. diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index cbff3d22185..3e8271299a0 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -44,6 +44,15 @@ constexpr bool isInInversionList(const std::array& ranges, wchar_t n return (idx & 1) != 0; } +constexpr D2D1_COLOR_F colorFromU32(uint32_t rgba) +{ + const auto r = static_cast((rgba >> 0) & 0xff) / 255.0f; + const auto g = static_cast((rgba >> 8) & 0xff) / 255.0f; + const auto b = static_cast((rgba >> 16) & 0xff) / 255.0f; + const auto a = static_cast((rgba >> 24) & 0xff) / 255.0f; + return { r, g, b, a }; +} + using namespace Microsoft::Console::Render; #pragma region IRenderEngine @@ -53,15 +62,15 @@ using namespace Microsoft::Console::Render; [[nodiscard]] HRESULT AtlasEngine::Present() noexcept try { - _adjustAtlasSize(); - _processGlyphQueue(); - - if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::Cursor)) + if (_r.d2dMode) { - _drawCursor(); - WI_ClearFlag(_r.invalidations, RenderInvalidations::Cursor); + _d2dPresent(); + return S_OK; } + _adjustAtlasSize(); + _processGlyphQueue(); + // The values the constant buffer depends on are potentially updated after BeginPaint(). if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::ConstBuffer)) { @@ -91,19 +100,8 @@ try // > IDXGISwapChain::Present: Partial Presentation (using a dirty rects or scroll) is not supported // > for SwapChains created with DXGI_SWAP_EFFECT_DISCARD or DXGI_SWAP_EFFECT_FLIP_DISCARD. // ---> No need to call IDXGISwapChain1::Present1. - // TODO: Would IDXGISwapChain1::Present1 and its dirty rects have benefits for remote desktop? THROW_IF_FAILED(_r.swapChain->Present(1, 0)); - // On some GPUs with tile based deferred rendering (TBDR) architectures, binding - // RenderTargets that already have contents in them (from previous rendering) incurs a - // cost for having to copy the RenderTarget contents back into tile memory for rendering. - // - // On Windows 10 with DXGI_SWAP_EFFECT_FLIP_DISCARD we get this for free. - if (!_sr.isWindows10OrGreater) - { - _r.deviceContext->DiscardView(_r.renderTargetView.get()); - } - return S_OK; } catch (const wil::ResultException& exception) @@ -205,13 +203,8 @@ void AtlasEngine::_adjustAtlasSize() _r.deviceContext->CopySubresourceRegion1(atlasBuffer.get(), 0, 0, 0, 0, _r.atlasBuffer.get(), 0, &box, D3D11_COPY_NO_OVERWRITE); } - _r.atlasSizeInPixel = requiredSize; - _r.atlasBuffer = std::move(atlasBuffer); - _r.atlasView = std::move(atlasView); - _setShaderResources(); - { - const auto surface = _r.atlasBuffer.query(); + const auto surface = atlasBuffer.query(); wil::com_ptr renderingParams; DWrite_GetRenderParams(_sr.dwriteFactory.get(), &_r.gamma, &_r.cleartypeEnhancedContrast, &_r.grayscaleEnhancedContrast, renderingParams.addressof()); @@ -234,55 +227,53 @@ void AtlasEngine::_adjustAtlasSize() } { static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 }; - wil::com_ptr brush; - THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, brush.addressof())); - _r.brush = brush.query(); + THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _r.brush.put())); + _r.brushColor = 0xffffffff; } + _r.atlasSizeInPixel = requiredSize; + _r.atlasBuffer = std::move(atlasBuffer); + _r.atlasView = std::move(atlasView); + _setShaderResources(); + WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer); WI_SetFlagIf(_r.invalidations, RenderInvalidations::Cursor, !copyFromExisting); } void AtlasEngine::_processGlyphQueue() { - if (_r.glyphQueue.empty()) + if (_r.glyphQueue.empty() && WI_IsFlagClear(_r.invalidations, RenderInvalidations::Cursor)) { return; } _r.d2dRenderTarget->BeginDraw(); - for (const auto& pair : _r.glyphQueue) + + if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::Cursor)) { - _drawGlyph(pair); + _drawCursor({ 0, 0, 1, 1 }, 0xffffffff, true); + WI_ClearFlag(_r.invalidations, RenderInvalidations::Cursor); } - THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw()); + for (const auto& it : _r.glyphQueue) + { + _drawGlyph(it); + } _r.glyphQueue.clear(); + + THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw()); } -void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const +void AtlasEngine::_drawGlyph(const TileHashMap::iterator& it) const { - const auto key = item.key->data(); - const auto value = item.value->data(); + const auto key = it->first.data(); + const auto value = it->second.data(); const auto coords = &value->coords[0]; const auto charsLength = key->charCount; - const auto cellCount = static_cast(key->attributes.cellCount); + const auto cellCount = key->attributes.cellCount; const auto textFormat = _getTextFormat(key->attributes.bold, key->attributes.italic); const auto coloredGlyph = WI_IsFlagSet(value->flags, CellFlags::ColoredGlyph); - const f32x2 layoutBox{ cellCount * _r.cellSizeDIP.x, _r.cellSizeDIP.y }; - - // See D2DFactory::DrawText - wil::com_ptr textLayout; - THROW_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(&key->chars[0], charsLength, textFormat, layoutBox.x, layoutBox.y, textLayout.addressof())); - if (_r.typography) - { - textLayout->SetTypography(_r.typography.get(), { 0, charsLength }); - } - - auto options = D2D1_DRAW_TEXT_OPTIONS_NONE; - // D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT enables a bunch of internal machinery - // which doesn't have to run if we know we can't use it anyways in the shader. - WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, coloredGlyph); + const auto cachedLayout = _getCachedGlyphLayout(&key->chars[0], charsLength, cellCount, textFormat, coloredGlyph); // Colored glyphs cannot be drawn in linear gamma. // That's why we're simply alpha-blending them in the shader. @@ -293,11 +284,53 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const _r.d2dRenderTarget->SetTextAntialiasMode(coloredGlyph ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE); } + for (u16 i = 0; i < cellCount; ++i) + { + const auto coord = coords[i]; + + D2D1_RECT_F rect; + rect.left = static_cast(coord.x) * _r.dipPerPixel; + rect.top = static_cast(coord.y) * _r.dipPerPixel; + rect.right = rect.left + _r.cellSizeDIP.x; + rect.bottom = rect.top + _r.cellSizeDIP.y; + + D2D1_POINT_2F origin; + origin.x = rect.left - i * _r.cellSizeDIP.x; + origin.y = rect.top; + + _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); + _r.d2dRenderTarget->Clear(); + + cachedLayout.applyScaling(_r.d2dRenderTarget.get(), origin); + + // Now that we're done using origin to calculate the center point for our transformation + // we can use it for its intended purpose to slightly shift the glyph around. + origin.x += cachedLayout.offset.x; + origin.y += cachedLayout.offset.y; + _r.d2dRenderTarget->DrawTextLayout(origin, cachedLayout.textLayout.get(), _r.brush.get(), cachedLayout.options); + + cachedLayout.undoScaling(_r.d2dRenderTarget.get()); + + _r.d2dRenderTarget->PopAxisAlignedClip(); + } +} + +AtlasEngine::CachedGlyphLayout AtlasEngine::_getCachedGlyphLayout(const wchar_t* chars, u16 charsLength, u16 cellCount, IDWriteTextFormat* textFormat, bool coloredGlyph) const +{ + const f32x2 layoutBox{ cellCount * _r.cellSizeDIP.x, _r.cellSizeDIP.y }; const f32x2 halfSize{ layoutBox.x * 0.5f, layoutBox.y * 0.5f }; bool scalingRequired = false; f32x2 offset{ 0, 0 }; f32x2 scale{ 1, 1 }; + // See D2DFactory::DrawText + wil::com_ptr textLayout; + THROW_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(chars, charsLength, textFormat, layoutBox.x, layoutBox.y, textLayout.addressof())); + if (_r.typography) + { + textLayout->SetTypography(_r.typography.get(), { 0, charsLength }); + } + // Block Element and Box Drawing characters need to be handled separately, // because unlike regular ones they're supposed to fill the entire layout box. // @@ -319,14 +352,14 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const // clang-format on }; - if (charsLength == 1 && isInInversionList(blockCharacters, key->chars[0])) + if (charsLength == 1 && isInInversionList(blockCharacters, chars[0])) { wil::com_ptr fontCollection; THROW_IF_FAILED(textFormat->GetFontCollection(fontCollection.addressof())); const auto baseWeight = textFormat->GetFontWeight(); const auto baseStyle = textFormat->GetFontStyle(); - TextAnalysisSource analysisSource{ &key->chars[0], 1 }; + TextAnalysisSource analysisSource{ chars, 1 }; UINT32 mappedLength = 0; wil::com_ptr mappedFont; FLOAT mappedScale = 0; @@ -351,7 +384,7 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const DWRITE_FONT_METRICS metrics; fontFace->GetMetrics(&metrics); - const u32 codePoint = key->chars[0]; + const u32 codePoint = chars[0]; u16 glyphIndex; THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex)); @@ -470,6 +503,10 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const offset.y += (baselineFixed - baseline) / scale.y; } + auto options = D2D1_DRAW_TEXT_OPTIONS_NONE; + // D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT enables a bunch of internal machinery + // which doesn't have to run if we know we can't use it anyways in the shader. + WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, coloredGlyph); // !!! IMPORTANT !!! // DirectWrite/2D snaps the baseline to whole pixels, which is something we technically // want (it makes text look crisp), but fails in weird ways if `scalingRequired` is true. @@ -481,57 +518,17 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const // where every single "=" might be blatantly misaligned vertically (same for any box drawings). WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_NO_SNAP, scalingRequired); - const f32x2 inverseScale{ 1.0f - scale.x, 1.0f - scale.y }; - - for (u32 i = 0; i < cellCount; ++i) - { - const auto coord = coords[i]; - - D2D1_RECT_F rect; - rect.left = static_cast(coord.x) * _r.dipPerPixel; - rect.top = static_cast(coord.y) * _r.dipPerPixel; - rect.right = rect.left + _r.cellSizeDIP.x; - rect.bottom = rect.top + _r.cellSizeDIP.y; - - D2D1_POINT_2F origin; - origin.x = rect.left - i * _r.cellSizeDIP.x; - origin.y = rect.top; - - { - _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); - _r.d2dRenderTarget->Clear(); - } - if (scalingRequired) - { - const D2D1_MATRIX_3X2_F transform{ - scale.x, - 0, - 0, - scale.y, - (origin.x + halfSize.x) * inverseScale.x, - (origin.y + halfSize.y) * inverseScale.y, - }; - _r.d2dRenderTarget->SetTransform(&transform); - } - { - // Now that we're done using origin to calculate the center point for our transformation - // we can use it for its intended purpose to slightly shift the glyph around. - origin.x += offset.x; - origin.y += offset.y; - _r.d2dRenderTarget->DrawTextLayout(origin, textLayout.get(), _r.brush.get(), options); - } - if (scalingRequired) - { - static constexpr D2D1_MATRIX_3X2_F identity{ 1, 0, 0, 1, 0, 0 }; - _r.d2dRenderTarget->SetTransform(&identity); - } - { - _r.d2dRenderTarget->PopAxisAlignedClip(); - } - } + return CachedGlyphLayout{ + .textLayout = textLayout, + .halfSize = halfSize, + .offset = offset, + .scale = scale, + .options = options, + .scalingRequired = scalingRequired, + }; } -void AtlasEngine::_drawCursor() +void AtlasEngine::_drawCursor(u16r rect, u32 color, bool clear) { // lineWidth is in D2D's DIPs. For instance if we have a 150-200% zoom scale we want to draw a 2px wide line. // At 150% scale lineWidth thus needs to be 1.33333... because at a zoom scale of 1.5 this results in a 2px wide line. @@ -540,21 +537,21 @@ void AtlasEngine::_drawCursor() // `clip` is the rectangle within our texture atlas that's reserved for our cursor texture, ... D2D1_RECT_F clip; - clip.left = 0.0f; - clip.top = 0.0f; - clip.right = _r.cellSizeDIP.x; - clip.bottom = _r.cellSizeDIP.y; + clip.left = static_cast(rect.left) * _r.cellSizeDIP.x; + clip.top = static_cast(rect.top) * _r.cellSizeDIP.y; + clip.right = static_cast(rect.right) * _r.cellSizeDIP.x; + clip.bottom = static_cast(rect.bottom) * _r.cellSizeDIP.y; // ... whereas `rect` is just the visible (= usually white) portion of our cursor. - auto rect = clip; + auto box = clip; switch (cursorType) { case CursorType::Legacy: - rect.top = _r.cellSizeDIP.y * static_cast(100 - _r.cursorOptions.heightPercentage) / 100.0f; + box.top = box.bottom - _r.cellSizeDIP.y * static_cast(_r.cursorOptions.heightPercentage) / 100.0f; break; case CursorType::VerticalBar: - rect.right = lineWidth; + box.right = box.left + lineWidth; break; case CursorType::EmptyBox: { @@ -562,42 +559,414 @@ void AtlasEngine::_drawCursor() // coordinates in such a way that the line border extends half the width to each side. // --> Our coordinates have to be 0.5 DIP off in order to draw a 2px line on a 200% scaling. const auto halfWidth = lineWidth / 2.0f; - rect.left = halfWidth; - rect.top = halfWidth; - rect.right -= halfWidth; - rect.bottom -= halfWidth; + box.left += halfWidth; + box.top += halfWidth; + box.right -= halfWidth; + box.bottom -= halfWidth; break; } case CursorType::Underscore: case CursorType::DoubleUnderscore: - rect.top = _r.cellSizeDIP.y - lineWidth; + box.top = box.bottom - lineWidth; break; default: break; } - _r.d2dRenderTarget->BeginDraw(); + const auto brush = _brushWithColor(color); + // We need to clip the area we draw in to ensure we don't // accidentally draw into any neighboring texture atlas tiles. _r.d2dRenderTarget->PushAxisAlignedClip(&clip, D2D1_ANTIALIAS_MODE_ALIASED); - _r.d2dRenderTarget->Clear(); + + if (clear) + { + _r.d2dRenderTarget->Clear(); + } if (cursorType == CursorType::EmptyBox) { - _r.d2dRenderTarget->DrawRectangle(&rect, _r.brush.get(), lineWidth); + _r.d2dRenderTarget->DrawRectangle(&box, brush, lineWidth); } else { - _r.d2dRenderTarget->FillRectangle(&rect, _r.brush.get()); + _r.d2dRenderTarget->FillRectangle(&box, brush); } if (cursorType == CursorType::DoubleUnderscore) { - rect.top -= 2.0f; - rect.bottom -= 2.0f; - _r.d2dRenderTarget->FillRectangle(&rect, _r.brush.get()); + const auto offset = lineWidth * 2.0f; + box.top -= offset; + box.bottom -= offset; + _r.d2dRenderTarget->FillRectangle(&box, brush); } _r.d2dRenderTarget->PopAxisAlignedClip(); +} + +ID2D1Brush* AtlasEngine::_brushWithColor(u32 color) +{ + if (_r.brushColor != color) + { + const auto d2dColor = colorFromU32(color); + THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&d2dColor, nullptr, _r.brush.put())); + _r.brushColor = color; + } + return _r.brush.get(); +} + +AtlasEngine::CachedGlyphLayout::operator bool() const noexcept +{ + return static_cast(textLayout); +} + +void AtlasEngine::CachedGlyphLayout::reset() noexcept +{ + textLayout.reset(); +} + +void AtlasEngine::CachedGlyphLayout::applyScaling(ID2D1RenderTarget* d2dRenderTarget, D2D1_POINT_2F origin) const noexcept +{ + __assume(d2dRenderTarget != nullptr); + + if (scalingRequired) + { + const D2D1_MATRIX_3X2_F transform{ + scale.x, + 0, + 0, + scale.y, + (origin.x + halfSize.x) * (1.0f - scale.x), + (origin.y + halfSize.y) * (1.0f - scale.y), + }; + d2dRenderTarget->SetTransform(&transform); + } +} + +void AtlasEngine::CachedGlyphLayout::undoScaling(ID2D1RenderTarget* d2dRenderTarget) const noexcept +{ + __assume(d2dRenderTarget != nullptr); + + if (scalingRequired) + { + static constexpr D2D1_MATRIX_3X2_F identity{ 1, 0, 0, 1, 0, 0 }; + d2dRenderTarget->SetTransform(&identity); + } +} + +void AtlasEngine::_d2dPresent() +{ + auto dirtyRectInPx = _r.dirtyRect; + dirtyRectInPx.left *= _r.fontMetrics.cellSize.x; + dirtyRectInPx.top *= _r.fontMetrics.cellSize.y; + dirtyRectInPx.right *= _r.fontMetrics.cellSize.x; + dirtyRectInPx.bottom *= _r.fontMetrics.cellSize.y; + + if (const auto intersection = _r.previousDirtyRectInPx & dirtyRectInPx) + { + wil::com_ptr backBuffer; + wil::com_ptr frontBuffer; + THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(backBuffer), backBuffer.put_void())); + THROW_IF_FAILED(_r.swapChain->GetBuffer(1, __uuidof(frontBuffer), frontBuffer.put_void())); + + D3D11_BOX intersectBox; + intersectBox.left = intersection.left; + intersectBox.top = intersection.top; + intersectBox.front = 0; + intersectBox.right = intersection.right; + intersectBox.bottom = intersection.bottom; + intersectBox.back = 1; + _r.deviceContext->CopySubresourceRegion1(backBuffer.get(), 0, intersection.left, intersection.top, 0, frontBuffer.get(), 0, &intersectBox, 0); + } + + _d2dCreateRenderTarget(); + _d2dDrawDirtyArea(); + + // See documentation for IDXGISwapChain2::GetFrameLatencyWaitableObject method: + // > For every frame it renders, the app should wait on this handle before starting any rendering operations. + // > Note that this requirement includes the first frame the app renders with the swap chain. + assert(debugGeneralPerformance || _r.frameLatencyWaitableObjectUsed); + + if (dirtyRectInPx) + { + { + RECT scrollRect{}; + POINT scrollOffset{}; + DXGI_PRESENT_PARAMETERS params{ + .DirtyRectsCount = 1, + .pDirtyRects = dirtyRectInPx.as_win32_rect(), + }; + + if (_r.scrollOffset) + { + scrollRect = { + 0, + std::max(0, _r.scrollOffset), + _r.cellCount.x, + _r.cellCount.y + std::min(0, _r.scrollOffset), + }; + scrollOffset = { + 0, + _r.scrollOffset, + }; + + scrollRect.top *= _r.fontMetrics.cellSize.y; + scrollRect.right *= _r.fontMetrics.cellSize.x; + scrollRect.bottom *= _r.fontMetrics.cellSize.y; + + scrollOffset.y *= _r.fontMetrics.cellSize.y; + + params.pScrollRect = &scrollRect; + params.pScrollOffset = &scrollOffset; + } + + THROW_IF_FAILED(_r.swapChain->Present1(1, 0, ¶ms)); + } + } + else + { + THROW_IF_FAILED(_r.swapChain->Present(1, 0)); + } + + _r.previousDirtyRectInPx = dirtyRectInPx; +} + +void AtlasEngine::_d2dCreateRenderTarget() +{ + if (_r.d2dRenderTarget) + { + return; + } + + { + wil::com_ptr buffer; + THROW_IF_FAILED(_r.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), buffer.put_void())); + + const auto surface = buffer.query(); + + D2D1_RENDER_TARGET_PROPERTIES props{}; + props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; + props.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }; + props.dpiX = static_cast(_r.dpi); + props.dpiY = static_cast(_r.dpi); + THROW_IF_FAILED(_sr.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, _r.d2dRenderTarget.put())); + + // In case _api.realizedAntialiasingMode is D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE we'll + // continuously adjust it in AtlasEngine::_drawGlyph. See _drawGlyph. + _r.d2dRenderTarget->SetTextAntialiasMode(static_cast(_api.realizedAntialiasingMode)); + } + { + static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 }; + THROW_IF_FAILED(_r.d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _r.brush.put())); + _r.brushColor = 0xffffffff; + } +} + +void AtlasEngine::_d2dDrawDirtyArea() +{ + struct CellFlagHandler + { + CellFlags filter; + decltype(&AtlasEngine::_d2dCellFlagRendererCursor) func; + }; + + static constexpr std::array cellFlagHandlers{ + // Ordered by lowest to highest "layer". + // The selection for instance is drawn on top of underlines, not under them. + CellFlagHandler{ CellFlags::Underline, &AtlasEngine::_d2dCellFlagRendererUnderline }, + CellFlagHandler{ CellFlags::UnderlineDotted, &AtlasEngine::_d2dCellFlagRendererUnderlineDotted }, + CellFlagHandler{ CellFlags::UnderlineDouble, &AtlasEngine::_d2dCellFlagRendererUnderlineDouble }, + CellFlagHandler{ CellFlags::Strikethrough, &AtlasEngine::_d2dCellFlagRendererStrikethrough }, + CellFlagHandler{ CellFlags::Cursor, &AtlasEngine::_d2dCellFlagRendererCursor }, + CellFlagHandler{ CellFlags::Selected, &AtlasEngine::_d2dCellFlagRendererSelected }, + }; + + u16 left = 0; + u16 top = 0; + u16 right = _r.cellCount.x; + u16 bottom = _r.cellCount.y; + if (_r.dirtyRect) + { + left = gsl::narrow(_r.dirtyRect.left); + top = gsl::narrow(_r.dirtyRect.top); + right = gsl::narrow(_r.dirtyRect.right); + bottom = gsl::narrow(_r.dirtyRect.bottom); + } + + _r.d2dRenderTarget->BeginDraw(); + + for (u16 y = top; y < bottom; ++y) + { + const Cell* cells = _getCell(0, y); + const TileHashMap::iterator* cellGlyphMappings = _getCellGlyphMapping(0, y); + + // Draw background. + { + auto x1 = left; + auto x2 = gsl::narrow_cast(x1 + 1); + auto currentColor = cells[x1].color.y; + + for (; x2 < right; ++x2) + { + const auto color = cells[x2].color.y; + + if (currentColor != color) + { + const u16r rect{ x1, y, x2, gsl::narrow_cast(y + 1) }; + _d2dFillRectangle(rect, currentColor); + x1 = x2; + currentColor = color; + } + } + + { + const u16r rect{ x1, y, x2, gsl::narrow_cast(y + 1) }; + _d2dFillRectangle(rect, currentColor); + } + } + + // Draw text. + for (u16 x = left, cellCount = 0; x < right; x += cellCount) + { + const auto& it = cellGlyphMappings[x]; + const u16x2 coord{ x, y }; + const auto color = cells[x].color.x; + cellCount = _d2dDrawGlyph(it, coord, color); + } + + // Draw underlines, cursors, selections, etc. + for (const auto& handler : cellFlagHandlers) + { + auto x1 = left; + auto currentFlags = CellFlags::None; + + for (u16 x2 = left; x2 < right; ++x2) + { + const auto flags = cells[x2].flags & handler.filter; + + if (currentFlags != flags) + { + if (currentFlags != CellFlags::None) + { + const u16r rect{ x1, y, x2, gsl::narrow_cast(y + 1) }; + const auto color = cells[x1].color.x; + (this->*handler.func)(rect, color); + } + + x1 = x2; + currentFlags = flags; + } + } + + if (currentFlags != CellFlags::None) + { + const u16r rect{ x1, y, right, gsl::narrow_cast(y + 1) }; + const auto color = cells[x1].color.x; + (this->*handler.func)(rect, color); + } + } + } + THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw()); } + +// See _drawGlyph() for reference. +AtlasEngine::u16 AtlasEngine::_d2dDrawGlyph(const TileHashMap::iterator& it, const u16x2 coord, const u32 color) +{ + const auto key = it->first.data(); + const auto value = it->second.data(); + const auto charsLength = key->charCount; + const auto cellCount = key->attributes.cellCount; + const auto textFormat = _getTextFormat(key->attributes.bold, key->attributes.italic); + const auto coloredGlyph = WI_IsFlagSet(value->flags, CellFlags::ColoredGlyph); + + auto& cachedLayout = it->second.cachedLayout; + if (!cachedLayout) + { + cachedLayout = _getCachedGlyphLayout(&key->chars[0], charsLength, cellCount, textFormat, coloredGlyph); + } + + D2D1_RECT_F rect; + rect.left = static_cast(coord.x) * _r.cellSizeDIP.x; + rect.top = static_cast(coord.y) * _r.cellSizeDIP.y; + rect.right = static_cast(coord.x + cellCount) * _r.cellSizeDIP.x; + rect.bottom = rect.top + _r.cellSizeDIP.y; + + D2D1_POINT_2F origin; + origin.x = rect.left; + origin.y = rect.top; + + _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); + + cachedLayout.applyScaling(_r.d2dRenderTarget.get(), origin); + + origin.x += cachedLayout.offset.x; + origin.y += cachedLayout.offset.y; + _r.d2dRenderTarget->DrawTextLayout(origin, cachedLayout.textLayout.get(), _brushWithColor(color), cachedLayout.options); + + cachedLayout.undoScaling(_r.d2dRenderTarget.get()); + + _r.d2dRenderTarget->PopAxisAlignedClip(); + + return cellCount; +} + +void AtlasEngine::_d2dDrawLine(u16r rect, u16 pos, u16 width, u32 color, ID2D1StrokeStyle* strokeStyle) +{ + const auto w = static_cast(width) * _r.dipPerPixel; + const auto y1 = static_cast(rect.top) * _r.cellSizeDIP.y + static_cast(pos) * _r.dipPerPixel + w * 0.5f; + const auto x1 = static_cast(rect.left) * _r.cellSizeDIP.x; + const auto x2 = static_cast(rect.right) * _r.cellSizeDIP.x; + const auto brush = _brushWithColor(color); + _r.d2dRenderTarget->DrawLine({ x1, y1 }, { x2, y1 }, brush, w, strokeStyle); +} + +void AtlasEngine::_d2dFillRectangle(u16r rect, u32 color) +{ + const D2D1_RECT_F r{ + .left = static_cast(rect.left) * _r.cellSizeDIP.x, + .top = static_cast(rect.top) * _r.cellSizeDIP.y, + .right = static_cast(rect.right) * _r.cellSizeDIP.x, + .bottom = static_cast(rect.bottom) * _r.cellSizeDIP.y, + }; + const auto brush = _brushWithColor(color); + _r.d2dRenderTarget->FillRectangle(r, brush); +} + +void AtlasEngine::_d2dCellFlagRendererCursor(u16r rect, u32 color) +{ + _drawCursor(rect, _r.cursorOptions.cursorColor, false); +} + +void AtlasEngine::_d2dCellFlagRendererSelected(u16r rect, u32 color) +{ + _d2dFillRectangle(rect, _r.selectionColor); +} + +void AtlasEngine::_d2dCellFlagRendererUnderline(u16r rect, u32 color) +{ + _d2dDrawLine(rect, _r.fontMetrics.underlinePos, _r.fontMetrics.underlineWidth, color); +} + +void AtlasEngine::_d2dCellFlagRendererUnderlineDotted(u16r rect, u32 color) +{ + if (!_r.dottedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 1, 2 }; + THROW_IF_FAILED(_sr.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _r.dottedStrokeStyle.addressof())); + } + + _d2dDrawLine(rect, _r.fontMetrics.underlinePos, _r.fontMetrics.underlineWidth, color, _r.dottedStrokeStyle.get()); +} + +void AtlasEngine::_d2dCellFlagRendererUnderlineDouble(u16r rect, u32 color) +{ + _d2dDrawLine(rect, _r.fontMetrics.doubleUnderlinePos.x, _r.fontMetrics.thinLineWidth, color); + _d2dDrawLine(rect, _r.fontMetrics.doubleUnderlinePos.y, _r.fontMetrics.thinLineWidth, color); +} + +void AtlasEngine::_d2dCellFlagRendererStrikethrough(u16r rect, u32 color) +{ + _d2dDrawLine(rect, _r.fontMetrics.strikethroughPos, _r.fontMetrics.strikethroughWidth, color); +}