Skip to content

Commit 41f7ed7

Browse files
authored
ConPTY: Fix missing flush on console mode changes (#15991)
Previously, all unknown escape sequences would lead to an immediate call to `VtEngine::_Flush()`. This lead to problems with nushell which uses FTCS marks that were unknown to us. Combined with the linewise redrawing that nushell does, Terminal would get the prompt in two separate frames, causing a slight flickering. #14677 fixed this by suppressing the `_Flush()` call when unknown sequences are encountered. Unfortunately, this triggered a bug due to our somewhat "inconsistent" architecture in conhost: `XtermEngine::WriteTerminalW` isn't just used to flush unknown sequences but also used directly by `InputBuffer::PassThroughWin32MouseRequest` to write its mouse sequence directly to the ConPTY host. `VtEngine` already contains a number of specialized member functions like `RequestWin32Input()` to ensure that `_Flush()` is called immediately and another member could've been added to solve this issue. This commit now adds `RequestMouseMode` in the same vein. But I believe we can make the system more robust in general by using eager flushing by default (= safe), similar to how a `write()` on a TCP socket flushes by default, and instead only selectively pause and unpause flushing with a system similar to `TCP_CORK`. This seems to work fairly well, as it solves: * The original nushell bug * The new bug * Improves overall throughput by ~33% (due to less flushing) In particular the last point is noteworthy, as this commit removes the last performance bottleneck in ConPTY that isn't `VtEngine`. Around ~95% of all CPU and wall time is spent in there now and any improvements to `VtEngine` should yield immediately results. Closes #15711 ## Validation Steps Performed * Clone/Run https://github.com/chrisant996/repro_enable_mouse_input * Hold Ctrl+Alt and circle with the mouse over the viewport * Repro.exe prints the current cursor coordinates ✅ * Run nushell * No flickering when typing in the prompt ✅
1 parent d38bb90 commit 41f7ed7

11 files changed

+87
-76
lines changed

src/host/VtIo.cpp

+21-1
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,6 @@ bool VtIo::IsUsingVt() const
255255
{
256256
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
257257
g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
258-
g.getConsoleInformation().GetActiveInputBuffer()->SetTerminalConnection(_pVtRenderEngine.get());
259258

260259
// Force the whole window to be put together first.
261260
// We don't really need the handle, we just want to leverage the setup steps.
@@ -497,6 +496,18 @@ void VtIo::EndResize()
497496
}
498497
}
499498

499+
// The name of this method is an analogy to TCP_CORK. It instructs
500+
// the VT renderer to stop flushing its buffer to the output pipe.
501+
// Don't forget to uncork it!
502+
void VtIo::CorkRenderer(bool corked) const noexcept
503+
{
504+
_pVtRenderEngine->Cork(corked);
505+
if (!corked)
506+
{
507+
LOG_IF_FAILED(ServiceLocator::LocateGlobals().pRender->PaintFrame());
508+
}
509+
}
510+
500511
#ifdef UNIT_TESTING
501512
// Method Description:
502513
// - This is a test helper method. It can be used to trick VtIo into responding
@@ -547,3 +558,12 @@ bool VtIo::IsResizeQuirkEnabled() const
547558
}
548559
return S_OK;
549560
}
561+
562+
[[nodiscard]] HRESULT VtIo::RequestMouseMode(bool enable) const noexcept
563+
{
564+
if (_pVtRenderEngine)
565+
{
566+
return _pVtRenderEngine->RequestMouseMode(enable);
567+
}
568+
return S_OK;
569+
}

src/host/VtIo.hpp

+3
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,16 @@ namespace Microsoft::Console::VirtualTerminal
4343
void BeginResize();
4444
void EndResize();
4545

46+
void CorkRenderer(bool corked) const noexcept;
47+
4648
#ifdef UNIT_TESTING
4749
void EnableConptyModeForTests(std::unique_ptr<Microsoft::Console::Render::VtEngine> vtRenderEngine);
4850
#endif
4951

5052
bool IsResizeQuirkEnabled() const;
5153

5254
[[nodiscard]] HRESULT ManuallyClearScrollback() const noexcept;
55+
[[nodiscard]] HRESULT RequestMouseMode(bool enable) const noexcept;
5356

5457
void CreatePseudoWindow();
5558
void SetWindowVisibility(bool showOrHide) noexcept;

src/host/_stream.cpp

+13-6
Original file line numberDiff line numberDiff line change
@@ -334,17 +334,24 @@ try
334334
return CONSOLE_STATUS_WAIT;
335335
}
336336

337-
auto restoreVtQuirk{
338-
wil::scope_exit([&]() { screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); })
339-
};
340-
337+
const auto vtIo = ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo();
338+
const auto restoreVtQuirk = wil::scope_exit([&]() {
339+
if (requiresVtQuirk)
340+
{
341+
screenInfo.ResetIgnoreLegacyEquivalentVTAttributes();
342+
}
343+
if (vtIo->IsUsingVt())
344+
{
345+
vtIo->CorkRenderer(false);
346+
}
347+
});
341348
if (requiresVtQuirk)
342349
{
343350
screenInfo.SetIgnoreLegacyEquivalentVTAttributes();
344351
}
345-
else
352+
if (vtIo->IsUsingVt())
346353
{
347-
restoreVtQuirk.release();
354+
vtIo->CorkRenderer(true);
348355
}
349356

350357
const std::wstring_view str{ pwchBuffer, *pcbBuffer / sizeof(WCHAR) };

src/host/getset.cpp

+12-10
Original file line numberDiff line numberDiff line change
@@ -368,17 +368,19 @@ void ApiRoutines::GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept
368368
WI_ClearFlag(gci.Flags, CONSOLE_USE_PRIVATE_FLAGS);
369369
}
370370

371-
const auto newQuickEditMode{ WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE) };
372-
373-
// Mouse input should be received when mouse mode is on and quick edit mode is off
374-
// (for more information regarding the quirks of mouse mode and why/how it relates
375-
// to quick edit mode, see GH#9970)
376-
const auto oldMouseMode{ !oldQuickEditMode && WI_IsFlagSet(context.InputMode, ENABLE_MOUSE_INPUT) };
377-
const auto newMouseMode{ !newQuickEditMode && WI_IsFlagSet(mode, ENABLE_MOUSE_INPUT) };
378-
379-
if (oldMouseMode != newMouseMode)
371+
if (gci.IsInVtIoMode())
380372
{
381-
gci.GetActiveInputBuffer()->PassThroughWin32MouseRequest(newMouseMode);
373+
// Mouse input should be received when mouse mode is on and quick edit mode is off
374+
// (for more information regarding the quirks of mouse mode and why/how it relates
375+
// to quick edit mode, see GH#9970)
376+
const auto newQuickEditMode{ WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE) };
377+
const auto oldMouseMode{ !oldQuickEditMode && WI_IsFlagSet(context.InputMode, ENABLE_MOUSE_INPUT) };
378+
const auto newMouseMode{ !newQuickEditMode && WI_IsFlagSet(mode, ENABLE_MOUSE_INPUT) };
379+
380+
if (oldMouseMode != newMouseMode)
381+
{
382+
LOG_IF_FAILED(gci.GetVtIo()->RequestMouseMode(newMouseMode));
383+
}
382384
}
383385

384386
context.InputMode = mode;

src/host/inputBuffer.cpp

+1-22
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ using namespace Microsoft::Console;
2525
// Return Value:
2626
// - A new instance of InputBuffer
2727
InputBuffer::InputBuffer() :
28-
InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE },
29-
_pTtyConnection(nullptr)
28+
InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE }
3029
{
3130
// initialize buffer header
3231
fInComposition = false;
@@ -342,26 +341,6 @@ void InputBuffer::FlushAllButKeys()
342341
_storage.erase(newEnd, _storage.end());
343342
}
344343

345-
void InputBuffer::SetTerminalConnection(_In_ Render::VtEngine* const pTtyConnection)
346-
{
347-
this->_pTtyConnection = pTtyConnection;
348-
}
349-
350-
void InputBuffer::PassThroughWin32MouseRequest(bool enable)
351-
{
352-
if (_pTtyConnection)
353-
{
354-
if (enable)
355-
{
356-
LOG_IF_FAILED(_pTtyConnection->WriteTerminalW(L"\x1b[?1003;1006h"));
357-
}
358-
else
359-
{
360-
LOG_IF_FAILED(_pTtyConnection->WriteTerminalW(L"\x1b[?1003;1006l"));
361-
}
362-
}
363-
}
364-
365344
// Routine Description:
366345
// - This routine reads from the input buffer.
367346
// - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition

src/host/inputBuffer.hpp

-3
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ class InputBuffer final : public ConsoleObjectHeader
6363

6464
bool IsInVirtualTerminalInputMode() const;
6565
Microsoft::Console::VirtualTerminal::TerminalInput& GetTerminalInput();
66-
void SetTerminalConnection(_In_ Microsoft::Console::Render::VtEngine* const pTtyConnection);
67-
void PassThroughWin32MouseRequest(bool enable);
6866

6967
private:
7068
enum class ReadingMode : uint8_t
@@ -86,7 +84,6 @@ class InputBuffer final : public ConsoleObjectHeader
8684
INPUT_RECORD _writePartialByteSequence{};
8785
bool _writePartialByteSequenceAvailable = false;
8886
Microsoft::Console::VirtualTerminal::TerminalInput _termInput;
89-
Microsoft::Console::Render::VtEngine* _pTtyConnection;
9087

9188
// This flag is used in _HandleTerminalInputCallback
9289
// If the InputBuffer leads to a _HandleTerminalInputCallback call,

src/renderer/vt/XtermEngine.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,8 @@ CATCH_RETURN();
555555
{
556556
RETURN_IF_FAILED(_Write("\x1b[2t"));
557557
}
558-
return _Flush();
558+
_Flush();
559+
return S_OK;
559560
}
560561

561562
// Method Description:

src/renderer/vt/invalidate.cpp

-5
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,7 @@ CATCH_RETURN();
120120
_circled = circled;
121121
}
122122

123-
// If we flushed for any reason other than circling (i.e, a sequence that we
124-
// didn't understand), we don't need to push the buffer out on EndPaint.
125-
_noFlushOnEnd = !circled;
126-
127123
_trace.TraceTriggerCircling(*pForcePaint);
128-
129124
return S_OK;
130125
}
131126

src/renderer/vt/paint.cpp

+1-16
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,7 @@ using namespace Microsoft::Console::Types;
9494
RETURN_IF_FAILED(_MoveCursor(_deferredCursorPos));
9595
}
9696

97-
// If this frame was triggered because we encountered a VT sequence which
98-
// required the buffered state to get printed, we don't want to flush this
99-
// frame to the pipe. That might result in us rendering half the output of a
100-
// particular frame (as emitted by the client).
101-
//
102-
// Instead, we'll leave this frame in _buffer, and just keep appending to
103-
// it as needed.
104-
if (_noFlushOnEnd) [[unlikely]]
105-
{
106-
_noFlushOnEnd = false;
107-
}
108-
else
109-
{
110-
RETURN_IF_FAILED(_Flush());
111-
}
112-
97+
_Flush();
11398
return S_OK;
11499
}
115100

src/renderer/vt/state.cpp

+29-9
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
4646
_circled(false),
4747
_firstPaint(true),
4848
_skipCursor(false),
49-
_exitResult{ S_OK },
5049
_terminalOwner{ nullptr },
5150
_newBottomLine{ false },
5251
_deferredCursorPos{ INVALID_COORDS },
@@ -136,25 +135,39 @@ CATCH_RETURN();
136135
CATCH_RETURN();
137136
}
138137

139-
[[nodiscard]] HRESULT VtEngine::_Flush() noexcept
138+
void VtEngine::_Flush() noexcept
139+
{
140+
if (!_corked && !_buffer.empty())
141+
{
142+
_flushImpl();
143+
}
144+
}
145+
146+
// _corked is often true and separating _flushImpl() out allows _flush() to be inlined.
147+
void VtEngine::_flushImpl() noexcept
140148
{
141149
if (_hFile)
142150
{
143-
auto fSuccess = !!WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast<DWORD>(_buffer.size()), nullptr, nullptr);
151+
const auto fSuccess = WriteFile(_hFile.get(), _buffer.data(), gsl::narrow_cast<DWORD>(_buffer.size()), nullptr, nullptr);
144152
_buffer.clear();
145153
if (!fSuccess)
146154
{
147-
_exitResult = HRESULT_FROM_WIN32(GetLastError());
155+
LOG_LAST_ERROR();
148156
_hFile.reset();
149157
if (_terminalOwner)
150158
{
151159
_terminalOwner->CloseOutput();
152160
}
153-
return _exitResult;
154161
}
155162
}
163+
}
156164

157-
return S_OK;
165+
// The name of this method is an analogy to TCP_CORK. It instructs
166+
// the VT renderer to stop flushing its buffer to the output pipe.
167+
// Don't forget to uncork it!
168+
void VtEngine::Cork(bool corked) noexcept
169+
{
170+
_corked = corked;
158171
}
159172

160173
// Method Description:
@@ -425,7 +438,7 @@ void VtEngine::SetTerminalOwner(Microsoft::Console::VirtualTerminal::VtIo* const
425438
HRESULT VtEngine::RequestCursor() noexcept
426439
{
427440
RETURN_IF_FAILED(_RequestCursor());
428-
RETURN_IF_FAILED(_Flush());
441+
_Flush();
429442
return S_OK;
430443
}
431444

@@ -546,13 +559,20 @@ HRESULT VtEngine::RequestWin32Input() noexcept
546559
// in the connected terminal after passing through an RIS sequence.
547560
RETURN_IF_FAILED(_RequestWin32Input());
548561
RETURN_IF_FAILED(_RequestFocusEventMode());
549-
RETURN_IF_FAILED(_Flush());
562+
_Flush();
550563
return S_OK;
551564
}
552565

553566
HRESULT VtEngine::SwitchScreenBuffer(const bool useAltBuffer) noexcept
554567
{
555568
RETURN_IF_FAILED(_SwitchScreenBuffer(useAltBuffer));
556-
RETURN_IF_FAILED(_Flush());
569+
_Flush();
557570
return S_OK;
558571
}
572+
573+
HRESULT VtEngine::RequestMouseMode(const bool enable) noexcept
574+
{
575+
const auto status = _WriteFormatted(FMT_COMPILE("\x1b[?1003;1006{}"), enable ? 'h' : 'l');
576+
_Flush();
577+
return status;
578+
}

src/renderer/vt/vtrenderer.hpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ namespace Microsoft::Console::Render
9090
[[nodiscard]] HRESULT RequestWin32Input() noexcept;
9191
[[nodiscard]] virtual HRESULT SetWindowVisibility(const bool showOrHide) noexcept = 0;
9292
[[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer) noexcept;
93+
[[nodiscard]] HRESULT RequestMouseMode(bool enable) noexcept;
94+
void Cork(bool corked) noexcept;
9395

9496
protected:
9597
wil::unique_hfile _hFile;
@@ -127,7 +129,6 @@ namespace Microsoft::Console::Render
127129
bool _newBottomLine;
128130
til::point _deferredCursorPos;
129131

130-
HRESULT _exitResult;
131132
Microsoft::Console::VirtualTerminal::VtIo* _terminalOwner;
132133

133134
Microsoft::Console::VirtualTerminal::RenderTracing _trace;
@@ -139,12 +140,13 @@ namespace Microsoft::Console::Render
139140

140141
bool _resizeQuirk{ false };
141142
bool _passthrough{ false };
142-
bool _noFlushOnEnd{ false };
143+
bool _corked{ false };
143144
std::optional<TextColor> _newBottomLineBG{ std::nullopt };
144145

145146
[[nodiscard]] HRESULT _WriteFill(const size_t n, const char c) noexcept;
146147
[[nodiscard]] HRESULT _Write(std::string_view const str) noexcept;
147-
[[nodiscard]] HRESULT _Flush() noexcept;
148+
void _Flush() noexcept;
149+
void _flushImpl() noexcept;
148150

149151
template<typename S, typename... Args>
150152
[[nodiscard]] HRESULT _WriteFormatted(S&& format, Args&&... args)

0 commit comments

Comments
 (0)