Skip to content

Conversation

@lhecker
Copy link
Member

@lhecker lhecker commented Sep 8, 2025

Note that this PR is not quite finished (~90%?). Missing:

  • Some polish in both conhost & particularly WT integration (e.g. caching the blink interval value)
  • Propagating the user blink setting on the WT side

This PR moves the cursor blinker and VT blink rendition timer into Renderer. To do so, this PR introduces a generic timer system with which you can schedule arbitrary timer jobs. Thanks to this, this PR removes a crapton of code, particularly throughout conhost, and improves throughput by another ~10%. On my PC it can now churn through >400MB/s while rendering at 240FPS. Fun fact: Processing 100kB of text and rendering it in conhost now takes less time than MSCTF (TSF) needs to process 1 keyboard character. When I look at our shell code.

Validation Steps Performed

  • TBD

@github-actions

This comment has been minimized.

@lhecker lhecker force-pushed the dev/lhecker/cursor-blinker branch from 4de989d to 8d17e85 Compare September 8, 2025 19:44
@lhecker lhecker changed the base branch from main to dev/lhecker/BEGONE-FOUL-SPIRIT September 14, 2025 11:16
@lhecker lhecker force-pushed the dev/lhecker/cursor-blinker branch from 8d17e85 to 8390d60 Compare September 14, 2025 11:26
DHowett pushed a commit that referenced this pull request Sep 22, 2025
Goal: Remove `CursorBlinker`.
Problem: Spooky action at a distance via `Cursor::HasMoved`.
Solution: Moved all the a11y event raising into `_stream.cpp` and pray
for the best.

Goal: Prevent node.js from tanking conhost performance via MSAA (WHY).
Problem: `ServiceLocator`.
Solution: Unserviced the locator. Debounced event raising. Performance
increased by >10x.
Problem 2: Lots of files changed.

This PR is a prerequisite for #19330

## Validation Steps Performed
Ran NVDA with and without UIA enabled and with different delays. ✅
@lhecker lhecker force-pushed the dev/lhecker/cursor-blinker branch from 8390d60 to 5742c64 Compare September 22, 2025 22:59
@lhecker lhecker changed the base branch from dev/lhecker/BEGONE-FOUL-SPIRIT to main September 22, 2025 22:59
@lhecker lhecker marked this pull request as ready for review September 22, 2025 22:59
Copy link
Member

@DHowett DHowett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

50/56

// - <none>
// Return Value:
// - <none>
void Window::_UpdateSystemMetrics() const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Make sure nobody else called this who cares about the cursor metrics (probably not)

cursor.SetSize(_d->ulSavedCursorSize);
cursor.SetIsVisible(_d->fSavedCursorVisible);
cursor.SetType(_d->savedCursorType);
cursor.SetIsOn(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 make sure selection has the same cursor behavior

{
// Enter Mark Mode
// NOTE: directly set cursor state. We already should have locked before calling this function.
_activeBuffer().GetCursor().SetIsOn(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to inhibit the cursor?


void Terminal::BlinkCursor() noexcept
{
if (_selectionMode != SelectionInteractionMode::Mark)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to inhibit the cursor for mark mode

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that we let the cursor blink during selections. Why do we inhibit it for mark mode then, even though mark mode is essentially just keyboard-selection?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind, we want the cursor to blink during mark mode to know which endpoint is currently focused. That way, you'll know if you're moving the start or end before you press on an arrow key.

// Return Value:
// - <none>
void Cursor::CopyProperties(const Cursor& OtherCursor) noexcept
void Cursor::CopyProperties(const Cursor& other) noexcept
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to update _parentBuffer too?


void TermControl::CursorVisibility(Control::CursorDisplayState cursorVisibility)
{
_cursorVisibility = cursorVisibility;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like cursor visibility is stored across 3 variables:

  • TermControl::_cursorVisibility
  • ControlCore::_forceCursorVisible
  • Renderer::_cursorVisibilityInhibitors

Could we remove the one in TermControl and just query the one in ControlCore? Or, even better, just pipe the value from Renderer all the way up, rather than having a separate boolean member on each layer?


void Terminal::BlinkCursor() noexcept
{
if (_selectionMode != SelectionInteractionMode::Mark)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind, we want the cursor to blink during mark mode to know which endpoint is currently focused. That way, you'll know if you're moving the start or end before you press on an arrow key.

_previewControl = Control::TermControl(settings, settings, *_previewConnection);
_previewControl.IsEnabled(false);
_previewControl.AllowFocusWhenDisabled(false);
_previewControl.CursorVisibility(Microsoft::Terminal::Control::CursorDisplayState::Shown);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Verified that the cursor still blinks

static DWORD _timerToMillis(TimerRepr t) noexcept;

// Actual rendering
[[nodiscard]] HRESULT PaintFrame();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit weird that this one is marked private, but doesn't have the _ prefix. Plus there's also _PaintFrame() just below it.

pWindow->UpdateWindowText();

auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier;
an.SelectionChanged();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we still need this?

// Fire off an event to let accessibility apps know the selection has changed.
auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier;
an.CursorChanged(coordBufferPos, true);
an.SelectionChanged();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@carlos-zamora
Copy link
Member

Fully reviewed. I want to get some resolution on my comments before approving.

Regarding accessibility (#19374), I plan on taking a look at a reported memory leak in WT when UIA is enabled soon-ish. I'll lump looking into #19374 too and Leonard and I can tag team work on a11y then.

@carlos-zamora
Copy link
Member

Oh, and you probably want to update the PR body too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants