chromium/components/exo/surface_tree_host_unittest.cc

// 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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#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