| // 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/lib/fsl/handles/object_info.h" |
| #include "src/ui/scenic/lib/display/tests/mock_display_controller.h" |
| #include "src/ui/scenic/lib/flatland/buffers/util.h" |
| #include "src/ui/scenic/lib/flatland/engine/tests/common.h" |
| #include "src/ui/scenic/lib/flatland/engine/tests/mock_display_controller.h" |
| #include "src/ui/scenic/lib/flatland/renderer/mock_renderer.h" |
| |
| using ::testing::_; |
| using ::testing::Return; |
| |
| using flatland::ImageMetadata; |
| using flatland::LinkSystem; |
| using flatland::MockDisplayController; |
| 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 flatland { |
| namespace test { |
| |
| class EngineTest : public EngineTestBase { |
| public: |
| void SetUp() override { |
| EngineTestBase::SetUp(); |
| |
| // Create the SysmemAllocator. |
| zx_status_t status = fdio_service_connect( |
| "/svc/fuchsia.sysmem.Allocator", sysmem_allocator_.NewRequest().TakeChannel().release()); |
| EXPECT_EQ(status, ZX_OK); |
| sysmem_allocator_->SetDebugClientInfo(fsl::GetCurrentProcessName(), |
| fsl::GetCurrentProcessKoid()); |
| |
| renderer_ = std::make_shared<flatland::MockRenderer>(); |
| |
| zx::channel device_channel_server; |
| zx::channel device_channel_client; |
| FX_CHECK(ZX_OK == zx::channel::create(0, &device_channel_server, &device_channel_client)); |
| zx::channel controller_channel_server; |
| zx::channel controller_channel_client; |
| FX_CHECK(ZX_OK == |
| zx::channel::create(0, &controller_channel_server, &controller_channel_client)); |
| |
| mock_display_controller_ = std::make_unique<flatland::MockDisplayController>(); |
| mock_display_controller_->Bind(std::move(device_channel_server), |
| std::move(controller_channel_server)); |
| |
| auto shared_display_controller = |
| std::make_shared<fuchsia::hardware::display::ControllerSyncPtr>(); |
| shared_display_controller->Bind(std::move(controller_channel_client)); |
| |
| engine_ = std::make_unique<flatland::Engine>(std::move(shared_display_controller), renderer_, |
| render_data_func()); |
| } |
| |
| void TearDown() override { |
| renderer_.reset(); |
| engine_.reset(); |
| mock_display_controller_.reset(); |
| |
| EngineTestBase::TearDown(); |
| } |
| |
| fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> CreateToken() { |
| fuchsia::sysmem::BufferCollectionTokenSyncPtr token; |
| zx_status_t status = sysmem_allocator_->AllocateSharedCollection(token.NewRequest()); |
| FX_DCHECK(status == ZX_OK); |
| status = token->Sync(); |
| FX_DCHECK(status == ZX_OK); |
| return token; |
| } |
| |
| protected: |
| std::unique_ptr<flatland::MockDisplayController> mock_display_controller_; |
| std::shared_ptr<flatland::MockRenderer> renderer_; |
| std::unique_ptr<flatland::Engine> engine_; |
| fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator_; |
| }; |
| |
| TEST_F(EngineTest, ImportAndReleaseBufferCollectionTest) { |
| auto mock = mock_display_controller_.get(); |
| // Set the mock display controller functions and wait for messages. |
| std::thread server([&mock]() mutable { |
| // Wait once for call to ImportBufferCollection, once for setting the |
| // constraints, and once for call to ReleaseBufferCollection. Finally |
| // one call for the deleter. |
| for (uint32_t i = 0; i < 4; i++) { |
| mock->WaitForMessage(); |
| } |
| }); |
| |
| const sysmem_util::GlobalBufferCollectionId kGlobalBufferCollectionId = 15; |
| |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportBufferCollection(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(testing::Invoke( |
| [](uint64_t, fidl::InterfaceHandle<class ::fuchsia::sysmem::BufferCollectionToken>, |
| MockDisplayController::ImportBufferCollectionCallback callback) { callback(ZX_OK); })); |
| EXPECT_CALL(*mock_display_controller_.get(), |
| SetBufferCollectionConstraints(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(testing::Invoke( |
| [](uint64_t collection_id, fuchsia::hardware::display::ImageConfig config, |
| MockDisplayController::SetBufferCollectionConstraintsCallback callback) { |
| callback(ZX_OK); |
| })); |
| |
| EXPECT_CALL(*renderer_.get(), ImportBufferCollection(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(Return(true)); |
| engine_->ImportBufferCollection(kGlobalBufferCollectionId, sysmem_allocator_.get(), |
| CreateToken()); |
| |
| EXPECT_CALL(*mock_display_controller_.get(), ReleaseBufferCollection(kGlobalBufferCollectionId)) |
| .WillOnce(Return()); |
| EXPECT_CALL(*renderer_.get(), ReleaseBufferCollection(kGlobalBufferCollectionId)) |
| .WillOnce(Return()); |
| engine_->ReleaseBufferCollection(kGlobalBufferCollectionId); |
| |
| EXPECT_CALL(*mock_display_controller_.get(), CheckConfig(_, _)) |
| .WillOnce(testing::Invoke([&](bool, MockDisplayController::CheckConfigCallback callback) { |
| fuchsia::hardware::display::ConfigResult result = |
| fuchsia::hardware::display::ConfigResult::OK; |
| std::vector<fuchsia::hardware::display::ClientCompositionOp> ops; |
| callback(result, ops); |
| })); |
| |
| engine_.reset(); |
| server.join(); |
| } |
| |
| TEST_F(EngineTest, ImportImageErrorCases) { |
| const sysmem_util::GlobalBufferCollectionId kGlobalBufferCollectionId = 30; |
| const sysmem_util::GlobalImageId kImageId = 50; |
| const uint32_t kVmoCount = 2; |
| const uint32_t kVmoIdx = 1; |
| const uint32_t kMaxWidth = 100; |
| const uint32_t kMaxHeight = 200; |
| uint32_t num_times_import_image_called = 0; |
| |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportBufferCollection(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(testing::Invoke( |
| [](uint64_t, fidl::InterfaceHandle<class ::fuchsia::sysmem::BufferCollectionToken>, |
| MockDisplayController::ImportBufferCollectionCallback callback) { callback(ZX_OK); })); |
| |
| EXPECT_CALL(*mock_display_controller_.get(), |
| SetBufferCollectionConstraints(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(testing::Invoke( |
| [](uint64_t collection_id, fuchsia::hardware::display::ImageConfig config, |
| MockDisplayController::SetBufferCollectionConstraintsCallback callback) { |
| callback(ZX_OK); |
| })); |
| |
| EXPECT_CALL(*renderer_.get(), ImportBufferCollection(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(Return(true)); |
| |
| // Set the mock display controller functions and wait for messages. |
| auto mock = mock_display_controller_.get(); |
| std::thread server([&mock]() mutable { |
| // Wait once for call to ImportBufferCollection, once for setting |
| // the buffer collection constraints, a single valid call to |
| // ImportBufferImage() 1 invalid call to ImportBufferImage(), and a single |
| // call to ReleaseBufferImage(). Although there are more than three |
| // invalid calls to ImportBufferImage() below, only 3 of them make it |
| // all the way to the display controller, which is why we only |
| // have to wait 3 times. Finally add one call for the deleter. |
| for (uint32_t i = 0; i < 6; i++) { |
| mock->WaitForMessage(); |
| } |
| }); |
| |
| engine_->ImportBufferCollection(kGlobalBufferCollectionId, sysmem_allocator_.get(), |
| CreateToken()); |
| |
| ImageMetadata metadata = { |
| .collection_id = kGlobalBufferCollectionId, |
| .identifier = kImageId, |
| .vmo_index = kVmoIdx, |
| .width = 20, |
| .height = 30, |
| }; |
| |
| // Make sure that the engine returns true if the display controller returns true. |
| const uint64_t kDisplayImageId = 70; |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _)) |
| .WillOnce(testing::Invoke([](fuchsia::hardware::display::ImageConfig image_config, |
| uint64_t collection_id, uint32_t index, |
| MockDisplayController::ImportImageCallback callback) { |
| callback(ZX_OK, /*display_image_id*/ kDisplayImageId); |
| })); |
| |
| EXPECT_CALL(*renderer_.get(), ImportBufferImage(metadata)).WillOnce(Return(true)); |
| |
| auto result = engine_->ImportBufferImage(metadata); |
| EXPECT_TRUE(result); |
| |
| // Make sure we can release the image properly. |
| EXPECT_CALL(*mock_display_controller_, ReleaseImage(kDisplayImageId)).WillOnce(Return()); |
| EXPECT_CALL(*renderer_.get(), ReleaseBufferImage(metadata.identifier)).WillOnce(Return()); |
| |
| engine_->ReleaseBufferImage(metadata.identifier); |
| |
| // Make sure that the engine returns false if the display controller returns an error |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _)) |
| .WillOnce(testing::Invoke([](fuchsia::hardware::display::ImageConfig image_config, |
| uint64_t collection_id, uint32_t index, |
| MockDisplayController::ImportImageCallback callback) { |
| callback(ZX_ERR_INVALID_ARGS, /*display_image_id*/ 0); |
| })); |
| |
| // This should still return false for the engine even if the renderer returns true. |
| EXPECT_CALL(*renderer_.get(), ImportBufferImage(metadata)).WillOnce(Return(true)); |
| |
| result = engine_->ImportBufferImage(metadata); |
| EXPECT_FALSE(result); |
| |
| // Collection ID can't be invalid. This shouldn't reach the display controller. |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _)) |
| .Times(0); |
| auto copy_metadata = metadata; |
| copy_metadata.collection_id = sysmem_util::kInvalidId; |
| result = engine_->ImportBufferImage(copy_metadata); |
| EXPECT_FALSE(result); |
| |
| // Image Id can't be 0. This shouldn't reach the display controller. |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _)) |
| .Times(0); |
| copy_metadata = metadata; |
| copy_metadata.identifier = 0; |
| result = engine_->ImportBufferImage(copy_metadata); |
| EXPECT_FALSE(result); |
| |
| // Width can't be 0. This shouldn't reach the display controller. |
| EXPECT_CALL(*mock_display_controller_.get(), |
| ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _)) |
| .Times(0); |
| copy_metadata = metadata; |
| copy_metadata.width = 0; |
| result = engine_->ImportBufferImage(copy_metadata); |
| EXPECT_FALSE(result); |
| |
| // Height can't be 0. This shouldn't reach the display controller. |
| EXPECT_CALL(*mock_display_controller_.get(), ImportImage(_, _, 0, _)).Times(0); |
| copy_metadata = metadata; |
| copy_metadata.height = 0; |
| result = engine_->ImportBufferImage(copy_metadata); |
| EXPECT_FALSE(result); |
| |
| EXPECT_CALL(*mock_display_controller_, CheckConfig(_, _)) |
| .WillOnce(testing::Invoke([&](bool, MockDisplayController::CheckConfigCallback callback) { |
| fuchsia::hardware::display::ConfigResult result = |
| fuchsia::hardware::display::ConfigResult::OK; |
| std::vector<fuchsia::hardware::display::ClientCompositionOp> ops; |
| callback(result, ops); |
| })); |
| |
| engine_.reset(); |
| |
| server.join(); |
| } |
| |
| // 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) { |
| const uint64_t kGlobalBufferCollectionId = 1; |
| |
| // 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. |
| ImageMetadata parent_image_metadata = ImageMetadata{ |
| .collection_id = kGlobalBufferCollectionId, |
| .identifier = 1, |
| .vmo_index = 0, |
| .width = 128, |
| .height = 256, |
| }; |
| parent_struct->images[parent_image_handle] = parent_image_metadata; |
| |
| 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. |
| ImageMetadata child_image_metadata = ImageMetadata{ |
| .collection_id = kGlobalBufferCollectionId, |
| .identifier = 2, |
| .vmo_index = 1, |
| .width = 512, |
| .height = 1024, |
| }; |
| child_struct->images[child_image_handle] = child_image_metadata; |
| 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)); |
| |
| 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. |
| fuchsia::hardware::display::Frame sources[2] = { |
| {.x_pos = 0u, .y_pos = 0u, .width = 512, .height = 1024u}, |
| {.x_pos = 0u, .y_pos = 0u, .width = 128u, .height = 256u}}; |
| |
| fuchsia::hardware::display::Frame destinations[2] = { |
| {.x_pos = 5u, .y_pos = 7u, .width = 30, .height = 40u}, |
| {.x_pos = 9u, .y_pos = 13u, .width = 10u, .height = 20u}}; |
| |
| // Set the mock display controller functions and wait for messages. |
| auto mock = mock_display_controller_.get(); |
| std::thread server([&mock]() mutable { |
| // Since we have 2 rectangles with images with 1 buffer collection, we have to wait |
| // for...: |
| // - 2 calls for importing and setting constraints on the collection |
| // - 2 calls to import the images |
| // - 2 calls to initialize layers |
| // - 1 call to set the layers on the display |
| // - 1 call to discard the config. |
| // - 2 calls to set each layer image |
| // - 2 calls to set the layer primary config |
| // - 2 calls to set the layer primary alpha. |
| // - 2 calls to set the layer primary positions |
| // - 1 call to check the config |
| // - 1 call to apply the config |
| // - 1 call to DiscardConfig |
| // -2 calls to destroy layer. |
| for (uint32_t i = 0; i < 21; i++) { |
| mock->WaitForMessage(); |
| } |
| }); |
| |
| EXPECT_CALL(*mock, ImportBufferCollection(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(testing::Invoke( |
| [](uint64_t, fidl::InterfaceHandle<class ::fuchsia::sysmem::BufferCollectionToken>, |
| MockDisplayController::ImportBufferCollectionCallback callback) { callback(ZX_OK); })); |
| EXPECT_CALL(*mock, SetBufferCollectionConstraints(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(testing::Invoke( |
| [](uint64_t collection_id, fuchsia::hardware::display::ImageConfig config, |
| MockDisplayController::SetBufferCollectionConstraintsCallback callback) { |
| callback(ZX_OK); |
| })); |
| |
| EXPECT_CALL(*renderer_.get(), ImportBufferCollection(kGlobalBufferCollectionId, _, _)) |
| .WillOnce(Return(true)); |
| |
| engine_->ImportBufferCollection(kGlobalBufferCollectionId, sysmem_allocator_.get(), |
| CreateToken()); |
| |
| const uint64_t kParentDisplayImageId = 2; |
| EXPECT_CALL(*mock, ImportImage(_, kGlobalBufferCollectionId, 0, _)) |
| .WillOnce(testing::Invoke([](fuchsia::hardware::display::ImageConfig, uint64_t, uint32_t, |
| MockDisplayController::ImportImageCallback callback) { |
| callback(ZX_OK, kParentDisplayImageId); |
| })); |
| |
| EXPECT_CALL(*renderer_.get(), ImportBufferImage(parent_image_metadata)).WillOnce(Return(true)); |
| |
| engine_->ImportBufferImage(parent_image_metadata); |
| |
| const uint64_t kChildDisplayImageId = 3; |
| EXPECT_CALL(*mock, ImportImage(_, kGlobalBufferCollectionId, 1, _)) |
| .WillOnce(testing::Invoke([](fuchsia::hardware::display::ImageConfig, uint64_t, uint32_t, |
| MockDisplayController::ImportImageCallback callback) { |
| callback(ZX_OK, kChildDisplayImageId); |
| })); |
| |
| EXPECT_CALL(*renderer_.get(), ImportBufferImage(child_image_metadata)).WillOnce(Return(true)); |
| engine_->ImportBufferImage(child_image_metadata); |
| |
| // We start the frame by clearing the config. |
| EXPECT_CALL(*mock, CheckConfig(true, _)) |
| .WillOnce(testing::Invoke([&](bool, MockDisplayController::CheckConfigCallback callback) { |
| fuchsia::hardware::display::ConfigResult result = |
| fuchsia::hardware::display::ConfigResult::OK; |
| std::vector<fuchsia::hardware::display::ClientCompositionOp> ops; |
| callback(result, ops); |
| })); |
| |
| // Setup the EXPECT_CALLs for gmock. |
| uint64_t layer_id = 1; |
| EXPECT_CALL(*mock, CreateLayer(_)) |
| .WillRepeatedly(testing::Invoke([&](MockDisplayController::CreateLayerCallback callback) { |
| callback(ZX_OK, layer_id++); |
| })); |
| |
| std::vector<uint64_t> layers = {1u, 2u}; |
| EXPECT_CALL(*mock, SetDisplayLayers(display_id, layers)).Times(1); |
| |
| // Make sure each layer has all of its components set properly. |
| uint64_t collection_ids[] = {kChildDisplayImageId, kParentDisplayImageId}; |
| for (uint32_t i = 0; i < 2; i++) { |
| EXPECT_CALL(*mock, SetLayerPrimaryConfig(layers[i], _)).Times(1); |
| EXPECT_CALL(*mock, SetLayerPrimaryPosition(layers[i], _, _, _)) |
| .WillOnce( |
| testing::Invoke([sources, destinations, index = i]( |
| uint64_t layer_id, fuchsia::hardware::display::Transform transform, |
| fuchsia::hardware::display::Frame src_frame, |
| fuchsia::hardware::display::Frame dest_frame) { |
| EXPECT_TRUE(fidl::Equals(src_frame, sources[index])); |
| EXPECT_TRUE(fidl::Equals(dest_frame, destinations[index])); |
| })); |
| EXPECT_CALL(*mock, SetLayerPrimaryAlpha(layers[i], _, _)).Times(1); |
| EXPECT_CALL(*mock, SetLayerImage(layers[i], collection_ids[i], _, _)).Times(1); |
| } |
| |
| EXPECT_CALL(*mock, CheckConfig(false, _)) |
| .WillOnce(testing::Invoke([&](bool, MockDisplayController::CheckConfigCallback callback) { |
| fuchsia::hardware::display::ConfigResult result = |
| fuchsia::hardware::display::ConfigResult::OK; |
| std::vector<fuchsia::hardware::display::ClientCompositionOp> ops; |
| callback(result, ops); |
| })); |
| |
| EXPECT_CALL(*mock, ApplyConfig()).WillOnce(Return()); |
| |
| engine_->AddDisplay(display_id, {parent_root_handle, resolution}, sysmem_allocator_.get(), |
| /*num_vmos*/ 0); |
| |
| engine_->RenderFrame(); |
| |
| for (uint32_t i = 0; i < 2; i++) { |
| EXPECT_CALL(*mock, DestroyLayer(layers[i])); |
| } |
| |
| EXPECT_CALL(*mock_display_controller_, CheckConfig(_, _)) |
| .WillOnce(testing::Invoke([&](bool, MockDisplayController::CheckConfigCallback callback) { |
| fuchsia::hardware::display::ConfigResult result = |
| fuchsia::hardware::display::ConfigResult::OK; |
| std::vector<fuchsia::hardware::display::ClientCompositionOp> ops; |
| callback(result, ops); |
| })); |
| |
| engine_.reset(); |
| |
| server.join(); |
| } |
| |
| } // namespace test |
| } // namespace flatland |