| // Copyright 2020 The Fuchsia 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 "src/ui/scenic/lib/flatland/engine.h" |
| |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async-testing/test_loop.h> |
| #include <lib/async/cpp/executor.h> |
| #include <lib/async/cpp/wait.h> |
| #include <lib/async/default.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/eventpair.h> |
| |
| #include <limits> |
| #include <thread> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/fsl/handles/object_info.h" |
| #include "src/lib/testing/loop_fixture/real_loop_fixture.h" |
| #include "src/ui/lib/display/get_hardware_display_controller.h" |
| #include "src/ui/scenic/lib/display/display_manager.h" |
| #include "src/ui/scenic/lib/display/tests/mock_display_controller.h" |
| #include "src/ui/scenic/lib/flatland/flatland.h" |
| #include "src/ui/scenic/lib/flatland/global_image_data.h" |
| #include "src/ui/scenic/lib/flatland/global_matrix_data.h" |
| #include "src/ui/scenic/lib/flatland/global_topology_data.h" |
| #include "src/ui/scenic/lib/flatland/renderer/null_renderer.h" |
| #include "src/ui/scenic/lib/flatland/renderer/renderer.h" |
| #include "src/ui/scenic/lib/scheduling/frame_scheduler.h" |
| #include "src/ui/scenic/lib/scheduling/id.h" |
| |
| #include <glm/gtx/matrix_transform_2d.hpp> |
| |
| using ::testing::_; |
| using ::testing::Return; |
| |
| using flatland::ImageMetadata; |
| using flatland::LinkSystem; |
| using flatland::Renderer; |
| using flatland::TransformGraph; |
| using flatland::TransformHandle; |
| using flatland::UberStruct; |
| using flatland::UberStructSystem; |
| using fuchsia::ui::scenic::internal::ContentLink; |
| using fuchsia::ui::scenic::internal::ContentLinkStatus; |
| using fuchsia::ui::scenic::internal::ContentLinkToken; |
| using fuchsia::ui::scenic::internal::GraphLink; |
| using fuchsia::ui::scenic::internal::GraphLinkToken; |
| using fuchsia::ui::scenic::internal::LayoutInfo; |
| using fuchsia::ui::scenic::internal::LinkProperties; |
| |
| namespace { |
| |
| class EngineTest : public gtest::RealLoopFixture { |
| public: |
| EngineTest() |
| : uber_struct_system_(std::make_shared<UberStructSystem>()), |
| link_system_(std::make_shared<LinkSystem>(uber_struct_system_->GetNextInstanceId())), |
| display_controller_objs_(scenic_impl::display::test::CreateMockDisplayController()) {} |
| |
| void SetUp() override { |
| gtest::RealLoopFixture::SetUp(); |
| |
| // Create the SysmemAllocator. |
| zx_status_t status = fdio_service_connect( |
| "/svc/fuchsia.sysmem.Allocator", sysmem_allocator_.NewRequest().TakeChannel().release()); |
| |
| async_set_default_dispatcher(dispatcher()); |
| |
| // TODO(fxbug.dev/59646): We want all of the flatland tests to be "headless" and not make use |
| // of the real display controller. This isn't fully possible at the moment since we need the |
| // real DC's functionality to register buffer collections. Once the new hardware independent |
| // display controller driver is ready, we can hook that up to the fidl interface pointer instead |
| // and keep the tests hardware agnostic. |
| executor_ = std::make_unique<async::Executor>(dispatcher()); |
| display_manager_ = std::make_unique<scenic_impl::display::DisplayManager>([]() {}); |
| auto hdc_promise = ui_display::GetHardwareDisplayController(); |
| executor_->schedule_task( |
| hdc_promise.then([this](fit::result<ui_display::DisplayControllerHandles>& handles) { |
| display_manager_->BindDefaultDisplayController(std::move(handles.value().controller), |
| std::move(handles.value().dc_device)); |
| |
| // This global renderer will only be used with tests that have access to the |
| // real display controller. |
| renderer_ = std::make_shared<flatland::NullRenderer>( |
| display_manager_->default_display_controller()); |
| })); |
| |
| zx::duration timeout = zx::sec(5); |
| RunLoopWithTimeoutOrUntil([this] { return display_manager_->default_display() != nullptr; }, |
| timeout); |
| } |
| |
| void TearDown() override { |
| executor_.reset(); |
| display_manager_.reset(); |
| sysmem_allocator_ = nullptr; |
| renderer_.reset(); |
| gtest::RealLoopFixture::TearDown(); |
| } |
| |
| class FakeFlatlandSession { |
| public: |
| FakeFlatlandSession(const std::shared_ptr<UberStructSystem>& uber_struct_system, |
| const std::shared_ptr<LinkSystem>& link_system, EngineTest* harness) |
| : uber_struct_system_(uber_struct_system), |
| link_system_(link_system), |
| harness_(harness), |
| id_(uber_struct_system_->GetNextInstanceId()), |
| graph_(id_), |
| queue_(uber_struct_system_->AllocateQueueForSession(id_)) {} |
| |
| // Use the TransformGraph API to create and manage transforms and their children. |
| TransformGraph& graph() { return graph_; } |
| |
| // Returns the link_origin for this session. |
| TransformHandle GetLinkOrigin() { |
| EXPECT_TRUE(parent_link_.has_value()); |
| return parent_link_.value().parent_link.link_origin; |
| } |
| |
| // Clears the ParentLink for this session, if one exists. |
| void ClearParentLink() { parent_link_.reset(); } |
| |
| // Holds the ContentLink and LinkSystem::ChildLink objects since if they fall out of scope, |
| // the LinkSystem will delete the link. Tests should add |child_link.link_handle| to their |
| // TransformGraphs to use the ChildLink in a topology. |
| struct ChildLink { |
| fidl::InterfacePtr<ContentLink> content_link; |
| LinkSystem::ChildLink child_link; |
| |
| // Returns the handle the parent should add as a child in its local topology to include the |
| // link in the topology. |
| TransformHandle GetLinkHandle() const { return child_link.link_handle; } |
| }; |
| |
| // Links this session to |parent_session| and returns the ChildLink, which should be used with |
| // the parent session. If the return value drops out of scope, tests should call |
| // ClearParentLink() on this session. |
| ChildLink LinkToParent(FakeFlatlandSession& parent_session) { |
| // Create the tokens. |
| ContentLinkToken parent_token; |
| GraphLinkToken child_token; |
| EXPECT_EQ(zx::eventpair::create(0, &parent_token.value, &child_token.value), ZX_OK); |
| |
| // Create the parent link. |
| fidl::InterfacePtr<GraphLink> graph_link; |
| LinkSystem::ParentLink parent_link = link_system_->CreateParentLink( |
| std::move(child_token), graph_link.NewRequest(), graph_.CreateTransform()); |
| |
| // Create the child link. |
| fidl::InterfacePtr<ContentLink> content_link; |
| LinkSystem::ChildLink child_link = link_system_->CreateChildLink( |
| std::move(parent_token), LinkProperties(), content_link.NewRequest(), |
| parent_session.graph_.CreateTransform()); |
| |
| // Run the loop to establish the link. |
| harness_->RunLoopUntilIdle(); |
| |
| parent_link_ = ParentLink({ |
| .graph_link = std::move(graph_link), |
| .parent_link = std::move(parent_link), |
| }); |
| |
| return ChildLink({ |
| .content_link = std::move(content_link), |
| .child_link = std::move(child_link), |
| }); |
| } |
| |
| // Allocates a new UberStruct with a local_topology rooted at |local_root|. If this session has |
| // a ParentLink, the link_origin of that ParentLink will be used instead. |
| std::unique_ptr<UberStruct> CreateUberStructWithCurrentTopology(TransformHandle local_root) { |
| auto uber_struct = std::make_unique<UberStruct>(); |
| |
| // Only use the supplied |local_root| if no there is no ParentLink, otherwise use the |
| // |link_origin| from the ParentLink. |
| const TransformHandle root = |
| parent_link_.has_value() ? parent_link_.value().parent_link.link_origin : local_root; |
| |
| // Compute the local topology and place it in the UberStruct. |
| auto local_topology_data = |
| graph_.ComputeAndCleanup(root, std::numeric_limits<uint64_t>::max()); |
| EXPECT_NE(local_topology_data.iterations, std::numeric_limits<uint64_t>::max()); |
| EXPECT_TRUE(local_topology_data.cyclical_edges.empty()); |
| |
| uber_struct->local_topology = local_topology_data.sorted_transforms; |
| |
| return uber_struct; |
| } |
| |
| // Pushes |uber_struct| to the UberStructSystem and updates the system so that it represents |
| // this session in the InstanceMap. |
| void PushUberStruct(std::unique_ptr<UberStruct> uber_struct) { |
| EXPECT_FALSE(uber_struct->local_topology.empty()); |
| EXPECT_EQ(uber_struct->local_topology[0].handle.GetInstanceId(), id_); |
| |
| queue_->Push(/*present_id=*/0, std::move(uber_struct)); |
| uber_struct_system_->UpdateSessions({{id_, 0}}); |
| } |
| |
| private: |
| // Shared systems for all sessions. |
| std::shared_ptr<UberStructSystem> uber_struct_system_; |
| std::shared_ptr<LinkSystem> link_system_; |
| |
| // The test harness to give access to RunLoopUntilIdle(). |
| EngineTest* harness_; |
| |
| // Data specific this session. |
| scheduling::SessionId id_; |
| TransformGraph graph_; |
| std::shared_ptr<UberStructSystem::UberStructQueue> queue_; |
| |
| // Holds the GraphLink and LinkSystem::ParentLink objects since if they fall out of scope, |
| // the LinkSystem will delete the link. When |parent_link_| has a value, the |
| // |parent_link.link_origin| from this object is used as the root TransformHandle. |
| struct ParentLink { |
| fidl::InterfacePtr<GraphLink> graph_link; |
| LinkSystem::ParentLink parent_link; |
| }; |
| std::optional<ParentLink> parent_link_; |
| }; |
| |
| FakeFlatlandSession CreateSession() { |
| return FakeFlatlandSession(uber_struct_system_, link_system_, this); |
| } |
| |
| protected: |
| // Systems that are populated with data from Flatland instances. |
| const std::shared_ptr<UberStructSystem> uber_struct_system_; |
| const std::shared_ptr<LinkSystem> link_system_; |
| const scenic_impl::display::test::DisplayControllerObjects display_controller_objs_; |
| std::shared_ptr<flatland::NullRenderer> renderer_; |
| std::unique_ptr<async::Executor> executor_; |
| std::unique_ptr<scenic_impl::display::DisplayManager> display_manager_; |
| fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator_; |
| }; |
| |
| } // namespace |
| |
| namespace flatland { |
| namespace test { |
| |
| // Test bad input to the engine |RegisterTargetCollectionFunction|. |
| TEST_F(EngineTest, BadBufferRegistration) { |
| auto display_controller = display_manager_->default_display_controller(); |
| if (!display_controller) { |
| return; |
| } |
| |
| auto display = display_manager_->default_display(); |
| if (!display) { |
| return; |
| } |
| |
| ASSERT_TRUE(renderer_); |
| Engine engine(display_controller, renderer_, link_system_, uber_struct_system_); |
| |
| const uint32_t kDisplayId = display->display_id(); |
| const uint32_t kWidth = display->width_in_px(); |
| const uint32_t kHeight = display->height_in_px(); |
| const uint32_t kNumVmos = 2; |
| |
| // Try to register a buffer collection without first adding a display. |
| auto renderer_id = engine.RegisterTargetCollection(sysmem_allocator_.get(), kDisplayId, kNumVmos); |
| EXPECT_EQ(renderer_id, sysmem_util::kInvalidId); |
| |
| // Now add the display. |
| engine.AddDisplay(kDisplayId, TransformHandle(), glm::uvec2(kWidth, kHeight)); |
| |
| // Try again with 0 vmos. This should also fail. |
| auto renderer_id_2 = |
| engine.RegisterTargetCollection(sysmem_allocator_.get(), kDisplayId, /*num_vmos*/ 0); |
| EXPECT_EQ(renderer_id_2, sysmem_util::kInvalidId); |
| |
| // Now use a positive vmo number, this should work. |
| auto renderer_id_3 = |
| engine.RegisterTargetCollection(sysmem_allocator_.get(), kDisplayId, kNumVmos); |
| EXPECT_NE(renderer_id_3, sysmem_util::kInvalidId); |
| } |
| |
| // Test to make sure we can register framebuffers to the renderer and display |
| // via the engine. Requires the use of the real display controller. |
| TEST_F(EngineTest, BufferRegistrationTest) { |
| auto display_controller = display_manager_->default_display_controller(); |
| if (!display_controller) { |
| return; |
| } |
| |
| auto display = display_manager_->default_display(); |
| ASSERT_TRUE(display_controller); |
| if (!display) { |
| return; |
| } |
| |
| const uint32_t kDisplayId = display->display_id(); |
| const uint32_t kWidth = display->width_in_px(); |
| const uint32_t kHeight = display->height_in_px(); |
| const uint32_t kNumVmos = 2; |
| |
| ASSERT_TRUE(renderer_); |
| Engine engine(display_controller, renderer_, link_system_, uber_struct_system_); |
| engine.AddDisplay(kDisplayId, TransformHandle(), glm::uvec2(kWidth, kHeight)); |
| auto renderer_id = engine.RegisterTargetCollection(sysmem_allocator_.get(), kDisplayId, kNumVmos); |
| EXPECT_NE(renderer_id, sysmem_util::kInvalidId); |
| |
| // We can check the result of buffer registration by the engine through the renderer. |
| // We should see the same number of vmos we told the engine to create, as well as each |
| // vmo being the same width and height in pixels as the display. |
| auto result = renderer_->Validate(renderer_id); |
| EXPECT_TRUE(result.has_value()); |
| EXPECT_EQ(result->vmo_count, kNumVmos); |
| EXPECT_EQ(result->image_constraints.required_min_coded_width, kWidth); |
| EXPECT_EQ(result->image_constraints.required_min_coded_height, kHeight); |
| } |
| |
| // When compositing directly to a hardware display layer, the display controller |
| // takes in source and destination Frame object types, which mirrors flatland usage. |
| // The source frames are nonnormalized UV coordinates and the destination frames are |
| // screenspace coordinates given in pixels. So this test makes sure that the rectangle |
| // and frame data that is generated by flatland sends along to the display controller |
| // the proper source and destination frame data. Each source and destination frame pair |
| // should be added to its own layer on the display. |
| TEST_F(EngineTest, HardwareFrameCorrectnessTest) { |
| // Create a parent and child session. |
| auto parent_session = CreateSession(); |
| auto child_session = CreateSession(); |
| |
| // Create a link between the two. |
| auto child_link = child_session.LinkToParent(parent_session); |
| |
| // Create the root handle for the parent and a handle that will have an image attached. |
| const TransformHandle parent_root_handle = parent_session.graph().CreateTransform(); |
| const TransformHandle parent_image_handle = parent_session.graph().CreateTransform(); |
| |
| // Add the two children to the parent root: link, then image. |
| parent_session.graph().AddChild(parent_root_handle, child_link.GetLinkHandle()); |
| parent_session.graph().AddChild(parent_root_handle, parent_image_handle); |
| |
| // Create an image handle for the child. |
| const TransformHandle child_image_handle = child_session.graph().CreateTransform(); |
| |
| // Attach that image handle to the link_origin. |
| child_session.graph().AddChild(child_session.GetLinkOrigin(), child_image_handle); |
| |
| // Get an UberStruct for the parent session. |
| auto parent_struct = parent_session.CreateUberStructWithCurrentTopology(parent_root_handle); |
| |
| // Add an image. |
| parent_struct->images[parent_image_handle] = ImageMetadata({ |
| .vmo_idx = 1, |
| .width = 128, |
| .height = 256, |
| }); |
| |
| parent_struct->local_matrices[parent_image_handle] = glm::mat3(1); |
| parent_struct->local_matrices[parent_image_handle] = |
| glm::scale(glm::translate(glm::mat3(1.0), glm::vec2(9, 13)), glm::vec2(10, 20)); |
| |
| // Submit the UberStruct. |
| parent_session.PushUberStruct(std::move(parent_struct)); |
| |
| // Get an UberStruct for the child session. Note that the argument will be ignored anyway. |
| auto child_struct = |
| child_session.CreateUberStructWithCurrentTopology(child_session.GetLinkOrigin()); |
| |
| // Add an image. |
| child_struct->images[child_image_handle] = ImageMetadata({ |
| .vmo_idx = 2, |
| .width = 512, |
| .height = 1024, |
| }); |
| child_struct->local_matrices[child_image_handle] = |
| glm::scale(glm::translate(glm::mat3(1), glm::vec2(5, 7)), glm::vec2(30, 40)); |
| |
| // Submit the UberStruct. |
| child_session.PushUberStruct(std::move(child_struct)); |
| |
| auto display_controller = display_controller_objs_.interface_ptr; |
| auto& mock = display_controller_objs_.mock; |
| |
| uint64_t display_id = 1; |
| glm::uvec2 resolution(1024, 768); |
| |
| // We will end up with 2 source frames, 2 destination frames, and two layers beind sent to the |
| // display. |
| bool set_display_layers_called = false; |
| uint32_t set_layer_called_count = 0; |
| fuchsia::hardware::display::Frame sources[2]; |
| fuchsia::hardware::display::Frame destinations[2]; |
| uint64_t layer_ids[2]; |
| |
| // Set the mock display controller functions and wait for messages. |
| std::thread server([&display_id, &set_display_layers_called, &set_layer_called_count, &layer_ids, |
| &sources, &destinations, &mock]() mutable { |
| async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); |
| |
| mock->set_set_display_layers_fn([&](uint64_t in_display_id, ::std::vector<uint64_t> layer_ids) { |
| EXPECT_EQ(display_id, in_display_id); |
| set_display_layers_called = true; |
| EXPECT_EQ(layer_ids[0], 1u); |
| EXPECT_EQ(layer_ids[1], 2u); |
| |
| // This function should be called before we call the SetLayerPrimaryPosition function. |
| EXPECT_EQ(set_layer_called_count, 0u); |
| }); |
| |
| mock->set_layer_primary_position_fn( |
| [&](uint64_t layer_id, fuchsia::hardware::display::Transform transform, |
| fuchsia::hardware::display::Frame src, fuchsia::hardware::display::Frame dst) { |
| layer_ids[set_layer_called_count] = layer_id; |
| sources[set_layer_called_count] = src; |
| destinations[set_layer_called_count] = dst; |
| set_layer_called_count++; |
| }); |
| |
| // Since we have 2 rectangles with images, we have to wait for 2 calls to initialize layers, |
| // 1 call to set the layers on the display, and 2 calls to set the layer primary positions. |
| // This all happens when we call engine.RenderFrame() below. |
| for (uint32_t i = 0; i < 5; i++) { |
| mock->WaitForMessage(); |
| } |
| }); |
| |
| // Create an engine. Since this test doesn't make use of the real display controller. Create |
| // a local version of the renderer that uses the fake display controller and pass that into |
| // the engine. |
| auto renderer = std::make_shared<NullRenderer>(display_controller); |
| Engine engine(display_controller, renderer, link_system_, uber_struct_system_); |
| |
| engine.AddDisplay(display_id, parent_root_handle, resolution); |
| engine.RenderFrame(); |
| |
| server.join(); |
| |
| EXPECT_EQ(set_layer_called_count, 2u); |
| EXPECT_EQ(layer_ids[0], 1u); |
| EXPECT_EQ(layer_ids[1], 2u); |
| |
| EXPECT_EQ(sources[0].x_pos, 0u); |
| EXPECT_EQ(sources[0].y_pos, 0u); |
| EXPECT_EQ(sources[0].width, 512u); |
| EXPECT_EQ(sources[0].height, 1024u); |
| |
| EXPECT_EQ(destinations[0].x_pos, 5u); |
| EXPECT_EQ(destinations[0].y_pos, 7u); |
| EXPECT_EQ(destinations[0].width, 30u); |
| EXPECT_EQ(destinations[0].height, 40u); |
| |
| EXPECT_EQ(sources[1].x_pos, 0u); |
| EXPECT_EQ(sources[1].y_pos, 0u); |
| EXPECT_EQ(sources[1].width, 128u); |
| EXPECT_EQ(sources[1].height, 256u); |
| |
| EXPECT_EQ(destinations[1].x_pos, 9u); |
| EXPECT_EQ(destinations[1].y_pos, 13u); |
| EXPECT_EQ(destinations[1].width, 10u); |
| EXPECT_EQ(destinations[1].height, 20u); |
| } |
| |
| } // namespace test |
| } // namespace flatland |