Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit de89124

Browse files
authored
[Windows] Allow adding/removing views (#51923)
This enables the Windows embedder to render to multiple views using the new `FlutterEngineAddView` and `FlutterEngineRemoveView` embedder APIs. See: https://flutter.dev/go/multi-view-embedder-apis   Prepares for flutter/flutter#144810 Part of flutter/flutter#142845 ### Sync over async Windows expects synchronous operations: windows are created, resized, and destroyed synchronously. However, Flutter native is asynchronous due to its [threading model](https://github.com/flutter/flutter/wiki/The-Engine-architecture#threading). This change blocks the platform thread when a non-implicit view is added or removed. See: https://flutter.dev/go/multi-view-sync-over-async ### Synchronization The embedder and engine have separate view states that they synchronize asynchronously. The engine can present a view on the raster thread while the embedder is destroying that same view on the platform thread. This change introduces a mutex to protect against this: 1. The platform thread acquires a **shared** lock whenever it needs to access the view. 2. The platform thread acquires an **exclusive** lock to add a view to the embedder, _before_ it notifies the engine of the view 3. The platform thread acquires an **exclusive** lock to remove a view from the embedder, *after* the engine has acknowledged the view's removal but *before* the embedder destroys the view. 4. The raster thread acquires a **shared** lock to present to a view. This lock is held for the entirety of the present operation, thereby blocking the platform thread from destroying the view. The implicit view is an important corner case. The framework/engine believe the implicit view **always** exists, even if the app is in headless mode. The embedder does not notify the engine when it destroys the implicit view. In other words, the embedder must safely ignore presents to the implicit view when it does not exist. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 98a8ad1 commit de89124

File tree

9 files changed

+275
-25
lines changed

9 files changed

+275
-25
lines changed

shell/platform/embedder/embedder.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3375,6 +3375,8 @@ FlutterEngineResult FlutterEngineGetProcAddresses(
33753375
SET_PROC(NotifyDisplayUpdate, FlutterEngineNotifyDisplayUpdate);
33763376
SET_PROC(ScheduleFrame, FlutterEngineScheduleFrame);
33773377
SET_PROC(SetNextFrameCallback, FlutterEngineSetNextFrameCallback);
3378+
SET_PROC(AddView, FlutterEngineAddView);
3379+
SET_PROC(RemoveView, FlutterEngineRemoveView);
33783380
#undef SET_PROC
33793381

33803382
return kSuccess;

shell/platform/embedder/embedder.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3287,6 +3287,12 @@ typedef FlutterEngineResult (*FlutterEngineSetNextFrameCallbackFnPtr)(
32873287
FLUTTER_API_SYMBOL(FlutterEngine) engine,
32883288
VoidCallback callback,
32893289
void* user_data);
3290+
typedef FlutterEngineResult (*FlutterEngineAddViewFnPtr)(
3291+
FLUTTER_API_SYMBOL(FlutterEngine) engine,
3292+
const FlutterAddViewInfo* info);
3293+
typedef FlutterEngineResult (*FlutterEngineRemoveViewFnPtr)(
3294+
FLUTTER_API_SYMBOL(FlutterEngine) engine,
3295+
const FlutterRemoveViewInfo* info);
32903296

32913297
/// Function-pointer-based versions of the APIs above.
32923298
typedef struct {
@@ -3333,6 +3339,8 @@ typedef struct {
33333339
FlutterEngineNotifyDisplayUpdateFnPtr NotifyDisplayUpdate;
33343340
FlutterEngineScheduleFrameFnPtr ScheduleFrame;
33353341
FlutterEngineSetNextFrameCallbackFnPtr SetNextFrameCallback;
3342+
FlutterEngineAddViewFnPtr AddView;
3343+
FlutterEngineRemoveViewFnPtr RemoveView;
33363344
} FlutterEngineProcTable;
33373345

33383346
//------------------------------------------------------------------------------

shell/platform/windows/fixtures/main.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,12 @@ void signalViewIds() {
361361

362362
signalStringValue('View IDs: [${viewIds.join(', ')}]');
363363
}
364+
365+
@pragma('vm:entry-point')
366+
void onMetricsChangedSignalViewIds() {
367+
ui.PlatformDispatcher.instance.onMetricsChanged = () {
368+
signalViewIds();
369+
};
370+
371+
signal();
372+
}

shell/platform/windows/flutter_windows.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ static FlutterDesktopViewControllerRef CreateViewController(
9393

9494
std::unique_ptr<flutter::FlutterWindowsView> view =
9595
engine_ptr->CreateView(std::move(window_wrapper));
96+
if (!view) {
97+
return nullptr;
98+
}
99+
96100
auto controller = std::make_unique<flutter::FlutterWindowsViewController>(
97101
std::move(engine), std::move(view));
98102

shell/platform/windows/flutter_windows_engine.cc

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "flutter/fml/logging.h"
1313
#include "flutter/fml/paths.h"
1414
#include "flutter/fml/platform/win/wstring_conversion.h"
15+
#include "flutter/fml/synchronization/waitable_event.h"
1516
#include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h"
1617
#include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h"
1718
#include "flutter/shell/platform/common/path_utils.h"
@@ -149,6 +150,7 @@ FlutterWindowsEngine::FlutterWindowsEngine(
149150
: project_(std::make_unique<FlutterProjectBundle>(project)),
150151
windows_proc_table_(std::move(windows_proc_table)),
151152
aot_data_(nullptr, nullptr),
153+
views_mutex_(fml::SharedMutex::Create()),
152154
lifecycle_manager_(std::make_unique<WindowsLifecycleManager>(this)) {
153155
if (windows_proc_table_ == nullptr) {
154156
windows_proc_table_ = std::make_shared<WindowsProcTable>();
@@ -492,34 +494,118 @@ bool FlutterWindowsEngine::Stop() {
492494

493495
std::unique_ptr<FlutterWindowsView> FlutterWindowsEngine::CreateView(
494496
std::unique_ptr<WindowBindingHandler> window) {
495-
// TODO(loicsharma): Remove implicit view assumption.
496-
// https://github.com/flutter/flutter/issues/142845
497+
auto view_id = next_view_id_;
497498
auto view = std::make_unique<FlutterWindowsView>(
498-
kImplicitViewId, this, std::move(window), windows_proc_table_);
499+
view_id, this, std::move(window), windows_proc_table_);
499500

500501
view->CreateRenderSurface();
501502

502-
views_[kImplicitViewId] = view.get();
503+
next_view_id_++;
504+
505+
{
506+
// Add the view to the embedder. This must happen before the engine
507+
// is notified the view exists and starts presenting to it.
508+
fml::UniqueLock write_lock{*views_mutex_};
509+
FML_DCHECK(views_.find(view_id) == views_.end());
510+
views_[view_id] = view.get();
511+
}
512+
513+
if (!view->IsImplicitView()) {
514+
FML_DCHECK(running());
515+
516+
struct Captures {
517+
fml::AutoResetWaitableEvent latch;
518+
bool added;
519+
};
520+
Captures captures = {};
521+
522+
FlutterWindowMetricsEvent metrics = view->CreateWindowMetricsEvent();
523+
524+
FlutterAddViewInfo info = {};
525+
info.struct_size = sizeof(FlutterAddViewInfo);
526+
info.view_id = view_id;
527+
info.view_metrics = &metrics;
528+
info.user_data = &captures;
529+
info.add_view_callback = [](const FlutterAddViewResult* result) {
530+
Captures* captures = reinterpret_cast<Captures*>(result->user_data);
531+
captures->added = result->added;
532+
captures->latch.Signal();
533+
};
534+
535+
embedder_api_.AddView(engine_, &info);
536+
537+
// Block the platform thread until the engine has added the view.
538+
// TODO(loicsharma): This blocks the platform thread eagerly and can
539+
// cause unnecessary delay in input processing. Instead, this should block
540+
// lazily only when the app does an operation which needs the view.
541+
// https://github.com/flutter/flutter/issues/146248
542+
captures.latch.Wait();
543+
544+
if (!captures.added) {
545+
// Adding the view failed. Update the embedder's state to match the
546+
// engine's state. This is unexpected and indicates a bug in the Windows
547+
// embedder.
548+
FML_LOG(ERROR) << "FlutterEngineAddView failed to add view";
549+
fml::UniqueLock write_lock{*views_mutex_};
550+
views_.erase(view_id);
551+
return nullptr;
552+
}
553+
}
503554

504555
return std::move(view);
505556
}
506557

507558
void FlutterWindowsEngine::RemoveView(FlutterViewId view_id) {
508559
FML_DCHECK(running());
509-
FML_DCHECK(views_.find(view_id) != views_.end());
510560

511-
if (view_id == kImplicitViewId) {
512-
// The engine and framework assume the implicit view always exists.
513-
// Attempts to render to the implicit view will be ignored.
514-
views_.erase(view_id);
515-
return;
561+
// Notify the engine to stop rendering to the view if it isn't the implicit
562+
// view. The engine and framework assume the implicit view always exists and
563+
// can continue presenting.
564+
if (view_id != kImplicitViewId) {
565+
struct Captures {
566+
fml::AutoResetWaitableEvent latch;
567+
bool removed;
568+
};
569+
Captures captures = {};
570+
571+
FlutterRemoveViewInfo info = {};
572+
info.struct_size = sizeof(FlutterRemoveViewInfo);
573+
info.view_id = view_id;
574+
info.user_data = &captures;
575+
info.remove_view_callback = [](const FlutterRemoveViewResult* result) {
576+
// This is invoked on the raster thread, the same thread that the present
577+
// callback is invoked. If |FlutterRemoveViewResult.removed| is `true`,
578+
// the engine guarantees the view won't be presented.
579+
Captures* captures = reinterpret_cast<Captures*>(result->user_data);
580+
captures->removed = result->removed;
581+
captures->latch.Signal();
582+
};
583+
584+
embedder_api_.RemoveView(engine_, &info);
585+
586+
// Block the platform thread until the engine has removed the view.
587+
// TODO(loicsharma): This blocks the platform thread eagerly and can
588+
// cause unnecessary delay in input processing. Instead, this should block
589+
// lazily only when an operation needs the view.
590+
// https://github.com/flutter/flutter/issues/146248
591+
captures.latch.Wait();
592+
593+
if (!captures.removed) {
594+
// Removing the view failed. This is unexpected and indicates a bug in the
595+
// Windows embedder.
596+
FML_LOG(ERROR) << "FlutterEngineRemoveView failed to remove view";
597+
return;
598+
}
516599
}
517600

518-
// TODO(loicsharma): Remove the view from the engine using the
519-
// `FlutterEngineRemoveView` embedder API. Windows does not
520-
// support views other than the implicit view yet.
521-
// https://github.com/flutter/flutter/issues/144810
522-
FML_UNREACHABLE();
601+
{
602+
// The engine no longer presents to the view. Remove the view from the
603+
// embedder.
604+
fml::UniqueLock write_lock{*views_mutex_};
605+
606+
FML_DCHECK(views_.find(view_id) != views_.end());
607+
views_.erase(view_id);
608+
}
523609
}
524610

525611
void FlutterWindowsEngine::OnVsync(intptr_t baton) {
@@ -551,6 +637,8 @@ std::chrono::nanoseconds FlutterWindowsEngine::FrameInterval() {
551637
}
552638

553639
FlutterWindowsView* FlutterWindowsEngine::view(FlutterViewId view_id) const {
640+
fml::SharedLock read_lock{*views_mutex_};
641+
554642
auto iterator = views_.find(view_id);
555643
if (iterator == views_.end()) {
556644
return nullptr;
@@ -779,6 +867,8 @@ bool FlutterWindowsEngine::DispatchSemanticsAction(
779867

780868
void FlutterWindowsEngine::UpdateSemanticsEnabled(bool enabled) {
781869
if (engine_ && semantics_enabled_ != enabled) {
870+
fml::SharedLock read_lock{*views_mutex_};
871+
782872
semantics_enabled_ = enabled;
783873
embedder_api_.UpdateSemanticsEnabled(engine_, enabled);
784874
for (auto iterator = views_.begin(); iterator != views_.end(); iterator++) {
@@ -844,6 +934,8 @@ void FlutterWindowsEngine::OnQuit(std::optional<HWND> hwnd,
844934
}
845935

846936
void FlutterWindowsEngine::OnDwmCompositionChanged() {
937+
fml::SharedLock read_lock{*views_mutex_};
938+
847939
for (auto iterator = views_.begin(); iterator != views_.end(); iterator++) {
848940
iterator->second->OnDwmCompositionChanged();
849941
}
@@ -875,6 +967,11 @@ void FlutterWindowsEngine::OnChannelUpdate(std::string name, bool listening) {
875967
}
876968

877969
bool FlutterWindowsEngine::Present(const FlutterPresentViewInfo* info) {
970+
// This runs on the raster thread. Lock the views map for the entirety of the
971+
// present operation to block the platform thread from destroying the
972+
// view during the present.
973+
fml::SharedLock read_lock{*views_mutex_};
974+
878975
auto iterator = views_.find(info->view_id);
879976
if (iterator == views_.end()) {
880977
return false;

shell/platform/windows/flutter_windows_engine.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "flutter/fml/closure.h"
1818
#include "flutter/fml/macros.h"
19+
#include "flutter/fml/synchronization/shared_mutex.h"
1920
#include "flutter/shell/platform/common/accessibility_bridge.h"
2021
#include "flutter/shell/platform/common/app_lifecycle_state.h"
2122
#include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h"
@@ -121,6 +122,8 @@ class FlutterWindowsEngine {
121122
virtual bool Stop();
122123

123124
// Create a view that can display this engine's content.
125+
//
126+
// Returns null on failure.
124127
std::unique_ptr<FlutterWindowsView> CreateView(
125128
std::unique_ptr<WindowBindingHandler> window);
126129

@@ -359,9 +362,30 @@ class FlutterWindowsEngine {
359362
// AOT data, if any.
360363
UniqueAotDataPtr aot_data_;
361364

365+
// The ID that the next view will have.
366+
FlutterViewId next_view_id_ = kImplicitViewId;
367+
362368
// The views displaying the content running in this engine, if any.
369+
//
370+
// This is read and mutated by the platform thread. This is read by the raster
371+
// thread to present content to a view.
372+
//
373+
// Reads to this object on non-platform threads must be protected
374+
// by acquiring a shared lock on |views_mutex_|.
375+
//
376+
// Writes to this object must only happen on the platform thread
377+
// and must be protected by acquiring an exclusive lock on |views_mutex_|.
363378
std::unordered_map<FlutterViewId, FlutterWindowsView*> views_;
364379

380+
// The mutex that protects the |views_| map.
381+
//
382+
// The raster thread acquires a shared lock to present to a view.
383+
//
384+
// The platform thread acquires a shared lock to access the view.
385+
// The platform thread acquires an exclusive lock before adding
386+
// a view to the engine or after removing a view from the engine.
387+
std::unique_ptr<fml::SharedMutex> views_mutex_;
388+
365389
// Task runner for tasks posted from the engine.
366390
std::unique_ptr<TaskRunner> task_runner_;
367391

shell/platform/windows/flutter_windows_unittests.cc

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,5 +570,65 @@ TEST_F(WindowsTest, GetKeyboardStateHeadless) {
570570
}
571571
}
572572

573+
// Verify the embedder can add and remove views.
574+
TEST_F(WindowsTest, AddRemoveView) {
575+
std::mutex mutex;
576+
std::string view_ids;
577+
578+
auto& context = GetContext();
579+
WindowsConfigBuilder builder(context);
580+
builder.SetDartEntrypoint("onMetricsChangedSignalViewIds");
581+
582+
fml::AutoResetWaitableEvent ready_latch;
583+
context.AddNativeFunction(
584+
"Signal", CREATE_NATIVE_ENTRY(
585+
[&](Dart_NativeArguments args) { ready_latch.Signal(); }));
586+
587+
context.AddNativeFunction(
588+
"SignalStringValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
589+
auto handle = Dart_GetNativeArgument(args, 0);
590+
ASSERT_FALSE(Dart_IsError(handle));
591+
592+
std::scoped_lock lock{mutex};
593+
view_ids = tonic::DartConverter<std::string>::FromDart(handle);
594+
}));
595+
596+
// Create the implicit view.
597+
ViewControllerPtr first_controller{builder.Run()};
598+
ASSERT_NE(first_controller, nullptr);
599+
600+
ready_latch.Wait();
601+
602+
// Create a second view.
603+
FlutterDesktopEngineRef engine =
604+
FlutterDesktopViewControllerGetEngine(first_controller.get());
605+
FlutterDesktopViewControllerProperties properties = {};
606+
properties.width = 100;
607+
properties.height = 100;
608+
ViewControllerPtr second_controller{
609+
FlutterDesktopEngineCreateViewController(engine, &properties)};
610+
ASSERT_NE(second_controller, nullptr);
611+
612+
// Pump messages for the Windows platform task runner until the view is added.
613+
while (true) {
614+
PumpMessage();
615+
std::scoped_lock lock{mutex};
616+
if (view_ids == "View IDs: [0, 1]") {
617+
break;
618+
}
619+
}
620+
621+
// Delete the second view and pump messages for the Windows platform task
622+
// runner until the view is removed.
623+
second_controller.reset();
624+
while (true) {
625+
PumpMessage();
626+
std::scoped_lock lock{mutex};
627+
if (view_ids == "View IDs: [0]") {
628+
break;
629+
}
630+
}
631+
}
632+
573633
} // namespace testing
574634
} // namespace flutter

0 commit comments

Comments
 (0)