diff --git a/shell/platform/fuchsia/flutter/gfx_external_view_embedder.cc b/shell/platform/fuchsia/flutter/gfx_external_view_embedder.cc index a98fb019e4152..225cf31b0c3c0 100644 --- a/shell/platform/fuchsia/flutter/gfx_external_view_embedder.cc +++ b/shell/platform/fuchsia/flutter/gfx_external_view_embedder.cc @@ -10,6 +10,7 @@ #include // For std::clamp +#include "flutter/fml/logging.h" #include "flutter/fml/trace_event.h" #include "third_party/skia/include/core/SkPicture.h" #include "third_party/skia/include/core/SkSurface.h" @@ -169,8 +170,9 @@ void GfxExternalViewEmbedder::PrerollCompositeEmbeddedView( zx_handle_t handle = static_cast(view_id); FML_CHECK(frame_layers_.count(handle) == 0); - frame_layers_.emplace(std::make_pair(EmbedderLayerId{handle}, - EmbedderLayer(frame_size_, *params))); + frame_layers_.emplace(std::make_pair( + EmbedderLayerId{handle}, + EmbedderLayer(frame_size_, *params, flutter::RTreeFactory()))); frame_composition_order_.push_back(handle); } @@ -200,8 +202,9 @@ void GfxExternalViewEmbedder::BeginFrame( frame_dpr_ = device_pixel_ratio; // Create the root layer. - frame_layers_.emplace( - std::make_pair(kRootLayerId, EmbedderLayer(frame_size, std::nullopt))); + frame_layers_.emplace(std::make_pair( + kRootLayerId, + EmbedderLayer(frame_size, std::nullopt, flutter::RTreeFactory()))); frame_composition_order_.push_back(kRootLayerId); // Set up the input interceptor at the top of the scene, if applicable. @@ -271,6 +274,19 @@ void GfxExternalViewEmbedder::SubmitFrame( } } + // Finish recording SkPictures. + { + TRACE_EVENT0("flutter", "FinishRecordingPictures"); + + for (const auto& surface_index : frame_surface_indices) { + const auto& layer = frame_layers_.find(surface_index.first); + FML_CHECK(layer != frame_layers_.end()); + layer->second.picture = + layer->second.recorder->finishRecordingAsPicture(); + FML_CHECK(layer->second.picture != nullptr); + } + } + // Submit layers and platform views to Scenic in composition order. { TRACE_EVENT0("flutter", "SubmitLayers"); @@ -437,10 +453,18 @@ void GfxExternalViewEmbedder::SubmitFrame( FML_CHECK(scenic_layer_index <= scenic_layers_.size()); if (scenic_layer_index == scenic_layers_.size()) { ScenicLayer new_layer{ - .shape_node = scenic::ShapeNode(session_->get()), - .material = scenic::Material(session_->get()), + .layer_node = scenic::EntityNode(session_->get()), + .image = + ScenicImage{ + .shape_node = scenic::ShapeNode(session_->get()), + .material = scenic::Material(session_->get()), + }, + // We'll set hit regions later. + .hit_regions = {}, }; - new_layer.shape_node.SetMaterial(new_layer.material); + new_layer.layer_node.SetLabel("Flutter::Layer"); + new_layer.layer_node.AddChild(new_layer.image.shape_node); + new_layer.image.shape_node.SetMaterial(new_layer.image.material); scenic_layers_.emplace_back(std::move(new_layer)); } @@ -491,25 +515,50 @@ void GfxExternalViewEmbedder::SubmitFrame( embedded_views_height; auto& scenic_layer = scenic_layers_[scenic_layer_index]; auto& scenic_rect = found_rects->second[rect_index]; - scenic_layer.shape_node.SetLabel("Flutter::Layer"); - scenic_layer.shape_node.SetShape(scenic_rect); - scenic_layer.shape_node.SetTranslation( + auto& image = scenic_layer.image; + image.shape_node.SetLabel("Flutter::Layer::Image"); + image.shape_node.SetShape(scenic_rect); + image.shape_node.SetTranslation( layer->second.surface_size.width() * 0.5f, layer->second.surface_size.height() * 0.5f, -layer_elevation); - scenic_layer.material.SetColor(SK_AlphaOPAQUE, SK_AlphaOPAQUE, - SK_AlphaOPAQUE, layer_opacity); - scenic_layer.material.SetTexture(surface_for_layer->GetImageId()); - - // Only the first (i.e. the bottom-most) layer should receive input. - // TODO: Workaround for invisible overlays stealing input. Remove when - // the underlying bug is fixed. - const fuchsia::ui::gfx::HitTestBehavior layer_hit_test_behavior = - first_layer ? fuchsia::ui::gfx::HitTestBehavior::kDefault - : fuchsia::ui::gfx::HitTestBehavior::kSuppress; - scenic_layer.shape_node.SetHitTestBehavior(layer_hit_test_behavior); + image.material.SetColor(SK_AlphaOPAQUE, SK_AlphaOPAQUE, SK_AlphaOPAQUE, + layer_opacity); + image.material.SetTexture(surface_for_layer->GetImageId()); + + // We'll set hit regions expliclty on a separate ShapeNode, so the image + // itself should be unhittable and semantically invisible. + image.shape_node.SetHitTestBehavior( + fuchsia::ui::gfx::HitTestBehavior::kSuppress); + image.shape_node.SetSemanticVisibility(false); // Attach the ScenicLayer to the main scene graph. - layer_tree_node_.AddChild(scenic_layer.shape_node); + layer_tree_node_.AddChild(scenic_layer.layer_node); + + // Compute the set of non-overlapping set of bounding boxes for the + // painted content in this layer. + { + FML_CHECK(layer->second.rtree); + std::list intersection_rects = + layer->second.rtree->searchNonOverlappingDrawnRects( + SkRect::Make(layer->second.surface_size)); + + // SkRect joined_rect = SkRect::MakeEmpty(); + for (const SkRect& rect : intersection_rects) { + auto paint_bounds = + scenic::Rectangle(session_->get(), rect.width(), rect.height()); + auto hit_region = scenic::ShapeNode(session_->get()); + hit_region.SetLabel("Flutter::Layer::HitRegion"); + hit_region.SetShape(paint_bounds); + hit_region.SetTranslation(rect.centerX(), rect.centerY(), + -layer_elevation); + hit_region.SetHitTestBehavior( + fuchsia::ui::gfx::HitTestBehavior::kDefault); + hit_region.SetSemanticVisibility(true); + + scenic_layer.layer_node.AddChild(hit_region); + scenic_layer.hit_regions.push_back(std::move(hit_region)); + } + } } // Reset for the next pass: @@ -527,7 +576,11 @@ void GfxExternalViewEmbedder::SubmitFrame( session_->Present(); } - // Render the recorded SkPictures into the surfaces. + // Flush pending skia operations. + // NOTE: This operation MUST occur AFTER the `Present() ` call above. We + // pipeline the Skia rendering work with scenic IPC, and scenic will delay + // internally until Skia is finished. So, doing this work before calling + // `Present()` would adversely affect performance. { TRACE_EVENT0("flutter", "RasterizeSurfaces"); @@ -548,13 +601,10 @@ void GfxExternalViewEmbedder::SubmitFrame( const auto& layer = frame_layers_.find(surface_index.first); FML_CHECK(layer != frame_layers_.end()); - sk_sp picture = - layer->second.recorder->finishRecordingAsPicture(); - FML_CHECK(picture != nullptr); canvas->setMatrix(SkMatrix::I()); canvas->clear(SK_ColorTRANSPARENT); - canvas->drawPicture(picture); + canvas->drawPicture(layer->second.picture); canvas->flush(); } } @@ -636,7 +686,16 @@ void GfxExternalViewEmbedder::Reset() { // Clear images on all layers so they aren't cached unnecessarily. for (auto& layer : scenic_layers_) { - layer.material.SetTexture(0); + layer.image.material.SetTexture(0); + + // Detach hit regions; otherwise, they may persist across frames + // incorrectly. + for (auto& hit_region : layer.hit_regions) { + hit_region.Detach(); + } + + // Remove cached hit regions so that we don't recreate stale ones. + layer.hit_regions.clear(); } } diff --git a/shell/platform/fuchsia/flutter/gfx_external_view_embedder.h b/shell/platform/fuchsia/flutter/gfx_external_view_embedder.h index 1e0a5e3eab67d..81b061776973b 100644 --- a/shell/platform/fuchsia/flutter/gfx_external_view_embedder.h +++ b/shell/platform/fuchsia/flutter/gfx_external_view_embedder.h @@ -17,6 +17,7 @@ #include #include "flutter/flow/embedded_views.h" +#include "flutter/flow/rtree.h" #include "flutter/fml/logging.h" #include "flutter/fml/macros.h" #include "flutter/shell/common/canvas_spy.h" @@ -138,18 +139,29 @@ class GfxExternalViewEmbedder final : public flutter::ExternalViewEmbedder { struct EmbedderLayer { EmbedderLayer(const SkISize& frame_size, - std::optional view_params) - : embedded_view_params(std::move(view_params)), + std::optional view_params, + flutter::RTreeFactory rtree_factory) + : rtree(rtree_factory.getInstance()), + embedded_view_params(std::move(view_params)), recorder(std::make_unique()), canvas_spy(std::make_unique( - recorder->beginRecording(frame_size.width(), - frame_size.height()))), - surface_size(frame_size) {} + recorder->beginRecording(SkRect::Make(frame_size), + &rtree_factory))), + surface_size(frame_size), + picture(nullptr) {} + + // Records paint operations applied to this layer's `SkCanvas`. + // These records are used to determine which portions of this layer + // contain content. The embedder propagates this information to scenic, so + // that scenic can accurately decide which portions of this layer may + // interact with input. + sk_sp rtree; std::optional embedded_view_params; std::unique_ptr recorder; std::unique_ptr canvas_spy; SkISize surface_size; + sk_sp picture; }; using EmbedderLayerId = std::optional; constexpr static EmbedderLayerId kRootLayerId = EmbedderLayerId{}; @@ -172,11 +184,34 @@ class GfxExternalViewEmbedder final : public flutter::ExternalViewEmbedder { bool pending_focusable = true; }; - struct ScenicLayer { + // GFX resources required to render a composited flutter layer (i.e. an + // SkPicture). + struct ScenicImage { scenic::ShapeNode shape_node; scenic::Material material; }; + // All resources required to represent a flutter layer in the GFX scene + // graph. The structure of the subgraph for a particular layer is: + // + // layer_node + // / \ + // image node hit regions (zero or more) + // + // NOTE: `hit_regions` must be cleared before submitting each new frame; + // otherwise, we will report stale hittable geometry to scenic. + struct ScenicLayer { + // Root of the subtree containing the scenic resources for this layer. + scenic::EntityNode layer_node; + + // Scenic resources used to render this layer's image. + ScenicImage image; + + // Scenic resources that specify which parts of this layer are responsive + // to input. + std::vector hit_regions; + }; + std::shared_ptr session_; std::shared_ptr surface_producer_; diff --git a/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc b/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc index 58690960d03cb..f99c54fb1904a 100644 --- a/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc +++ b/shell/platform/fuchsia/flutter/tests/gfx_external_view_embedder_unittests.cc @@ -149,18 +149,6 @@ class FakeSurfaceProducer : public SurfaceProducer { uint32_t buffer_id_{1}; }; -struct FakeCompositorLayer { - enum class LayerType : uint32_t { - Image, - View, - }; - - std::shared_ptr layer_root; - - LayerType layer_type{LayerType::Image}; - size_t layer_index{0}; -}; - std::string GetCurrentTestName() { return ::testing::UnitTest::GetInstance()->current_test_info()->name(); } @@ -253,12 +241,28 @@ void ExpectRootSceneGraph( EXPECT_EQ(scene_graph.resource_map.size(), 3u); } -void ExpectImageCompositorLayer(const FakeCompositorLayer& layer, - const SkISize layer_size) { +/// Verifies the scene subgraph for a particular flutter embedder layer. +/// +/// ARGUMENTS +/// +/// scenic_node: The root of the layer's scenic subgraph. +/// +/// layer_size: The expected dimensions of the layer's image. +/// +/// flutter_layer_index: This layer's 0-indexed position in the list of +/// flutter layers present in this frame, sorted in paint order. +/// +/// paint_regions: List of non-overlapping rects, in canvas coordinate space, +/// where content was drawn. For each "paint region" present in the frame, the +/// embedder reports a corresponding "hit region" to scenic as a hittable +/// ShapeNode. ShapeNodes are centered at (0, 0), by default, so they must be +/// translated to match the locations of the corresopnding paint regions. +void ExpectImageCompositorLayer(const FakeResource& scenic_node, + const SkISize layer_size, + size_t flutter_layer_index, + std::vector paint_regions) { const SkSize float_layer_size = SkSize::Make(layer_size.width(), layer_size.height()); - const size_t flutter_layer_index = - (layer.layer_index + 1) / 2; // Integer division const float views_under_layer_depth = flutter_layer_index * GfxExternalViewEmbedder::kScenicZElevationForPlatformView; @@ -266,27 +270,50 @@ void ExpectImageCompositorLayer(const FakeCompositorLayer& layer, flutter_layer_index * GfxExternalViewEmbedder::kScenicZElevationBetweenLayers + views_under_layer_depth; - const bool layer_hit_testable = (flutter_layer_index == 0) - ? FakeNode::kIsHitTestable - : FakeNode::kIsNotHitTestable; const float layer_opacity = (flutter_layer_index == 0) ? GfxExternalViewEmbedder::kBackgroundLayerOpacity / 255.f : GfxExternalViewEmbedder::kOverlayLayerOpacity / 255.f; - EXPECT_EQ(layer.layer_type, FakeCompositorLayer::LayerType::Image); - EXPECT_EQ(layer.layer_index % 2, 0u); + EXPECT_THAT( - layer.layer_root, - Pointee(FieldsAre( + scenic_node, + FieldsAre( _, "Flutter::Layer", FakeResource::kDefaultEmptyEventMask, + VariantWith(FieldsAre( + FieldsAre( + // Verify children separately below, since the + // expected number of children may vary across + // different test cases that call this method. + _, FakeNode::kDefaultZeroRotation, FakeNode::kDefaultOneScale, + FakeNode::kDefaultZeroTranslation, + FakeNode::kDefaultZeroAnchor, FakeNode::kIsHitTestable, + FakeNode::kIsSemanticallyVisible), + _)))); + + const auto* layer_node_state = + std::get_if(&scenic_node.state); + ASSERT_TRUE(layer_node_state); + + const auto& layer_node_children = layer_node_state->node_state.children; + + // The layer entity node should have a child node for the image, and + // separate children for each of the hit regions. + ASSERT_EQ(layer_node_children.size(), paint_regions.size() + 1); + + // Verify image node. + EXPECT_THAT( + layer_node_children[0], + Pointee(FieldsAre( + _, "Flutter::Layer::Image", FakeResource::kDefaultEmptyEventMask, VariantWith(FieldsAre( FieldsAre(IsEmpty(), FakeNode::kDefaultZeroRotation, FakeNode::kDefaultOneScale, std::array{float_layer_size.width() / 2.f, float_layer_size.height() / 2.f, -layer_depth}, - FakeNode::kDefaultZeroAnchor, layer_hit_testable, - FakeNode::kIsSemanticallyVisible), + FakeNode::kDefaultZeroAnchor, + FakeNode::kIsNotHitTestable, + FakeNode::kIsNotSemanticallyVisible), Pointee( FieldsAre(_, "", FakeResource::kDefaultEmptyEventMask, VariantWith( @@ -305,13 +332,39 @@ void ExpectImageCompositorLayer(const FakeCompositorLayer& layer, IsNull())))), std::array{1.f, 1.f, 1.f, layer_opacity}))))))))); + + // Verify hit regions. + for (size_t i = 0; i < paint_regions.size(); ++i) { + ASSERT_LT(i, layer_node_children.size()); + const auto& paint_region = paint_regions[i]; + EXPECT_THAT( + layer_node_children[i + 1], + Pointee(FieldsAre( + _, "Flutter::Layer::HitRegion", + FakeResource::kDefaultEmptyEventMask, + VariantWith(FieldsAre( + FieldsAre(IsEmpty(), FakeNode::kDefaultZeroRotation, + FakeNode::kDefaultOneScale, + std::array{ + paint_region.x() + paint_region.width() / 2.f, + paint_region.y() + paint_region.height() / 2.f, + -layer_depth}, + FakeNode::kDefaultZeroAnchor, + FakeNode::kIsHitTestable, + FakeNode::kIsSemanticallyVisible), + Pointee(FieldsAre( + _, "", FakeResource::kDefaultEmptyEventMask, + VariantWith(FieldsAre( + VariantWith(FieldsAre( + paint_region.width(), paint_region.height())))))), + IsNull()))))); + } } -void ExpectViewCompositorLayer(const FakeCompositorLayer& layer, +void ExpectViewCompositorLayer(const FakeResource& scenic_node, const fuchsia::ui::views::ViewToken& view_token, - const flutter::EmbeddedViewParams& view_params) { - const size_t flutter_layer_index = - (layer.layer_index + 1) / 2; // Integer division + const flutter::EmbeddedViewParams& view_params, + size_t flutter_layer_index) { const float views_under_layer_depth = flutter_layer_index > 0 ? (flutter_layer_index - 1) * @@ -321,11 +374,9 @@ void ExpectViewCompositorLayer(const FakeCompositorLayer& layer, flutter_layer_index * GfxExternalViewEmbedder::kScenicZElevationBetweenLayers + views_under_layer_depth; - EXPECT_EQ(layer.layer_type, FakeCompositorLayer::LayerType::View); - EXPECT_EQ(layer.layer_index % 2, 1u); EXPECT_THAT( - layer.layer_root, - Pointee(FieldsAre( + scenic_node, + FieldsAre( _, _ /*"Flutter::PlatformView::OpacityMutator" */, FakeResource::kDefaultEmptyEventMask, VariantWith(FieldsAre( @@ -372,10 +423,10 @@ void ExpectViewCompositorLayer(const FakeCompositorLayer& layer, FakeNode::kDefaultZeroTranslation, FakeNode::kDefaultZeroAnchor, FakeNode::kIsHitTestable, FakeNode::kIsSemanticallyVisible), - FakeOpacityNode::kDefaultOneOpacity))))); + FakeOpacityNode::kDefaultOneOpacity)))); } -std::vector ExtractLayersFromSceneGraph( +std::vector ExtractLayersFromSceneGraph( const FakeSceneGraph& scene_graph) { AssertRootSceneGraph(scene_graph, false); @@ -387,20 +438,12 @@ std::vector ExtractLayersFromSceneGraph( auto* layer_tree_state = std::get_if( &metrics_watcher_state->node_state.children[0]->state); - std::vector layers; + std::vector scenic_layers; for (auto& layer_resource : layer_tree_state->node_state.children) { - const size_t layer_index = layers.size(); - const FakeCompositorLayer::LayerType layer_type = - (layer_index % 2 == 0) ? FakeCompositorLayer::LayerType::Image - : FakeCompositorLayer::LayerType::View; - layers.emplace_back(FakeCompositorLayer{ - .layer_root = layer_resource, - .layer_type = layer_type, - .layer_index = layer_index, - }); + scenic_layers.push_back(*layer_resource); } - return layers; + return scenic_layers; } void DrawSimpleFrame(GfxExternalViewEmbedder& external_view_embedder, @@ -528,6 +571,7 @@ class GfxExternalViewEmbedderTest std::shared_ptr fake_surface_producer_; }; +// Tests the trivial case where flutter does not present any content to scenic. TEST_F(GfxExternalViewEmbedderTest, RootScene) { const std::string debug_name = GetCurrentTestName(); auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); @@ -558,6 +602,7 @@ TEST_F(GfxExternalViewEmbedderTest, RootScene) { view_holder_token, view_ref); } +// Tests the case where flutter renders a single image. TEST_F(GfxExternalViewEmbedderTest, SimpleScene) { const std::string debug_name = GetCurrentTestName(); auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); @@ -579,29 +624,34 @@ TEST_F(GfxExternalViewEmbedderTest, SimpleScene) { // Draw the scene. The scene graph shouldn't change yet. const SkISize frame_size = SkISize::Make(512, 512); + SkRect paint_region; DrawSimpleFrame( - external_view_embedder, frame_size, 1.f, [](SkCanvas* canvas) { + external_view_embedder, frame_size, 1.f, + [&paint_region](SkCanvas* canvas) { const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), canvas->imageInfo().height()); SkPaint rect_paint; rect_paint.setColor(SK_ColorGREEN); - canvas->translate(canvas_size.width() / 4.f, - canvas_size.height() / 2.f); - canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, - canvas_size.height() / 32.f), - rect_paint); + + paint_region = SkRect::MakeXYWH( + canvas_size.width() / 4.f, canvas_size.height() / 2.f, + canvas_size.width() / 32.f, canvas_size.height() / 32.f); + + canvas->drawRect(paint_region, rect_paint); }); ExpectRootSceneGraph(fake_session().SceneGraph(), debug_name, view_holder_token, view_ref); // Pump the message loop. The scene updates should propogate to Scenic. loop().RunUntilIdle(); - std::vector compositor_layers = + std::vector scenic_layers = ExtractLayersFromSceneGraph(fake_session().SceneGraph()); - EXPECT_EQ(compositor_layers.size(), 1u); - ExpectImageCompositorLayer(compositor_layers[0], frame_size); + EXPECT_EQ(scenic_layers.size(), 1u); + ExpectImageCompositorLayer(scenic_layers[0], frame_size, + /* flutter layer index = */ 0, {paint_region}); } +// Tests the case where flutter embeds a platform view on top of an image layer. TEST_F(GfxExternalViewEmbedderTest, SceneWithOneView) { const std::string debug_name = GetCurrentTestName(); auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); @@ -633,42 +683,51 @@ TEST_F(GfxExternalViewEmbedderTest, SceneWithOneView) { // Draw the scene. The scene graph shouldn't change yet. const SkISize frame_size = SkISize::Make(512, 512); + + SkRect main_surface_paint_region, overlay_paint_region; + DrawFrameWithView( external_view_embedder, frame_size, 1.f, child_view_id, child_view_params, - [](SkCanvas* canvas) { + [&main_surface_paint_region](SkCanvas* canvas) { const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), canvas->imageInfo().height()); + + main_surface_paint_region = SkRect::MakeXYWH( + canvas_size.width() / 4.f, canvas_size.width() / 2.f, + canvas_size.width() / 32.f, canvas_size.height() / 32.f); + SkPaint rect_paint; rect_paint.setColor(SK_ColorGREEN); - canvas->translate(canvas_size.width() / 4.f, - canvas_size.height() / 2.f); - canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, - canvas_size.height() / 32.f), - rect_paint); + canvas->drawRect(main_surface_paint_region, rect_paint); }, - [](SkCanvas* canvas) { + [&overlay_paint_region](SkCanvas* canvas) { const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), canvas->imageInfo().height()); + overlay_paint_region = SkRect::MakeXYWH( + canvas_size.width() * 3.f / 4.f, canvas_size.height() / 2.f, + canvas_size.width() / 32.f, canvas_size.height() / 32.f); + SkPaint rect_paint; rect_paint.setColor(SK_ColorRED); - canvas->translate(canvas_size.width() * 3.f / 4.f, - canvas_size.height() / 2.f); - canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f, - canvas_size.height() / 32.f), - rect_paint); + canvas->drawRect(overlay_paint_region, rect_paint); }); ExpectRootSceneGraph(fake_session().SceneGraph(), debug_name, view_holder_token, view_ref); // Pump the message loop. The scene updates should propagate to Scenic. loop().RunUntilIdle(); - std::vector compositor_layers = + std::vector scenic_layers = ExtractLayersFromSceneGraph(fake_session().SceneGraph()); - EXPECT_EQ(compositor_layers.size(), 3u); - ExpectImageCompositorLayer(compositor_layers[0], frame_size); - ExpectViewCompositorLayer(compositor_layers[1], child_view_token, - child_view_params); - ExpectImageCompositorLayer(compositor_layers[2], frame_size); + EXPECT_EQ(scenic_layers.size(), 3u); + ExpectImageCompositorLayer(scenic_layers[0], frame_size, + /* flutter layer index = */ 0, + {main_surface_paint_region}); + ExpectViewCompositorLayer(scenic_layers[1], child_view_token, + child_view_params, + /* flutter layer index = */ 1); + ExpectImageCompositorLayer(scenic_layers[2], frame_size, + /* flutter layer index = */ 1, + {overlay_paint_region}); // Destroy the view. external_view_embedder.DestroyView(child_view_id, [](scenic::ResourceId) {}); @@ -677,4 +736,127 @@ TEST_F(GfxExternalViewEmbedderTest, SceneWithOneView) { loop().RunUntilIdle(); } +// Tests the case where flutter renders an image with two non-overlapping pieces +// of content. In this case, the embedder should report two separate hit regions +// to scenic. +TEST_F(GfxExternalViewEmbedderTest, SimpleSceneDisjointHitRegions) { + const std::string debug_name = GetCurrentTestName(); + auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); + auto view_ref_pair = scenic::ViewRefPair::New(); + fuchsia::ui::views::ViewRef view_ref; + view_ref_pair.view_ref.Clone(&view_ref); + + // Create the `GfxExternalViewEmbedder` and pump the message loop until + // the initial scene graph is setup. + GfxExternalViewEmbedder external_view_embedder( + debug_name, std::move(view_token), std::move(view_ref_pair), + session_connection(), fake_surface_producer()); + loop().RunUntilIdle(); + fake_session().FireOnFramePresentedEvent( + MakeFramePresentedInfoForOnePresent(0, 0)); + loop().RunUntilIdle(); + ExpectRootSceneGraph(fake_session().SceneGraph(), debug_name, + view_holder_token, view_ref); + + // Draw the scene. The scene graph shouldn't change yet. + SkRect paint_region_1, paint_region_2; + const SkISize frame_size = SkISize::Make(512, 512); + DrawSimpleFrame( + external_view_embedder, frame_size, 1.f, + [&paint_region_1, &paint_region_2](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + + paint_region_1 = SkRect::MakeXYWH( + canvas_size.width() / 4.f, canvas_size.height() / 2.f, + canvas_size.width() / 32.f, canvas_size.height() / 32.f); + + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->drawRect(paint_region_1, rect_paint); + + paint_region_2 = SkRect::MakeXYWH( + canvas_size.width() * 3.f / 4.f, canvas_size.height() / 2.f, + canvas_size.width() / 32.f, canvas_size.height() / 32.f); + + rect_paint.setColor(SK_ColorRED); + canvas->drawRect(paint_region_2, rect_paint); + }); + ExpectRootSceneGraph(fake_session().SceneGraph(), debug_name, + view_holder_token, view_ref); + + // Pump the message loop. The scene updates should propogate to Scenic. + loop().RunUntilIdle(); + std::vector scenic_layers = + ExtractLayersFromSceneGraph(fake_session().SceneGraph()); + EXPECT_EQ(scenic_layers.size(), 1u); + ExpectImageCompositorLayer(scenic_layers[0], frame_size, + /* flutter layer index = */ 0, + {paint_region_1, paint_region_2}); +} + +// Tests the case where flutter renders an image with two overlapping pieces of +// content. In this case, the embedder should report a single joint hit region +// to scenic. +TEST_F(GfxExternalViewEmbedderTest, SimpleSceneOverlappingHitRegions) { + const std::string debug_name = GetCurrentTestName(); + auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); + auto view_ref_pair = scenic::ViewRefPair::New(); + fuchsia::ui::views::ViewRef view_ref; + view_ref_pair.view_ref.Clone(&view_ref); + + // Create the `GfxExternalViewEmbedder` and pump the message loop until + // the initial scene graph is setup. + GfxExternalViewEmbedder external_view_embedder( + debug_name, std::move(view_token), std::move(view_ref_pair), + session_connection(), fake_surface_producer()); + loop().RunUntilIdle(); + fake_session().FireOnFramePresentedEvent( + MakeFramePresentedInfoForOnePresent(0, 0)); + loop().RunUntilIdle(); + ExpectRootSceneGraph(fake_session().SceneGraph(), debug_name, + view_holder_token, view_ref); + + // Draw the scene. The scene graph shouldn't change yet. + SkRect joined_paint_region = SkRect::MakeEmpty(); + const SkISize frame_size = SkISize::Make(512, 512); + DrawSimpleFrame( + external_view_embedder, frame_size, 1.f, + [&joined_paint_region](SkCanvas* canvas) { + const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(), + canvas->imageInfo().height()); + + auto paint_region_1 = SkRect::MakeXYWH( + canvas_size.width() / 4.f, canvas_size.height() / 4.f, + canvas_size.width() / 2.f, canvas_size.height() / 2.f); + SkPaint rect_paint; + rect_paint.setColor(SK_ColorGREEN); + canvas->drawRect(paint_region_1, rect_paint); + + auto paint_region_2 = SkRect::MakeXYWH( + canvas_size.width() * 3.f / 8.f, canvas_size.height() / 4.f, + canvas_size.width() / 2.f, canvas_size.height() / 2.f); + rect_paint.setColor(SK_ColorRED); + canvas->drawRect(paint_region_2, rect_paint); + + joined_paint_region.join(paint_region_1); + joined_paint_region.join(paint_region_2); + }); + ExpectRootSceneGraph(fake_session().SceneGraph(), debug_name, + view_holder_token, view_ref); + + EXPECT_EQ(joined_paint_region.x(), 128.f); + EXPECT_EQ(joined_paint_region.y(), 128.f); + EXPECT_EQ(joined_paint_region.width(), 320.f); + EXPECT_EQ(joined_paint_region.height(), 256.f); + // Pump the message loop. The scene updates should propogate to Scenic. + loop().RunUntilIdle(); + std::vector scenic_layers = + ExtractLayersFromSceneGraph(fake_session().SceneGraph()); + EXPECT_EQ(scenic_layers.size(), 1u); + ExpectImageCompositorLayer(scenic_layers[0], frame_size, + /* flutter layer index = */ 0, + {joined_paint_region}); +} + } // namespace flutter_runner::testing