Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Monarch/Peasant & Make UI single-threaded #18215

Merged
merged 29 commits into from
Dec 12, 2024
Merged

Conversation

lhecker
Copy link
Member

@lhecker lhecker commented Nov 19, 2024

As before, a minor refactor:

  • I started off by removing the Monarch/Peasant with the goal of moving
    it into and deduplicating its functionality with WindowEmperor.
  • Since I needed a replacement for the Monarch (= ensures that there's
    a single instance), I wrote single-instance code with a NT mutex
    and by yeeting data across processes with WM_COPYDATA.
  • This resulted in severe threading issues, because it now started up
    way faster. The more I tried to solve them the deeper I had to dig,
    because you can't just put a mutex around CascadiaSettings.
    I then tried to seeif WinUI can run multiple windows on a single
    thread and, as it turns out, it can.
    So, I removed the multi- from the window threading.
  • At this point I had dig about 1 mile deep and brought no ladder.
    So, to finish it up, I had to clean up the entire eventing system
    around WindowEmperor, cleaned up all the coroutines,
    and cleaned up all the callbacks.

Closes #16183
Closes #16221
Closes #16487
Closes #16532
Closes #16733
Closes #16755
Closes #17015
Closes #17360
Closes #17420
Closes #17457
Closes #17799
Closes #17976
Closes #18057
Closes #18084
Closes #18169
Closes #18176
Closes #18191

Validation Steps Performed

  • It does not crash ✅
  • New/close tab ✅
  • New/close window ✅
  • Move tabs between windows ✅
  • Split tab into new window ✅
  • Persist windows on exit / restore startup ✅

@lhecker lhecker changed the title Remove Monarch/Peasant, Single-threaded UI, and Windowing cleanup Remove Monarch/Peasant & Make UI single-threaded Nov 19, 2024
@microsoft-github-policy-service microsoft-github-policy-service bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Area-Quality Stability, Performance, Etc. Product-Terminal The new Windows Terminal. Severity-Crash Crashes are real bad news. labels Nov 19, 2024
@lhecker lhecker force-pushed the dev/lhecker/dethronement branch from b39c5a7 to 7853853 Compare November 19, 2024 00:29
@@ -170,7 +170,6 @@ namespace winrt::TerminalApp::implementation

const auto canDragDrop = CanDragDrop();

_tabRow.PointerMoved({ get_weak(), &TerminalPage::_RestorePointerCursorHandler });
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved into TerminalControl as a different workaround is needed for this functionality.

{
auto weakThis{ get_weak() };
co_await wil::resume_foreground(Dispatcher());
Copy link
Member Author

Choose a reason for hiding this comment

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

No multi-threading = No problem. (Suppress whitespace in the diff.)

<CurrentXamlPackage Include="@(AppxPackageRegistration)"
Condition="'%(AppxPackageRegistration.Architecture)'=='$(Platform)' AND
$([System.String]::new('%(AppxPackageRegistration.Filename)').StartsWith('Microsoft.UI.Xaml'))" />
<CurrentXamlPackage Include="@(AppxPackageRegistration)" Condition="'%(AppxPackageRegistration.Architecture)'=='$(Platform)' AND&#xD;&#xA; $([System.String]::new('%(AppxPackageRegistration.Filename)').StartsWith('Microsoft.UI.Xaml'))" />
Copy link
Member

Choose a reason for hiding this comment

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

VS mangled this :(

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.

86/154! It's easy to review deletions ;)

@DHowett
Copy link
Member

DHowett commented Nov 19, 2024

  • different branding versions of terminal do not interact
  • unpackaged terminal doesn't interact with:
    • any other unpackaged terminal
    • any other packaged terminal

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.

139/154

auto isCurrentlyDark = Theme::IsSystemInDarkTheme();
if (isCurrentlyDark != _currentSystemThemeIsDark)
{
_currentSystemThemeIsDark = isCurrentlyDark;
Copy link
Member

Choose a reason for hiding this comment

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

📝 DH - Guessing this moved to the Message Window; confirm

@microsoft-github-policy-service microsoft-github-policy-service bot added the Area-Windowing Window frame, quake mode, tearout label Nov 19, 2024
@microsoft-github-policy-service microsoft-github-policy-service bot added the Priority-3 A description (P3) label Nov 20, 2024
src/cascadia/TerminalApp/Remoting.idl Show resolved Hide resolved
src/cascadia/TerminalApp/Remoting.idl Show resolved Hide resolved
// On the foreground thread:
co_await wil::resume_foreground(_rootGrid.Dispatcher());
_summonWindowRoutineBody(args);
_summonWindowRoutineBody(std::move(args));
}

// Method Description:
// - As above.
// BODGY: ARM64 BUILD FAILED WITH fatal error C1001: Internal compiler error
Copy link
Member

Choose a reason for hiding this comment

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

now that we got a new compiler, is this still valid

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 not even a coroutine any longer, so we can probably dispense with the separation entirely...

Comment on lines -86 to -87
ScopedResourceLoader loader{ L"TerminalApp/ContextMenu" };
const auto appNameLoc = loader.GetLocalizedString(L"AppName");
Copy link
Member

Choose a reason for hiding this comment

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

📝 DH - Loading up somebody else's resources - probably moved to Emperor

HWND GetMainWindow() const noexcept;
AppHost* GetWindowById(uint64_t id) const noexcept;
AppHost* GetWindowByName(std::wstring_view name) const noexcept;
void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args);
Copy link
Member

Choose a reason for hiding this comment

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

nit: why not const&?

bool _registerHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept;
void _unregisterHotKey(const int index) noexcept;
safe_void_coroutine _setupGlobalHotkeys();
enum class TriBool : uint8_t
Copy link
Member

Choose a reason for hiding this comment

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

HAHAHAHAHAHAHAHAHAHAH

hahahahahaha

hahah

std::optional<bool>?

Copy link
Member Author

Choose a reason for hiding this comment

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

std::optional<bool> is two bools in a trench coat, so... meh.

...but yeah I could do that.

const auto absoluteWindowOrigin = CoreWindow::GetForCurrentThread().Bounds();
// Get the offset (margin + tabs, etc..) of the control within the window
const auto controlOrigin = TransformToVisual(nullptr).TransformPoint({});
const auto inverseScale = 1.0f / static_cast<float>(XamlRoot().RasterizationScale());
Copy link
Member

Choose a reason for hiding this comment

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

these don't seem related to regicide

Copy link
Member Author

Choose a reason for hiding this comment

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

CoreWindow::GetForCurrentThread().Bounds() won't work anymore, so this had to be rewritten. I also chose to use the technically correct way to get the rasterization scale (we do it incorrectly everywhere, according to the docs).

This comment has been minimized.


if (const auto manager = *s_desktopManager.lock_shared())
{
return manager;
Copy link
Member

Choose a reason for hiding this comment

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

but this will still return a dead RPC object if we had one stored, right? there's no way to communicate failure and invalidate it

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 tried it by killing explorer.exe but it still worked for me. Does the client perhaps reconnect automatically?

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.

Classic Leonard code - linear, straightforward, and readable. Another tour-de-force.

const wil::unique_environstrings_ptr strings{ GetEnvironmentStringsW() };
const auto beg = strings.get();
auto end = beg;
wil::unique_mutex mutex{ CreateMutexW(nullptr, TRUE, className) };
Copy link
Member

Choose a reason for hiding this comment

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

fwiw unique_mutex_nothrow et al have .try_create which returns false if it fails and populates an out param boolean

src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
src/cascadia/WindowsTerminal/WindowEmperor.cpp Outdated Show resolved Hide resolved
Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

116/164, but the baby's awake

Exit();
{
MSG msg = {};
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
Copy link
Member

Choose a reason for hiding this comment

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

📝 did this get moved somewhere into like, main()

// us. On Windows 10, that CoreWindow will show up as a visible
// window on the taskbar, unless we hide it manually. So, go get it
// and do the SW_HIDE thing on it.
if (const auto& coreWindow{ winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread() })
Copy link
Member

Choose a reason for hiding this comment

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

I know this sounds dumb - but we tested this on a Win10 VM right?

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 did test it and from what I can tell it seems to work.
Better yet, I just tested #16183 and that's fixed now.

@@ -5,16 +5,18 @@
********************************************************/
#include "pch.h"
#include "NonClientIslandWindow.h"

#include <dwmapi.h>
Copy link
Member

Choose a reason for hiding this comment

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

unrelated?

Copy link
Member Author

Choose a reason for hiding this comment

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

Due to the removal of some other files, this .cpp file now lacks the necessary imports for the DWM API without this.

break;
}
case WM_CONTEXTMENU:
if (const auto menu = CreatePopupMenu())
Copy link
Member

Choose a reason for hiding this comment

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

this is definitely grosser to read now. I kinda liked the idea that the logic for the tray icon was all encapsulated into its own file. Even if it's not a separate class, just anything that's not deep in a switch/case...

Copy link
Member Author

@lhecker lhecker Dec 11, 2024

Choose a reason for hiding this comment

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

We can also encapsulate it into its own function. That seems like a good trade-off to me. It's better than using a separate class IMO and sharing a HWND and any other implicit state across class boundaries.


if (const auto title = logic.Title(); !title.empty())
{
fmt::format_to(std::back_inserter(displayText), L": {}", title);
Copy link
Member

Choose a reason for hiding this comment

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

... like look here - this line starts with 32 columns of whitespace 😨

@@ -496,12 +480,6 @@ namespace winrt::TerminalApp::implementation

void _TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex);

bool _shouldMouseVanish{ false };
Copy link
Member

Choose a reason for hiding this comment

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

📝where'd this go

Copy link
Member

Choose a reason for hiding this comment

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

➡️TermControl.cpp

pointerPosition.Y - windowOrigin.Y - tabPosition.Y,
};
const auto inverseScale = 1.0f / static_cast<float>(eventTab.XamlRoot().RasterizationScale());
POINT cursorPos;
Copy link
Member

Choose a reason for hiding this comment

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

📝 IIRC the old one gets the offest within the tab, right? so that we create the new window s.t. the tab itself doesn't seem to move from where we dropped it.

Now we're stashing the relative position within the old window of the mouse pointer. I'd guess for a tab farther to the right in the row, that causes us to spawn the new window to the left of where we'd expect...

Copy link
Member Author

Choose a reason for hiding this comment

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

From what I can tell, the new logic works correct on my 150% scale display.

src/cascadia/TerminalApp/TerminalWindow.cpp Outdated Show resolved Hide resolved
// - <none>
// Return Value:
// - true iff we should exit the application before even starting the window
bool TerminalWindow::ShouldExitEarly()
Copy link
Member

Choose a reason for hiding this comment

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

📝 check that we still handle this scenario

Copy link
Member Author

Choose a reason for hiding this comment

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

This is handled by the following code in WindowEmperor.cpp:

// If we created no windows, e.g. because the args are "/?" we can just exit now.
_postQuitMessageIfNeeded();

But thank you for bringing this up, because I just noticed that wtd /? does not work.

// Once the loop exits, the app exits, and the message box will be closed.
_messageBoxCount += 1;
const auto decrement = wil::scope_exit([hwnd = _window.get()]() noexcept {
PostMessageW(hwnd, WM_MESSAGE_BOX_CLOSED, 0, 0);
Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW the reason I went with PostMessageW instead of resume_foreground is because I think it's crucial that this does not fail so that the ref-count does not go out of sync. I think PostMessageW is fundamentally more robust in that regard.

Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

161/164, but alas, the burrito screams

sometimes when I get that deep into a PR as big as this, I just assume the last couple files must be good too

}

void WindowEmperor::WaitForWindows()
void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args)
Copy link
Member

Choose a reason for hiding this comment

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

🔖

Comment on lines +366 to +370
{
if (const auto interop = coreWindow.try_as<ICoreWindowInterop>())
{
HWND coreHandle = nullptr;
if (SUCCEEDED(interop->get_WindowHandle(&coreHandle)) && coreHandle)
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 do this before we start spawning windows (and CLI processes?)

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 think we could do this at any point. Do you have something in mind?

Comment on lines +515 to +518
else
{
winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) };
request.WindowName(std::move(windowName));
Copy link
Member

Choose a reason for hiding this comment

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

Entirely sytlistically, it's a little weird that this isn't like window = _mostRecentWindowCurrentDesktop(); break;. It's the only early return in what is otherwise a very methodical function

Copy link
Member Author

Choose a reason for hiding this comment

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

That's unfortunately necessary because _mostRecentWindowCurrentDesktop is a coroutine. We could of course turn this function into a coroutine as well and then do

window = co_await _mostRecentWindowCurrentDesktop();
break;

It's mostly a coincidence that I drew the boundary like that.

}
}
else if (!args.WindowName.empty())
{
Copy link
Member

Choose a reason for hiding this comment

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

📝: you know, the code is cleaner if it's one for loop with the if(args.WindowID) / else if (!args.WindowName.empty()) statement on the inside, but it's less efficient then. 🤷

Copy link
Member Author

Choose a reason for hiding this comment

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

Nah, it'd would not really be a big difference (branch prediction + it usually never loops >10 times anyway). This is also not really intentional, and I'll just move the if condition inside.

Copy link
Member Author

Choose a reason for hiding this comment

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

Eh, you know what, I'll leave this as-is for now, because it makes the window = _mostRecentWindow(); branch at the end easier to express. If anyone ever happens to change this code I would not mind at all.

wchar_t guidStr[39];
guidStr[0] = L'{';
memcpy(&guidStr[1], name.data() + 7, 36 * sizeof(wchar_t));
guidStr[37] = L'}';
Copy link
Member

Choose a reason for hiding this comment

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

of course it can't be, that would be too easy

Copy link
Member Author

Choose a reason for hiding this comment

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

In my defense, when I wrote that GuidFromPlainString didn't exist yet. 😅
It's quite ugly code indeed.

Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

image

template<typename T>
class BaseWindow
{
public:
static constexpr UINT CM_UPDATE_TITLE = WM_USER + 0;
Copy link
Member

Choose a reason for hiding this comment

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

feels weird that this guy isnt with the others in WindowEmperor. ik its lt a message "for the emperor", just threw me

Copy link
Member Author

Choose a reason for hiding this comment

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

WM_USER messages are technically per-window-class from what I know, so I felt like they should be defined in their respective window classes as well.

QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&counter);

const auto period = freq.QuadPart * 4;
Copy link
Member

Choose a reason for hiding this comment

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

oh intriguing, all windows will have the same frame xolor now cool

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, woops, I wanted to back out this change and submit it as a separate PR.
And yeah, that, and fixing floating imprecision once WT has run for a long time, were my intentions with this.

@lhecker
Copy link
Member Author

lhecker commented Dec 12, 2024

Thanks for linking all the issues! I tested them and all but the last one seems fixed.

@lhecker lhecker merged commit 8bbf00e into main Dec 12, 2024
20 checks passed
@lhecker lhecker deleted the dev/lhecker/dethronement branch December 12, 2024 21:21
@microsoft-github-policy-service microsoft-github-policy-service bot added Area-DefApp Area-Remoting Communication layer between windows. Often for windowing behavior, quake mode, etc. Area-UserInterface Issues pertaining to the user interface of the Console or Terminal labels Dec 12, 2024
lhecker added a commit that referenced this pull request Dec 13, 2024
Forgetti di spaghetti in #18215.

Closes #18324

## Validation Steps Performed

* Launch through the start menu
* Explicitly minimize
* Then...
  * Launch through the start menu again ✅
  * Launch via wtd.exe in Win+R ✅
  * Launch via wtd.exe in another Terminal ✅
  * Launch via handoff ✅
lhecker added a commit that referenced this pull request Dec 20, 2024
lhecker added a commit that referenced this pull request Dec 20, 2024
lhecker added a commit that referenced this pull request Dec 20, 2024
DHowett pushed a commit that referenced this pull request Jan 6, 2025
Fixes
* Cursor vanishing, because
  ```cpp
  CoreWindow::GetForCurrentThread().PointerCursor()
  ```
  returned a non-null, but still invisible cursor.
* Alt/F7 not dispatching to newly created windows, because the
  `WM_ACTIVATE` message now arrives before the `AppHost` is initialized.
  This caused the messages to be delivered to old windows.
* Windows Terminal blocking expedited shutdown,
  because we return `FALSE` on non-`ENDSESSION_CLOSEAPP` messages.

Closes #18331
Closes #18335

## Validation Steps Performed
* Cursor still doesn't really vanish for me in the first place ✅
* Alt/F7 work in new windows without triggering old ones ✅
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-DefApp Area-Quality Stability, Performance, Etc. Area-Remoting Communication layer between windows. Often for windowing behavior, quake mode, etc. Area-Settings Issues related to settings and customizability, for console or terminal Area-UserInterface Issues pertaining to the user interface of the Console or Terminal Area-Windowing Window frame, quake mode, tearout Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-2 A description (P2) Priority-3 A description (P3) Product-Terminal The new Windows Terminal. Severity-Crash Crashes are real bad news.
Projects
None yet
3 participants