chromium/ui/ozone/platform/flatland/flatland_surface_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.

#include "ui/ozone/platform/flatland/flatland_surface.h"

#include <fuchsia/ui/composition/cpp/fidl.h>
#include <lib/ui/scenic/cpp/testing/fake_flatland.h>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>

#include "base/fuchsia/koid.h"
#include "base/fuchsia/scoped_service_publisher.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/bind.h"
#include "base/test/fidl_matchers.h"
#include "base/test/task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/gpu_fence_handle.h"
#include "ui/gfx/overlay_plane_data.h"
#include "ui/ozone/platform/flatland/flatland_surface_factory.h"
#include "ui/ozone/platform/flatland/flatland_sysmem_native_pixmap.h"
#include "ui/ozone/public/overlay_plane.h"

using ::fuchsia::math::SizeU;
using ::fuchsia::math::Vec;
using ::fuchsia::ui::composition::ImageFlip;
using ::fuchsia::ui::composition::Orientation;
using ::scenic::FakeGraph;
using ::scenic::FakeImage;
using ::scenic::FakeTransform;
using ::scenic::FakeTransformPtr;
using ::scenic::FakeView;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::Optional;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::SaveArg;
using ::testing::VariantWith;

namespace ui {

namespace {

Matcher<FakeGraph> IsSurfaceGraph(
    const fuchsia::ui::composition::ParentViewportWatcherPtr&
        parent_viewport_watcher,
    const fuchsia::ui::views::ViewportCreationToken& viewport_creation_token,
    const fuchsia::math::VecF& scale,
    std::vector<Matcher<FakeTransformPtr>> child_transform_matchers) {
  auto view_token_koid = base::GetRelatedKoid(viewport_creation_token.value);
  auto watcher_koid = base::GetRelatedKoid(parent_viewport_watcher.channel());

  return AllOf(
      Field("root_transform", &FakeGraph::root_transform,
            Pointee(AllOf(
                Field("translation", &FakeTransform::translation,
                      ::base::test::FidlEq(FakeTransform::kDefaultTranslation)),
                Field("scale", &FakeTransform::scale,
                      ::base::test::FidlEq(scale)),
                Field("opacity", &FakeTransform::opacity,
                      FakeTransform::kDefaultOpacity),
                Field("children", &FakeTransform::children,
                      ElementsAreArray(child_transform_matchers))))),
      Field("view", &FakeGraph::view,
            Optional(AllOf(
                Field("view_token", &FakeView::view_token, view_token_koid),
                Field("parent_viewport_watcher",
                      &FakeView::parent_viewport_watcher, watcher_koid)))));
}

Matcher<fuchsia::ui::composition::ImageProperties> IsImageProperties(
    const fuchsia::math::SizeU& size) {
  return AllOf(
      Property("has_size", &fuchsia::ui::composition::ImageProperties::has_size,
               true),
      Property("size", &fuchsia::ui::composition::ImageProperties::size,
               ::base::test::FidlEq(size)));
}

Matcher<FakeTransformPtr> IsImageTransform(
    const fuchsia::math::SizeU& size,
    fuchsia::ui::composition::BlendMode blend_mode,
    const fuchsia::math::Vec& translation = FakeTransform::kDefaultTranslation,
    Orientation orientation = FakeTransform::kDefaultOrientation,
    const fuchsia::math::RectF& sample_region = FakeImage::kDefaultSampleRegion,
    const fuchsia::math::SizeU& destination_size =
        FakeImage::kDefaultDestinationSize,
    float image_opacity = FakeImage::kDefaultOpacity,
    ImageFlip image_flip = FakeImage::kDefaultFlip) {
  return Pointee(AllOf(
      Field("translation", &FakeTransform::translation,
            ::base::test::FidlEq(translation)),
      Field("orientation", &FakeTransform::orientation, orientation),
      Field("scale", &FakeTransform::scale,
            ::base::test::FidlEq(FakeTransform::kDefaultScale)),
      Field("opacity", &FakeTransform::opacity, FakeTransform::kDefaultOpacity),
      Field("children", &FakeTransform::children, IsEmpty()),
      Field("content", &FakeTransform::content,
            Pointee(VariantWith<FakeImage>(
                AllOf(Field("image_properties", &FakeImage::image_properties,
                            IsImageProperties(size)),
                      Field("sample_region", &FakeImage::sample_region,
                            ::base::test::FidlEq(sample_region)),
                      Field("destination_size", &FakeImage::destination_size,
                            ::base::test::FidlEq(destination_size)),
                      Field("blend_mode", &FakeImage::blend_mode, blend_mode),
                      Field("opacity", &FakeImage::opacity, image_opacity),
                      Field("flip", &FakeImage::flip, image_flip)))))));
}

scoped_refptr<FlatlandSysmemNativePixmap> CreateFlatlandSysmemNativePixmap(
    uint32_t image_size) {
  gfx::NativePixmapHandle handle;
  zx::eventpair service_handle;
  zx::eventpair::create(0, &service_handle, &handle.buffer_collection_handle);
  auto collection = base::MakeRefCounted<FlatlandSysmemBufferCollection>();
  collection->InitializeForTesting(std::move(service_handle),
                                   gfx::BufferUsage::SCANOUT);
  return base::MakeRefCounted<FlatlandSysmemNativePixmap>(
      collection, std::move(handle), gfx::Size(image_size, image_size));
}

}  // namespace

class MockFlatlandSurfaceFactory : public FlatlandSurfaceFactory {
 public:
  MockFlatlandSurfaceFactory() = default;
  ~MockFlatlandSurfaceFactory() override {}

  MOCK_METHOD2(AddSurface,
               void(gfx::AcceleratedWidget widget, FlatlandSurface* surface));
  MOCK_METHOD1(RemoveSurface, void(gfx::AcceleratedWidget widget));
};

template <class T>
class FlatlandSurfaceTestBase : public T {
 protected:
  FlatlandSurfaceTestBase()
      : fake_flatland_publisher_(test_context_.additional_services(),
                                 fake_flatland_.GetFlatlandRequestHandler()),
        fake_allocator_publisher_(test_context_.additional_services(),
                                  fake_flatland_.GetAllocatorRequestHandler()) {
  }
  ~FlatlandSurfaceTestBase() override = default;

  FlatlandSurface* CreateFlatlandSurface() {
    EXPECT_CALL(mock_factory_, AddSurface(_, _));
    EXPECT_CALL(mock_factory_, RemoveSurface(_));
    flatland_surface_ = std::make_unique<FlatlandSurface>(
        &mock_factory_, gfx::kNullAcceleratedWidget);
    return flatland_surface_.get();
  }

  void SetLayoutInfo(uint32_t width, uint32_t height, float dpr) {
    fuchsia::ui::composition::LayoutInfo layout_info;
    layout_info.set_logical_size({width, height});
    layout_info.set_device_pixel_ratio({dpr, dpr});
    flatland_surface_->OnGetLayout(std::move(layout_info));
  }

  size_t NumberOfPendingClosures() {
    return flatland_surface_->pending_present_closures_.size();
  }

  const fuchsia::ui::composition::ParentViewportWatcherPtr&
  parent_viewport_watcher() {
    return flatland_surface_->parent_viewport_watcher_;
  }

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::MainThreadType::IO};
  scenic::FakeFlatland fake_flatland_;

  base::TestComponentContextForProcess test_context_;
  // Injects binding for responding to Flatland protocol connection requests.
  const base::ScopedServicePublisher<fuchsia::ui::composition::Flatland>
      fake_flatland_publisher_;
  // Injects binding for responding to Allocator protocol connection requests.
  const base::ScopedServicePublisher<fuchsia::ui::composition::Allocator>
      fake_allocator_publisher_;
  MockFlatlandSurfaceFactory mock_factory_;
  std::unique_ptr<FlatlandSurface> flatland_surface_;
};

class FlatlandSurfaceTest : public FlatlandSurfaceTestBase<testing::Test> {};

TEST_F(FlatlandSurfaceTest, Initialization) {
  MockFlatlandSurfaceFactory mock_factory;
  gfx::AcceleratedWidget widget;
  EXPECT_CALL(mock_factory, AddSurface(_, _)).WillOnce(SaveArg<0>(&widget));

  FlatlandSurface surface(&mock_factory, gfx::kNullAcceleratedWidget);

  ASSERT_EQ(widget, gfx::kNullAcceleratedWidget);
  EXPECT_CALL(mock_factory, RemoveSurface(widget));

  // Check that there are no crashes after flushing tasks.
  task_environment_.RunUntilIdle();
}

TEST_F(FlatlandSurfaceTest, PresentPrimaryPlane) {
  size_t presents_called = 0u;
  fake_flatland_.SetPresentHandler(
      [&presents_called](auto) { ++presents_called; });

  FlatlandSurface* surface = CreateFlatlandSurface();
  auto platform_handle = surface->CreateView();
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  viewport_creation_token.value = zx::channel(platform_handle.TakeHandle());

  const int kTestLogicalSize = 100;
  const float kTestDevicePixelRatio = 1.5;
  SetLayoutInfo(kTestLogicalSize, kTestLogicalSize, kTestDevicePixelRatio);
  const float expected_scale = 1 / kTestDevicePixelRatio;
  const uint32_t expected_image_size = kTestLogicalSize * kTestDevicePixelRatio;

  auto primary_plane_pixmap =
      CreateFlatlandSysmemNativePixmap(expected_image_size);
  surface->Present(
      primary_plane_pixmap, std::vector<ui::OverlayPlane>(),
      std::vector<gfx::GpuFenceHandle>(), std::vector<gfx::GpuFenceHandle>(),
      base::BindOnce([](gfx::SwapCompletionResult result) {}),
      base::BindOnce([](const gfx::PresentationFeedback& feedback) {}));

  // There should be a present call in FakeFlatland after flushing the tasks.
  EXPECT_EQ(0u, presents_called);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(1u, presents_called);

  EXPECT_THAT(
      fake_flatland_.graph(),
      IsSurfaceGraph(
          parent_viewport_watcher(), viewport_creation_token,
          {expected_scale, expected_scale},
          {IsImageTransform({expected_image_size, expected_image_size},
                            fuchsia::ui::composition::BlendMode::SRC_OVER)}));
}

TEST_F(FlatlandSurfaceTest, PresentBeforeLayoutInfo) {
  size_t presents_called = 0u;
  fake_flatland_.SetPresentHandler(
      [&presents_called](auto) { ++presents_called; });

  FlatlandSurface* surface = CreateFlatlandSurface();

  auto platform_handle = surface->CreateView();
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  viewport_creation_token.value = zx::channel(platform_handle.TakeHandle());

  const int kTestLogicalSize = 80;
  const float kTestDevicePixelRatio = 2;
  const float expected_scale = 1 / kTestDevicePixelRatio;
  const uint32_t expected_image_size = kTestLogicalSize * kTestDevicePixelRatio;

  auto primary_plane_pixmap =
      CreateFlatlandSysmemNativePixmap(expected_image_size);
  surface->Present(
      primary_plane_pixmap, std::vector<ui::OverlayPlane>(),
      std::vector<gfx::GpuFenceHandle>(), std::vector<gfx::GpuFenceHandle>(),
      base::BindOnce([](gfx::SwapCompletionResult result) {}),
      base::BindOnce([](const gfx::PresentationFeedback& feedback) {}));

  // There should be a one pending present.
  EXPECT_EQ(1u, NumberOfPendingClosures());

  SetLayoutInfo(kTestLogicalSize, kTestLogicalSize, kTestDevicePixelRatio);
  EXPECT_EQ(0u, NumberOfPendingClosures());

  EXPECT_EQ(0u, presents_called);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(1u, presents_called);
  EXPECT_THAT(
      fake_flatland_.graph(),
      IsSurfaceGraph(
          parent_viewport_watcher(), viewport_creation_token,
          {expected_scale, expected_scale},
          {IsImageTransform({expected_image_size, expected_image_size},
                            fuchsia::ui::composition::BlendMode::SRC_OVER)}));
}

// The parameters for an overlay transform test are:
// - The input overlay transform.
// - The expected Flatland Transform orientation.
// - The expected Flatland image flip.
// - The expected Flatland Transform translation.
// - The expected destination image size.
using OverlayPlaneTestParams =
    std::tuple<gfx::OverlayTransform, Orientation, ImageFlip, Vec, SizeU>;

class FlatlandSurfaceOverlayPlaneTransformTest
    : public FlatlandSurfaceTestBase<
          testing::TestWithParam<OverlayPlaneTestParams>> {
 protected:
  const int kTestLogicalSize = 100;
  const float kTestDevicePixelRatio = 1.5;

  // Expected image size should be equal to |kTestLogicalSize| *
  // |kTestDevicePixelRatioSize|.
  const uint32_t kExpectedImageSize = 150;

  // Overlay properties.
  const int32_t kOverlayX = 10;
  const int32_t kOverlayY = 20;
  const uint32_t kOverlayWidth = 120;
  const uint32_t kOverlayHeight = 115;
};

INSTANTIATE_TEST_SUITE_P(
    ParameterizedFlatlandSurfaceOverlayPlaneTransformTest,
    FlatlandSurfaceOverlayPlaneTransformTest,
    // Expected translation and expected destination image size are based on the
    // overlay position and size as defined in
    // |FlatlandSurfaceOverlayPlaneTransformTest|.
    testing::Values(std::make_tuple(gfx::OVERLAY_TRANSFORM_NONE,
                                    Orientation::CCW_0_DEGREES,
                                    ImageFlip::NONE,
                                    Vec{10, 20},
                                    SizeU{120U, 115U}),
                    std::make_tuple(gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90,
                                    Orientation::CCW_270_DEGREES,
                                    ImageFlip::NONE,
                                    Vec{130, 20},
                                    SizeU{115U, 120U}),
                    std::make_tuple(gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180,
                                    Orientation::CCW_180_DEGREES,
                                    ImageFlip::NONE,
                                    Vec{130, 135},
                                    SizeU{120U, 115U}),
                    std::make_tuple(gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270,
                                    Orientation::CCW_90_DEGREES,
                                    ImageFlip::NONE,
                                    Vec{10, 135},
                                    SizeU{115U, 120U}),
                    std::make_tuple(gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL,
                                    Orientation::CCW_0_DEGREES,
                                    ImageFlip::LEFT_RIGHT,
                                    Vec{10, 20},
                                    SizeU{120U, 115U}),
                    std::make_tuple(gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL,
                                    Orientation::CCW_0_DEGREES,
                                    ImageFlip::UP_DOWN,
                                    Vec{10, 20},
                                    SizeU{120U, 115U})));

TEST_P(FlatlandSurfaceOverlayPlaneTransformTest, PresentOverlayPlane) {
  size_t presents_called = 0u;
  fake_flatland_.SetPresentHandler(
      [&presents_called](auto) { ++presents_called; });

  const auto [input_transform, expected_orientation, expected_image_flip,
              expected_translation, expected_image_destination_size] =
      GetParam();

  FlatlandSurface* surface = CreateFlatlandSurface();
  auto platform_handle = surface->CreateView();
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  viewport_creation_token.value = zx::channel(platform_handle.TakeHandle());

  SetLayoutInfo(kTestLogicalSize, kTestLogicalSize, kTestDevicePixelRatio);
  const float expected_scale = 1 / kTestDevicePixelRatio;

  auto primary_plane_pixmap =
      CreateFlatlandSysmemNativePixmap(kExpectedImageSize);

  const float kOverlayOpacity = .7f;
  const gfx::RectF kOverlayBounds(kOverlayX, kOverlayY, kOverlayWidth,
                                  kOverlayHeight);
  const float kCropX = 2.2f;
  const float kCropY = 6.6f;
  const float kCropWidth = 14.4f;
  const float kCropHeight = 16.6f;
  gfx::OverlayPlaneData overlay_data(
      /*z_order=*/1, input_transform, kOverlayBounds,
      gfx::RectF(kCropX, kCropY, kCropWidth, kCropHeight),
      /*enable_blend=*/true,
      /*damage_rect=*/gfx::Rect(), kOverlayOpacity,
      gfx::OverlayPriorityHint::kNone,
      /*rounded_corners=*/gfx::RRectF(), gfx::ColorSpace(),
      /*hdr_metadata=*/std::nullopt);
  ui::OverlayPlane overlay_plane(
      CreateFlatlandSysmemNativePixmap(kExpectedImageSize), nullptr,
      overlay_data);
  std::vector<ui::OverlayPlane> overlays;
  overlays.push_back(std::move(overlay_plane));

  surface->Present(
      primary_plane_pixmap, std::move(overlays),
      std::vector<gfx::GpuFenceHandle>(), std::vector<gfx::GpuFenceHandle>(),
      base::BindOnce([](gfx::SwapCompletionResult result) {}),
      base::BindOnce([](const gfx::PresentationFeedback& feedback) {}));

  EXPECT_EQ(0u, presents_called);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(1u, presents_called);
  EXPECT_THAT(
      fake_flatland_.graph(),
      IsSurfaceGraph(
          parent_viewport_watcher(), viewport_creation_token,
          {expected_scale, expected_scale},
          {IsImageTransform({kExpectedImageSize, kExpectedImageSize},
                            fuchsia::ui::composition::BlendMode::SRC_OVER),
           IsImageTransform(
               {kExpectedImageSize, kExpectedImageSize},
               fuchsia::ui::composition::BlendMode::SRC_OVER,
               expected_translation, expected_orientation,
               {kCropX * kExpectedImageSize, kCropY * kExpectedImageSize,
                kCropWidth * kExpectedImageSize,
                kCropHeight * kExpectedImageSize},
               expected_image_destination_size, kOverlayOpacity,
               expected_image_flip)}));
}

}  // namespace ui