diff --git a/shell/platform/fuchsia/flutter/BUILD.gn b/shell/platform/fuchsia/flutter/BUILD.gn index c4d126e84adf7..31dc61b1c0e45 100644 --- a/shell/platform/fuchsia/flutter/BUILD.gn +++ b/shell/platform/fuchsia/flutter/BUILD.gn @@ -452,6 +452,7 @@ executable("flutter_runner_unittests") { "platform_view_unittest.cc", "runner_unittest.cc", "tests/engine_unittests.cc", + "tests/fake_session_unittests.cc", "tests/flutter_runner_product_configuration_unittests.cc", "tests/gfx_session_connection_unittests.cc", ] diff --git a/shell/platform/fuchsia/flutter/tests/fake_session_unittests.cc b/shell/platform/fuchsia/flutter/tests/fake_session_unittests.cc new file mode 100644 index 0000000000000..c7526229bb5cf --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/fake_session_unittests.cc @@ -0,0 +1,459 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "fakes/scenic/fake_session.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "flutter/fml/logging.h" +#include "gmock/gmock.h" // For EXPECT_THAT and matchers +#include "gtest/gtest.h" + +#include "fakes/scenic/fake_resources.h" + +using ::testing::_; +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::FieldsAre; +using ::testing::IsEmpty; +using ::testing::Matcher; +using ::testing::Not; +using ::testing::Pair; +using ::testing::Pointee; +using ::testing::SizeIs; +using ::testing::VariantWith; + +namespace flutter_runner::testing { +namespace { + +std::string GetCurrentTestName() { + return ::testing::UnitTest::GetInstance()->current_test_info()->name(); +} + +zx_koid_t GetKoid(zx_handle_t handle) { + if (handle == ZX_HANDLE_INVALID) { + return ZX_KOID_INVALID; + } + + zx_info_handle_basic_t info; + zx_status_t status = zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, + sizeof(info), nullptr, nullptr); + return status == ZX_OK ? info.koid : ZX_KOID_INVALID; +} + +zx_koid_t GetPeerKoid(zx_handle_t handle) { + if (handle == ZX_HANDLE_INVALID) { + return ZX_KOID_INVALID; + } + + zx_info_handle_basic_t info; + zx_status_t status = zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, + sizeof(info), nullptr, nullptr); + return status == ZX_OK ? info.related_koid : ZX_KOID_INVALID; +} + +Matcher IsEntityNode( + Matcher id, + Matcher label, + Matcher children) { + return FieldsAre( + id, label, FakeResource::kDefaultEmptyEventMask, + VariantWith(FieldsAre( + FieldsAre(children, FakeNode::kDefaultZeroRotation, + FakeNode::kDefaultOneScale, + FakeNode::kDefaultZeroTranslation, + FakeNode::kDefaultZeroAnchor, FakeNode::kIsHitTestable, + FakeNode::kIsSemanticallyVisible), + IsEmpty()))); +} + +Matcher IsEmptySceneGraph() { + return FieldsAre(IsEmpty(), IsEmpty(), IsEmpty(), kInvalidFakeResourceId); +} + +MATCHER_P5(IsBasicSceneGraph, + debug_label, + node_label, + view_holder_koid, + view_ref_control_koid, + view_ref_koid, + "") { + static_assert(std::is_same_v>); + static_assert( + std::is_same_v>); + static_assert( + std::is_same_v>); + static_assert( + std::is_same_v>); + static_assert( + std::is_same_v>); + static_assert( + std::is_same_v>); + + return ExplainMatchResult( + FieldsAre( + IsEmpty(), + AllOf(SizeIs(2u), + Contains(Pair(arg.root_view_id, + Pointee(FieldsAre( + arg.root_view_id, "", + FakeResource::kDefaultEmptyEventMask, + VariantWith(FieldsAre( + view_holder_koid, view_ref_control_koid, + view_ref_koid, debug_label, + ElementsAre(Pointee(IsEntityNode( + _, node_label, IsEmpty()))), + FakeView::kDebugBoundsDisbaled))))))), + _, AllOf(Not(kInvalidFakeResourceId), arg.root_view_id)), + arg, result_listener); +} + +} // namespace + +class FakeSessionTest : public ::testing::Test, + public fuchsia::ui::scenic::SessionListener { + protected: + FakeSessionTest() + : session_listener_(this), session_subloop_(loop_.StartNewLoop()) {} + ~FakeSessionTest() override = default; + + async::TestLoop& loop() { return loop_; } + + FakeSession& fake_session() { return fake_session_; } + + scenic::Session CreateSession() { + FML_CHECK(!fake_session_.is_bound()); + FML_CHECK(!session_listener_.is_bound()); + + auto [session, session_listener] = + fake_session_.Bind(session_subloop_->dispatcher()); + session_listener_.Bind(std::move(session_listener)); + + return scenic::Session(session.Bind()); + } + + private: + // |fuchsia::ui::scenic::SessionListener| + void OnScenicError(std::string error) override { FML_CHECK(false); } + + // |fuchsia::ui::scenic::SessionListener| + void OnScenicEvent(std::vector events) override { + FML_CHECK(false); + } + + async::TestLoop loop_; // Must come before FIDL bindings. + + fuchsia::ui::scenic::SessionPtr session_ptr_; + fidl::Binding session_listener_; + + std::unique_ptr session_subloop_; + FakeSession fake_session_; +}; + +TEST_F(FakeSessionTest, Initialization) { + EXPECT_EQ(fake_session().debug_name(), ""); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); + + // Pump the loop one time; the session should retain its initial state. + loop().RunUntilIdle(); + EXPECT_EQ(fake_session().debug_name(), ""); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); +} + +TEST_F(FakeSessionTest, DebugLabel) { + const std::string debug_label = GetCurrentTestName(); + scenic::Session session = CreateSession(); + + // Set the session's debug name. The `SetDebugName` hasn't been processed + // yet, so the session's view of the debug name is still empty. + session.SetDebugName(debug_label); + EXPECT_EQ(fake_session().debug_name(), ""); + + // Pump the loop; the contents of the initial `SetDebugName` should be + // processed. + loop().RunUntilIdle(); + EXPECT_EQ(fake_session().debug_name(), debug_label); +} + +TEST_F(FakeSessionTest, SimpleResourceLifecycle) { + const std::string node_label = "EntityNode"; + scenic::Session session = CreateSession(); + + // Create entity node for testing; no creation commands have been processed + // yet, so the session's view of the scene graph is empty. + std::optional node(&session); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); + + // Pump the loop; the initial creation command should be enqueued but still + // not processed yet, so the session's view of the scene graph is empty. + loop().RunUntilIdle(); + EXPECT_GE(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); + + // Present initial scene graph. The `Present` hasn't been processed yet, so + // the session's view of the scene graph is still empty. + session.Present2(0u, 0u, [](auto...) {}); + EXPECT_GE(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); + + // Pump the loop; the contents of the initial `Present` should be processed. + loop().RunUntilIdle(); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + { + auto scene_graph = fake_session().SceneGraph(); + EXPECT_EQ(scene_graph.root_view_id, kInvalidFakeResourceId); + EXPECT_EQ(scene_graph.buffer_collection_map.size(), 0u); + EXPECT_EQ(scene_graph.resource_map.size(), 1u); + EXPECT_EQ(scene_graph.labels_map.size(), 1u); + ASSERT_EQ(scene_graph.labels_map.count(""), 1u); + + const auto first_resource_it = scene_graph.resource_map.begin(); + const auto null_label_resources = scene_graph.labels_map[""]; + EXPECT_EQ(null_label_resources.size(), 1u); + EXPECT_FALSE(null_label_resources[0].expired()); + EXPECT_EQ(null_label_resources[0].lock(), first_resource_it->second); + EXPECT_THAT(first_resource_it->second, + Pointee(IsEntityNode(_, "", IsEmpty()))); + } + + // Present a simple property update on the test entity node. + node->SetLabel(node_label); + session.Present2(0u, 0u, [](auto...) {}); + loop().RunUntilIdle(); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + { + auto scene_graph = fake_session().SceneGraph(); + EXPECT_EQ(scene_graph.root_view_id, kInvalidFakeResourceId); + EXPECT_EQ(scene_graph.buffer_collection_map.size(), 0u); + EXPECT_EQ(scene_graph.resource_map.size(), 1u); + EXPECT_EQ(scene_graph.labels_map.size(), 1u); + ASSERT_EQ(scene_graph.labels_map.count(""), 0u); + ASSERT_EQ(scene_graph.labels_map.count(node_label), 1u); + + const auto first_resource_it = scene_graph.resource_map.begin(); + const auto node_label_resources = scene_graph.labels_map[node_label]; + EXPECT_EQ(node_label_resources.size(), 1u); + EXPECT_FALSE(node_label_resources[0].expired()); + EXPECT_EQ(node_label_resources[0].lock(), first_resource_it->second); + EXPECT_THAT(first_resource_it->second, + Pointee(IsEntityNode(_, node_label, IsEmpty()))); + } + + // Present the destruction of the entity node. + node.reset(); + session.Present2(0u, 0u, [](auto...) {}); + loop().RunUntilIdle(); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); +} + +TEST_F(FakeSessionTest, ResourceReferenceCounting) { + const std::string node_label = "EntityNode"; + scenic::Session session = CreateSession(); + + // Present a chain of 4 entity nodes for testing. + std::array, 4> nodes{ + std::optional(&session), + std::optional(&session), + std::optional(&session), + std::optional(&session)}; + for (size_t i = 0; i < 4; i++) { + nodes[i]->SetLabel(node_label + std::string(1, '0' + i)); + if (i < 3) { + nodes[i]->AddChild(*nodes[i + 1]); + } + } + session.Present2(0u, 0u, [](auto...) {}); + loop().RunUntilIdle(); + { + auto scene_graph = fake_session().SceneGraph(); + EXPECT_EQ(scene_graph.root_view_id, kInvalidFakeResourceId); + EXPECT_EQ(scene_graph.buffer_collection_map.size(), 0u); + EXPECT_EQ(scene_graph.resource_map.size(), 4u); + EXPECT_EQ(scene_graph.labels_map.size(), 4u); + for (size_t i = 0; i < 4; i++) { + const std::string node_i_label = node_label + std::string(1, '0' + i); + ASSERT_EQ(scene_graph.resource_map.count(nodes[i]->id()), 1u); + ASSERT_EQ(scene_graph.labels_map.count(node_i_label), 1u); + + const auto node_i = scene_graph.resource_map[nodes[i]->id()]; + const auto node_i_label_resources = scene_graph.labels_map[node_i_label]; + EXPECT_EQ(node_i_label_resources.size(), 1u); + EXPECT_FALSE(node_i_label_resources[0].expired()); + EXPECT_EQ(node_i_label_resources[0].lock(), node_i); + } + + EXPECT_THAT( + scene_graph.resource_map[nodes[0]->id()], + Pointee(IsEntityNode( + nodes[0]->id(), node_label + std::string(1, '0'), + ElementsAre(Pointee(IsEntityNode( + nodes[1]->id(), node_label + std::string(1, '1'), + ElementsAre(Pointee(IsEntityNode( + nodes[2]->id(), node_label + std::string(1, '2'), + ElementsAre(Pointee(IsEntityNode( + nodes[3]->id(), node_label + std::string(1, '3'), + IsEmpty())))))))))))); + } + + // Destroy node #0. It should be dropped immediately since it has no parent. + nodes[0].reset(); + session.Present2(0u, 0u, [](auto...) {}); + loop().RunUntilIdle(); + { + auto scene_graph = fake_session().SceneGraph(); + EXPECT_EQ(scene_graph.root_view_id, kInvalidFakeResourceId); + EXPECT_EQ(scene_graph.buffer_collection_map.size(), 0u); + EXPECT_EQ(scene_graph.resource_map.size(), 3u); + EXPECT_EQ(scene_graph.labels_map.size(), 3u); + for (size_t i = 1; i < 4; i++) { + const std::string node_i_label = node_label + std::string(1, '0' + i); + ASSERT_EQ(scene_graph.resource_map.count(nodes[i]->id()), 1u); + ASSERT_EQ(scene_graph.labels_map.count(node_i_label), 1u); + + const auto node_i = scene_graph.resource_map[nodes[i]->id()]; + const auto node_i_label_resources = scene_graph.labels_map[node_i_label]; + EXPECT_EQ(node_i_label_resources.size(), 1u); + EXPECT_FALSE(node_i_label_resources[0].expired()); + EXPECT_EQ(node_i_label_resources[0].lock(), node_i); + } + + EXPECT_EQ(scene_graph.resource_map.count(nodes[0]->id()), 0u); + EXPECT_THAT(scene_graph.resource_map[nodes[1]->id()], + Pointee(IsEntityNode( + nodes[1]->id(), node_label + std::string(1, '1'), + ElementsAre(Pointee(IsEntityNode( + nodes[2]->id(), node_label + std::string(1, '2'), + ElementsAre(Pointee(IsEntityNode( + nodes[3]->id(), node_label + std::string(1, '3'), + IsEmpty()))))))))); + } + + // Destroy node #2. It should still exist in the tree and the labels map + // because it has a parent, but it is removed from the resource map. + nodes[2].reset(); + session.Present2(0u, 0u, [](auto...) {}); + loop().RunUntilIdle(); + { + auto scene_graph = fake_session().SceneGraph(); + EXPECT_EQ(scene_graph.root_view_id, kInvalidFakeResourceId); + EXPECT_EQ(scene_graph.buffer_collection_map.size(), 0u); + EXPECT_EQ(scene_graph.resource_map.size(), 2u); + EXPECT_EQ(scene_graph.labels_map.size(), 3u); + for (size_t i = 1; i < 4; i++) { + const std::string node_i_label = node_label + std::string(1, '0' + i); + ASSERT_EQ(scene_graph.labels_map.count(node_i_label), 1u); + ASSERT_EQ(scene_graph.resource_map.count(nodes[i]->id()), + i != 2 ? 1u : 0u); + + const auto node_i_label_resources = scene_graph.labels_map[node_i_label]; + EXPECT_EQ(node_i_label_resources.size(), 1u); + EXPECT_FALSE(node_i_label_resources[0].expired()); + + if (i != 2) { + const auto node_i = scene_graph.resource_map[nodes[i]->id()]; + EXPECT_EQ(node_i_label_resources[0].lock(), node_i); + } else { + EXPECT_EQ(scene_graph.resource_map.count(nodes[i]->id()), 0u); + } + } + + EXPECT_THAT(scene_graph.resource_map[nodes[1]->id()], + Pointee(IsEntityNode( + nodes[1]->id(), node_label + std::string(1, '1'), + ElementsAre(Pointee(IsEntityNode( + nodes[2]->id(), node_label + std::string(1, '2'), + ElementsAre(Pointee(IsEntityNode( + nodes[3]->id(), node_label + std::string(1, '3'), + IsEmpty()))))))))); + } + + // Destroy node #3. It should still exist in the tree and the labels map + // because it has a grand-parent, but it is removed from the resource map. + nodes[3].reset(); + session.Present2(0u, 0u, [](auto...) {}); + loop().RunUntilIdle(); + { + auto scene_graph = fake_session().SceneGraph(); + EXPECT_EQ(scene_graph.root_view_id, kInvalidFakeResourceId); + EXPECT_EQ(scene_graph.buffer_collection_map.size(), 0u); + EXPECT_EQ(scene_graph.resource_map.size(), 1u); + EXPECT_EQ(scene_graph.labels_map.size(), 3u); + for (size_t i = 1; i < 4; i++) { + const std::string node_i_label = node_label + std::string(1, '0' + i); + ASSERT_EQ(scene_graph.labels_map.count(node_i_label), 1u); + ASSERT_EQ(scene_graph.resource_map.count(nodes[i]->id()), + i < 2 ? 1u : 0u); + + const auto node_i_label_resources = scene_graph.labels_map[node_i_label]; + EXPECT_EQ(node_i_label_resources.size(), 1u); + EXPECT_FALSE(node_i_label_resources[0].expired()); + + if (i < 2) { + const auto node_i = scene_graph.resource_map[nodes[i]->id()]; + EXPECT_EQ(node_i_label_resources[0].lock(), node_i); + } else { + EXPECT_EQ(scene_graph.resource_map.count(nodes[i]->id()), 0u); + } + } + + EXPECT_THAT(scene_graph.resource_map[nodes[1]->id()], + Pointee(IsEntityNode( + nodes[1]->id(), node_label + std::string(1, '1'), + ElementsAre(Pointee(IsEntityNode( + nodes[2]->id(), node_label + std::string(1, '2'), + ElementsAre(Pointee(IsEntityNode( + nodes[3]->id(), node_label + std::string(1, '3'), + IsEmpty()))))))))); + } +} + +TEST_F(FakeSessionTest, BasicSceneGraph) { + const std::string debug_label = GetCurrentTestName(); + const std::string node_label = "ChildNode"; + scenic::Session session = CreateSession(); + + // Create and present initial scene graph. The `Present` hasn't been + // processed yet, so the session's view of the scene graph is still empty. + fuchsia::ui::views::ViewRef view_ref; + auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); + auto view_ref_pair = scenic::ViewRefPair::New(); + view_ref_pair.view_ref.Clone(&view_ref); + scenic::View root_view(&session, std::move(view_token), + std::move(view_ref_pair.control_ref), + std::move(view_ref_pair.view_ref), debug_label); + scenic::EntityNode child_node(&session); + child_node.SetLabel(node_label); + root_view.AddChild(child_node); + session.Present2(0u, 0u, [](auto...) {}); + EXPECT_GE(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), IsEmptySceneGraph()); + + // Pump the loop; the contents of the initial `Present` should be processed. + loop().RunUntilIdle(); + EXPECT_EQ(fake_session().command_queue().size(), 0u); + EXPECT_THAT(fake_session().SceneGraph(), + IsBasicSceneGraph(debug_label, node_label, + GetPeerKoid(view_holder_token.value.get()), + GetPeerKoid(view_ref.reference.get()), + GetKoid(view_ref.reference.get()))); +} + +} // namespace flutter_runner::testing diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn b/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn index 0888597d26f51..1fbf1e07172e5 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/BUILD.gn @@ -8,14 +8,19 @@ source_set("scenic") { testonly = true sources = [ + "fake_resources.cc", + "fake_resources.h", "fake_session.cc", "fake_session.h", ] public_deps = [ + "//build/fuchsia/fidl:fuchsia.images", "//build/fuchsia/fidl:fuchsia.scenic.scheduling", + "//build/fuchsia/fidl:fuchsia.ui.composition", "//build/fuchsia/fidl:fuchsia.ui.gfx", "//build/fuchsia/fidl:fuchsia.ui.scenic", + "//build/fuchsia/fidl:fuchsia.ui.views", "//build/fuchsia/pkg:async-cpp", "//build/fuchsia/pkg:async-testing", "//build/fuchsia/pkg:fidl_cpp", diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.cc new file mode 100644 index 0000000000000..2f7ca047a01bd --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.cc @@ -0,0 +1,315 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "fake_resources.h" + +#include "flutter/fml/logging.h" + +namespace flutter_runner::testing { +namespace { + +using FakeResourceCache = + std::unordered_map>; + +std::shared_ptr ResourceFromState( + const std::shared_ptr& resource, + FakeResourceCache& cache); +std::vector> ResourcesFromStates( + const std::vector>& resources, + FakeResourceCache& cache); + +FakeNode NodeFromState(FakeNodeState* node, FakeResourceCache& cache) { + FML_CHECK(node); + + return FakeNode{ + .children = ResourcesFromStates(node->children, cache), + .rotation_quaternion = node->rotation_quaternion, + .scale_vector = node->scale_vector, + .translation_vector = node->translation_vector, + .anchor_vector = node->anchor_vector, + .hit_testable = node->hit_testable, + .semantically_visible = node->semantically_visible, + }; +} + +FakeEntityNode EntityNodeFromState(FakeEntityNodeState* entity_node, + FakeResourceCache& cache) { + FML_CHECK(entity_node); + + // Convert clip planes. + std::vector clip_planes; + for (auto& clip_plane : entity_node->clip_planes) { + clip_planes.emplace_back(FakeEntityNode::ClipPlane{ + .dir = clip_plane.dir, + .dist = clip_plane.dist, + }); + } + + return FakeEntityNode{ + .node_state = NodeFromState(&entity_node->node_state, cache), + .clip_planes = std::move(clip_planes), + }; +} + +FakeOpacityNode OpacityNodeFromState(FakeOpacityNodeState* opacity_node, + FakeResourceCache& cache) { + FML_CHECK(opacity_node); + + return FakeOpacityNode{ + .node_state = NodeFromState(&opacity_node->node_state, cache), + .opacity = opacity_node->opacity, + }; +} + +FakeShapeNode ShapeNodeFromState(FakeShapeNodeState* shape_node, + FakeResourceCache& cache) { + FML_CHECK(shape_node); + + return FakeShapeNode{ + .node_state = NodeFromState(&shape_node->node_state, cache), + .shape = ResourceFromState(shape_node->shape, cache), + .material = ResourceFromState(shape_node->material, cache), + }; +} + +FakeView ViewFromState(FakeViewState* view, FakeResourceCache& cache) { + FML_CHECK(view); + + return FakeView{ + .token = view->token.koid, + .control_ref = view->control_ref.koid, + .view_ref = view->view_ref.koid, + .debug_name = view->debug_name, + .children = ResourcesFromStates(view->children, cache), + .enable_debug_bounds = view->enable_debug_bounds, + }; +} + +FakeViewHolder ViewHolderFromState(FakeViewHolderState* view_holder, + FakeResourceCache& cache) { + FML_CHECK(view_holder); + + return FakeViewHolder{ + .token = view_holder->token.koid, + .debug_name = view_holder->debug_name, + .properties = view_holder->properties, + .bounds_color = view_holder->bounds_color, + }; +} + +FakeShape ShapeFromState(FakeShapeState* shape, FakeResourceCache& cache) { + FML_CHECK(shape); + + auto snapshot = FakeShape{}; + std::visit( + [&snapshot](auto&& shape_def) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + snapshot.shape_def = FakeShape::CircleDef{ + .radius = shape_def.radius, + }; + } else if constexpr (std::is_same_v) { + snapshot.shape_def = FakeShape::RectangleDef{ + .width = shape_def.width, + .height = shape_def.height, + }; + } else if constexpr (std::is_same_v< + T, FakeShapeState::RoundedRectangleDef>) { + snapshot.shape_def = FakeShape::RoundedRectangleDef{ + .width = shape_def.width, + .height = shape_def.height, + .top_left_radius = shape_def.top_left_radius, + .top_right_radius = shape_def.top_right_radius, + .bottom_right_radius = shape_def.bottom_right_radius, + .bottom_left_radius = shape_def.bottom_left_radius, + }; + } else { + FML_CHECK(false); + } + }, + shape->shape_def); + + return snapshot; +} + +FakeMaterial MaterialFromState(FakeMaterialState* material, + FakeResourceCache& cache) { + FML_CHECK(material); + + return FakeMaterial{ + .image = ResourceFromState(material->image, cache), + .color = material->color, + }; +} + +FakeImage ImageFromState(FakeImageState* image, FakeResourceCache& cache) { + FML_CHECK(image); + + auto snapshot = FakeImage{ + .memory = ResourceFromState(image->memory, cache), + }; + std::visit( + [&snapshot](auto&& image_def) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + snapshot.image_def = FakeImage::ImageDef{ + .info = image_def.info, + .memory_offset = image_def.memory_offset, + }; + } else if constexpr (std::is_same_v) { + snapshot.image_def = FakeImage::Image2Def{ + .buffer_collection_id = image_def.buffer_collection_id, + .buffer_collection_index = image_def.buffer_collection_index, + .width = image_def.width, + .height = image_def.height, + }; + } else if constexpr (std::is_same_v) { + snapshot.image_def = FakeImage::Image3Def{ + .import_token = image_def.import_token.koid, + .buffer_collection_index = image_def.buffer_collection_index, + .width = image_def.width, + .height = image_def.height, + }; + } else if constexpr (std::is_same_v) { + snapshot.image_def = FakeImage::ImagePipeDef{ + .image_pipe_request = image_def.image_pipe_request.koid, + }; + } else if constexpr (std::is_same_v) { + snapshot.image_def = FakeImage::ImagePipe2Def{ + .image_pipe_request = image_def.image_pipe_request.koid, + }; + } else { + FML_CHECK(false); + } + }, + image->image_def); + + return snapshot; +} + +FakeMemory MemoryFromState(FakeMemoryState* memory, FakeResourceCache& cache) { + FML_CHECK(memory); + + return FakeMemory{ + .vmo = memory->vmo.koid, + .allocation_size = memory->allocation_size, + .is_device_memory = memory->is_device_memory, + }; +} + +std::shared_ptr ResourceFromState( + const std::shared_ptr& resource, + FakeResourceCache& cache) { + if (!resource) { + return std::shared_ptr(); + } + + // Try to hit the cache first... + auto cache_it = cache.find(resource.get()); + if (cache_it != cache.end()) { + return cache_it->second; + } + + // Otherwise create a brand-new snapshot. + std::shared_ptr snapshot = + std::make_shared(FakeResource{ + .id = resource->id, + .label = resource->label, + .event_mask = resource->event_mask, + }); + std::visit( + [&snapshot, &resource, &cache](auto&& state) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + snapshot->state = EntityNodeFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = OpacityNodeFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = ShapeNodeFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = ViewFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = ViewHolderFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = ShapeFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = MaterialFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = ImageFromState( + std::get_if(&resource->state), cache); + } else if constexpr (std::is_same_v) { + snapshot->state = MemoryFromState( + std::get_if(&resource->state), cache); + } else { + FML_CHECK(false); + } + }, + resource->state); + auto [_, cache_success] = + cache.emplace(std::make_pair(resource.get(), snapshot)); + FML_CHECK(cache_success); + + return snapshot; +} + +std::vector> ResourcesFromStates( + const std::vector>& resources, + FakeResourceCache& cache) { + std::vector> snapshots; + + for (auto& resource : resources) { + snapshots.emplace_back(ResourceFromState(resource, cache)); + } + return snapshots; +} + +} // namespace + +FakeSceneGraph SceneGraphFromState(const FakeSceneGraphState& state) { + FakeResourceCache resource_cache; + FakeSceneGraph scene_graph; + + // Snapshot all buffer collections. + for (auto& buffer_collection : state.buffer_collection_map) { + scene_graph.buffer_collection_map.emplace( + std::make_pair(buffer_collection.first, buffer_collection.second.koid)); + } + + // Snapshot resources in the map recursively. + for (auto& resource : state.resource_map) { + scene_graph.resource_map.emplace(std::make_pair( + resource.first, ResourceFromState(resource.second, resource_cache))); + } + + // Snapshot labels in the map. + for (auto& label_resources : state.labels_map) { + auto [label_iter, label_success] = + scene_graph.labels_map.emplace(std::make_pair( + label_resources.first, std::vector>())); + FML_CHECK(label_success); + auto& snapshot_label_resources = label_iter->second; + + for (auto& resource : label_resources.second) { + auto resource_ptr = resource.lock(); + FML_CHECK(resource_ptr); + + snapshot_label_resources.emplace_back( + ResourceFromState(resource_ptr, resource_cache)); + } + } + + // Snapshot the view id. + scene_graph.root_view_id = state.root_view_id; + + return scene_graph; +} + +} // namespace flutter_runner::testing diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.h new file mode 100644 index 0000000000000..f737b8511cca2 --- /dev/null +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.h @@ -0,0 +1,555 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_RESOURCES_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_RESOURCES_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +inline bool operator==(const fuchsia::images::ImageInfo& a, + const fuchsia::images::ImageInfo& b) { + return a.height == b.height && a.width == b.width && a.stride == b.stride && + a.tiling == b.tiling && a.transform == b.transform && + a.alpha_format == b.alpha_format && a.pixel_format == b.pixel_format && + a.color_space == b.color_space; +} + +inline bool operator==(const fuchsia::ui::gfx::vec3& a, + const fuchsia::ui::gfx::vec3& b) { + return a.x == b.x && a.y == b.y && a.z == b.z; +} + +inline bool operator==(const fuchsia::ui::gfx::BoundingBox& a, + const fuchsia::ui::gfx::BoundingBox& b) { + return a.min == b.min && a.max == b.max; +} + +inline bool operator==(const fuchsia::ui::gfx::ViewProperties& a, + const fuchsia::ui::gfx::ViewProperties& b) { + return a.downward_input == b.downward_input && + a.focus_change == b.focus_change && a.bounding_box == b.bounding_box && + a.inset_from_min == b.inset_from_min && + a.inset_from_max == b.inset_from_max; +} + +namespace flutter_runner::testing { + +// Forward declarations +template +struct FakeResourceT; + +// Unique (within a Session) identifier for a Resource. +using FakeResourceId = decltype(fuchsia::ui::gfx::CreateResourceCmd::id); +constexpr FakeResourceId kInvalidFakeResourceId = 0u; + +// Tag type for a Resource "state". The Resource state keeps alive any handles +// associated with a resource e.g. view token or sysmem token. +// +// "snapshot" Resources are generated from "state" Resources during +// `SceneGraphFromState()` calls. +// +// The `FakeSession` stores `FakeResourceT` internally. +struct StateT { + using ResourceT = FakeResourceT; + + template + struct HandleT { + T value; + zx_koid_t koid; + }; + + template + static bool HandlesAreEqual(const HandleT& token, + const HandleT& other_token) { + return token.koid == other_token.koid; + } +}; + +// Tag type for a Resource "snapshot". The Resource snapshot only stores koids +// for any handles assocaites with the resource; in this way it doesn't have any +// control over the underlying handle lifetime. +// +// "snapshot" Resources are generated from "state" Resources during +// `SceneGraphFromState()` calls. +// +// The `FakeSession` returns `FakeResourceT` from its `SceneGraph()` +// accessor. +struct SnapshotT { + using ResourceT = FakeResourceT; + + template + using HandleT = zx_koid_t; + + template + static bool HandlesAreEqual(const HandleT& token, + const HandleT& other_token) { + return token == other_token; + } +}; + +// Common state for Node-typed Resources: EntityNode, OpacityNode, ShapeNode, +// ViewHolder. +// +// FakeNodeT's are never used directly, only as an embedded field of a +// Node-typed resource. +template +struct FakeNodeT { + bool operator==(const FakeNodeT& other) const; + + constexpr static std::array kDefaultZeroRotation{0.f, 0.f, 0.f, + 1.f}; + constexpr static std::array kDefaultOneScale{1.f, 1.f, 1.f}; + constexpr static std::array kDefaultZeroTranslation{0.f, 0.f, 0.f}; + constexpr static std::array kDefaultZeroAnchor{0.f, 0.f, 0.f}; + constexpr static bool kIsHitTestable{true}; + constexpr static bool kIsNotHitTestable{false}; + constexpr static bool kIsSemanticallyVisible{true}; + constexpr static bool kIsNotSemanticallyVisible{false}; + + std::vector> children; + std::array rotation_quaternion{kDefaultZeroRotation}; + std::array scale_vector{kDefaultOneScale}; + std::array translation_vector{kDefaultZeroTranslation}; + std::array anchor_vector{kDefaultZeroAnchor}; + bool hit_testable{kIsHitTestable}; + bool semantically_visible{kIsSemanticallyVisible}; +}; + +using FakeNodeState = FakeNodeT; +using FakeNode = FakeNodeT; + +// EntityNode Resource state. +template +struct FakeEntityNodeT { + struct ClipPlane { + bool operator==(const ClipPlane& other) const; + + constexpr static std::array kDefaultZeroDir{0.f, 0.f, 0.f}; + constexpr static float kDefaultZeroDist{0.f}; + + std::array dir{kDefaultZeroDir}; + float dist{kDefaultZeroDist}; + }; + + bool operator==(const FakeEntityNodeT& other) const; + + FakeNodeT node_state; + + std::vector clip_planes; +}; +using FakeEntityNodeState = FakeEntityNodeT; +using FakeEntityNode = FakeEntityNodeT; + +// OpacityNode Resource state. +template +struct FakeOpacityNodeT { + bool operator==(const FakeOpacityNodeT& other) const; + + FakeNodeT node_state; + + constexpr static float kDefaultOneOpacity{1.f}; + + float opacity{kDefaultOneOpacity}; +}; +using FakeOpacityNodeState = FakeOpacityNodeT; +using FakeOpacityNode = FakeOpacityNodeT; + +// ShapeNode Resource state. +template +struct FakeShapeNodeT { + bool operator==(const FakeShapeNodeT& other) const; + + FakeNodeT node_state; + + std::shared_ptr shape; + std::shared_ptr material; +}; +using FakeShapeNodeState = FakeShapeNodeT; +using FakeShapeNode = FakeShapeNodeT; + +// View Resource state. +template +struct FakeViewT { + bool operator==(const FakeViewT& other) const; + + constexpr static bool kDebugBoundsEnabled{true}; + constexpr static bool kDebugBoundsDisbaled{false}; + + typename S::template HandleT token{}; + typename S::template HandleT + control_ref{}; + typename S::template HandleT view_ref{}; + std::string debug_name{}; + + std::vector> children; + bool enable_debug_bounds{kDebugBoundsDisbaled}; +}; +using FakeViewState = FakeViewT; +using FakeView = FakeViewT; + +// ViewHolder Resource state. +template +struct FakeViewHolderT { + bool operator==(const FakeViewHolderT& other) const; + + FakeNodeT node_state; + + constexpr static std::array kDefaultBoundsColorWhite{1.f, 1.f, 1.f, + 1.f}; + typename S::template HandleT token{}; + std::string debug_name{}; + + fuchsia::ui::gfx::ViewProperties properties; + std::array bounds_color{kDefaultBoundsColorWhite}; +}; +using FakeViewHolderState = FakeViewHolderT; +using FakeViewHolder = FakeViewHolderT; + +// Shape Resource state. +template +struct FakeShapeT { + bool operator==(const FakeShapeT& other) const; + + struct CircleDef { + bool operator==(const CircleDef& other) const; + + float radius{0.f}; + }; + + struct RectangleDef { + bool operator==(const RectangleDef& other) const; + + float width{0.f}; + float height{0.f}; + }; + + struct RoundedRectangleDef { + bool operator==(const RoundedRectangleDef& other) const; + + float width{0.f}; + float height{0.f}; + float top_left_radius{0.f}; + float top_right_radius{0.f}; + float bottom_right_radius{0.f}; + float bottom_left_radius{0.f}; + }; + + std::variant shape_def; +}; +using FakeShapeState = FakeShapeT; +using FakeShape = FakeShapeT; + +// Material Resource state. +template +struct FakeMaterialT { + bool operator==(const FakeMaterialT& other) const; + + constexpr static std::array kDefaultColorWhite{1.f, 1.f, 1.f, 1.f}; + + std::shared_ptr image; + std::array color{kDefaultColorWhite}; +}; +using FakeMaterialState = FakeMaterialT; +using FakeMaterial = FakeMaterialT; + +// Image Resource state. +template +struct FakeImageT { + struct ImageDef { + bool operator==(const ImageDef& other) const; + + fuchsia::images::ImageInfo info{}; + uint32_t memory_offset{}; + }; + + struct Image2Def { + bool operator==(const Image2Def& other) const; + + uint32_t buffer_collection_id{}; + uint32_t buffer_collection_index{}; + uint32_t width{}; + uint32_t height{}; + }; + + struct Image3Def { + bool operator==(const Image3Def& other) const; + + typename S::template HandleT< + fuchsia::ui::composition::BufferCollectionImportToken> + import_token{}; + uint32_t buffer_collection_index{}; + uint32_t width{}; + uint32_t height{}; + }; + + struct ImagePipeDef { + bool operator==(const ImagePipeDef& other) const; + + typename S::template HandleT< + fidl::InterfaceRequest> + image_pipe_request{}; + }; + + struct ImagePipe2Def { + bool operator==(const ImagePipe2Def& other) const; + + typename S::template HandleT< + fidl::InterfaceRequest> + image_pipe_request{}; + }; + + bool operator==(const FakeImageT& other) const; + + std::variant + image_def; + std::shared_ptr memory; +}; +using FakeImageState = FakeImageT; +using FakeImage = FakeImageT; + +// Memory Resource state. +template +struct FakeMemoryT { + bool operator==(const FakeMemoryT& other) const; + + constexpr static bool kIsDeviceMemory{true}; + constexpr static bool kIsNotDeviceMemory{false}; + + typename S::template HandleT vmo{}; + uint64_t allocation_size{}; + bool is_device_memory{kIsNotDeviceMemory}; +}; +using FakeMemoryState = FakeMemoryT; +using FakeMemory = FakeMemoryT; + +// A complete Resource which records common Resource data and stores it's +// type-specific state inside of a variant. +template +struct FakeResourceT { + bool operator==(const FakeResourceT& other) const; + + constexpr static uint32_t kDefaultEmptyEventMask{0}; + + FakeResourceId id{kInvalidFakeResourceId}; + + std::string label{}; + uint32_t event_mask{kDefaultEmptyEventMask}; + + std::variant, + FakeOpacityNodeT, + FakeShapeNodeT, + FakeViewT, + FakeViewHolderT, + FakeShapeT, + FakeMaterialT, + FakeImageT, + FakeMemoryT> + state; +}; +using FakeResourceState = FakeResourceT; +using FakeResource = FakeResourceT; + +// A complete scene graph which records a forest of Resource trees. +// +// It also records auxiliary data like buffer collection IDs and resource labels +// for fast lookup. +// +// Each Session / scene graph may only have a single View Resource which is +// treated as the root of that scene. The root View and all Resources +// descending from it are what the real scenic implementation would submit for +// rendering. +template +struct FakeSceneGraphT { + bool operator==(const FakeSceneGraphT& other) const; + + std::unordered_map>> + buffer_collection_map; + + std::unordered_map>> + resource_map; + std::unordered_map>>> + labels_map; + FakeResourceId root_view_id{kInvalidFakeResourceId}; +}; +using FakeSceneGraphState = FakeSceneGraphT; +using FakeSceneGraph = FakeSceneGraphT; + +// Generate a snapshot of a scene graph from that scene graph's state. +// +// The lifetime of the snapshot and its Resources has no influence on the +// lifetime of the source scene graph or its Resources. +FakeSceneGraph SceneGraphFromState(const FakeSceneGraphState& state); + +template +bool FakeEntityNodeT::ClipPlane::operator==( + const FakeEntityNodeT::ClipPlane& other) const { + return dir == other.dir && dist == other.dist; +} + +template +bool FakeNodeT::operator==(const FakeNodeT& other) const { + return children == other.children && + rotation_quaternion == other.rotation_quaternion && + scale_vector == other.scale_vector && + translation_vector == other.translation_vector && + anchor_vector == other.anchor_vector && + hit_testable == other.hit_testable && + semantically_visible == other.semantically_visible; +} + +template +bool FakeEntityNodeT::operator==(const FakeEntityNodeT& other) const { + return node_state == other.node_state && clip_planes == other.clip_planes; +} + +template +bool FakeOpacityNodeT::operator==(const FakeOpacityNodeT& other) const { + return node_state == other.node_state && opacity == other.opacity; +} + +template +bool FakeShapeNodeT::operator==(const FakeShapeNodeT& other) const { + return node_state == other.node_state && shape == other.shape && + material == other.material; +} + +template +bool FakeViewT::operator==(const FakeViewT& other) const { + return S::template HandlesAreEqual( + token, other.token) && + S::template HandlesAreEqual( + control_ref, other.control_ref) && + S::template HandlesAreEqual( + view_ref, other.view_ref) && + children == other.children && debug_name == other.debug_name && + enable_debug_bounds == other.enable_debug_bounds; +} + +template +bool FakeViewHolderT::operator==(const FakeViewHolderT& other) const { + return FakeNodeT::operator==(other) && + S::template HandlesAreEqual( + token, other.token) && + debug_name == other.debug_name && properties == other.properties && + bounds_color == other.bounds_color; +} + +template +bool FakeShapeT::CircleDef::operator==( + const FakeShapeT::CircleDef& other) const { + return radius == other.radius; +} + +template +bool FakeShapeT::RectangleDef::operator==( + const FakeShapeT::RectangleDef& other) const { + return width == other.width && height == other.height; +} + +template +bool FakeShapeT::RoundedRectangleDef::operator==( + const FakeShapeT::RoundedRectangleDef& other) const { + return width == other.width && height == other.height && + top_left_radius == other.top_left_radius && + top_right_radius == other.top_right_radius && + bottom_right_radius == other.bottom_right_radius && + bottom_left_radius == other.bottom_left_radius; +} + +template +bool FakeShapeT::operator==(const FakeShapeT& other) const { + return shape_def == other.shape_def; +} + +template +bool FakeMaterialT::operator==(const FakeMaterialT& other) const { + return image == other.image && color == other.color; +} + +template +bool FakeImageT::ImageDef::operator==( + const FakeImageT::ImageDef& other) const { + return info == other.info && memory_offset == other.memory_offset; +} + +template +bool FakeImageT::Image2Def::operator==( + const FakeImageT::Image2Def& other) const { + return buffer_collection_id == other.buffer_collection_id && + buffer_collection_index == other.buffer_collection_index && + width == other.width && height == other.height; +} + +template +bool FakeImageT::Image3Def::operator==( + const FakeImageT::Image3Def& other) const { + return S::template HandlesAreEqual< + fuchsia::ui::composition::BufferCollectionImportToken>( + import_token, other.import_token) && + buffer_collection_index == other.buffer_collection_index && + width == other.width && height == other.height; +} + +template +bool FakeImageT::ImagePipeDef::operator==( + const FakeImageT::ImagePipeDef& other) const { + return S::template HandlesAreEqual< + fidl::InterfaceRequest>( + image_pipe_request, other.image_pipe_request); +} + +template +bool FakeImageT::ImagePipe2Def::operator==( + const FakeImageT::ImagePipe2Def& other) const { + return S::template HandlesAreEqual< + fidl::InterfaceRequest>( + image_pipe_request, other.image_pipe_request); +} + +template +bool FakeImageT::operator==(const FakeImageT& other) const { + return image_def == other.image_def && memory == other.memory; +} + +template +bool FakeMemoryT::operator==(const FakeMemoryT& other) const { + return S::template HandlesAreEqual(vmo, other.vmo) && + allocation_size == other.allocation_size && + is_device_memory == other.is_device_memory; +} + +template +bool FakeResourceT::operator==(const FakeResourceT& other) const { + return id == other.id && label == other.label && + event_mask == other.event_mask && state == other.state; +} + +template +bool FakeSceneGraphT::operator==(const FakeSceneGraphT& other) const { + return buffer_collection_map == other.buffer_collection_map && + resource_map == + other.resource_map && // labels_map == other.labels_map && + root_view_id == other.root_view_id; +} + +} // namespace flutter_runner::testing + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_TESTS_FAKES_SCENIC_FAKE_RESOURCES_H_ diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.cc index 1b5599a614b16..0e248fdd2e25a 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.cc +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.cc @@ -4,12 +4,49 @@ #include "fake_session.h" -#include // For make_move_iterator +#include + +#include // For remove_if +#include // For make_move_iterator +#include #include "flutter/fml/logging.h" -#include "lib/async/dispatcher.h" +#include "flutter/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_resources.h" +#include "fuchsia/images/cpp/fidl.h" namespace flutter_runner::testing { +namespace { + +template +constexpr bool is_node_v = std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; + +template +bool ResourceIs(const FakeResourceState& resource) { + return std::holds_alternative(resource.state); +} + +bool ResourceIsNode(const FakeResourceState& resource) { + return ResourceIs(resource) || + ResourceIs(resource) || + ResourceIs(resource) || + ResourceIs(resource); +} + +zx_koid_t GetKoid(zx_handle_t handle) { + if (handle == ZX_HANDLE_INVALID) { + return ZX_KOID_INVALID; + } + + zx_info_handle_basic_t info; + zx_status_t status = zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, + sizeof(info), nullptr, nullptr); + return status == ZX_OK ? info.koid : ZX_KOID_INVALID; +} + +} // namespace FakeSession::FakeSession() : binding_(this) {} @@ -52,6 +89,10 @@ void FakeSession::DisconnectSession() { listener_.Unbind(); } +void FakeSession::NotImplemented_(const std::string& name) { + FML_LOG(FATAL) << "FakeSession does not implement " << name; +} + void FakeSession::Enqueue(std::vector cmds) { // Append `cmds` to the end of the command queue, preferring to move elements // when possible. @@ -64,11 +105,12 @@ void FakeSession::Present(uint64_t presentation_time, std::vector acquire_fences, std::vector release_fences, PresentCallback callback) { - if (!present_handler_) { - return NotImplemented_("Present"); - } + ApplyCommands(); - command_queue_.clear(); // TODO Process commands + PresentHandler present_handler = + present_handler_ ? present_handler_ : [](auto... args) -> auto { + return fuchsia::images::PresentationInfo{}; + }; auto present_info = present_handler_( presentation_time, std::move(acquire_fences), std::move(release_fences)); @@ -79,13 +121,17 @@ void FakeSession::Present(uint64_t presentation_time, void FakeSession::Present2(fuchsia::ui::scenic::Present2Args args, Present2Callback callback) { - if (!present2_handler_) { - return NotImplemented_("Present2"); - } + ApplyCommands(); - command_queue_.clear(); // TODO Process commands + Present2Handler present2_handler = + present2_handler_ ? present2_handler_ : [](auto args) -> auto { + return fuchsia::scenic::scheduling::FuturePresentationTimes{ + .future_presentations = {}, + .remaining_presents_in_flight_allowed = 1, + }; + }; - auto future_presentation_times = present2_handler_(std::move(args)); + auto future_presentation_times = present2_handler(std::move(args)); if (callback) { callback(std::move(future_presentation_times)); } @@ -94,12 +140,17 @@ void FakeSession::Present2(fuchsia::ui::scenic::Present2Args args, void FakeSession::RequestPresentationTimes( int64_t requested_prediction_span, RequestPresentationTimesCallback callback) { - if (!request_presentation_times_handler_) { - return NotImplemented_("RequestPresentationTimes"); - } + RequestPresentationTimesHandler request_presentation_times_handler = + request_presentation_times_handler_ ? request_presentation_times_handler_ + : [](auto args) -> auto { + return fuchsia::scenic::scheduling::FuturePresentationTimes{ + .future_presentations = {}, + .remaining_presents_in_flight_allowed = 1, + }; + }; auto future_presentation_times = - request_presentation_times_handler_(requested_prediction_span); + request_presentation_times_handler(requested_prediction_span); if (callback) { callback(std::move(future_presentation_times)); } @@ -108,19 +159,1023 @@ void FakeSession::RequestPresentationTimes( void FakeSession::RegisterBufferCollection( uint32_t buffer_id, fidl::InterfaceHandle token) { - NotImplemented_("RegisterBufferCollection"); // TODO + zx_koid_t token_koid = GetKoid(token.channel().get()); + auto [_, buffer_success] = + scene_graph_.buffer_collection_map.emplace(std::make_pair( + buffer_id, + StateT::HandleT< + fidl::InterfaceHandle>{ + std::move(token), token_koid})); + FML_CHECK(buffer_success); } void FakeSession::DeregisterBufferCollection(uint32_t buffer_id) { - NotImplemented_("DeregisterBufferCollection"); // TODO + size_t erased = scene_graph_.buffer_collection_map.erase(buffer_id); + FML_CHECK(erased == 1); } void FakeSession::SetDebugName(std::string debug_name) { debug_name_ = std::move(debug_name); } -void FakeSession::NotImplemented_(const std::string& name) { - FML_LOG(FATAL) << "FakeSession does not implement " << name; +std::shared_ptr FakeSession::GetResource(FakeResourceId id) { + FML_CHECK(id != kInvalidFakeResourceId); + auto resource_it = scene_graph_.resource_map.find(id); + FML_CHECK(resource_it != scene_graph_.resource_map.end()); + auto resource_ptr = resource_it->second; + FML_CHECK(resource_ptr); + + return resource_ptr; +} + +void FakeSession::AddResource(FakeResourceState&& resource) { + const FakeResourceId resource_id = resource.id; + FML_CHECK(resource_id != kInvalidFakeResourceId); + + // Track the view id if the resource is a view. + if (ResourceIs(resource)) { + // If there was already a View in the scene graph, scenic prints a warning + // here but doesn't update the "root view" and allows the Session to + // continue. See also: fxbug.dev/24450 + if (scene_graph_.root_view_id == kInvalidFakeResourceId) { + scene_graph_.root_view_id = resource_id; + } + } + + // Add to initial spot in parents map. + auto resource_ptr = std::make_shared( + std::forward(resource)); + if (ResourceIsNode(*resource_ptr)) { + auto [_, parents_success] = parents_map_.emplace(std::make_pair( + resource_ptr.get(), + std::make_pair(std::weak_ptr(resource_ptr), + std::weak_ptr()))); + FML_CHECK(parents_success); + } + + // Add to initial spot in labels map. + auto empty_label_it = scene_graph_.labels_map.find(""); + if (empty_label_it == scene_graph_.labels_map.end()) { + auto [emplace_it, empty_label_success] = scene_graph_.labels_map.emplace( + std::make_pair("", std::vector>())); + FML_CHECK(empty_label_success); + empty_label_it = emplace_it; + } + empty_label_it->second.emplace_back(resource_ptr); + + // Add to resource map. + auto [__, resource_success] = scene_graph_.resource_map.emplace( + std::make_pair(resource_id, std::move(resource_ptr))); + FML_CHECK(resource_success); +} + +void FakeSession::DetachResourceFromParent( + std::shared_ptr resource_ptr, + std::shared_ptr new_parent_ptr) { + FML_CHECK(resource_ptr); + + // Remove reference from the parent's `children` array. + auto parent_it = parents_map_.find(resource_ptr.get()); + FML_CHECK(parent_it != parents_map_.end()); + if (auto parent_ptr = parent_it->second.second.lock()) { + std::visit( + [&resource_ptr](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + auto erase_it = + std::remove_if(state.node_state.children.begin(), + state.node_state.children.end(), + [&resource_ptr](const auto& resource) { + return resource == resource_ptr; + }); + FML_CHECK(erase_it != state.node_state.children.end()); + state.node_state.children.erase(erase_it); + } else if constexpr (std::is_same_v) { + auto erase_it = + std::remove_if(state.children.begin(), state.children.end(), + [&resource_ptr](const auto& resource) { + return resource == resource_ptr; + }); + FML_CHECK(erase_it != state.children.end()); + state.children.erase(erase_it); + } else { + FML_CHECK(false); + } + }, + parent_ptr->state); + } + + // Fix up the parent ptr. + if (new_parent_ptr) { + parent_it->second.second = new_parent_ptr; + } else { + parent_it->second.second = std::weak_ptr(); + } +} + +void FakeSession::PruneDeletedResourceRefs() { + // Remove expired resurces from the parents map. + for (auto parent_it = parents_map_.begin(), parent_end = parents_map_.end(); + parent_it != parent_end;) { + if (parent_it->second.first.expired()) { + parent_it = parents_map_.erase(parent_it); + } else { + ++parent_it; + } + } + + // Remove expired resurces from the labels map. + for (auto scene_it = scene_graph_.labels_map.begin(), + scene_end = scene_graph_.labels_map.end(); + scene_it != scene_end;) { + auto erase_it = std::remove_if( + scene_it->second.begin(), scene_it->second.end(), + [](const auto& weak_resource) { return weak_resource.expired(); }); + if (erase_it != scene_it->second.end()) { + scene_it->second.erase(erase_it); + } + + if (scene_it->second.empty()) { + scene_it = scene_graph_.labels_map.erase(scene_it); + } else { + ++scene_it; + } + } +} + +void FakeSession::ApplyCommands() { + while (!command_queue_.empty()) { + auto scenic_command = std::move(command_queue_.front()); + command_queue_.pop_front(); + + if (!scenic_command.is_gfx()) { + FML_LOG(FATAL) << "FakeSession: Unexpected non-gfx command (type " + << scenic_command.Which() << ")"; + continue; + } + + auto& command = scenic_command.gfx(); + switch (command.Which()) { + case fuchsia::ui::gfx::Command::Tag::kCreateResource: + ApplyCreateResourceCmd(std::move(command.create_resource())); + break; + case fuchsia::ui::gfx::Command::Tag::kReleaseResource: + ApplyReleaseResourceCmd(std::move(command.release_resource())); + break; + case fuchsia::ui::gfx::Command::Tag::kAddChild: + ApplyAddChildCmd(std::move(command.add_child())); + break; + case fuchsia::ui::gfx::Command::Tag::kDetach: + ApplyDetachCmd(std::move(command.detach())); + break; + case fuchsia::ui::gfx::Command::Tag::kDetachChildren: + ApplyDetachChildrenCmd(std::move(command.detach_children())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetTranslation: + ApplySetTranslationCmd(std::move(command.set_translation())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetScale: + ApplySetScaleCmd(std::move(command.set_scale())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetRotation: + ApplySetRotationCmd(std::move(command.set_rotation())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetAnchor: + ApplySetAnchorCmd(std::move(command.set_anchor())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetOpacity: + ApplySetOpacityCmd(command.set_opacity()); + break; + case fuchsia::ui::gfx::Command::Tag::kSetShape: + ApplySetShapeCmd(std::move(command.set_shape())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetMaterial: + ApplySetMaterialCmd(std::move(command.set_material())); + break; + case ::fuchsia::ui::gfx::Command::Tag::kSetClipPlanes: + ApplySetClipPlanesCmd(std::move(command.set_clip_planes())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetHitTestBehavior: + ApplySetHitTestBehaviorCmd(std::move(command.set_hit_test_behavior())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetSemanticVisibility: + ApplySetSemanticVisibilityCmd( + std::move(command.set_semantic_visibility())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetViewProperties: + ApplySetViewPropertiesCmd(std::move(command.set_view_properties())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetTexture: + ApplySetTextureCmd(std::move(command.set_texture())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetColor: + ApplySetColorCmd(std::move(command.set_color())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetEventMask: + ApplySetEventMaskCmd(std::move(command.set_event_mask())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetLabel: + ApplySetLabelCmd(std::move(command.set_label())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetEnableViewDebugBounds: + ApplySetEnableViewDebugBoundsCmd( + std::move(command.set_enable_view_debug_bounds())); + break; + case fuchsia::ui::gfx::Command::Tag::kSetViewHolderBoundsColor: + ApplySetViewHolderBoundsColorCmd( + std::move(command.set_view_holder_bounds_color())); + break; + case fuchsia::ui::gfx::Command::Tag::kExportResource: + NotImplemented_("ExportResourceCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kImportResource: + NotImplemented_("ImportResourceCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetTag: + NotImplemented_("SetTagCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetSize: + NotImplemented_("SetSizeCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSendSizeChangeHintHack: + NotImplemented_("SendSizeChangedHintHackCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kAddPart: + NotImplemented_("AddPartCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetClip: + NotImplemented_("SetClipCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetCamera: + NotImplemented_("SetCameraCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetCameraTransform: + NotImplemented_("SetCameraTransformCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetCameraProjection: + NotImplemented_("SetCameraProjectionCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetStereoCameraProjection: + NotImplemented_("SetStereoCameraProjectionCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetCameraClipSpaceTransform: + NotImplemented_("SetCameraClipSpaceTransformCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetCameraPoseBuffer: + NotImplemented_("SetCameraPoseBufferCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetLightColor: + NotImplemented_("SetLightColorCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetLightDirection: + NotImplemented_("SetLightDirectionCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetPointLightPosition: + NotImplemented_("SetPointLightPositionCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetPointLightFalloff: + NotImplemented_("SetPointLightFalloffCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kAddLight: + NotImplemented_("AddLightCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kScene_AddAmbientLight: + NotImplemented_("Scene_AddAmbientLightCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kScene_AddDirectionalLight: + NotImplemented_("Scene_AddDirectionalLightCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kScene_AddPointLight: + NotImplemented_("Scene_AddPointLightCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kDetachLight: + NotImplemented_("DetachLightCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kDetachLights: + NotImplemented_("DetachLightsCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kBindMeshBuffers: + NotImplemented_("BindMeshBuffersCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kAddLayer: + NotImplemented_("AddLayerCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kRemoveLayer: + NotImplemented_("RemoveLayerCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kRemoveAllLayers: + NotImplemented_("RemoveAllLayersCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetLayerStack: + NotImplemented_("SetLayerStackCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetRenderer: + NotImplemented_("SetRendererCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetRendererParam: + NotImplemented_("SetRendererParamCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetDisableClipping: + NotImplemented_("SetDisableClippingCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetImportFocus: + NotImplemented_("SetImportFocusCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kTakeSnapshotCmd: + NotImplemented_("TakeSnapshotCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetDisplayColorConversion: + NotImplemented_("SetDisplayColorConversionCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetDisplayRotation: + NotImplemented_("SetDisplayRotationCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::kSetDisplayMinimumRgb: + NotImplemented_("SetDisplayMinimumRgbCmd"); + break; + case fuchsia::ui::gfx::Command::Tag::Invalid: + FML_LOG(FATAL) << "FakeSession found Invalid gfx command"; + break; + } + } + + // Clean up resource refs after processing commands. + PruneDeletedResourceRefs(); +} + +void FakeSession::ApplyCreateResourceCmd( + fuchsia::ui::gfx::CreateResourceCmd command) { + const FakeResourceId resource_id = command.id; + FML_CHECK(resource_id != 0); + + switch (command.resource.Which()) { + case fuchsia::ui::gfx::ResourceArgs::Tag::kMemory: + ApplyCreateMemory(resource_id, std::move(command.resource.memory())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImage: + ApplyCreateImage(resource_id, std::move(command.resource.image())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImage2: + ApplyCreateImage2(resource_id, std::move(command.resource.image2())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImage3: + ApplyCreateImage3(resource_id, std::move(command.resource.image3())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImagePipe: + ApplyCreateImagePipe(resource_id, + std::move(command.resource.image_pipe())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImagePipe2: + ApplyCreateImagePipe2(resource_id, + std::move(command.resource.image_pipe2())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kRectangle: + ApplyCreateRectangle(resource_id, + std::move(command.resource.rectangle())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kRoundedRectangle: + ApplyCreateRoundedRectangle( + resource_id, std::move(command.resource.rounded_rectangle())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kCircle: + ApplyCreateCircle(resource_id, std::move(command.resource.circle())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kMaterial: + ApplyCreateMaterial(resource_id, std::move(command.resource.material())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kView: + ApplyCreateView(resource_id, std::move(command.resource.view())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kView3: + ApplyCreateView(resource_id, std::move(command.resource.view3())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kViewHolder: + ApplyCreateViewHolder(resource_id, + std::move(command.resource.view_holder())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kOpacityNode: + ApplyCreateOpacityNode(resource_id, command.resource.opacity_node()); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kEntityNode: + ApplyCreateEntityNode(resource_id, + std::move(command.resource.entity_node())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kShapeNode: + ApplyCreateShapeNode(resource_id, + std::move(command.resource.shape_node())); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kBuffer: + NotImplemented_("CreateBufferResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kScene: + NotImplemented_("CreateSceneResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kCamera: + NotImplemented_("CreateCameraResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kStereoCamera: + NotImplemented_("CreateStereoCameraResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kRenderer: + NotImplemented_("CreateRendererResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kAmbientLight: + NotImplemented_("CreateAmbientLightResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kDirectionalLight: + NotImplemented_("CreateDirectionalLightResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kPointLight: + NotImplemented_("CreatePointLightResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kMesh: + NotImplemented_("CreateMeshResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kClipNode: + NotImplemented_("CreateClipNodeResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kCompositor: + NotImplemented_("CreateCompositorResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kDisplayCompositor: + NotImplemented_("CreateDisplayCompositorResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kImagePipeCompositor: + NotImplemented_("CreateImagePipeCompositorResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kLayerStack: + NotImplemented_("CreateLayerStackResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kLayer: + NotImplemented_("CreateLayerResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::kVariable: + NotImplemented_("CreateVariableResource"); + break; + case fuchsia::ui::gfx::ResourceArgs::Tag::Invalid: + FML_LOG(FATAL) << "FakeSession found Invalid CreateResource command"; + break; + } +} + +void FakeSession::ApplyReleaseResourceCmd( + fuchsia::ui::gfx::ReleaseResourceCmd command) { + auto resource_ptr = GetResource(command.id); + if (ResourceIs(*resource_ptr)) { + FML_CHECK(scene_graph_.root_view_id == resource_ptr->id); + scene_graph_.root_view_id = kInvalidFakeResourceId; + } + + scene_graph_.resource_map.erase(command.id); +} + +void FakeSession::ApplyAddChildCmd(fuchsia::ui::gfx::AddChildCmd command) { + auto parent_node_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIsNode(*parent_node_ptr) || + ResourceIs(*parent_node_ptr)); + + auto child_node_ptr = GetResource(command.child_id); + FML_CHECK(ResourceIsNode(*child_node_ptr)); + + // Add the Node as a child of the new parent. + std::visit( + [&child_node_ptr](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.children.emplace_back(child_node_ptr); + } else if constexpr (std::is_same_v) { + state.children.emplace_back(child_node_ptr); + } else { + FML_CHECK(false); + } + }, + parent_node_ptr->state); + + // Remove the Node as a child of the old parent and fix up the parent ptr. + DetachResourceFromParent(child_node_ptr, parent_node_ptr); +} + +void FakeSession::ApplyDetachCmd(fuchsia::ui::gfx::DetachCmd command) { + auto resource_ptr = GetResource(command.id); + FML_CHECK(ResourceIsNode(*resource_ptr)); + + DetachResourceFromParent(std::move(resource_ptr)); +} + +void FakeSession::ApplyDetachChildrenCmd( + fuchsia::ui::gfx::DetachChildrenCmd command) { + auto resource_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIsNode(*resource_ptr)); + + std::visit( + [this](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + for (auto& child : state.node_state.children) { + DetachResourceFromParent(child); + } + state.node_state.children.clear(); + } else { + FML_CHECK(false); + } + }, + resource_ptr->state); +} + +void FakeSession::ApplySetTranslationCmd( + fuchsia::ui::gfx::SetTranslationCmd command) { + auto resource_ptr = GetResource(command.id); + FML_CHECK(ResourceIsNode(*resource_ptr)); + + const std::array translation = { + command.value.value.x, command.value.value.y, command.value.value.z}; + std::visit( + [&translation](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.translation_vector = translation; + } else { + FML_CHECK(false); + } + }, + resource_ptr->state); +} + +void FakeSession::ApplySetScaleCmd(fuchsia::ui::gfx::SetScaleCmd command) { + auto resource_ptr = GetResource(command.id); + FML_CHECK(ResourceIsNode(*resource_ptr)); + + const std::array scale = { + command.value.value.x, command.value.value.y, command.value.value.z}; + std::visit( + [&scale](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.scale_vector = scale; + } else { + FML_CHECK(false); + } + }, + resource_ptr->state); +} + +void FakeSession::ApplySetRotationCmd( + fuchsia::ui::gfx::SetRotationCmd command) { + auto resource_ptr = GetResource(command.id); + FML_CHECK(ResourceIsNode(*resource_ptr)); + + const std::array rotation = { + command.value.value.x, command.value.value.y, command.value.value.z, + command.value.value.w}; + std::visit( + [&rotation](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.rotation_quaternion = rotation; + } else { + FML_CHECK(false); + } + }, + resource_ptr->state); +} + +void FakeSession::ApplySetAnchorCmd(fuchsia::ui::gfx::SetAnchorCmd command) { + auto resource_ptr = GetResource(command.id); + FML_CHECK(ResourceIsNode(*resource_ptr)); + + const std::array anchor = { + command.value.value.x, command.value.value.y, command.value.value.z}; + std::visit( + [&anchor](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.anchor_vector = anchor; + } else { + FML_CHECK(false); + } + }, + resource_ptr->state); +} + +void FakeSession::ApplySetOpacityCmd(fuchsia::ui::gfx::SetOpacityCmd command) { + auto resource_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIs(*resource_ptr)); + + const bool opacity = command.opacity; + std::visit( + [opacity](auto&& state) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + state.opacity = opacity; + } else { + FML_CHECK(false); + } + }, + resource_ptr->state); +} + +void FakeSession::ApplySetShapeCmd(fuchsia::ui::gfx::SetShapeCmd command) { + auto shape_node_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIs(*shape_node_ptr)); + auto* shape_node_state = + std::get_if(&shape_node_ptr->state); + FML_CHECK(shape_node_state != nullptr); + auto shape_ptr = GetResource(command.shape_id); + FML_CHECK(ResourceIs(*shape_ptr)); + + shape_node_state->shape = shape_ptr; +} + +void FakeSession::ApplySetMaterialCmd( + fuchsia::ui::gfx::SetMaterialCmd command) { + auto shape_node_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIs(*shape_node_ptr)); + auto* shape_node_state = + std::get_if(&shape_node_ptr->state); + FML_CHECK(shape_node_state != nullptr); + auto material_ptr = GetResource(command.material_id); + FML_CHECK(ResourceIs(*material_ptr)); + + shape_node_state->material = material_ptr; +} + +void FakeSession::ApplySetClipPlanesCmd( + fuchsia::ui::gfx::SetClipPlanesCmd command) { + auto node_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIs(*node_ptr)); + + std::vector clip_planes; + for (auto& clip_plane : command.clip_planes) { + clip_planes.emplace_back(FakeEntityNodeState::ClipPlane{ + .dir = {clip_plane.dir.x, clip_plane.dir.y, clip_plane.dir.z}, + .dist = clip_plane.dist, + }); + } + std::visit( + [clip_planes = std::move(clip_planes)](auto&& state) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + state.clip_planes = std::move(clip_planes); + } else { + FML_CHECK(false); + } + }, + node_ptr->state); +} + +void FakeSession::ApplySetViewPropertiesCmd( + fuchsia::ui::gfx::SetViewPropertiesCmd command) { + auto view_holder_ptr = GetResource(command.view_holder_id); + FML_CHECK(ResourceIs(*view_holder_ptr)); + auto* view_holder_state = + std::get_if(&view_holder_ptr->state); + FML_CHECK(view_holder_state != nullptr); + + view_holder_state->properties = command.properties; +} + +void FakeSession::ApplySetHitTestBehaviorCmd( + fuchsia::ui::gfx::SetHitTestBehaviorCmd command) { + auto node_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIsNode(*node_ptr)); + + const bool hit_testable = + command.hit_test_behavior == fuchsia::ui::gfx::HitTestBehavior::kDefault; + std::visit( + [hit_testable](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.hit_testable = hit_testable; + } else { + FML_CHECK(false); + } + }, + node_ptr->state); +} + +void FakeSession::ApplySetSemanticVisibilityCmd( + fuchsia::ui::gfx::SetSemanticVisibilityCmd command) { + auto node_ptr = GetResource(command.node_id); + FML_CHECK(ResourceIsNode(*node_ptr)); + + const bool semantic_visibility = command.visible; + std::visit( + [semantic_visibility](auto&& state) { + using T = std::decay_t; + if constexpr (is_node_v) { + state.node_state.semantically_visible = semantic_visibility; + } else { + FML_CHECK(false); + } + }, + node_ptr->state); +} + +void FakeSession::ApplySetTextureCmd(fuchsia::ui::gfx::SetTextureCmd command) { + auto material_ptr = GetResource(command.material_id); + FML_CHECK(ResourceIs(*material_ptr)); + auto* material_state = std::get_if(&material_ptr->state); + FML_CHECK(material_state != nullptr); + auto image_ptr = GetResource(command.texture_id); + FML_CHECK(ResourceIs(*image_ptr)); + + material_state->image = image_ptr; +} + +void FakeSession::ApplySetColorCmd(fuchsia::ui::gfx::SetColorCmd command) { + auto material_ptr = GetResource(command.material_id); + FML_CHECK(ResourceIs(*material_ptr)); + auto* material_state = std::get_if(&material_ptr->state); + FML_CHECK(material_state != nullptr); + + material_state->color = {(command.color.value.red * 1.f) / 255.f, + (command.color.value.green * 1.f) / 255.f, + (command.color.value.blue * 1.f) / 255.f, + (command.color.value.alpha * 1.f) / 255.f}; +} + +void FakeSession::ApplySetEventMaskCmd( + fuchsia::ui::gfx::SetEventMaskCmd command) { + auto resource_ptr = GetResource(command.id); + + resource_ptr->event_mask = command.event_mask; +} + +void FakeSession::ApplySetLabelCmd(fuchsia::ui::gfx::SetLabelCmd command) { + auto resource_ptr = GetResource(command.id); + + // Erase from old spot in the labels map. + auto current_label_it = scene_graph_.labels_map.find(resource_ptr->label); + FML_CHECK(current_label_it != scene_graph_.labels_map.end()); + auto current_erase_it = std::remove_if( + current_label_it->second.begin(), current_label_it->second.end(), + [&resource_ptr](const auto& weak_resource) { + return resource_ptr == weak_resource.lock(); + }); + FML_CHECK(current_erase_it != current_label_it->second.end()); + current_label_it->second.erase(current_erase_it); + + // Add to new spot in labels map. + auto new_label_it = scene_graph_.labels_map.find(command.label); + if (new_label_it == scene_graph_.labels_map.end()) { + auto [emplace_it, current_label_success] = + scene_graph_.labels_map.emplace(std::make_pair( + command.label, std::vector>())); + FML_CHECK(current_label_success); + new_label_it = emplace_it; + } + new_label_it->second.emplace_back(resource_ptr); + + resource_ptr->label = std::move(command.label); +} + +void FakeSession::ApplySetEnableViewDebugBoundsCmd( + fuchsia::ui::gfx::SetEnableDebugViewBoundsCmd command) { + auto view_ptr = GetResource(command.view_id); + FML_CHECK(ResourceIs(*view_ptr)); + auto* view_state = std::get_if(&view_ptr->state); + FML_CHECK(view_state != nullptr); + + view_state->enable_debug_bounds = command.enable; +} + +void FakeSession::ApplySetViewHolderBoundsColorCmd( + fuchsia::ui::gfx::SetViewHolderBoundsColorCmd command) { + auto view_holder_ptr = GetResource(command.view_holder_id); + FML_CHECK(ResourceIs(*view_holder_ptr)); + auto* view_holder_state = + std::get_if(&view_holder_ptr->state); + FML_CHECK(view_holder_state != nullptr); + + view_holder_state->bounds_color = {command.color.value.red, + command.color.value.green, + command.color.value.blue, 1.f}; +} + +void FakeSession::ApplyCreateMemory(FakeResourceId id, + fuchsia::ui::gfx::MemoryArgs args) { + zx_koid_t vmo_koid = GetKoid(args.vmo.get()); + AddResource( + {.id = id, + .state = FakeMemoryState{ + .vmo = {std::move(args.vmo), vmo_koid}, + .allocation_size = args.allocation_size, + .is_device_memory = (args.memory_type == + fuchsia::images::MemoryType::VK_DEVICE_MEMORY), + }}); +} + +void FakeSession::ApplyCreateImage(FakeResourceId id, + fuchsia::ui::gfx::ImageArgs args) { + AddResource({.id = id, + .state = FakeImageState{ + .image_def = + FakeImageState::ImageDef{ + .info = std::move(args.info), + .memory_offset = args.memory_offset, + }, + }}); + + // Hook up the memory resource to the image + auto image_ptr = GetResource(id); + FML_CHECK(ResourceIs(*image_ptr)); + auto* image_state = std::get_if(&image_ptr->state); + FML_CHECK(image_state != nullptr); + auto memory_ptr = GetResource(args.memory_id); + FML_CHECK(ResourceIs(*memory_ptr)); + + image_state->memory = memory_ptr; +} + +void FakeSession::ApplyCreateImage2(FakeResourceId id, + fuchsia::ui::gfx::ImageArgs2 args) { + AddResource( + {.id = id, + .state = FakeImageState{ + .image_def = + FakeImageState::Image2Def{ + .buffer_collection_id = args.buffer_collection_id, + .buffer_collection_index = args.buffer_collection_index, + .width = args.width, + .height = args.height, + }, + }}); +} + +void FakeSession::ApplyCreateImage3(FakeResourceId id, + fuchsia::ui::gfx::ImageArgs3 args) { + zx_koid_t import_token_koid = GetKoid(args.import_token.value.get()); + AddResource( + {.id = id, + .state = FakeImageState{ + .image_def = + FakeImageState::Image3Def{ + .import_token = {std::move(args.import_token), + import_token_koid}, + .buffer_collection_index = args.buffer_collection_index, + .width = args.width, + .height = args.height, + }, + }}); +} + +void FakeSession::ApplyCreateImagePipe(FakeResourceId id, + fuchsia::ui::gfx::ImagePipeArgs args) { + zx_koid_t image_pipe_request_koid = + GetKoid(args.image_pipe_request.channel().get()); + AddResource( + {.id = id, + .state = FakeImageState{ + .image_def = + FakeImageState::ImagePipeDef{ + .image_pipe_request = {std::move(args.image_pipe_request), + image_pipe_request_koid}, + }, + }}); +} + +void FakeSession::ApplyCreateImagePipe2(FakeResourceId id, + fuchsia::ui::gfx::ImagePipe2Args args) { + zx_koid_t image_pipe_request_koid = + GetKoid(args.image_pipe_request.channel().get()); + AddResource( + {.id = id, + .state = FakeImageState{ + .image_def = + FakeImageState::ImagePipe2Def{ + .image_pipe_request = {std::move(args.image_pipe_request), + image_pipe_request_koid}, + }, + }}); +} + +void FakeSession::ApplyCreateRectangle(FakeResourceId id, + fuchsia::ui::gfx::RectangleArgs args) { + FML_CHECK(args.width.is_vector1()); + FML_CHECK(args.height.is_vector1()); + + AddResource({.id = id, + .state = FakeShapeState{ + .shape_def = + FakeShapeState::RectangleDef{ + .width = args.width.vector1(), + .height = args.height.vector1(), + }, + }}); +} + +void FakeSession::ApplyCreateRoundedRectangle( + FakeResourceId id, + fuchsia::ui::gfx::RoundedRectangleArgs args) { + FML_CHECK(args.width.is_vector1()); + FML_CHECK(args.height.is_vector1()); + FML_CHECK(args.top_left_radius.is_vector1()); + FML_CHECK(args.top_right_radius.is_vector1()); + FML_CHECK(args.bottom_right_radius.is_vector1()); + FML_CHECK(args.bottom_left_radius.is_vector1()); + + AddResource( + {.id = id, + .state = FakeShapeState{ + .shape_def = + FakeShapeState::RoundedRectangleDef{ + .width = args.width.vector1(), + .height = args.height.vector1(), + .top_left_radius = args.top_left_radius.vector1(), + .top_right_radius = args.top_right_radius.vector1(), + .bottom_right_radius = args.bottom_right_radius.vector1(), + .bottom_left_radius = args.bottom_left_radius.vector1(), + }, + }}); +} + +void FakeSession::ApplyCreateCircle(FakeResourceId id, + fuchsia::ui::gfx::CircleArgs args) { + FML_CHECK(args.radius.is_vector1()); + + AddResource({.id = id, + .state = FakeShapeState{ + .shape_def = + FakeShapeState::CircleDef{ + .radius = args.radius.vector1(), + }, + }}); +} + +void FakeSession::ApplyCreateMaterial(FakeResourceId id, + fuchsia::ui::gfx::MaterialArgs args) { + AddResource({ + .id = id, + .state = FakeMaterialState{}, + }); +} + +void FakeSession::ApplyCreateView(FakeResourceId id, + fuchsia::ui::gfx::ViewArgs args) { + FML_CHECK(scene_graph_.root_view_id == kInvalidFakeResourceId); + + zx_koid_t token_koid = GetKoid(args.token.value.get()); + AddResource({.id = id, + .state = FakeViewState{ + .token = {std::move(args.token), token_koid}, + .debug_name = std::string(args.debug_name->c_str(), + args.debug_name->length()), + }}); +} + +void FakeSession::ApplyCreateView(FakeResourceId id, + fuchsia::ui::gfx::ViewArgs3 args) { + zx_koid_t token_koid = GetKoid(args.token.value.get()); + zx_koid_t control_ref_koid = GetKoid(args.control_ref.reference.get()); + zx_koid_t view_ref_koid = GetKoid(args.view_ref.reference.get()); + AddResource( + {.id = id, + .state = FakeViewState{ + .token = {std::move(args.token), token_koid}, + .control_ref = {std::move(args.control_ref), control_ref_koid}, + .view_ref = {std::move(args.view_ref), view_ref_koid}, + .debug_name = + std::string(args.debug_name->c_str(), args.debug_name->length()), + }}); +} + +void FakeSession::ApplyCreateViewHolder(FakeResourceId id, + fuchsia::ui::gfx::ViewHolderArgs args) { + zx_koid_t token_koid = GetKoid(args.token.value.get()); + AddResource({ + .id = id, + .state = + FakeViewHolderState{ + .token = {std::move(args.token), token_koid}, + .debug_name = std::string(args.debug_name->c_str(), + args.debug_name->length()), + }, + }); +} + +void FakeSession::ApplyCreateEntityNode(FakeResourceId id, + fuchsia::ui::gfx::EntityNodeArgs args) { + AddResource({ + .id = id, + .state = FakeEntityNodeState{}, + }); +} + +void FakeSession::ApplyCreateOpacityNode( + FakeResourceId id, + fuchsia::ui::gfx::OpacityNodeArgsHACK args) { + AddResource({ + .id = id, + .state = FakeOpacityNodeState{}, + }); +} + +void FakeSession::ApplyCreateShapeNode(FakeResourceId id, + fuchsia::ui::gfx::ShapeNodeArgs args) { + AddResource({ + .id = id, + .state = FakeShapeNodeState{}, + }); } } // namespace flutter_runner::testing diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.h index e1dfa8ede3b26..ecf1fabc32f80 100644 --- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.h +++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_session.h @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -14,15 +16,58 @@ #include #include +#include #include +#include #include +#include #include // For std::pair #include #include "flutter/fml/macros.h" +#include "fake_resources.h" + namespace flutter_runner::testing { +// A lightweight fake implementation of the scenic Session API, also called +// the "gfx API". The fake has no side effects besides mutating its own +// internal state. +// +// The fake allows tests to do a few things that would be difficult using either +// a mock implementation or the real implementation: +// + It allows the user to hook `Present` invocations and respond with +// stubbed-out `FuturePresentationTimes`, but more crucially it mimics the +// real scenic behavior of only processing commands when a `Present` is +// invoked. +// + It allows the user to inspect a snapshot of the scene graph at any moment +// in time, via the `SceneGraph()` accessor. +// + It stores the various session resources generated by commands into a +// std::unordered_map, and also correctly manages the resource lifetimes via +// reference counting. This allows a resource to stay alive if its parent +// still holds a reference to it, in the same way the real scenic +// implementation would. +// + The resources returned by `SceneGraph()` that the test uses for +// inspection are decoupled from the resources managed internally by the +// `FakeSession` itself -- they are a snapshot of the scene graph at that +// moment in time, with all snapshot state being cloned from the underlying +// scene graph state. This allows the `FakeSession` and test to naturally use +// `shared_ptr` for reference counting and mimic the real scenic behavior +// exactly, instead of an awkward index-based API. +// +// Error handling / session disconnection is still WIP. FakeSession will likely +// generate a CHECK in any place where the real scenic would disconnect the +// session or send a ScenicError. +// +// root_presenter-only commands e.g. CreateLayer are not handled. +// +// Deprecated / obsolete commands are not handled. +// +// Input is not handled. +// +// Rendering is not handled. +// +// Cross-session links are not handled. class FakeSession : public fuchsia::ui::scenic::testing::Session_TestBase { public: using PresentHandler = @@ -46,29 +91,37 @@ class FakeSession : public fuchsia::ui::scenic::testing::Session_TestBase { const std::string& debug_name() const { return debug_name_; } - const std::vector& command_queue() { + const std::deque& command_queue() const { return command_queue_; } - // Bind method. Call to bind this session's FIDL channels to the |dispatcher| - // and allow processing of incoming FIDL requests. + // Generate a snapshot of the underlying scene graph for test inspection. + FakeSceneGraph SceneGraph() const { + return SceneGraphFromState(scene_graph_); + } + + // Bind this session's FIDL channels to the `dispatcher` and allow processing + // of incoming FIDL requests. SessionAndListenerClientPair Bind(async_dispatcher_t* dispatcher = nullptr); - // Stub methods. Call these to set a handler for the specified FIDL calls' - // return values. + // Set a handler for `Present`-related FIDL calls' return values. void SetPresentHandler(PresentHandler present_handler); void SetPresent2Handler(Present2Handler present2_handler); void SetRequestPresentationTimesHandler( RequestPresentationTimesHandler request_presentation_times_handler); - // Event methods. Call these to fire the associated FIDL event. + // Fire an `OnFramePresented` event. void FireOnFramePresentedEvent( fuchsia::scenic::scheduling::FramePresentedInfo frame_presented_info); - // Error method. Call to disconnect the session with an error. + // Disconnect the session with an error. + // TODO: Call this internally upon command error, instead of CHECK'ing. void DisconnectSession(); private: + // |fuchsia::ui::scenic::testing::Session_TestBase| + void NotImplemented_(const std::string& name) override; + // |fuchsia::ui::scenic::Session| void Enqueue(std::vector cmds) override; @@ -99,15 +152,80 @@ class FakeSession : public fuchsia::ui::scenic::testing::Session_TestBase { // |fuchsia::ui::scenic::Session| void SetDebugName(std::string debug_name) override; - // |fuchsia::ui::scenic::testing::Session_TestBase| - void NotImplemented_(const std::string& name) override; + // Resource management. + std::shared_ptr GetResource(FakeResourceId id); + void AddResource(FakeResourceState&& resource); + void DetachResourceFromParent( + std::shared_ptr resource_ptr, + std::shared_ptr new_parent_ptr = nullptr); + void PruneDeletedResourceRefs(); + + // Apply queued commands and mutate the resource map. + void ApplyCommands(); + void ApplyCreateResourceCmd(fuchsia::ui::gfx::CreateResourceCmd command); + void ApplyReleaseResourceCmd(fuchsia::ui::gfx::ReleaseResourceCmd command); + void ApplyAddChildCmd(fuchsia::ui::gfx::AddChildCmd command); + void ApplyDetachCmd(fuchsia::ui::gfx::DetachCmd command); + void ApplyDetachChildrenCmd(fuchsia::ui::gfx::DetachChildrenCmd command); + void ApplySetTranslationCmd(fuchsia::ui::gfx::SetTranslationCmd command); + void ApplySetScaleCmd(fuchsia::ui::gfx::SetScaleCmd command); + void ApplySetRotationCmd(fuchsia::ui::gfx::SetRotationCmd command); + void ApplySetAnchorCmd(fuchsia::ui::gfx::SetAnchorCmd command); + void ApplySetOpacityCmd(fuchsia::ui::gfx::SetOpacityCmd command); + void ApplySetShapeCmd(fuchsia::ui::gfx::SetShapeCmd command); + void ApplySetMaterialCmd(fuchsia::ui::gfx::SetMaterialCmd command); + void ApplySetClipPlanesCmd(fuchsia::ui::gfx::SetClipPlanesCmd command); + void ApplySetViewPropertiesCmd( + fuchsia::ui::gfx::SetViewPropertiesCmd command); + void ApplySetHitTestBehaviorCmd( + fuchsia::ui::gfx::SetHitTestBehaviorCmd command); + void ApplySetSemanticVisibilityCmd( + fuchsia::ui::gfx::SetSemanticVisibilityCmd command); + void ApplySetTextureCmd(fuchsia::ui::gfx::SetTextureCmd command); + void ApplySetColorCmd(fuchsia::ui::gfx::SetColorCmd command); + void ApplySetEventMaskCmd(fuchsia::ui::gfx::SetEventMaskCmd command); + void ApplySetLabelCmd(fuchsia::ui::gfx::SetLabelCmd command); + void ApplySetEnableViewDebugBoundsCmd( + fuchsia::ui::gfx::SetEnableDebugViewBoundsCmd command); + void ApplySetViewHolderBoundsColorCmd( + fuchsia::ui::gfx::SetViewHolderBoundsColorCmd command); + void ApplyCreateMemory(FakeResourceId id, fuchsia::ui::gfx::MemoryArgs args); + void ApplyCreateImage(FakeResourceId id, fuchsia::ui::gfx::ImageArgs args); + void ApplyCreateImage2(FakeResourceId id, fuchsia::ui::gfx::ImageArgs2 args); + void ApplyCreateImage3(FakeResourceId id, fuchsia::ui::gfx::ImageArgs3 args); + void ApplyCreateImagePipe(FakeResourceId id, + fuchsia::ui::gfx::ImagePipeArgs args); + void ApplyCreateImagePipe2(FakeResourceId id, + fuchsia::ui::gfx::ImagePipe2Args args); + void ApplyCreateRectangle(FakeResourceId id, + fuchsia::ui::gfx::RectangleArgs args); + void ApplyCreateRoundedRectangle(FakeResourceId id, + fuchsia::ui::gfx::RoundedRectangleArgs args); + void ApplyCreateCircle(FakeResourceId id, fuchsia::ui::gfx::CircleArgs args); + void ApplyCreateMaterial(FakeResourceId id, + fuchsia::ui::gfx::MaterialArgs args); + void ApplyCreateView(FakeResourceId id, fuchsia::ui::gfx::ViewArgs args); + void ApplyCreateViewHolder(FakeResourceId id, + fuchsia::ui::gfx::ViewHolderArgs args); + void ApplyCreateView(FakeResourceId id, fuchsia::ui::gfx::ViewArgs3 args); + void ApplyCreateEntityNode(FakeResourceId id, + fuchsia::ui::gfx::EntityNodeArgs args); + void ApplyCreateOpacityNode(FakeResourceId id, + fuchsia::ui::gfx::OpacityNodeArgsHACK args); + void ApplyCreateShapeNode(FakeResourceId id, + fuchsia::ui::gfx::ShapeNodeArgs args); fidl::Binding binding_; fuchsia::ui::scenic::SessionListenerPtr listener_; std::string debug_name_; - std::vector command_queue_; + std::deque command_queue_; + std::unordered_map, + std::weak_ptr>> + parents_map_; // Used to cache parent refs for `AddChildCmd` + FakeSceneGraphState scene_graph_; PresentHandler present_handler_; Present2Handler present2_handler_;