| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/exo/surface_tree_host.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/display/display_configuration_controller.h" |
| #include "ash/shell.h" |
| #include "base/test/bind.h" |
| #include "components/exo/shell_surface.h" |
| #include "components/exo/sub_surface.h" |
| #include "components/exo/surface.h" |
| #include "components/exo/test/exo_test_base.h" |
| #include "components/exo/test/shell_surface_builder.h" |
| #include "components/viz/test/test_raster_interface.h" |
| #include "gpu/config/gpu_feature_info.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/aura/layout_manager.h" |
| #include "ui/aura/window.h" |
| #include "ui/display/display.h" |
| #include "ui/display/test/display_manager_test_api.h" |
| #include "ui/display/types/display_constants.h" |
| |
| using ::testing::InSequence; |
| |
| namespace exo { |
| namespace { |
| |
| class SurfaceTreeHostTest : public test::ExoTestBase { |
| protected: |
| void SetUp() override { |
| test::ExoTestBase::SetUp(); |
| |
| shell_surface_ = test::ShellSurfaceBuilder({16, 16}).BuildShellSurface(); |
| } |
| |
| void TearDown() override { |
| shell_surface_.reset(); |
| |
| test::ExoTestBase::TearDown(); |
| } |
| |
| ash::DisplayConfigurationController* display_config_controller() { |
| return ash::Shell::Get()->display_configuration_controller(); |
| } |
| |
| std::unique_ptr<ShellSurface> shell_surface_; |
| }; |
| |
| class LayoutManagerChecker : public aura::LayoutManager { |
| public: |
| void OnWindowAddedToLayout(aura::Window* child) override {} |
| void OnWillRemoveWindowFromLayout(aura::Window* child) override {} |
| void OnWindowRemovedFromLayout(aura::Window* child) override {} |
| void OnChildWindowVisibilityChanged(aura::Window* child, |
| bool visible) override {} |
| void SetChildBounds(aura::Window* child, |
| const gfx::Rect& requested_bounds) override {} |
| |
| MOCK_METHOD(void, OnWindowResized, (), (override)); |
| }; |
| |
| } // namespace |
| |
| TEST_F(SurfaceTreeHostTest, UpdatePrimaryDisplayWithSurfaceUpdateFailure) { |
| UpdateDisplay("800x600,[email protected]"); |
| display::Display display1 = GetPrimaryDisplay(); |
| display::Display display2 = GetSecondaryDisplay(); |
| |
| std::vector<std::pair<int64_t, int64_t>> leave_enter_ids; |
| bool callback_return_value = true; |
| shell_surface_->root_surface()->set_leave_enter_callback( |
| base::BindLambdaForTesting( |
| [&leave_enter_ids, &callback_return_value](int64_t old_display_id, |
| int64_t new_display_id) { |
| leave_enter_ids.emplace_back(old_display_id, new_display_id); |
| return callback_return_value; |
| })); |
| |
| // Successfully update surface to display 2. |
| display_config_controller()->SetPrimaryDisplayId(display2.id(), false); |
| ASSERT_EQ(leave_enter_ids.size(), 1u); |
| EXPECT_EQ(leave_enter_ids[0], std::make_pair(display1.id(), display2.id())); |
| |
| // Fail to update surface to display 1. |
| callback_return_value = false; |
| display_config_controller()->SetPrimaryDisplayId(display1.id(), false); |
| ASSERT_EQ(leave_enter_ids.size(), 2u); |
| EXPECT_EQ(leave_enter_ids[1], std::make_pair(display2.id(), display1.id())); |
| |
| // Should still send an update for surface to enter display 2. |
| callback_return_value = true; |
| display_config_controller()->SetPrimaryDisplayId(display2.id(), false); |
| ASSERT_EQ(leave_enter_ids.size(), 3u); |
| EXPECT_EQ(leave_enter_ids[2], |
| std::make_pair(display::kInvalidDisplayId, display2.id())); |
| } |
| |
| TEST_F(SurfaceTreeHostTest, |
| BuiltinDisplayMirrorModeToExtendModeWithExternalDisplayAsPrimary) { |
| UpdateDisplay("800x600,[email protected]"); |
| |
| // Set first display as internal, so it'll be primary source in mirror mode. |
| int64_t internal_display_id = |
| display::test::DisplayManagerTestApi(display_manager()) |
| .SetFirstDisplayAsInternalDisplay(); |
| int64_t external_display_id = GetSecondaryDisplay().id(); |
| |
| ASSERT_NE(internal_display_id, external_display_id); |
| |
| std::vector<std::pair<int64_t, int64_t>> leave_enter_ids; |
| shell_surface_->root_surface()->set_leave_enter_callback( |
| base::BindLambdaForTesting( |
| [&leave_enter_ids](int64_t old_display_id, int64_t new_display_id) { |
| leave_enter_ids.emplace_back(old_display_id, new_display_id); |
| return true; |
| })); |
| |
| // Make external display primary. |
| display_config_controller()->SetPrimaryDisplayId(external_display_id, false); |
| |
| ASSERT_EQ(leave_enter_ids.size(), 1u); |
| EXPECT_EQ(leave_enter_ids[0], |
| std::make_pair(internal_display_id, external_display_id)); |
| |
| // Change to mirror mode, which should make internal display primary. |
| display_manager()->SetMirrorMode(display::MirrorMode::kNormal, std::nullopt); |
| base::RunLoop().RunUntilIdle(); |
| |
| ASSERT_EQ(leave_enter_ids.size(), 2u); |
| EXPECT_EQ(leave_enter_ids[1], |
| std::make_pair(external_display_id, internal_display_id)); |
| |
| // Switch back to extend mode, which should restore external as primary. |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, std::nullopt); |
| base::RunLoop().RunUntilIdle(); |
| |
| ASSERT_EQ(leave_enter_ids.size(), 3u); |
| EXPECT_EQ(leave_enter_ids[2], |
| std::make_pair(internal_display_id, external_display_id)); |
| } |
| |
| TEST_F(SurfaceTreeHostTest, |
| UpdateHostWindowBoundsOnlySetsNewBoundsIfContentSizeChanged) { |
| // Create 50x50 shell surface. |
| auto shell_surface = test::ShellSurfaceBuilder({50, 50}).BuildShellSurface(); |
| auto* surface = shell_surface->root_surface(); |
| |
| // Create 25x25 sub surface. |
| auto child_surface = std::make_unique<Surface>(); |
| auto child_buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(25, 25)); |
| auto sub_surface = std::make_unique<SubSurface>(child_surface.get(), surface); |
| child_surface->Attach(child_buffer.get()); |
| child_surface->Commit(); |
| |
| // Set a mocked LayoutManager for testing purposes. |
| shell_surface->host_window()->SetLayoutManager( |
| std::make_unique<LayoutManagerChecker>()); |
| auto* layout_manager_checker = static_cast<LayoutManagerChecker*>( |
| shell_surface->host_window()->layout_manager()); |
| |
| { |
| InSequence s; |
| |
| // SetBounds (and hence OnWindowResized) is called once when changing |
| // content bounds. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(1); |
| sub_surface->SetPosition({50, 50}); |
| surface->Commit(); |
| EXPECT_EQ(gfx::Rect(0, 0, 75, 75), shell_surface->host_window()->bounds()); |
| |
| // SetBounds (and hence OnWindowResized) is not called when |
| // UpdateHostWindowBounds() is called but content bounds have not changed |
| // in DP. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(0); |
| surface->Commit(); |
| EXPECT_EQ(gfx::Rect(0, 0, 75, 75), shell_surface->host_window()->bounds()); |
| |
| // SetBounds (and hence OnWindowResized) is called once when |
| // destroying the root surface. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(1); |
| test::ShellSurfaceBuilder::DestroyRootSurface(shell_surface.get()); |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0), shell_surface->host_window()->bounds()); |
| } |
| } |
| |
| TEST_F(SurfaceTreeHostTest, |
| UpdateHostWindowBoundsAllocatesLocalSurfaceIdWhenPixelSizeOnlyChanges) { |
| // Set device scale factor to 300%. |
| UpdateDisplay("800x600*3"); |
| |
| // Create 50x50 shell surface which submits in pixel coordinates. |
| auto shell_surface = test::ShellSurfaceBuilder({50, 50}) |
| .SetClientSubmitsInPixelCoordinates(true) |
| .BuildShellSurface(); |
| auto* surface = shell_surface->root_surface(); |
| |
| // Create 1x1 sub surface. |
| auto child_surface = std::make_unique<Surface>(); |
| auto child_buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(1, 1)); |
| auto sub_surface = std::make_unique<SubSurface>(child_surface.get(), surface); |
| child_surface->Attach(child_buffer.get()); |
| child_surface->Commit(); |
| |
| // Set a mocked LayoutManager for testing purposes. |
| shell_surface->host_window()->SetLayoutManager( |
| std::make_unique<LayoutManagerChecker>()); |
| auto* layout_manager_checker = static_cast<LayoutManagerChecker*>( |
| shell_surface->host_window()->layout_manager()); |
| |
| // The 50x50 content bound is scaled to 17x17. |
| EXPECT_EQ(gfx::Rect(0, 0, 17, 17), shell_surface->host_window()->bounds()); |
| |
| { |
| InSequence s; |
| |
| // Set a 51x51 content bound (also scaled to 17). |
| // AllocateLocalSurfaceId is called here because DP size has not changed, |
| // but pixel size has, so we need a new surface id. |
| // SetBounds (and hence OnWindowResized) is not called. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(0); |
| auto local_surface_id = shell_surface->host_window()->GetLocalSurfaceId(); |
| sub_surface->SetPosition({50, 50}); |
| surface->Commit(); |
| EXPECT_EQ(gfx::Rect(0, 0, 17, 17), shell_surface->host_window()->bounds()); |
| EXPECT_NE(shell_surface->host_window()->GetLocalSurfaceId(), |
| local_surface_id); |
| |
| // If we try again with the same pixel size, no surface id will be |
| // allocated. |
| // SetBounds (and hence OnWindowResized) is not called. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(0); |
| local_surface_id = shell_surface->host_window()->GetLocalSurfaceId(); |
| surface->Commit(); |
| EXPECT_EQ(gfx::Rect(0, 0, 17, 17), shell_surface->host_window()->bounds()); |
| EXPECT_EQ(shell_surface->host_window()->GetLocalSurfaceId(), |
| local_surface_id); |
| |
| // SetBounds (and hence OnWindowResized) is called once when |
| // destroying the root surface. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(1); |
| test::ShellSurfaceBuilder::DestroyRootSurface(shell_surface.get()); |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0), shell_surface->host_window()->bounds()); |
| } |
| } |
| |
| TEST_F(SurfaceTreeHostTest, |
| UpdateScaleFactorUpdatesHostWindowBoundsEvenWhenPixelSizeIsSame) { |
| // Create 50x50 shell surface. |
| auto shell_surface = test::ShellSurfaceBuilder({50, 50}) |
| .SetClientSubmitsInPixelCoordinates(true) |
| .BuildShellSurface(); |
| auto* surface = shell_surface->root_surface(); |
| |
| // Create 25x25 sub surface. |
| auto child_surface = std::make_unique<Surface>(); |
| auto child_buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(25, 25)); |
| auto sub_surface = std::make_unique<SubSurface>(child_surface.get(), surface); |
| child_surface->Attach(child_buffer.get()); |
| child_surface->Commit(); |
| |
| // Set a mocked LayoutManager for testing purposes. |
| shell_surface->host_window()->SetLayoutManager( |
| std::make_unique<LayoutManagerChecker>()); |
| auto* layout_manager_checker = static_cast<LayoutManagerChecker*>( |
| shell_surface->host_window()->layout_manager()); |
| |
| { |
| InSequence s; |
| |
| // SetBounds (and hence OnWindowResized) is called once when changing |
| // content bounds. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(1); |
| sub_surface->SetPosition({50, 50}); |
| surface->Commit(); |
| EXPECT_EQ(gfx::Rect(0, 0, 75, 75), shell_surface->host_window()->bounds()); |
| |
| // Changing scale factor can affect host window size as it's in DP |
| // coordinate. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(1); |
| shell_surface->SetScaleFactor(2.f); |
| shell_surface->host_window()->AllocateLocalSurfaceId(); |
| surface->Commit(); |
| EXPECT_EQ(gfx::Rect(0, 0, 38, 38), shell_surface->host_window()->bounds()); |
| |
| // SetBounds (and hence OnWindowResized) is called once when |
| // destroying the root surface. |
| EXPECT_CALL(*layout_manager_checker, OnWindowResized).Times(1); |
| test::ShellSurfaceBuilder::DestroyRootSurface(shell_surface.get()); |
| EXPECT_EQ(gfx::Rect(0, 0, 0, 0), shell_surface->host_window()->bounds()); |
| } |
| } |
| |
| namespace { |
| |
| // |
| class InterceptingTestRasterInterface : public viz::TestRasterInterface { |
| public: |
| InterceptingTestRasterInterface() = default; |
| ~InterceptingTestRasterInterface() override = default; |
| |
| // Returns verified and unverified sync tokens the raster interface received |
| // via VerifySyncTokensCHROMIUM. |
| std::pair<int, int> GetAndResetSyncTokensCount() { |
| auto verified_tokens = verified_sync_tokens_; |
| auto unverified_tokens = unverified_sync_tokens_; |
| ResetSyncTokensCount(); |
| return {verified_tokens, unverified_tokens}; |
| } |
| |
| void ResetSyncTokensCount() { |
| verified_sync_tokens_ = 0; |
| unverified_sync_tokens_ = 0; |
| } |
| |
| // gpu::raster::RasterInterface overrides: |
| void VerifySyncTokensCHROMIUM(GLbyte** sync_tokens, GLsizei count) override { |
| ResetSyncTokensCount(); |
| for (GLsizei i = 0; i < count; ++i) { |
| gpu::SyncToken sync_token_data; |
| memcpy(sync_token_data.GetData(), sync_tokens[i], |
| sizeof(sync_token_data)); |
| if (sync_token_data.verified_flush()) { |
| verified_sync_tokens_++; |
| } else { |
| unverified_sync_tokens_++; |
| } |
| } |
| viz::TestRasterInterface::VerifySyncTokensCHROMIUM(sync_tokens, count); |
| } |
| |
| private: |
| int verified_sync_tokens_ = 0; |
| int unverified_sync_tokens_ = 0; |
| }; |
| |
| class FakeRasterContextProvider |
| : public base::RefCountedThreadSafe<FakeRasterContextProvider>, |
| public viz::RasterContextProvider { |
| public: |
| FakeRasterContextProvider() = default; |
| |
| FakeRasterContextProvider(FakeRasterContextProvider&) = delete; |
| FakeRasterContextProvider& operator=(FakeRasterContextProvider&) = delete; |
| |
| void SetOnDestroyedClosure(base::OnceClosure on_destroyed) { |
| on_destroyed_ = std::move(on_destroyed); |
| } |
| |
| // viz::RasterContextProvider implementation; |
| void AddRef() const override { |
| base::RefCountedThreadSafe<FakeRasterContextProvider>::AddRef(); |
| } |
| void Release() const override { |
| base::RefCountedThreadSafe<FakeRasterContextProvider>::Release(); |
| } |
| gpu::ContextResult BindToCurrentSequence() override { |
| ADD_FAILURE(); |
| return gpu::ContextResult::kFatalFailure; |
| } |
| void AddObserver(viz::ContextLostObserver* obs) override { ADD_FAILURE(); } |
| void RemoveObserver(viz::ContextLostObserver* obs) override { ADD_FAILURE(); } |
| base::Lock* GetLock() override { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| viz::ContextCacheController* CacheController() override { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| gpu::ContextSupport* ContextSupport() override { return nullptr; } |
| class GrDirectContext* GrContext() override { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| gpu::SharedImageInterface* SharedImageInterface() override { |
| ADD_FAILURE(); |
| return nullptr; |
| } |
| const gpu::Capabilities& ContextCapabilities() const override { |
| ADD_FAILURE(); |
| static gpu::Capabilities dummy_caps; |
| return dummy_caps; |
| } |
| const gpu::GpuFeatureInfo& GetGpuFeatureInfo() const override { |
| ADD_FAILURE(); |
| static gpu::GpuFeatureInfo dummy_feature_info; |
| return dummy_feature_info; |
| } |
| gpu::raster::RasterInterface* RasterInterface() override { |
| return GetInterceptingTestRasterInterface(); |
| } |
| unsigned int GetGrGLTextureFormat( |
| viz::SharedImageFormat format) const override { |
| ADD_FAILURE(); |
| return 0; |
| } |
| |
| InterceptingTestRasterInterface* GetInterceptingTestRasterInterface() { |
| return &intercepting_test_raster_interface_; |
| } |
| |
| private: |
| friend class base::RefCountedThreadSafe<FakeRasterContextProvider>; |
| |
| ~FakeRasterContextProvider() override { |
| if (on_destroyed_) { |
| std::move(on_destroyed_).Run(); |
| } |
| } |
| |
| base::OnceClosure on_destroyed_; |
| |
| InterceptingTestRasterInterface intercepting_test_raster_interface_; |
| }; |
| |
| } // namespace |
| |
| // The SurfaceTreeHost can set sync tokens as verified in advance to have less |
| // load on the IPC if they were verified in the previous frame. |
| TEST_F(SurfaceTreeHostTest, DoesntVerifyVerifiedSyncTokens) { |
| auto shell_surface = test::ShellSurfaceBuilder({50, 50}) |
| .SetClientSubmitsInPixelCoordinates(true) |
| .BuildShellSurface(); |
| auto* surface = shell_surface->root_surface(); |
| |
| scoped_refptr<FakeRasterContextProvider> ctx_prodiver = |
| base::MakeRefCounted<FakeRasterContextProvider>(); |
| auto old_provider = |
| shell_surface->SetRasterContextProviderForTesting(ctx_prodiver); |
| |
| auto* raster_interface = ctx_prodiver->GetInterceptingTestRasterInterface(); |
| raster_interface->ResetSyncTokensCount(); |
| |
| // Create a buffer and attach it to the surface. |
| auto buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(50, 50)); |
| surface->Attach(buffer.get()); |
| |
| surface->Commit(); |
| |
| // Its a new buffer and a newly generated sync token that shouldn't be known |
| // by the surface_tree_host. Thus, it must be unverified. |
| std::pair<int, int> sync_tokens_count = |
| raster_interface->GetAndResetSyncTokensCount(); |
| EXPECT_EQ(0, sync_tokens_count.first); |
| EXPECT_EQ(1, sync_tokens_count.second); |
| |
| // Commit the same buffer the second time. |
| surface->Commit(); |
| // Same buffer, nothing has been changed. The sync token is known by the |
| // surface_tree_host. Thus, it must be a verified token. |
| sync_tokens_count = raster_interface->GetAndResetSyncTokensCount(); |
| EXPECT_EQ(1, sync_tokens_count.first); |
| EXPECT_EQ(0, sync_tokens_count.second); |
| |
| shell_surface->SetRasterContextProviderForTesting(old_provider); |
| } |
| |
| } // namespace exo |