diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index b336b3dde6c83..062d31c88b85c 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -179,6 +179,10 @@ void RuntimeController::AddView(int64_t view_id, platform_data_.viewport_metrics_for_views[view_id] = view_metrics; bool added = platform_configuration->AddView(view_id, view_metrics); + if (added) { + ScheduleFrame(); + } + callback(added); } diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index 8bb01efecabc5..46077b5a847d2 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -193,6 +193,7 @@ class RuntimeController : public PlatformConfigurationClient { /// flushed to the isolate when it starts. Calling `RemoveView` /// before the isolate is launched cancels the add operation. /// + /// If the isolate is running, a frame will be scheduled. /// /// @param[in] view_id The ID of the new view. /// @param[in] viewport_metrics The initial viewport metrics for the view. diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 52e49646c009a..9666906c5ba7b 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -2183,6 +2183,66 @@ FlutterEngineResult FlutterEngineRunInitialized( return kSuccess; } +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineAddView(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterAddViewInfo* info) { + if (!engine) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Engine handle was invalid."); + } + if (!info || !info->view_metrics || !info->add_view_callback) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "Add view info handle was invalid."); + } + + FlutterViewId view_id = info->view_id; + if (view_id == kFlutterImplicitViewId) { + return LOG_EMBEDDER_ERROR( + kInvalidArguments, + "Add view info was invalid. The implicit view cannot be added."); + } + if (SAFE_ACCESS(info->view_metrics, view_id, kFlutterImplicitViewId) != + view_id) { + if (view_id == kFlutterImplicitViewId) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "Add view info was invalid. The info and " + "window metric view IDs must match."); + } + } + + // TODO(loicsharma): Return an error if the engine was initialized with + // callbacks that are incompatible with multiple views. + // https://github.com/flutter/flutter/issues/144806 + + std::variant metrics_or_error = + MakeViewportMetricsFromWindowMetrics(info->view_metrics); + + if (const std::string* error = std::get_if(&metrics_or_error)) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, error->c_str()); + } + + auto metrics = std::get(metrics_or_error); + + // The engine must be running to add a view. + auto embedder_engine = reinterpret_cast(engine); + if (!embedder_engine->IsValid()) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Engine handle was invalid."); + } + + flutter::Shell::AddViewCallback callback = + [c_callback = info->add_view_callback, + user_data = info->user_data](bool added) { + FlutterAddViewResult result = {}; + result.struct_size = sizeof(FlutterAddViewResult); + result.added = added; + result.user_data = user_data; + c_callback(&result); + }; + + embedder_engine->GetShell().AddView(view_id, metrics, callback); + return kSuccess; +} + FLUTTER_EXPORT FlutterEngineResult FlutterEngineRemoveView(FLUTTER_API_SYMBOL(FlutterEngine) engine, @@ -2201,6 +2261,10 @@ FlutterEngineResult FlutterEngineRemoveView(FLUTTER_API_SYMBOL(FlutterEngine) "Remove view info was invalid. The implicit view cannot be removed."); } + // TODO(loicsharma): Return an error if the engine was initialized with + // callbacks that are incompatible with multiple views. + // https://github.com/flutter/flutter/issues/144806 + // The engine must be running to remove a view. auto embedder_engine = reinterpret_cast(engine); if (!embedder_engine->IsValid()) { diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index d26e2215adf3a..86a3a22ee0ad7 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -831,6 +831,86 @@ typedef struct { }; } FlutterRendererConfig; +/// Display refers to a graphics hardware system consisting of a framebuffer, +/// typically a monitor or a screen. This ID is unique per display and is +/// stable until the Flutter application restarts. +typedef uint64_t FlutterEngineDisplayId; + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterWindowMetricsEvent). + size_t struct_size; + /// Physical width of the window. + size_t width; + /// Physical height of the window. + size_t height; + /// Scale factor for the physical screen. + double pixel_ratio; + /// Horizontal physical location of the left side of the window on the screen. + size_t left; + /// Vertical physical location of the top of the window on the screen. + size_t top; + /// Top inset of window. + double physical_view_inset_top; + /// Right inset of window. + double physical_view_inset_right; + /// Bottom inset of window. + double physical_view_inset_bottom; + /// Left inset of window. + double physical_view_inset_left; + /// The identifier of the display the view is rendering on. + FlutterEngineDisplayId display_id; + /// The view that this event is describing. + int64_t view_id; +} FlutterWindowMetricsEvent; + +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterAddViewResult). + size_t struct_size; + + /// True if the add view operation succeeded. + bool added; + + /// The |FlutterAddViewInfo.user_data|. + void* user_data; +} FlutterAddViewResult; + +/// The callback invoked by the engine when the engine has attempted to add a +/// view. +/// +/// The |FlutterAddViewResult| is only guaranteed to be valid during this +/// callback. +typedef void (*FlutterAddViewCallback)(const FlutterAddViewResult* result); + +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterAddViewInfo). + size_t struct_size; + + /// The identifier for the view to add. This must be unique. + FlutterViewId view_id; + + /// The view's properties. + /// + /// The metric's |view_id| must match this struct's |view_id|. + const FlutterWindowMetricsEvent* view_metrics; + + /// A baton that is not interpreted by the engine in any way. It will be given + /// back to the embedder in |add_view_callback|. Embedder resources may be + /// associated with this baton. + void* user_data; + + /// Called once the engine has attempted to add the view. This callback is + /// required. + /// + /// The embedder/app must not use the view until the callback is invoked with + /// an `added` value of `true`. + /// + /// This callback is invoked on an internal engine managed thread. Embedders + /// must re-thread if necessary. + FlutterAddViewCallback add_view_callback; +} FlutterAddViewInfo; + typedef struct { /// The size of this struct. /// Must be sizeof(FlutterRemoveViewResult). @@ -846,7 +926,8 @@ typedef struct { /// The callback invoked by the engine when the engine has attempted to remove /// a view. /// -/// The |FlutterRemoveViewResult| will be deallocated once the callback returns. +/// The |FlutterRemoveViewResult| is only guaranteed to be valid during this +/// callback. typedef void (*FlutterRemoveViewCallback)( const FlutterRemoveViewResult* /* result */); @@ -878,38 +959,6 @@ typedef struct { FlutterRemoveViewCallback remove_view_callback; } FlutterRemoveViewInfo; -/// Display refers to a graphics hardware system consisting of a framebuffer, -/// typically a monitor or a screen. This ID is unique per display and is -/// stable until the Flutter application restarts. -typedef uint64_t FlutterEngineDisplayId; - -typedef struct { - /// The size of this struct. Must be sizeof(FlutterWindowMetricsEvent). - size_t struct_size; - /// Physical width of the window. - size_t width; - /// Physical height of the window. - size_t height; - /// Scale factor for the physical screen. - double pixel_ratio; - /// Horizontal physical location of the left side of the window on the screen. - size_t left; - /// Vertical physical location of the top of the window on the screen. - size_t top; - /// Top inset of window. - double physical_view_inset_top; - /// Right inset of window. - double physical_view_inset_right; - /// Bottom inset of window. - double physical_view_inset_bottom; - /// Left inset of window. - double physical_view_inset_left; - /// The identifier of the display the view is rendering on. - FlutterEngineDisplayId display_id; - /// The view that this event is describing. - int64_t view_id; -} FlutterWindowMetricsEvent; - /// The phase of the pointer event. typedef enum { kCancel, @@ -1851,7 +1900,9 @@ typedef struct { /// Callback invoked by the engine to composite the contents of each layer /// onto the implicit view. /// - /// DEPRECATED: Use |present_view_callback| to support multiple views. + /// DEPRECATED: Use `present_view_callback` to support multiple views. + /// If this callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. /// /// Only one of `present_layers_callback` and `present_view_callback` may be /// provided. Providing both is an error and engine initialization will @@ -2175,6 +2226,10 @@ typedef struct { /// `update_semantics_callback`, and /// `update_semantics_callback2` may be provided; the others /// should be set to null. + /// + /// This callback is incompatible with multiple views. If this + /// callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. FlutterUpdateSemanticsNodeCallback update_semantics_node_callback; /// The legacy callback invoked by the engine in order to give the embedder /// the chance to respond to updates to semantics custom actions from the Dart @@ -2191,6 +2246,10 @@ typedef struct { /// `update_semantics_callback`, and /// `update_semantics_callback2` may be provided; the others /// should be set to null. + /// + /// This callback is incompatible with multiple views. If this + /// callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. FlutterUpdateSemanticsCustomActionCallback update_semantics_custom_action_callback; /// Path to a directory used to store data that is cached across runs of a @@ -2340,6 +2399,10 @@ typedef struct { /// `update_semantics_callback`, and /// `update_semantics_callback2` may be provided; the others /// must be set to null. + /// + /// This callback is incompatible with multiple views. If this + /// callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. FlutterUpdateSemanticsCallback update_semantics_callback; /// The callback invoked by the engine in order to give the embedder the @@ -2505,6 +2568,25 @@ FLUTTER_EXPORT FlutterEngineResult FlutterEngineRunInitialized( FLUTTER_API_SYMBOL(FlutterEngine) engine); +//------------------------------------------------------------------------------ +/// @brief Adds a view. +/// +/// This is an asynchronous operation. The view should not be used +/// until the |add_view_callback| is invoked with an `added` of +/// `true`. +/// +/// @param[in] engine A running engine instance. +/// @param[in] info The add view arguments. This can be deallocated +/// once |FlutterEngineAddView| returns, before +/// |add_view_callback| is invoked. +/// +/// @return The result of *starting* the asynchronous operation. If +/// `kSuccess`, the |add_view_callback| will be invoked. +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineAddView(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterAddViewInfo* info); + //------------------------------------------------------------------------------ /// @brief Removes a view. /// diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index d5758ffd92756..f1c46a5040153 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -843,6 +843,28 @@ void render_implicit_view() { PlatformDispatcher.instance.scheduleFrame(); } +@pragma('vm:entry-point') +void render_all_views() { + PlatformDispatcher.instance.onBeginFrame = (Duration duration) { + for (final FlutterView view in PlatformDispatcher.instance.views) { + final Size size = Size(800.0, 600.0); + final Color red = Color.fromARGB(127, 255, 0, 0); + + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0.0, 0.0); + + builder.addPicture( + Offset(0.0, 0.0), CreateColoredBox(red, size)); + + builder.pop(); + + view.render(builder.build()); + } + }; + PlatformDispatcher.instance.scheduleFrame(); +} + @pragma('vm:entry-point') void render_gradient() { PlatformDispatcher.instance.onBeginFrame = (Duration duration) { @@ -1307,6 +1329,18 @@ void can_schedule_frame() { signalNativeTest(); } +@pragma('vm:entry-point') +void add_view_schedules_frame() { + PlatformDispatcher.instance.onBeginFrame = (Duration beginTime) { + for (final FlutterView view in PlatformDispatcher.instance.views) { + if (view.viewId == 123) { + signalNativeCount(beginTime.inMicroseconds); + } + } + }; + signalNativeTest(); +} + void drawSolidColor(Color c) { PlatformDispatcher.instance.onBeginFrame = (Duration duration) { final SceneBuilder builder = SceneBuilder(); @@ -1399,6 +1433,20 @@ void window_metrics_event_view_id() { signalNativeTest(); } +@pragma('vm:entry-point') +void window_metrics_event_all_view_ids() { + PlatformDispatcher.instance.onMetricsChanged = () { + final List viewIds = + PlatformDispatcher.instance.views.map((view) => view.viewId).toList(); + + viewIds.sort(); + + signalNativeMessage('View IDs: [${viewIds.join(', ')}]'); + }; + + signalNativeTest(); +} + @pragma('vm:entry-point') Future channel_listener_response() async { channelBuffers.setListener('test/listen', diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index b573b1db6cd9e..fc0fa938bfe95 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -1265,11 +1265,163 @@ TEST_F(EmbedderTest, CanDeinitializeAnEngine) { engine.reset(); } +//------------------------------------------------------------------------------ +/// Test that a view can be added to a running engine. +/// +TEST_F(EmbedderTest, CanAddView) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); + + fml::AutoResetWaitableEvent ready_latch, message_latch; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + + std::string message; + context.AddNativeCallback("SignalNativeMessage", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + message = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + message_latch.Signal(); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + ready_latch.Wait(); + + FlutterWindowMetricsEvent metrics = {}; + metrics.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics.width = 800; + metrics.height = 600; + metrics.pixel_ratio = 1.0; + metrics.view_id = 123; + + FlutterAddViewInfo info = {}; + info.struct_size = sizeof(FlutterAddViewInfo); + info.view_id = 123; + info.view_metrics = &metrics; + info.add_view_callback = [](const FlutterAddViewResult* result) { + EXPECT_TRUE(result->added); + }; + ASSERT_EQ(FlutterEngineAddView(engine.get(), &info), kSuccess); + message_latch.Wait(); + ASSERT_EQ("View IDs: [0, 123]", message); +} + +//------------------------------------------------------------------------------ +/// Test that adding a view schedules a frame. +/// +TEST_F(EmbedderTest, AddViewSchedulesFrame) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("add_view_schedules_frame"); + fml::AutoResetWaitableEvent latch; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&latch](Dart_NativeArguments args) { latch.Signal(); })); + + fml::AutoResetWaitableEvent check_latch; + context.AddNativeCallback( + "SignalNativeCount", + CREATE_NATIVE_ENTRY( + [&check_latch](Dart_NativeArguments args) { check_latch.Signal(); })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Wait for the application to attach the listener. + latch.Wait(); + + FlutterWindowMetricsEvent metrics = {}; + metrics.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics.width = 800; + metrics.height = 600; + metrics.pixel_ratio = 1.0; + metrics.view_id = 123; + + FlutterAddViewInfo info = {}; + info.struct_size = sizeof(FlutterAddViewInfo); + info.view_id = 123; + info.view_metrics = &metrics; + info.add_view_callback = [](const FlutterAddViewResult* result) { + EXPECT_TRUE(result->added); + }; + ASSERT_EQ(FlutterEngineAddView(engine.get(), &info), kSuccess); + + check_latch.Wait(); +} + +//------------------------------------------------------------------------------ +/// Test that a view that was added can be removed. +/// TEST_F(EmbedderTest, CanRemoveView) { - // TODO(loicsharma): We can't test this until views can be added! - // https://github.com/flutter/flutter/issues/144806 + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); + + fml::AutoResetWaitableEvent ready_latch, message_latch; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + + std::string message; + context.AddNativeCallback("SignalNativeMessage", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + message = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + message_latch.Signal(); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + ready_latch.Wait(); + + // Add view 123. + FlutterWindowMetricsEvent metrics = {}; + metrics.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics.width = 800; + metrics.height = 600; + metrics.pixel_ratio = 1.0; + metrics.view_id = 123; + + FlutterAddViewInfo add_info = {}; + add_info.struct_size = sizeof(FlutterAddViewInfo); + add_info.view_id = 123; + add_info.view_metrics = &metrics; + add_info.add_view_callback = [](const FlutterAddViewResult* result) { + ASSERT_TRUE(result->added); + }; + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_info), kSuccess); + message_latch.Wait(); + ASSERT_EQ(message, "View IDs: [0, 123]"); + + // Remove view 123. + FlutterRemoveViewInfo remove_info = {}; + remove_info.struct_size = sizeof(FlutterAddViewInfo); + remove_info.view_id = 123; + remove_info.remove_view_callback = [](const FlutterRemoveViewResult* result) { + EXPECT_TRUE(result->removed); + }; + ASSERT_EQ(FlutterEngineRemoveView(engine.get(), &remove_info), kSuccess); + message_latch.Wait(); + ASSERT_EQ(message, "View IDs: [0]"); } +//------------------------------------------------------------------------------ +/// The implicit view is a special view that the engine and framework assume +/// can *always* be rendered to. Test that this view cannot be removed. +/// TEST_F(EmbedderTest, CannotRemoveImplicitView) { auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); EmbedderConfigBuilder builder(context); @@ -1287,6 +1439,146 @@ TEST_F(EmbedderTest, CannotRemoveImplicitView) { ASSERT_EQ(FlutterEngineRemoveView(engine.get(), &info), kInvalidArguments); } +//------------------------------------------------------------------------------ +/// Test that a view cannot be added if its ID already exists. +/// +TEST_F(EmbedderTest, CannotAddDuplicateViews) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); + + fml::AutoResetWaitableEvent ready_latch, message_latch; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + + std::string message; + context.AddNativeCallback("SignalNativeMessage", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + message = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + message_latch.Signal(); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + ready_latch.Wait(); + + // Add view 123. + struct Captures { + std::atomic count = 0; + fml::AutoResetWaitableEvent failure_latch; + }; + Captures captures; + + FlutterWindowMetricsEvent metrics = {}; + metrics.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics.width = 800; + metrics.height = 600; + metrics.pixel_ratio = 1.0; + metrics.view_id = 123; + + FlutterAddViewInfo add_info = {}; + add_info.struct_size = sizeof(FlutterAddViewInfo); + add_info.view_id = 123; + add_info.view_metrics = &metrics; + add_info.user_data = &captures; + add_info.add_view_callback = [](const FlutterAddViewResult* result) { + auto captures = reinterpret_cast(result->user_data); + + int count = captures->count.fetch_add(1); + + if (count == 0) { + ASSERT_TRUE(result->added); + } else { + EXPECT_FALSE(result->added); + captures->failure_latch.Signal(); + } + }; + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_info), kSuccess); + message_latch.Wait(); + ASSERT_EQ(message, "View IDs: [0, 123]"); + ASSERT_FALSE(captures.failure_latch.IsSignaledForTest()); + + // Add view 123 a second time. + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_info), kSuccess); + captures.failure_latch.Wait(); + ASSERT_EQ(captures.count, 2); + ASSERT_FALSE(message_latch.IsSignaledForTest()); +} + +//------------------------------------------------------------------------------ +/// Test that a removed view's ID can be reused to add a new view. +/// +TEST_F(EmbedderTest, CanReuseViewIds) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); + + fml::AutoResetWaitableEvent ready_latch, message_latch; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + + std::string message; + context.AddNativeCallback("SignalNativeMessage", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + message = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + message_latch.Signal(); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + ready_latch.Wait(); + + // Add view 123. + FlutterWindowMetricsEvent metrics = {}; + metrics.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics.width = 800; + metrics.height = 600; + metrics.pixel_ratio = 1.0; + metrics.view_id = 123; + + FlutterAddViewInfo add_info = {}; + add_info.struct_size = sizeof(FlutterAddViewInfo); + add_info.view_id = 123; + add_info.view_metrics = &metrics; + add_info.add_view_callback = [](const FlutterAddViewResult* result) { + ASSERT_TRUE(result->added); + }; + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_info), kSuccess); + message_latch.Wait(); + ASSERT_EQ(message, "View IDs: [0, 123]"); + + // Remove view 123. + FlutterRemoveViewInfo remove_info = {}; + remove_info.struct_size = sizeof(FlutterAddViewInfo); + remove_info.view_id = 123; + remove_info.remove_view_callback = [](const FlutterRemoveViewResult* result) { + ASSERT_TRUE(result->removed); + }; + ASSERT_EQ(FlutterEngineRemoveView(engine.get(), &remove_info), kSuccess); + message_latch.Wait(); + ASSERT_EQ(message, "View IDs: [0]"); + + // Re-add view 123. + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_info), kSuccess); + message_latch.Wait(); + ASSERT_EQ(message, "View IDs: [0, 123]"); +} + +//------------------------------------------------------------------------------ +/// Test that attempting to remove a view that does not exist fails as expected. +/// TEST_F(EmbedderTest, CannotRemoveUnknownView) { auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); EmbedderConfigBuilder builder(context); @@ -1301,13 +1593,227 @@ TEST_F(EmbedderTest, CannotRemoveUnknownView) { info.view_id = 123; info.user_data = &latch; info.remove_view_callback = [](const FlutterRemoveViewResult* result) { - ASSERT_FALSE(result->removed); + EXPECT_FALSE(result->removed); reinterpret_cast(result->user_data)->Signal(); }; ASSERT_EQ(FlutterEngineRemoveView(engine.get(), &info), kSuccess); latch.Wait(); } +//------------------------------------------------------------------------------ +/// View operations - adding, removing, sending window metrics - must execute in +/// order even though they are asynchronous. This is necessary to ensure the +/// embedder's and engine's states remain synchronized. +/// +TEST_F(EmbedderTest, ViewOperationsOrdered) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("window_metrics_event_all_view_ids"); + + fml::AutoResetWaitableEvent ready_latch; + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&ready_latch](Dart_NativeArguments args) { ready_latch.Signal(); })); + + std::atomic message_count = 0; + context.AddNativeCallback("SignalNativeMessage", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + message_count.fetch_add(1); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + ready_latch.Wait(); + + // Enqueue multiple view operations at once: + // + // 1. Add view 123 - This must succeed. + // 2. Add duplicate view 123 - This must fail asynchronously. + // 3. Add second view 456 - This must succeed. + // 4. Remove second view 456 - This must succeed. + // + // The engine must execute view operations asynchronously in serial order. + // If step 2 succeeds instead of step 1, this indicates the engine did not + // execute the view operations in the correct order. If step 4 fails, + // this indicates the engine did not wait until the add second view completed. + FlutterWindowMetricsEvent metrics123 = {}; + metrics123.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics123.width = 800; + metrics123.height = 600; + metrics123.pixel_ratio = 1.0; + metrics123.view_id = 123; + + FlutterWindowMetricsEvent metrics456 = {}; + metrics456.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics456.width = 800; + metrics456.height = 600; + metrics456.pixel_ratio = 1.0; + metrics456.view_id = 456; + + struct Captures { + fml::AutoResetWaitableEvent add_first_view; + fml::AutoResetWaitableEvent add_duplicate_view; + fml::AutoResetWaitableEvent add_second_view; + fml::AutoResetWaitableEvent remove_second_view; + }; + Captures captures; + + // Add view 123. + FlutterAddViewInfo add_view_info = {}; + add_view_info.struct_size = sizeof(FlutterAddViewInfo); + add_view_info.view_id = 123; + add_view_info.view_metrics = &metrics123; + add_view_info.user_data = &captures; + add_view_info.add_view_callback = [](const FlutterAddViewResult* result) { + auto captures = reinterpret_cast(result->user_data); + + ASSERT_TRUE(result->added); + ASSERT_FALSE(captures->add_first_view.IsSignaledForTest()); + ASSERT_FALSE(captures->add_duplicate_view.IsSignaledForTest()); + ASSERT_FALSE(captures->add_second_view.IsSignaledForTest()); + ASSERT_FALSE(captures->remove_second_view.IsSignaledForTest()); + + captures->add_first_view.Signal(); + }; + + // Add duplicate view 123. + FlutterAddViewInfo add_duplicate_view_info = {}; + add_duplicate_view_info.struct_size = sizeof(FlutterAddViewInfo); + add_duplicate_view_info.view_id = 123; + add_duplicate_view_info.view_metrics = &metrics123; + add_duplicate_view_info.user_data = &captures; + add_duplicate_view_info.add_view_callback = + [](const FlutterAddViewResult* result) { + auto captures = reinterpret_cast(result->user_data); + + ASSERT_FALSE(result->added); + ASSERT_TRUE(captures->add_first_view.IsSignaledForTest()); + ASSERT_FALSE(captures->add_duplicate_view.IsSignaledForTest()); + ASSERT_FALSE(captures->add_second_view.IsSignaledForTest()); + ASSERT_FALSE(captures->remove_second_view.IsSignaledForTest()); + + captures->add_duplicate_view.Signal(); + }; + + // Add view 456. + FlutterAddViewInfo add_second_view_info = {}; + add_second_view_info.struct_size = sizeof(FlutterAddViewInfo); + add_second_view_info.view_id = 456; + add_second_view_info.view_metrics = &metrics456; + add_second_view_info.user_data = &captures; + add_second_view_info.add_view_callback = + [](const FlutterAddViewResult* result) { + auto captures = reinterpret_cast(result->user_data); + + ASSERT_TRUE(result->added); + ASSERT_TRUE(captures->add_first_view.IsSignaledForTest()); + ASSERT_TRUE(captures->add_duplicate_view.IsSignaledForTest()); + ASSERT_FALSE(captures->add_second_view.IsSignaledForTest()); + ASSERT_FALSE(captures->remove_second_view.IsSignaledForTest()); + + captures->add_second_view.Signal(); + }; + + // Remove view 456. + FlutterRemoveViewInfo remove_second_view_info = {}; + remove_second_view_info.struct_size = sizeof(FlutterRemoveViewInfo); + remove_second_view_info.view_id = 456; + remove_second_view_info.user_data = &captures; + remove_second_view_info.remove_view_callback = + [](const FlutterRemoveViewResult* result) { + auto captures = reinterpret_cast(result->user_data); + + ASSERT_TRUE(result->removed); + ASSERT_TRUE(captures->add_first_view.IsSignaledForTest()); + ASSERT_TRUE(captures->add_duplicate_view.IsSignaledForTest()); + ASSERT_TRUE(captures->add_second_view.IsSignaledForTest()); + ASSERT_FALSE(captures->remove_second_view.IsSignaledForTest()); + + captures->remove_second_view.Signal(); + }; + + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_view_info), kSuccess); + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_duplicate_view_info), + kSuccess); + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_second_view_info), + kSuccess); + ASSERT_EQ(FlutterEngineRemoveView(engine.get(), &remove_second_view_info), + kSuccess); + captures.remove_second_view.Wait(); + captures.add_second_view.Wait(); + captures.add_duplicate_view.Wait(); + captures.add_first_view.Wait(); + ASSERT_EQ(message_count, 3); +} + +//------------------------------------------------------------------------------ +/// Test the engine can present to multiple views. +/// +TEST_F(EmbedderTest, CanRenderMultipleViews) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetCompositor(); + builder.SetDartEntrypoint("render_all_views"); + + builder.SetRenderTargetType( + EmbedderTestBackingStoreProducer::RenderTargetType::kSoftwareBuffer); + + fml::AutoResetWaitableEvent latch0, latch123; + context.GetCompositor().SetPresentCallback( + [&](FlutterViewId view_id, const FlutterLayer** layers, + size_t layers_count) { + switch (view_id) { + case 0: + latch0.Signal(); + break; + case 123: + latch123.Signal(); + break; + default: + FML_UNREACHABLE(); + } + }, + /* one_shot= */ false); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Give the implicit view a non-zero size so that it renders something. + FlutterWindowMetricsEvent metrics0 = {}; + metrics0.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics0.width = 800; + metrics0.height = 600; + metrics0.pixel_ratio = 1.0; + metrics0.view_id = 0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &metrics0), + kSuccess); + + // Add view 123. + FlutterWindowMetricsEvent metrics123 = {}; + metrics123.struct_size = sizeof(FlutterWindowMetricsEvent); + metrics123.width = 800; + metrics123.height = 600; + metrics123.pixel_ratio = 1.0; + metrics123.view_id = 123; + + FlutterAddViewInfo add_view_info = {}; + add_view_info.struct_size = sizeof(FlutterAddViewInfo); + add_view_info.view_id = 123; + add_view_info.view_metrics = &metrics123; + add_view_info.add_view_callback = [](const FlutterAddViewResult* result) { + ASSERT_TRUE(result->added); + }; + + ASSERT_EQ(FlutterEngineAddView(engine.get(), &add_view_info), kSuccess); + + latch0.Wait(); + latch123.Wait(); +} + TEST_F(EmbedderTest, CanUpdateLocales) { auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); EmbedderConfigBuilder builder(context);