[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:
parent
fca021deee
commit
e6c333b555
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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())) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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_.
|
||||
|
@ -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
|
||||
|
@ -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 ==========
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -36,6 +36,8 @@ class TaskRunnerWindow {
|
||||
void AddDelegate(Delegate* delegate);
|
||||
void RemoveDelegate(Delegate* delegate);
|
||||
|
||||
void PollOnce(std::chrono::milliseconds timeout);
|
||||
|
||||
~TaskRunnerWindow();
|
||||
|
||||
private:
|
||||
|
Loading…
x
Reference in New Issue
Block a user