From 07d4dbd961505844fbce29e3e7b1dee73b3cdb4d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 12 Aug 2026 20:00:01 +0200 Subject: [PATCH 1/3] Fix visual artifacts when using QQ Wubi IME --- src/renderer/base/renderer.cpp | 38 ++++++++++++++++++++-------------- src/renderer/base/renderer.hpp | 2 +- src/tsf/Implementation.cpp | 23 +++++++++----------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index b5103427587..5115d059763 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -490,7 +490,19 @@ try // As we leave the scope, EndPaint will be called (declared above) return S_OK; } -CATCH_RETURN() +catch (...) +{ + // I found it useful during renderer development when exceptions aren't always silently caught and retried. + // Sometimes, the error goes away on the retry, but not for good reason. Catching such errors may be useful. +#ifndef NDEBUG + if (IsDebuggerPresent()) + { + __debugbreak(); + } +#endif + + RETURN_CAUGHT_EXCEPTION(); +} // NOTE: You must be holding the console lock when calling this function. void Renderer::SynchronizedOutputChanged() noexcept @@ -1027,6 +1039,8 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // relative to the entire buffer. const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1; const auto& activeComposition = _pData->GetActiveComposition(); + auto& buffer = _pData->GetTextBuffer(); + auto& scratchRow = buffer.GetScratchpadRow(); // This is effectively the number of cells on the visible screen that need to be redrawn. // The origin is always 0, 0 because it represents the screen itself, not the underlying buffer. @@ -1056,8 +1070,6 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // we need to walk through line-by-line and repaint onto the screen. const auto redraw = Viewport::Intersect(dirty, _viewport); - // Retrieve the text buffer so we can read information out of it. - auto& buffer = _pData->GetTextBuffer(); // Now walk through each row of text that we need to redraw. for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++) { @@ -1069,17 +1081,16 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // Draw the active composition. // We have to use some tricks here with const_cast, because the code after it relies on TextBufferCellIterator, // which isn't compatible with the scratchpad row. This forces us to back up and modify the actual row `r`. - ROW* rowBackup = nullptr; - if (row == compositionRow) - { - rowBackup = _PaintBufferOutputComposition(buffer, r, activeComposition); - } const auto restore = wil::scope_exit([&] { - if (rowBackup) + if (row == compositionRow) { - const_cast(r).CopyFrom(*rowBackup); + const_cast(r).CopyFrom(scratchRow); } }); + if (row == compositionRow) + { + _PaintBufferOutputComposition(r, scratchRow, activeComposition); + } // Convert the screen coordinates of the line to an equivalent // range of buffer cells, taking line rendition into account. @@ -1112,9 +1123,8 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) } } -ROW* Renderer::_PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition) +void Renderer::_PaintBufferOutputComposition(const ROW& r, ROW& scratch, const Composition& activeComposition) const { - auto& scratch = buffer.GetScratchpadRow(); scratch.CopyFrom(r); // *Overwrite* the original text with the active composition... @@ -1142,7 +1152,7 @@ ROW* Renderer::_PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, c attr.SetForeground(_compositionCache->baseAttribute.GetForeground()); } - state.text = text.substr(off, len); + state.text = til::safe_slice_len(text, off, len); state.columnBegin = state.columnEnd; const_cast(r).ReplaceText(state); const_cast(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr); @@ -1209,8 +1219,6 @@ ROW* Renderer::_PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, c i = spanEnd; } } - - return &scratch; } static bool _IsAllSpaces(const std::wstring_view v) diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 31fc7cc24df..5d8d643cf96 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -121,7 +121,7 @@ namespace Microsoft::Console::Render void _scheduleRenditionBlink(); [[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine); void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); - ROW* _PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition); + void _PaintBufferOutputComposition(const ROW& r, ROW& scratch, const Composition& activeComposition) const; void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target); void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget); bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept; diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp index 00c5613cbbd..4df14a6564c 100644 --- a/src/tsf/Implementation.cpp +++ b/src/tsf/Implementation.cpp @@ -647,6 +647,11 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec) } } + // Since we can't un-finalize finalized text, we only finalize text that's at the start of the document. + // In other words, don't put text that's in the middle between two active compositions into the finalized string. + const auto isActiveComposition = composing || activeCompositionEncountered; + auto& target = isActiveComposition ? activeComposition : finalizedString; + size_t totalLen = 0; for (;;) { @@ -658,17 +663,7 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec) ULONG len = bufCap; THROW_IF_FAILED(range->GetText(ec, TF_TF_MOVESTART, buf, len, &len)); - // Since we can't un-finalize finalized text, we only finalize text that's at the start of the document. - // In other words, don't put text that's in the middle between two active compositions into the finalized string. - if (!composing && !activeCompositionEncountered) - { - finalizedString.append(buf, len); - } - else - { - activeComposition.append(buf, len); - } - + target.append(buf, len); totalLen += len; if (len < bufCap) @@ -677,8 +672,10 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec) } } - const auto attr = _textAttributeFromAtom(atom); - activeCompositionRanges.emplace_back(totalLen, attr); + if (isActiveComposition) + { + activeCompositionRanges.emplace_back(totalLen, _textAttributeFromAtom(atom)); + } activeCompositionEncountered |= composing; } From 3b00dd4c4a43bab18f4c1873fd3b317b6bfbb9bc Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 13 Aug 2026 16:06:16 +0200 Subject: [PATCH 2/3] Investigation: Done --- src/interactivity/win32/windowproc.cpp | 19 +++++++++++++------ src/tsf/Implementation.cpp | 12 +++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index a4f46c29ec2..82e9b9964f0 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -280,6 +280,19 @@ static constexpr TsfDataProvider s_tsfDataProvider; { gci.Flags |= CONSOLE_IGNORE_NEXT_MOUSE_INPUT; } + + // NOTE: Initializing a TSF on WM_ACTIVATE (or earlier) ensures that we don't get a + // redundant, implicit IMM32 TSF client, which would unnecessarily bloat the process. + // (It is implicitly created during DefWindowProcW(WM_ACTIVATE).) + if (!g.tsf) + { + // The TSF implementation doesn't care per-se if it's STA or MTA, + // but it does require it to be initialized. STA because we're the UI thread. + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + g.tsf = TSF::Handle::Create(); + g.tsf.AssociateFocus(const_cast(&s_tsfDataProvider)); + } + goto CallDefWin; break; } @@ -294,12 +307,6 @@ static constexpr TsfDataProvider s_tsfDataProvider; renderer->AllowCursorVisibility(Render::InhibitionSource::Host, true); } - if (!g.tsf) - { - g.tsf = TSF::Handle::Create(); - g.tsf.AssociateFocus(const_cast(&s_tsfDataProvider)); - } - // set the text area to have focus for accessibility consumers if (_pUiaProvider) { diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp index 4df14a6564c..88fc0a246c4 100644 --- a/src/tsf/Implementation.cpp +++ b/src/tsf/Implementation.cpp @@ -42,9 +42,15 @@ using unique_tf_propertyval = wil::unique_struct Date: Thu, 13 Aug 2026 16:32:18 +0200 Subject: [PATCH 3/3] Fix spelling --- .github/actions/spelling/expect/expect.txt | 8 ++++---- src/tsf/Implementation.cpp | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 1c7f582f900..21d56589603 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1,8 +1,8 @@ aaaaabbb +ABANDONFONT abbcc abcc abgr -ABANDONFONT ABORTIFHUNG ACCESSTOKEN acidev @@ -166,6 +166,7 @@ changelist CHARSETINFO chshdng CHT +CInput CLASSSTRING cleartype CLICKACTIVE @@ -665,6 +666,7 @@ GHIJK GHIJKL gitcheckin gitfilters +gksks gle GLOBALFOCUS GLYPHENTRY @@ -1079,7 +1081,6 @@ NOCONTEXTHELP NOCOPYBITS nodiscard NODUP -NODEFAULT noexcepts NOFONT NOHIDDENTEXT @@ -1111,7 +1112,6 @@ NOSNAPSHOT NOTELLSHELL NOTHOUSANDS NOTICKS -notif NOTIMEOUTIFNOTHUNG NOTIMPL NOTOPMOST @@ -1568,7 +1568,6 @@ SMARTQUOTE SMTO snapcx snapcy -SND snk SOLIDBOX Solutiondir @@ -1756,6 +1755,7 @@ Unittesting unittests unknwn UNORM +unsend untextured UPDATEDISPLAY UPDOWN diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp index 88fc0a246c4..81322b8a571 100644 --- a/src/tsf/Implementation.cpp +++ b/src/tsf/Implementation.cpp @@ -48,9 +48,9 @@ using unique_tf_propertyval = wil::unique_struct