[windows] Implement merged UI and platform thread (#162935)

Original issue: https://github.com/flutter/flutter/issues/150525

Thread merging is currently disabled by default. 
It is controlled from the application through `DartProject`:
```cpp
flutter::DartProject project(L"data");
// Enables running UI isolate on platform thread
project.set_merged_platform_ui_thread(true);
```
Required changes to windows embedder: 
- Resize synchronization no longer blocks on condition variable, instead
it polls the Flutter run loop (ignoring other windows messages) until
the frame is available. This way resize synchronization works with both
thread merging enabled and disabled.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
This commit is contained in:
Matej Knopp 2025-02-20 11:33:13 +01:00 committed by GitHub
parent fca021deee
commit e6c333b555
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 115 additions and 56 deletions

View File

@ -19,6 +19,8 @@ FlutterEngine::FlutterEngine(const DartProject& project) {
c_engine_properties.dart_entrypoint = project.dart_entrypoint().c_str();
c_engine_properties.gpu_preference =
static_cast<FlutterDesktopGpuPreference>(project.gpu_preference());
c_engine_properties.merged_platform_ui_thread =
project.merged_platform_ui_thread();
const std::vector<std::string>& entrypoint_args =
project.dart_entrypoint_arguments();

View File

@ -90,6 +90,18 @@ class DartProject {
// Defaults to NoPreference.
GpuPreference gpu_preference() const { return gpu_preference_; }
// Sets whether the UI isolate should run on the platform thread.
// In a future release, this setting will become a no-op when
// Flutter Windows requires merged platform and UI threads.
void set_merged_platform_ui_thread(bool merged_platform_ui_thread) {
merged_platform_ui_thread_ = merged_platform_ui_thread;
}
// Returns whether the UI isolate should run on the platform thread.
// Defaults to false. In a future release, this setting will default
// to true.
bool merged_platform_ui_thread() const { return merged_platform_ui_thread_; }
private:
// Accessors for internals are private, so that they can be changed if more
// flexible options for project structures are needed later without it
@ -116,6 +128,8 @@ class DartProject {
std::vector<std::string> dart_entrypoint_arguments_;
// The preference for GPU to be used by flutter engine.
GpuPreference gpu_preference_ = GpuPreference::NoPreference;
// Whether the UI isolate should run on the platform thread.
bool merged_platform_ui_thread_ = false;
};
} // namespace flutter

View File

@ -32,6 +32,8 @@ FlutterProjectBundle::FlutterProjectBundle(
gpu_preference_ =
static_cast<FlutterGpuPreference>(properties.gpu_preference);
merged_platform_ui_thread_ = properties.merged_platform_ui_thread;
// Resolve any relative paths.
if (assets_path_.is_relative() || icu_path_.is_relative() ||
(!aot_library_path_.empty() && aot_library_path_.is_relative())) {

View File

@ -67,6 +67,9 @@ class FlutterProjectBundle {
// Returns the app's GPU preference.
FlutterGpuPreference gpu_preference() const { return gpu_preference_; }
// Whether the UI isolate should be running on the platform thread.
bool merged_platform_ui_thread() const { return merged_platform_ui_thread_; }
private:
std::filesystem::path assets_path_;
std::filesystem::path icu_path_;
@ -85,6 +88,9 @@ class FlutterProjectBundle {
// App's GPU preference.
FlutterGpuPreference gpu_preference_;
// Whether the UI isolate should be running on the platform thread.
bool merged_platform_ui_thread_;
};
} // namespace flutter

View File

@ -294,6 +294,12 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
custom_task_runners.thread_priority_setter =
&WindowsPlatformThreadPrioritySetter;
if (project_->merged_platform_ui_thread()) {
FML_LOG(WARNING)
<< "Running with merged platform and UI thread. Experimental.";
custom_task_runners.ui_task_runner = &platform_task_runner;
}
FlutterProjectArgs args = {};
args.struct_size = sizeof(FlutterProjectArgs);
args.shutdown_dart_vm_when_done = true;

View File

@ -18,7 +18,7 @@
namespace flutter {
namespace {
// The maximum duration to block the platform thread for while waiting
// The maximum duration to block the Windows event loop while waiting
// for a window resize operation to complete.
constexpr std::chrono::milliseconds kWindowResizeTimeout{100};
@ -191,10 +191,8 @@ void FlutterWindowsView::ForceRedraw() {
}
}
// Called on the platform thread.
bool FlutterWindowsView::OnWindowSizeChanged(size_t width, size_t height) {
// Called on the platform thread.
std::unique_lock<std::mutex> lock(resize_mutex_);
if (!engine_->egl_manager()) {
SendWindowMetrics(width, height, binding_handler_->GetDpiScale());
return true;
@ -214,19 +212,30 @@ bool FlutterWindowsView::OnWindowSizeChanged(size_t width, size_t height) {
return true;
}
resize_status_ = ResizeState::kResizeStarted;
resize_target_width_ = width;
resize_target_height_ = height;
{
std::unique_lock<std::mutex> lock(resize_mutex_);
resize_status_ = ResizeState::kResizeStarted;
resize_target_width_ = width;
resize_target_height_ = height;
}
SendWindowMetrics(width, height, binding_handler_->GetDpiScale());
// Block the platform thread until a frame is presented with the target
// size. See |OnFrameGenerated|, |OnEmptyFrameGenerated|, and
// |OnFramePresented|.
return resize_cv_.wait_for(lock, kWindowResizeTimeout,
[&resize_status = resize_status_] {
return resize_status == ResizeState::kDone;
});
std::chrono::time_point<std::chrono::steady_clock> start_time =
std::chrono::steady_clock::now();
while (true) {
if (std::chrono::steady_clock::now() > start_time + kWindowResizeTimeout) {
return false;
}
std::unique_lock<std::mutex> lock(resize_mutex_);
if (resize_status_ == ResizeState::kDone) {
break;
}
lock.unlock();
engine_->task_runner()->PollOnce(kWindowResizeTimeout);
}
return true;
}
void FlutterWindowsView::OnWindowRepaint() {
@ -664,10 +673,11 @@ void FlutterWindowsView::OnFramePresented() {
return;
case ResizeState::kFrameGenerated: {
// A frame was generated for a pending resize.
// Unblock the platform thread.
resize_status_ = ResizeState::kDone;
// Unblock the platform thread.
engine_->task_runner()->PostTask([this] {});
lock.unlock();
resize_cv_.notify_all();
// Blocking the raster thread until DWM flushes alleviates glitches where
// previous size surface is stretched over current size view.

View File

@ -429,10 +429,8 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate {
// Currently configured WindowBindingHandler for view.
std::unique_ptr<WindowBindingHandler> binding_handler_;
// Resize events are synchronized using this mutex and the corresponding
// condition variable.
// Protects resize_status_, resize_target_width_ and resize_target_height_.
std::mutex resize_mutex_;
std::condition_variable resize_cv_;
// Indicates the state of a window resize event. Platform thread will be
// blocked while this is not done. Guarded by resize_mutex_.

View File

@ -925,21 +925,18 @@ TEST(FlutterWindowsViewTest, WindowResizeTests) {
return kSuccess;
}));
fml::AutoResetWaitableEvent resized_latch;
std::thread([&resized_latch, &view]() {
// Start the window resize. This sends the new window metrics
// and then blocks until another thread completes the window resize.
EXPECT_TRUE(view->OnWindowSizeChanged(500, 500));
resized_latch.Signal();
// Simulate raster thread.
std::thread([&metrics_sent_latch, &view]() {
metrics_sent_latch.Wait();
// Frame generated and presented from the raster thread.
EXPECT_TRUE(view->OnFrameGenerated(500, 500));
view->OnFramePresented();
}).detach();
// Wait until the platform thread has started the window resize.
metrics_sent_latch.Wait();
// Complete the window resize by reporting a frame with the new window size.
ASSERT_TRUE(view->OnFrameGenerated(500, 500));
view->OnFramePresented();
resized_latch.Wait();
// Start the window resize. This sends the new window metrics
// and then blocks polling run loop until another thread completes the window
// resize.
EXPECT_TRUE(view->OnWindowSizeChanged(500, 500));
}
// Verify that an empty frame completes a view resize.
@ -989,21 +986,18 @@ TEST(FlutterWindowsViewTest, TestEmptyFrameResizes) {
engine_modifier.SetEGLManager(std::move(egl_manager));
view_modifier.SetSurface(std::move(surface));
fml::AutoResetWaitableEvent resized_latch;
std::thread([&resized_latch, &view]() {
// Start the window resize. This sends the new window metrics
// and then blocks until another thread completes the window resize.
EXPECT_TRUE(view->OnWindowSizeChanged(500, 500));
resized_latch.Signal();
// Simulate raster thread.
std::thread([&metrics_sent_latch, &view]() {
metrics_sent_latch.Wait();
// Empty frame generated and presented from the raster thread.
EXPECT_TRUE(view->OnEmptyFrameGenerated());
view->OnFramePresented();
}).detach();
// Wait until the platform thread has started the window resize.
metrics_sent_latch.Wait();
// Complete the window resize by reporting an empty frame.
view->OnEmptyFrameGenerated();
view->OnFramePresented();
resized_latch.Wait();
// Start the window resize. This sends the new window metrics
// and then blocks until another thread completes the window resize.
EXPECT_TRUE(view->OnWindowSizeChanged(500, 500));
}
// A window resize can be interleaved between a frame generation and
@ -1038,15 +1032,7 @@ TEST(FlutterWindowsViewTest, WindowResizeRace) {
// Inject a window resize between the frame generation and
// frame presentation. The new size invalidates the current frame.
fml::AutoResetWaitableEvent resized_latch;
std::thread([&resized_latch, &view]() {
// The resize is never completed. The view times out and returns false.
EXPECT_FALSE(view->OnWindowSizeChanged(500, 500));
resized_latch.Signal();
}).detach();
// Wait until the platform thread has started the window resize.
resized_latch.Wait();
EXPECT_FALSE(view->OnWindowSizeChanged(500, 500));
// Complete the invalidated frame while a resize is pending. Although this
// might mean that we presented a frame with the wrong size, this should not

View File

@ -80,6 +80,9 @@ typedef struct {
// GPU choice preference
FlutterDesktopGpuPreference gpu_preference;
// Whether the UI isolate should run on the platform thread.
bool merged_platform_ui_thread;
} FlutterDesktopEngineProperties;
// ========== View Controller ==========

View File

@ -92,6 +92,10 @@ void TaskRunner::PostTask(TaskClosure closure) {
EnqueueTask(std::move(task));
}
void TaskRunner::PollOnce(std::chrono::milliseconds timeout) {
task_runner_window_->PollOnce(timeout);
}
void TaskRunner::EnqueueTask(Task task) {
static std::atomic_uint64_t sGlobalTaskOrder(0);

View File

@ -46,6 +46,10 @@ class TaskRunner : public TaskRunnerWindow::Delegate {
// Post a task to the event loop.
void PostTask(TaskClosure task);
// Polls the event loop once. This will only process tasks scheduled through
// the task runner. It will not process messages sent to other windows.
void PollOnce(std::chrono::milliseconds timeout);
// Post a task to the event loop or run it immediately if this is being called
// from the runner's thread.
void RunNowOrPostTask(TaskClosure task) {

View File

@ -10,6 +10,11 @@
namespace flutter {
static const uintptr_t kTimerId = 0;
// Timer used for PollOnce timeout.
static const uintptr_t kPollTimeoutTimerId = 1;
TaskRunnerWindow::TaskRunnerWindow() {
WNDCLASS window_class = RegisterWindowClass();
window_handle_ =
@ -69,6 +74,16 @@ void TaskRunnerWindow::RemoveDelegate(Delegate* delegate) {
}
}
void TaskRunnerWindow::PollOnce(std::chrono::milliseconds timeout) {
MSG msg;
::SetTimer(window_handle_, kPollTimeoutTimerId, timeout.count(), nullptr);
if (GetMessage(&msg, window_handle_, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
::KillTimer(window_handle_, kPollTimeoutTimerId);
}
void TaskRunnerWindow::ProcessTasks() {
auto next = std::chrono::nanoseconds::max();
auto delegates_copy(delegates_);
@ -84,10 +99,10 @@ void TaskRunnerWindow::ProcessTasks() {
void TaskRunnerWindow::SetTimer(std::chrono::nanoseconds when) {
if (when == std::chrono::nanoseconds::max()) {
KillTimer(window_handle_, 0);
KillTimer(window_handle_, kTimerId);
} else {
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(when);
::SetTimer(window_handle_, 0, millis.count() + 1, nullptr);
::SetTimer(window_handle_, kTimerId, millis.count() + 1, nullptr);
}
}
@ -115,6 +130,13 @@ TaskRunnerWindow::HandleMessage(UINT const message,
LPARAM const lparam) noexcept {
switch (message) {
case WM_TIMER:
if (wparam == kPollTimeoutTimerId) {
// Ignore PollOnce timeout timer.
return 0;
}
FML_DCHECK(wparam == kTimerId);
ProcessTasks();
return 0;
case WM_NULL:
ProcessTasks();
return 0;

View File

@ -36,6 +36,8 @@ class TaskRunnerWindow {
void AddDelegate(Delegate* delegate);
void RemoveDelegate(Delegate* delegate);
void PollOnce(std::chrono::milliseconds timeout);
~TaskRunnerWindow();
private: